stremiok 2.2.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,43 @@
3
3
  What changed in each version of StremioK, newest first. How everything works is in the
4
4
  [user guide](docs/USER-GUIDE.md).
5
5
 
6
+ ## 2.2.2 — 2026-10-28
7
+ - **Torrents through a streaming server** (Settings › Content › Streaming server): with Stremio's
8
+ streaming server or a TorrServer running on your network (a computer or a NAS), torrent-only
9
+ sources play like any other. *Test* says whether it answers; torrents get longer to start (2
10
+ minutes by default).
11
+ - **Seeders** in Source ranking: torrent sources with more people sharing them first.
12
+ - **Zoom** (Settings › Appearance): 100% to 140%, everything bigger to read from further away;
13
+ subtitles keep their own size.
14
+ - **Leaving StremioK:** BACK on Home's top menu asks *Exit StremioK?* (OK exits), instead of
15
+ pressing BACK twice (#3).
16
+ - **The top menu scrolls** when its items don't all fit, the StremioK name staying at the right.
17
+ - **IPTV Auto-refresh** (Settings › Content › IPTV, *Off* by default): every 1, 3, 6 or 12 hours,
18
+ your playlists and their TV guides are read again; never while something plays.
19
+ - **Quicker switching:** subtitles (a track still coming isn't started over, the last 8 files
20
+ come back at once) and channels (what StremioK checks on each one stops when you move on).
21
+ - **Diagnostics says what went wrong** (*Timed out after 20 s*, *No answer…*) instead of
22
+ "HTTP 0"; slow subtitle servers get longer: 20 seconds for a file, 15 for an addon's list.
23
+ - **USB downloads:** a drive prepared with links can no longer make StremioK read or overwrite a
24
+ file outside it.
25
+ - **Fixed:** your IPTV rows (and the Radio tab) missing from Home at start-up until a Reload; a
26
+ next episode or a hold menu whose details came late opening after you'd left.
27
+
28
+ ## 2.2.1 — 2026-10-24
29
+ - **Every language OpenSubtitles uses** (94, Indonesian among them, #1) for subtitles and audio:
30
+ to pick as your default, second choice or second subtitle, listed by name, and each track
31
+ shown by its language's name ("Pan" is Punjabi).
32
+ - **Only my languages** (Settings › Playback › Subtitles, *Off* by default): the subtitle menu
33
+ lists just your languages, and tracks that don't say theirs; *Other languages* at its end has
34
+ the rest.
35
+ - **Shorter settings pages:** the subtitles' size, color, background, edge, position and font are
36
+ on a page of their own (*Appearance*); Source ranking keeps *Played before first* above its
37
+ order, with *Filters* and *When you press Play* as pages of their own.
38
+ - **Hold OK on a program in the Guide tab:** *▶ Watch now*, *★ Add to / Remove from favorites*
39
+ for its channel, and *Find in your addons* for that program.
40
+ - **Fixed:** back on the Guide tab after CH+ / CH− lands on the channel you ended on, at the time
41
+ now, instead of the first channel played.
42
+
6
43
  ## 2.2.0 — 2026-10-23
7
44
  - **A TV guide for your IPTV channels:** what's on now and next on each channel's card, in
8
45
  Home's details strip and while a channel plays, and its schedule when you hold OK on it. From
package/README.md CHANGED
@@ -16,6 +16,7 @@ video through Samsung's AVPlay API.
16
16
  - **[Changelog](CHANGELOG.md)**: what changed in each version.
17
17
  - **[Developer guide](docs/DEVELOPER-GUIDE.md)**: how it's put together, and running it in a
18
18
  browser.
19
+ - **[Security policy](docs/SECURITY.md)**: reporting a vulnerability, privately.
19
20
 
20
21
  ## Highlights
21
22
 
@@ -34,8 +35,9 @@ video through Samsung's AVPlay API.
34
35
  - **YouTube:** live channels, trailers, and your own YouTube channels, playlists and searches,
35
36
  with SponsorBlock, DeArrow, chapters and captions.
36
37
  - **Local:** the videos on a USB drive, matched to their titles.
38
+ - **Torrents**, through Stremio's streaming server or a TorrServer you run on your network.
37
39
  - **Made for the remote:** D-pad navigation throughout; hold OK where an item has more to
38
- offer (*Hold OK for more* says so).
40
+ offer (*Hold OK for more* says so); zoom up to 140% for reading from further away.
39
41
 
40
42
  ## Install
41
43
 
@@ -51,7 +53,8 @@ Three ways, all covered step by step in the **[installation guide](docs/INSTALL.
51
53
 
52
54
  1. **Addons:** add the addons you use, by link, or bring them in from your Stremio account,
53
55
  AIOManager or a configuration file. A stream addon that gives HTTP(S) links (for example
54
- through a debrid service) is what makes titles playable.
56
+ through a debrid service), or a torrent addon with a streaming server you run, is what makes
57
+ titles playable.
55
58
  2. **API keys**, optional: your own TMDB, OMDb and MDBList keys add ratings, *Cast & more*,
56
59
  *where to watch* and episode trailers.
57
60
  3. **Trakt**, optional: connect your account to share watched marks and Continue Watching.
@@ -66,8 +69,8 @@ Three ways, all covered step by step in the **[installation guide](docs/INSTALL.
66
69
  This app hosts **no** media. It is a client for the open Stremio addon protocol: catalogs and
67
70
  metadata come from the addons you install (**Cinemeta**, the official one, by default), and
68
71
  playable sources come from whatever **stream addons you add yourself** in Settings. Streams
69
- that only expose a torrent `infoHash` (no HTTP URL) are listed but not playable: AVPlay needs
70
- a direct HTTP(S)/HLS/DASH URL, and this app embeds no torrent or debrid engine.
72
+ that only expose a torrent `infoHash` play only through a streaming server you run (Stremio's
73
+ or TorrServer): this app embeds no torrent or debrid engine.
71
74
  > **Note on WatchHub:** StremioK doesn't install it. Its answers say it's only for clients
72
75
  > Stremio authorizes in writing, and StremioK isn't one; "where to watch" comes from TMDB
73
76
  > instead (with a TMDB key). You can still add it yourself as a third-party addon (Settings
@@ -84,8 +87,7 @@ your progress, settings and copies that let it start faster (see
84
87
  ## Known limitations
85
88
 
86
89
  - AVPlay behavior varies by TV generation, codec and HLS provider.
87
- - No torrent playback: sources need a direct HTTP(S)/HLS/DASH URL (e.g. through a debrid
88
- addon).
90
+ - Torrents play only through a streaming server you run on your network.
89
91
  - Home rows show a catalog's first page (up to 30 titles); Discover pages through the rest.
90
92
  - No StremioK account and no profiles: settings, addons and progress stay on the TV. The
91
93
  only sync is optional: Trakt (watched marks, Continue Watching, ratings).
package/config.xml CHANGED
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets"
3
- id="http://stremiok.tizen/StremioKTizen" version="2.2.0" viewmodes="maximized">
3
+ id="http://stremiok.tizen/StremioKTizen" version="2.2.2" viewmodes="maximized">
4
4
  <tizen:application id="StremioK01.StremioK" package="StremioK01" required_version="3.0"/>
5
5
  <content src="index.html"/>
6
6
  <feature name="http://tizen.org/feature/screen.size.all"/>
package/css/style.css CHANGED
@@ -19,20 +19,20 @@ html, body {
19
19
  #av-player, #html5-player {
20
20
  position: absolute;
21
21
  top: 0; left: 0;
22
- width: 1920px; height: 1080px;
22
+ width: 100%; height: 100%; /* (the page: 1920×1080, or less under a zoom, scaled up) */
23
23
  background: #000;
24
24
  display: none;
25
25
  z-index: 1;
26
26
  }
27
27
  #av-player.active, #html5-player.active { display: block; }
28
28
 
29
- #app { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; z-index: 10; }
30
- .screen { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; }
29
+ #app { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; }
30
+ .screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
31
31
  .hidden { display: none !important; }
32
32
 
33
33
  /* --- Header --- */
34
34
  .header { height: 96px; padding: 30px 60px 0 60px; display: flex; align-items: center; }
35
- .brand { font-size: 40px; font-weight: bold; color: #818cf8; letter-spacing: 1px; }
35
+ .brand { font-size: 40px; font-weight: bold; color: #818cf8; letter-spacing: 1px; flex-shrink: 0; }
36
36
  .brand .dot { color: #e6e9ef; }
37
37
  /* private viewing on: StremioK as an outline (the fill hidden; the outline takes each letter's
38
38
  own color, so it follows the accent color and a light background) */
@@ -94,7 +94,7 @@ html, body {
94
94
  .detail-hero .shade { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, rgba(14,17,23,0.96) 30%, rgba(14,17,23,0.35) 100%), linear-gradient(0deg, #0e1117 4%, transparent 40%); }
95
95
  /* The source and episode lists: the backdrop over the whole screen, shaded lighter, the rows
96
96
  see-through (a focused one stays solid) */
97
- .detail-hero.hero-full { height: 1080px; }
97
+ .detail-hero.hero-full { height: 100%; }
98
98
  .detail-hero.hero-full .shade { background: linear-gradient(90deg, rgba(14,17,23,0.9) 22%, rgba(14,17,23,0.4) 100%), linear-gradient(0deg, rgba(14,17,23,0.5) 0%, transparent 45%); }
99
99
  .streams-panel .stream, .list-panel .ep { background: rgba(22,27,38,0.7); }
100
100
  .streams-panel .stream-group, .streams-panel .stream-for, .streams-panel .streams-hidden { text-shadow: 0 1px 3px #000, 0 0 8px rgba(0,0,0,0.6); }
@@ -319,7 +319,7 @@ html, body {
319
319
  .card.landscape { width: 340px; height: 200px; }
320
320
 
321
321
  /* --- Confirm modal --- */
322
- .modal-overlay { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; z-index: 45; background: rgba(0,0,0,0.72); display: flex; align-items: center; justify-content: center; }
322
+ .modal-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 45; background: rgba(0,0,0,0.72); display: flex; align-items: center; justify-content: center; }
323
323
  .modal-box { width: 820px; background: #161b26; border: 2px solid #2a3346; border-radius: 16px; padding: 44px 48px; text-align: center; }
324
324
  .modal-msg { font-size: 30px; line-height: 1.4; color: #e6e9ef; margin-bottom: 36px; }
325
325
  .modal-row { display: flex; justify-content: center; }
@@ -365,7 +365,7 @@ html, body {
365
365
  /* --- Toast + spinner --- */
366
366
  #hold-hint { position: absolute; right: 36px; bottom: 20px; z-index: 20; font-size: 17px; color: #c7cbd4; background: rgba(0,0,0,0.6); padding: 6px 14px; border-radius: 16px; pointer-events: none; }
367
367
  #toast { position: absolute; bottom: 60px; left: 50%; margin-left: -300px; width: 600px; text-align: center; z-index: 40; background: rgba(0,0,0,0.85); padding: 18px 24px; border-radius: 10px; font-size: 22px; }
368
- #spinner { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; z-index: 50; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(0,0,0,0.3); }
368
+ #spinner { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 50; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(0,0,0,0.3); }
369
369
  /* status line under the spinner ("Opening stream at 1080p…"); empty when unused */
370
370
  #spinner-msg { margin-top: 26px; font-size: 28px; color: #fff; text-shadow: 0 2px 6px #000; text-align: center; }
371
371
  .spin { width: 70px; height: 70px; border: 7px solid rgba(255,255,255,0.2); border-top-color: #818cf8; border-radius: 50%; -webkit-animation: sp 0.9s linear infinite; animation: sp 0.9s linear infinite; }
@@ -373,7 +373,10 @@ html, body {
373
373
  @keyframes sp { to { transform: rotate(360deg); } }
374
374
 
375
375
  /* Home top row: tab pills + Search + Settings on the left, StremioK logo on the right */
376
- .home-header .tabbar-inline { display: flex; align-items: center; }
376
+ /* the items scroll sideways in their strip when they don't all fit (position: the items' offsets are the strip's) */
377
+ .home-header .menu-scroll { flex: 1 1 auto; min-width: 0; overflow: hidden; position: relative; display: flex; align-items: center; margin-right: 24px; padding: 4px 0; }
378
+ .home-header .tabbar-inline { display: flex; align-items: center; flex-shrink: 0; }
379
+ .home-header .menu-scroll > .hchip { flex-shrink: 0; }
377
380
  .tab-pill { display: inline-flex; align-items: center; font-size: 22px; padding: 8px 9px; margin-right: 5px; border-radius: 28px; background: #1a1f2b; color: #aeb6c4; border: 4px solid transparent; white-space: nowrap; }
378
381
  .tab-ic { margin-left: 6px; font-size: 22px; }
379
382
  .tab-pill.active { background: #2a3346; color: #fff; }
package/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=1920, height=1080, user-scalable=no">
6
6
  <title>StremioK</title>
7
- <link rel="stylesheet" href="css/style.css?v=2.2.0">
7
+ <link rel="stylesheet" href="css/style.css?v=2.2.2">
8
8
  </head>
9
9
  <body>
10
10
  <!-- CSS-applied sentinel (kept off-screen); app.js waits for its width before rendering -->
@@ -53,37 +53,37 @@
53
53
 
54
54
  <!-- Load order matters: polyfills -> util -> iptv -> data -> input -> tvapps -> player parts -> player -> ytplay -> view parts -> views -> app -->
55
55
  <!-- ?v= busts the TV webview cache on every release (keep in sync with package.json) -->
56
- <script src="js/polyfills.js?v=2.2.0"></script>
57
- <script src="js/util.js?v=2.2.0"></script>
58
- <script src="js/iptv.js?v=2.2.0"></script>
59
- <script src="js/epg.js?v=2.2.0"></script>
60
- <script src="js/youtube.js?v=2.2.0"></script>
61
- <script src="js/stremio.js?v=2.2.0"></script>
62
- <script src="js/trakt.js?v=2.2.0"></script>
63
- <script src="js/keys.js?v=2.2.0"></script>
64
- <script src="js/focus.js?v=2.2.0"></script>
65
- <script src="js/tvapps.js?v=2.2.0"></script>
66
- <script src="js/player-core.js?v=2.2.0"></script>
67
- <script src="js/player-store.js?v=2.2.0"></script>
68
- <script src="js/player-lang.js?v=2.2.0"></script>
69
- <script src="js/player-quality.js?v=2.2.0"></script>
70
- <script src="js/player-subs.js?v=2.2.0"></script>
71
- <script src="js/player-audio.js?v=2.2.0"></script>
72
- <script src="js/player-controls.js?v=2.2.0"></script>
73
- <script src="js/player.js?v=2.2.0"></script>
74
- <script src="js/ytplay.js?v=2.2.0"></script>
75
- <script src="js/local.js?v=2.2.0"></script>
76
- <script src="js/downloads.js?v=2.2.0"></script>
77
- <script src="js/stremio-account.js?v=2.2.0"></script>
78
- <script src="js/view-core.js?v=2.2.0"></script>
79
- <script src="js/view-home.js?v=2.2.0"></script>
80
- <script src="js/view-guide.js?v=2.2.0"></script>
81
- <script src="js/view-detail.js?v=2.2.0"></script>
82
- <script src="js/view-search.js?v=2.2.0"></script>
83
- <script src="js/view-settings.js?v=2.2.0"></script>
84
- <script src="js/view-discover.js?v=2.2.0"></script>
85
- <script src="js/views.js?v=2.2.0"></script>
86
- <script src="js/app.js?v=2.2.0"></script>
87
- <script src="js/hubpreview.js?v=2.2.0"></script>
56
+ <script src="js/polyfills.js?v=2.2.2"></script>
57
+ <script src="js/util.js?v=2.2.2"></script>
58
+ <script src="js/iptv.js?v=2.2.2"></script>
59
+ <script src="js/epg.js?v=2.2.2"></script>
60
+ <script src="js/youtube.js?v=2.2.2"></script>
61
+ <script src="js/stremio.js?v=2.2.2"></script>
62
+ <script src="js/trakt.js?v=2.2.2"></script>
63
+ <script src="js/keys.js?v=2.2.2"></script>
64
+ <script src="js/focus.js?v=2.2.2"></script>
65
+ <script src="js/tvapps.js?v=2.2.2"></script>
66
+ <script src="js/player-core.js?v=2.2.2"></script>
67
+ <script src="js/player-store.js?v=2.2.2"></script>
68
+ <script src="js/player-lang.js?v=2.2.2"></script>
69
+ <script src="js/player-quality.js?v=2.2.2"></script>
70
+ <script src="js/player-subs.js?v=2.2.2"></script>
71
+ <script src="js/player-audio.js?v=2.2.2"></script>
72
+ <script src="js/player-controls.js?v=2.2.2"></script>
73
+ <script src="js/player.js?v=2.2.2"></script>
74
+ <script src="js/ytplay.js?v=2.2.2"></script>
75
+ <script src="js/local.js?v=2.2.2"></script>
76
+ <script src="js/downloads.js?v=2.2.2"></script>
77
+ <script src="js/stremio-account.js?v=2.2.2"></script>
78
+ <script src="js/view-core.js?v=2.2.2"></script>
79
+ <script src="js/view-home.js?v=2.2.2"></script>
80
+ <script src="js/view-guide.js?v=2.2.2"></script>
81
+ <script src="js/view-detail.js?v=2.2.2"></script>
82
+ <script src="js/view-search.js?v=2.2.2"></script>
83
+ <script src="js/view-settings.js?v=2.2.2"></script>
84
+ <script src="js/view-discover.js?v=2.2.2"></script>
85
+ <script src="js/views.js?v=2.2.2"></script>
86
+ <script src="js/app.js?v=2.2.2"></script>
87
+ <script src="js/hubpreview.js?v=2.2.2"></script>
88
88
  </body>
89
89
  </html>
package/js/app.js CHANGED
@@ -5,7 +5,6 @@ var App = (function () {
5
5
  var _current = 'home';
6
6
  var _stack = []; // frames: { screen, focus }
7
7
  var _lastBackAt = 0; // debounce BACK across keydown + tizenhwkey
8
- var _exitArmedAt = 0; // double-Back-to-exit guard on the home root
9
8
 
10
9
  function screenEl(name) { return U.byId('screen-' + name); }
11
10
 
@@ -73,14 +72,17 @@ var App = (function () {
73
72
  // Close a live player directly back to the screen/card that launched it. The
74
73
  // originating screen stays mounted while the player is open, so restoring its
75
74
  // saved focus node also preserves the grid's horizontal and vertical scroll.
76
- // focusNode: the card to land on instead (the channel you switched to with CH+/CH−).
77
- function returnFromLive(focusNode) {
75
+ // focusNode: the card to land on instead (the channel you switched to with CH+/CH−); liveId:
76
+ // that channel, for the Guide tab, which lands on it at the time now (Views.guideBackOn: its
77
+ // cells from when it was played may be gone, or show hours long past).
78
+ function returnFromLive(focusNode, liveId) {
78
79
  var f = _stack.pop();
79
80
  if (!f) { goHome(); return; }
80
81
  if (f.screen !== _current) { abandon(_current); }
81
82
  showScreen(f.screen);
83
+ var guide = liveId && f.screen === 'home' && Views.guideBackOn ? Views.guideBackOn(liveId) : null;
82
84
  var alt = focusNode && document.documentElement.contains(focusNode) ? focusNode : null;
83
- Focus.setScope(screenEl(f.screen), alt || f.focus);
85
+ Focus.setScope(screenEl(f.screen), guide || alt || f.focus);
84
86
  }
85
87
 
86
88
  // After a screen is drawn, focus must be on it. A drawing path that doesn't put it there
@@ -118,8 +120,8 @@ var App = (function () {
118
120
  }
119
121
 
120
122
  // On a main page (Home's tabs, Discover, Search, Settings), BACK goes to the top menu first
121
- // (its current item; DOWN comes back), and from there does what it did: the page before,
122
- // or "Press Back again to exit" on Home. Also a way out if focus is ever lost on a page.
123
+ // (its current item; DOWN comes back), and from there to the page before, or (nothing
124
+ // before: Home) asks "Exit StremioK?". Also a way out if focus is ever lost on a page.
123
125
  var MAIN_SCREENS = { home: 1, discover: 1, search: 1, settings: 1 };
124
126
  function toTopMenu() {
125
127
  if (!MAIN_SCREENS[_current]) { return false; }
@@ -142,7 +144,7 @@ var App = (function () {
142
144
  if (_current === 'home' && Views.backInLocal && Views.backInLocal()) { return; } // (the Local tab: up a folder)
143
145
  if (toTopMenu()) { return; }
144
146
  var f = _stack.pop();
145
- if (!f) { tryExitApp(); return; }
147
+ if (!f) { askExit(); return; }
146
148
  if (f.screen !== _current) { abandon(_current); }
147
149
  showScreen(f.screen);
148
150
  if (f.detail) { Views.restoreDetail(f.detail); return; } // the previous title / person page
@@ -155,21 +157,21 @@ var App = (function () {
155
157
  }
156
158
  }
157
159
 
158
- // Require a deliberate double-Back from the home root to exit, so a single stray
159
- // Back (e.g. left over from closing the player) can never close the app by accident.
160
- function tryExitApp() {
161
- var now = Date.now();
162
- if (now - _exitArmedAt < 3000) {
163
- try {
164
- if (window.tizen && tizen.application) {
165
- U.sessionState('exit');
166
- tizen.application.getCurrentApplication().exit();
167
- return;
168
- }
169
- } catch (e) {}
170
- }
171
- _exitArmedAt = now;
172
- U.toast('Press Back again to exit', 3000);
160
+ // Asked, so a stray BACK (one left over from closing the player) never closes the app; focus
161
+ // starts on Exit, so BACK, OK leaves.
162
+ function askExit() { Views.confirm('Exit StremioK?', 'Exit', exitApp, { plain: true, focusYes: true }); }
163
+ // The TV's own exit where StremioK has it (the sideloaded app). A TizenBrew module's page
164
+ // doesn't get it: the window closed instead, and if it's still open, the TV's way said.
165
+ function exitApp() {
166
+ U.sessionState('exit');
167
+ try {
168
+ if (window.tizen && tizen.application) { tizen.application.getCurrentApplication().exit(); return; }
169
+ } catch (e) { U.log('app', 'Exit: the TV\'s exit failed (' + e.message + ')'); }
170
+ try { window.close(); } catch (e2) {}
171
+ setTimeout(function () {
172
+ U.log('app', 'Exit: still open after closing the window (' + (typeof YTPlay !== 'undefined' && YTPlay.installMode ? YTPlay.installMode() : '?') + ')');
173
+ U.toast('Hold BACK to close StremioK', 4000);
174
+ }, 500);
173
175
  }
174
176
 
175
177
  function onKey(action) {
package/js/focus.js CHANGED
@@ -42,7 +42,7 @@ var Focus = (function () {
42
42
  // Scrolls each overflowing box around the node so it's in view, with a margin. A box
43
43
  // marked __noScroll only clips (a page's text block): it never moves.
44
44
  function ensureVisible(node) {
45
- var p = node.parentNode;
45
+ var p = node.parentNode, z = (typeof U !== 'undefined' && U.zoom) ? U.zoom() : 1; // (rects are on screen, zoomed; scrolling isn't)
46
46
  while (p && p.nodeType === 1 && p !== document.body) {
47
47
  var canX = !p.__noScroll && p.scrollWidth > p.clientWidth + 2;
48
48
  var canY = !p.__noScroll && p.scrollHeight > p.clientHeight + 2;
@@ -51,13 +51,13 @@ var Focus = (function () {
51
51
  var nr = node.getBoundingClientRect();
52
52
  if (canX) {
53
53
  var mx = 90;
54
- if (nr.left < pr.left + mx) { p.scrollLeft -= (pr.left + mx - nr.left); }
55
- else if (nr.right > pr.right - mx) { p.scrollLeft += (nr.right - (pr.right - mx)); }
54
+ if (nr.left < pr.left + mx) { p.scrollLeft -= (pr.left + mx - nr.left) / z; }
55
+ else if (nr.right > pr.right - mx) { p.scrollLeft += (nr.right - (pr.right - mx)) / z; }
56
56
  }
57
57
  if (canY) {
58
58
  var my = 70;
59
- if (nr.top < pr.top + my) { p.scrollTop -= (pr.top + my - nr.top); }
60
- else if (nr.bottom > pr.bottom - my) { p.scrollTop += (nr.bottom - (pr.bottom - my)); }
59
+ if (nr.top < pr.top + my) { p.scrollTop -= (pr.top + my - nr.top) / z; }
60
+ else if (nr.bottom > pr.bottom - my) { p.scrollTop += (nr.bottom - (pr.bottom - my)) / z; }
61
61
  }
62
62
  }
63
63
  p = p.parentNode;
package/js/iptv.js CHANGED
@@ -467,7 +467,7 @@ var IPTV = (function () {
467
467
  var msg = (e && e.message) ? e.message : 'Could not fetch the playlist URL';
468
468
  // status 0 / network error on a URL that opens fine in a browser almost
469
469
  // always means the server sent no Access-Control-Allow-Origin header.
470
- if (/HTTP 0\b/.test(msg) || /Network error/i.test(msg)) {
470
+ if (/HTTP 0\b/.test(msg) || /Network error|^No answer/i.test(msg)) {
471
471
  msg += ' — the playlist server likely isn\'t sending an ' +
472
472
  'Access-Control-Allow-Origin header (CORS). Serve the .m3u with CORS enabled.';
473
473
  }
@@ -544,6 +544,19 @@ var IPTV = (function () {
544
544
  return d;
545
545
  });
546
546
  }
547
+ // Auto-refresh (Settings › Content › IPTV): every this many hours (0: Off), all playlists and
548
+ // their guides downloaded again, as Reload does (the Home ticker: see view-home's
549
+ // iptvRefreshTick). EVERY: the choices.
550
+ var EVERY_KEY = 'stremiok.iptv.every', EVERY = [0, 1, 3, 6, 12];
551
+ function refreshEvery() { var h = +U.store.get(EVERY_KEY, 0); return EVERY.indexOf(h) !== -1 ? h : 0; }
552
+ function setRefreshEvery(h) { h = +h; U.store.set(EVERY_KEY, EVERY.indexOf(h) !== -1 ? h : 0); }
553
+ // When the playlists switched on were last downloaded: the oldest of them (a saved copy's own
554
+ // date), 0 when one never was.
555
+ function readAt() {
556
+ var on = getPlaylists().filter(function (p) { return !p.off; }), t = Infinity;
557
+ on.forEach(function (p) { var st = _status[p.id]; t = Math.min(t, st && st.fetchedAt ? st.fetchedAt : 0); });
558
+ return on.length ? t : Infinity;
559
+ }
547
560
  function status(id) {
548
561
  if (id) { return _status[id] || { id: id, url: '', count: 0, groups: 0, error: '', title: '', fetchedAt: 0 }; }
549
562
  return _status;
@@ -649,7 +662,7 @@ var IPTV = (function () {
649
662
  removePlaylist: removePlaylist, renamePlaylist: renamePlaylist, setPlaylistOff: setPlaylistOff,
650
663
  movePlaylist: movePlaylist, setPlaylistGuide: setPlaylistGuide, label: labelOf,
651
664
  xtreamParts: xtreamParts, xtreamUrl: xtreamUrl, xtreamCheck: xtreamCheck, addXtream: addXtream, xtreamFormat: xtreamFormat, setXtreamFormat: setXtreamFormat,
652
- isFavorite: isFavorite, setFavorite: setFavorite,
665
+ isFavorite: isFavorite, setFavorite: setFavorite, refreshEvery: refreshEvery, setRefreshEvery: setRefreshEvery, readAt: readAt,
653
666
  channels: function () { return _cache ? _cache.channels : []; }, channel: function (id) { return _cache ? _cache.byId[id] || null : null; }, isEnabled: isEnabled, reset: reset, status: status,
654
667
  load: load, reload: reload, catalog: catalog, meta: meta, streams: streams, synthAddon: synthAddon, isReady: isReady,
655
668
  onProgress: onProgress
package/js/player-core.js CHANGED
@@ -178,12 +178,20 @@ var PlayerParts = {};
178
178
  } else {
179
179
  all[k] = { id: S.ctx.id, type: S.ctx.type, name: S.ctx.name, poster: S.ctx.poster, background: S.ctx.background, parent: S.ctx.parent,
180
180
  position: Math.floor(S.cur), duration: Math.floor(S.dur), updated: Date.now(),
181
- url: S.ctx.yt || S.ctx.url, headers: S.ctx._headers, // the exact stream, to resume it directly (YouTube: its page)
181
+ url: S.ctx.yt || savedUrl(S.ctx), headers: S.ctx._headers, // the exact stream, to resume it directly (YouTube: its page)
182
+ torrent: torrentOf(S.ctx.stream), // (through a streaming server: its link made again from the one set then)
182
183
  fileHints: (P.fileHints && P.fileHints(S.ctx.subExtra)) || undefined, // (its file, for subtitles made for it)
183
184
  added: added || undefined };
184
185
  }
185
186
  writeProgress(all);
186
187
  }
188
+ // What's saved to resume: a converted playback's own link (not the server's conversion), and a
189
+ // torrent's infoHash, file and trackers (its link depends on the server set when it resumes).
190
+ function savedUrl(ctx) { return (ctx._convertedFrom && /\/hlsv2\//.test(ctx.url || '')) ? ctx._convertedFrom : ctx.url; }
191
+ function torrentOf(st) {
192
+ if (!st || !st._torrent) { return undefined; }
193
+ return { infoHash: st.infoHash, fileIdx: st.fileIdx, sources: Array.isArray(st.sources) ? st.sources.slice(0, 20) : undefined };
194
+ }
187
195
 
188
196
  // ---------- OSD ----------
189
197
  // Subtitles move up above the progress bar while it's on screen (above the UP bar too,
@@ -400,7 +408,7 @@ var PlayerParts = {};
400
408
  ctx.localSubs = s.localSubs; ctx.localFile = s.localFile; ctx.subExtra = s.subExtra; ctx.sourceSubs = s.sourceSubs;
401
409
  ctx.formatInfo = (s.stream && typeof Stremio !== 'undefined' && Stremio.streamInfo) ? Stremio.streamInfo(s.stream) : undefined;
402
410
  ctx.stream = s.stream; // (the addon's source: ⬇ Download, player-controls)
403
- var learn = (old.learnFor && s.stream) ? old.learnFor(s.stream) : {};
411
+ var learn = (old.learnFor && s.stream) ? old.learnFor(s.stream, { converted: !!s.converted }) : {}; // (converted: not what the TV plays itself)
404
412
  ctx.onStarted = learn.onStarted; ctx.onFailed = learn.onFailed;
405
413
  ctx.resumeSec = keep ? old.resumeSec : (S.isLive ? 0 : S.cur);
406
414
  if (keep) { ctx.fromStart = old.fromStart; }
@@ -414,6 +422,8 @@ var PlayerParts = {};
414
422
  // Replacing a YouTube stream that's playing (CH+/CH− to the next channel): the TV's
415
423
  // player can freeze on an open straight after closing it, so settle first (startAV).
416
424
  var settle = !!(S.active && S.ctx && S.ctx.yt);
425
+ if (S.ctx !== ctx) { stopLiveLook(); } // (another channel: the look at the one before stopped)
426
+ stopFileLook();
417
427
  S.ctx = ctx; _onExit = onExit;
418
428
  // (private viewing, a link played as it is, or one started from a private playback —
419
429
  // a video followed from it, another source: nothing recorded, see player-store)
@@ -445,7 +455,27 @@ var PlayerParts = {};
445
455
  renderOSD(); // show the NEW title immediately (not the previous item's)
446
456
  U.spinner(true, ctx.switching ? 'Switching source…' : ctx.initialQualityHeight ? 'Opening stream at ' + ctx.initialQualityHeight + 'p…' : 'Opening stream…');
447
457
 
448
- if (useAV) { startLive(ctx, settle); } else { startHTML5(ctx.url); }
458
+ // (a TorrServer torrent that doesn't say which of its files: its file list asked for first)
459
+ var go = function () { if (useAV) { startLive(ctx, settle); } else { startHTML5(ctx.url); } };
460
+ if (ctx.stream && ctx.stream._pickFile && typeof Stremio !== 'undefined' && Stremio.torrentFile) {
461
+ // (the source before stopped first: its callbacks are about it, not this one)
462
+ ++_playToken; _openedAt = 0;
463
+ if (useAV) { try { avplay.stop(); } catch (e0) {} try { avplay.close(); } catch (e1) {} }
464
+ else if (S.videoEl) { try { S.videoEl.pause(); S.videoEl.removeAttribute('src'); } catch (e2) {} }
465
+ U.spinner(true, 'Asking your streaming server for the torrent’s files…');
466
+ var ep = /:(\d+):(\d+)$/.exec(ctx.id || ''), look = _fileLook = U.cancelToken();
467
+ Stremio.torrentFile(ctx.stream, ep ? +ep[1] : 0, ep ? +ep[2] : 0, look).then(function (url) {
468
+ if (look.cancelled || S.ctx !== ctx || !S.active) { return; }
469
+ _fileLook = null;
470
+ if (url) { ctx.url = url; go(); return; }
471
+ var why = 'The torrent has no file for S' + ep[1] + 'E' + ep[2]; // (another episode isn't played in its place)
472
+ U.log('player', why);
473
+ U.spinner(false);
474
+ if (nextSourceOnFail({ text: why })) { return; }
475
+ U.toast(why + '. Try another source.', 6000);
476
+ exitLater();
477
+ });
478
+ } else { go(); }
449
479
 
450
480
  // A YouTube video (not a live channel): its audio languages and its links (end screen,
451
481
  // cards) from the service, for the menu and the overlays (player-quality / -controls).
@@ -660,7 +690,7 @@ var PlayerParts = {};
660
690
  });
661
691
  try { avplay.setStreamingProperty('ADAPTIVE_INFO', adaptiveInfo()); } catch (e0) {}
662
692
  _openedAt = Date.now();
663
- setTimeout(startCheck, AV_START_MS + 500);
693
+ setTimeout(startCheck, startMs() + 500);
664
694
  avplay.prepareAsync(function () {
665
695
  if (!S.active || token !== _playToken) { return; }
666
696
  U.spinner(false);
@@ -1022,6 +1052,21 @@ var PlayerParts = {};
1022
1052
  U.spinner(true, msg); U.toast(msg, notice ? 7000 : 4000);
1023
1053
  return true;
1024
1054
  }
1055
+ // With Stremio's streaming server (Settings › Content › Streaming server, Convert on): a source
1056
+ // the TV refused is tried once more, converted by the server, before the next source. Once a
1057
+ // playback (the next sources aren't). Not a source that needs its own request headers.
1058
+ function convertOnFail() {
1059
+ var c = S.ctx;
1060
+ if (!c || !S.active || S.playing || S.isLive || c.trailer || c.yt || c.localFile || !c.stream || c._converted || c._headers) { return false; }
1061
+ var u = (typeof Stremio !== 'undefined' && Stremio.convertUrl) ? Stremio.convertUrl(c.url) : null;
1062
+ if (!u) { return false; }
1063
+ c._converted = true; c._convertedFrom = c.url; // (kept by what replaces it: Continue Watching saves its own link)
1064
+ var msg = 'Didn’t start: converting it on your streaming server…';
1065
+ U.log('player', 'Source didn’t start: converting it on the streaming server');
1066
+ replaceSource({ url: u, stream: c.stream, subExtra: c.subExtra, sourceSubs: c.sourceSubs, localSubs: c.localSubs, converted: true }, true);
1067
+ U.spinner(true, msg); U.toast(msg, 5000);
1068
+ return true;
1069
+ }
1025
1070
  // A start that failed: is the link a debrid addon's notice video (Torrentio's failed_access,
1026
1071
  // limits_exceeded…: noticeOf)? Then that's said, and for the account's ones the next source
1027
1072
  // skips the addon's links through that service; then the next source, or what's to say.
@@ -1033,6 +1078,7 @@ var PlayerParts = {};
1033
1078
  U.log('player', 'The source sent a notice: ' + notice.kind + (notice.service ? ' (' + notice.service + ')' : ''));
1034
1079
  if (notice.account && notice.tag) { c._avoid = (c._avoid || []).concat([{ addon: st._addon, tag: notice.tag }]); }
1035
1080
  }
1081
+ if (!notice && convertOnFail()) { return; }
1036
1082
  if (nextSourceOnFail(notice)) { return; }
1037
1083
  U.toast(notice ? notice.text : why, notice ? 8000 : undefined);
1038
1084
  exitLater();
@@ -1072,15 +1118,21 @@ var PlayerParts = {};
1072
1118
  return n;
1073
1119
  }
1074
1120
  // Not playing after AV_START_MS (no answer from the server, or opened and nothing shown): the
1075
- // next source when there's one; else it stops and says so (not while you've paused it).
1121
+ // next source when there's one; else it stops and says so (not while you've paused it). A
1122
+ // torrent through a streaming server has longer (Settings › Content › Streaming server: finding
1123
+ // people sharing it takes a while).
1124
+ function startMs() {
1125
+ var st = S.ctx && S.ctx.stream;
1126
+ return (st && st._torrent && typeof Stremio !== 'undefined' && Stremio.server) ? Stremio.server().wait * 1000 : AV_START_MS;
1127
+ }
1076
1128
  function startCheck() {
1077
- if (!S.active || S.playing || !useAV || !_openedAt || Date.now() - _openedAt < AV_START_MS) { return; }
1129
+ if (!S.active || S.playing || !useAV || !_openedAt || Date.now() - _openedAt < startMs()) { return; }
1078
1130
  if (_reco.tries) { liveDropped('Couldn’t reconnect'); return; } // (a reconnect's try: the next one)
1079
1131
  if (nextSourceOnFail()) { return; }
1080
1132
  if (S.playStarted && isPaused()) { return; }
1081
- U.log('player', 'Didn’t start: nothing played in ' + (AV_START_MS / 1000) + ' s (' + where(S.ctx && S.ctx.url) + ')');
1133
+ U.log('player', 'Didn’t start: nothing played in ' + (startMs() / 1000) + ' s (' + where(S.ctx && S.ctx.url) + ')');
1082
1134
  U.spinner(false);
1083
- U.toast('Didn’t start: nothing played in ' + (AV_START_MS / 1000) + ' seconds. Try another source.', 6000);
1135
+ U.toast('Didn’t start: nothing played in ' + (startMs() / 1000) + ' seconds. Try another source.', 6000);
1084
1136
  exitLater();
1085
1137
  }
1086
1138
  P.startCheck = startCheck;
@@ -1147,6 +1199,12 @@ var PlayerParts = {};
1147
1199
  startAV(ctx.url, settle);
1148
1200
  }, function () { if (S.ctx === ctx && S.active) { startAV(ctx.url, settle); } });
1149
1201
  }
1202
+ // The look under way (_liveLook, a cancel token): stopped when another channel starts or the
1203
+ // player closes (stopLiveLook), the service's downloads for it with it.
1204
+ var _fileLook = null; // (TorrServer's file list asked for: see play)
1205
+ function stopFileLook() { if (_fileLook) { _fileLook.cancel(); _fileLook = null; } }
1206
+ var _liveLook = null;
1207
+ function stopLiveLook() { if (_liveLook) { _liveLook.cancel(); _liveLook = null; } }
1150
1208
  function liveCheck() {
1151
1209
  var ctx = S.ctx;
1152
1210
  if (!ctx || ctx._liveChecked || !liveFixable(ctx)) { return; }
@@ -1154,7 +1212,10 @@ var PlayerParts = {};
1154
1212
  var known = liveKnown(ctx.url);
1155
1213
  if (known && ccOf(known)) { return; }
1156
1214
  var url = ctx.url;
1157
- YTPlay.liveOpen(url, ctx._headers, false).then(function (r) {
1215
+ stopLiveLook();
1216
+ var look = _liveLook = U.cancelToken();
1217
+ YTPlay.liveOpen(url, ctx._headers, false, null, look).then(function (r) {
1218
+ if (_liveLook === look) { _liveLook = null; }
1158
1219
  if (!r || !r.sure) { return; } // (couldn't tell: asked again next time)
1159
1220
  var cc = (r.ccSure || r.cc === undefined) ? (ccOf(r) || []) : null, fix = known ? !!known.fix : !!r.fix; // (captions with no text yet: asked again next time; the sound's answer: as first found)
1160
1221
  liveKeep(url, fix, cc);
@@ -1278,6 +1339,7 @@ var PlayerParts = {};
1278
1339
 
1279
1340
  function exit() {
1280
1341
  if (!S.active) { return; }
1342
+ stopLiveLook(); stopFileLook();
1281
1343
  if (U.crumb) { U.crumb('player closed'); }
1282
1344
  if (P.sleepClear) { P.sleepClear(); } // (the sleep timer: this playback's)
1283
1345
  saveProgress();