stremiok 2.2.1 → 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,28 @@
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
+
6
28
  ## 2.2.1 — 2026-10-24
7
29
  - **Every language OpenSubtitles uses** (94, Indonesian among them, #1) for subtitles and audio:
8
30
  to pick as your default, second choice or second subtitle, listed by name, and each track
package/README.md CHANGED
@@ -35,8 +35,9 @@ video through Samsung's AVPlay API.
35
35
  - **YouTube:** live channels, trailers, and your own YouTube channels, playlists and searches,
36
36
  with SponsorBlock, DeArrow, chapters and captions.
37
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.
38
39
  - **Made for the remote:** D-pad navigation throughout; hold OK where an item has more to
39
- offer (*Hold OK for more* says so).
40
+ offer (*Hold OK for more* says so); zoom up to 140% for reading from further away.
40
41
 
41
42
  ## Install
42
43
 
@@ -52,7 +53,8 @@ Three ways, all covered step by step in the **[installation guide](docs/INSTALL.
52
53
 
53
54
  1. **Addons:** add the addons you use, by link, or bring them in from your Stremio account,
54
55
  AIOManager or a configuration file. A stream addon that gives HTTP(S) links (for example
55
- 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.
56
58
  2. **API keys**, optional: your own TMDB, OMDb and MDBList keys add ratings, *Cast & more*,
57
59
  *where to watch* and episode trailers.
58
60
  3. **Trakt**, optional: connect your account to share watched marks and Continue Watching.
@@ -67,8 +69,8 @@ Three ways, all covered step by step in the **[installation guide](docs/INSTALL.
67
69
  This app hosts **no** media. It is a client for the open Stremio addon protocol: catalogs and
68
70
  metadata come from the addons you install (**Cinemeta**, the official one, by default), and
69
71
  playable sources come from whatever **stream addons you add yourself** in Settings. Streams
70
- that only expose a torrent `infoHash` (no HTTP URL) are listed but not playable: AVPlay needs
71
- 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.
72
74
  > **Note on WatchHub:** StremioK doesn't install it. Its answers say it's only for clients
73
75
  > Stremio authorizes in writing, and StremioK isn't one; "where to watch" comes from TMDB
74
76
  > instead (with a TMDB key). You can still add it yourself as a third-party addon (Settings
@@ -85,8 +87,7 @@ your progress, settings and copies that let it start faster (see
85
87
  ## Known limitations
86
88
 
87
89
  - AVPlay behavior varies by TV generation, codec and HLS provider.
88
- - No torrent playback: sources need a direct HTTP(S)/HLS/DASH URL (e.g. through a debrid
89
- addon).
90
+ - Torrents play only through a streaming server you run on your network.
90
91
  - Home rows show a catalog's first page (up to 30 titles); Discover pages through the rest.
91
92
  - No StremioK account and no profiles: settings, addons and progress stay on the TV. The
92
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.1" 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.1">
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.1"></script>
57
- <script src="js/util.js?v=2.2.1"></script>
58
- <script src="js/iptv.js?v=2.2.1"></script>
59
- <script src="js/epg.js?v=2.2.1"></script>
60
- <script src="js/youtube.js?v=2.2.1"></script>
61
- <script src="js/stremio.js?v=2.2.1"></script>
62
- <script src="js/trakt.js?v=2.2.1"></script>
63
- <script src="js/keys.js?v=2.2.1"></script>
64
- <script src="js/focus.js?v=2.2.1"></script>
65
- <script src="js/tvapps.js?v=2.2.1"></script>
66
- <script src="js/player-core.js?v=2.2.1"></script>
67
- <script src="js/player-store.js?v=2.2.1"></script>
68
- <script src="js/player-lang.js?v=2.2.1"></script>
69
- <script src="js/player-quality.js?v=2.2.1"></script>
70
- <script src="js/player-subs.js?v=2.2.1"></script>
71
- <script src="js/player-audio.js?v=2.2.1"></script>
72
- <script src="js/player-controls.js?v=2.2.1"></script>
73
- <script src="js/player.js?v=2.2.1"></script>
74
- <script src="js/ytplay.js?v=2.2.1"></script>
75
- <script src="js/local.js?v=2.2.1"></script>
76
- <script src="js/downloads.js?v=2.2.1"></script>
77
- <script src="js/stremio-account.js?v=2.2.1"></script>
78
- <script src="js/view-core.js?v=2.2.1"></script>
79
- <script src="js/view-home.js?v=2.2.1"></script>
80
- <script src="js/view-guide.js?v=2.2.1"></script>
81
- <script src="js/view-detail.js?v=2.2.1"></script>
82
- <script src="js/view-search.js?v=2.2.1"></script>
83
- <script src="js/view-settings.js?v=2.2.1"></script>
84
- <script src="js/view-discover.js?v=2.2.1"></script>
85
- <script src="js/views.js?v=2.2.1"></script>
86
- <script src="js/app.js?v=2.2.1"></script>
87
- <script src="js/hubpreview.js?v=2.2.1"></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
 
@@ -121,8 +120,8 @@ var App = (function () {
121
120
  }
122
121
 
123
122
  // On a main page (Home's tabs, Discover, Search, Settings), BACK goes to the top menu first
124
- // (its current item; DOWN comes back), and from there does what it did: the page before,
125
- // 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.
126
125
  var MAIN_SCREENS = { home: 1, discover: 1, search: 1, settings: 1 };
127
126
  function toTopMenu() {
128
127
  if (!MAIN_SCREENS[_current]) { return false; }
@@ -145,7 +144,7 @@ var App = (function () {
145
144
  if (_current === 'home' && Views.backInLocal && Views.backInLocal()) { return; } // (the Local tab: up a folder)
146
145
  if (toTopMenu()) { return; }
147
146
  var f = _stack.pop();
148
- if (!f) { tryExitApp(); return; }
147
+ if (!f) { askExit(); return; }
149
148
  if (f.screen !== _current) { abandon(_current); }
150
149
  showScreen(f.screen);
151
150
  if (f.detail) { Views.restoreDetail(f.detail); return; } // the previous title / person page
@@ -158,21 +157,21 @@ var App = (function () {
158
157
  }
159
158
  }
160
159
 
161
- // Require a deliberate double-Back from the home root to exit, so a single stray
162
- // Back (e.g. left over from closing the player) can never close the app by accident.
163
- function tryExitApp() {
164
- var now = Date.now();
165
- if (now - _exitArmedAt < 3000) {
166
- try {
167
- if (window.tizen && tizen.application) {
168
- U.sessionState('exit');
169
- tizen.application.getCurrentApplication().exit();
170
- return;
171
- }
172
- } catch (e) {}
173
- }
174
- _exitArmedAt = now;
175
- 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);
176
175
  }
177
176
 
178
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();
@@ -130,14 +130,14 @@
130
130
  var SIZE_PX = { small: 28, medium: 36, large: 46, xlarge: 58 };
131
131
  function styleSpan(span) {
132
132
  var cfg = getSubCfg();
133
- span.style.fontSize = (SIZE_PX[cfg.size] || 36) + 'px';
133
+ span.style.fontSize = (SIZE_PX[cfg.size] || 36) / U.zoom() + 'px'; // (their own size, whatever the zoom)
134
134
  span.style.fontFamily = subFontFamily(cfg.font);
135
135
  span.style.color = pick3(SUB_COLORS, cfg.color)[2];
136
136
  span.style.background = pick3(SUB_BGS, cfg.bg)[2];
137
137
  span.style.textShadow = pick3(SUB_EDGES, cfg.edge)[2];
138
138
  if (span.parentNode) { span.parentNode.style.bottom = Math.max(subsPosPx(), (P.S && P.S.subsLift) || 0) + 'px'; } // (raised over the progress bar: core's liftSubs)
139
139
  }
140
- function subsPosPx() { return pick3(SUB_POSITIONS, getSubCfg().pos)[2]; }
140
+ function subsPosPx() { return pick3(SUB_POSITIONS, getSubCfg().pos)[2] / U.zoom(); }
141
141
 
142
142
  // ---------- progress (Continue Watching) ----------
143
143
  function progressAll() { return U.store.get(PROGRESS_KEY, {}); }