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/js/view-detail.js CHANGED
@@ -203,6 +203,10 @@
203
203
  if (local) { resumeSources(startPlayback(local, p.id, p.name, how), p.id); return; }
204
204
  var st = { url: p.url, name: p.name, _headers: U.sanitizeHeaders(p.headers) || undefined,
205
205
  behaviorHints: (p.fileHints && typeof p.fileHints === 'object') ? p.fileHints : undefined }; // (its file: for subtitles made for it)
206
+ if (p.torrent && typeof p.torrent.infoHash === 'string') { // (a torrent: its link from the streaming server set now)
207
+ st = Stremio.torrentSource({ infoHash: p.torrent.infoHash, fileIdx: p.torrent.fileIdx, sources: p.torrent.sources, name: p.name, behaviorHints: st.behaviorHints });
208
+ if (!st.url) { U.toast('This one plays through a streaming server: Settings › Content › Streaming server', 6000); return; }
209
+ }
206
210
  if (/^https:\/\/www\.youtube\.com\/watch\?v=[A-Za-z0-9_-]{11}$/.test(p.url || '')) { st._yt = true; st.ytPage = p.url; } // (a YouTube video: through the service again)
207
211
  resumeSources(startPlayback(st, p.id, p.name, how), p.id);
208
212
  }
@@ -321,7 +325,7 @@
321
325
  if (!panel || (' ' + body.className + ' ').indexOf(' series-body ') === -1) { return; }
322
326
  var sr = s.getBoundingClientRect(), br = body.getBoundingClientRect();
323
327
  if (!(br.height > 0)) { return; }
324
- var top = Math.round(br.bottom - sr.top) + PANEL_GAP;
328
+ var top = Math.round((br.bottom - sr.top) / U.zoom()) + PANEL_GAP; // (measured on screen, zoomed)
325
329
  panel.style.top = Math.max(PANEL_MIN, Math.min(PANEL_MAX, top)) + 'px';
326
330
  }
327
331
 
@@ -724,7 +728,7 @@
724
728
  function fileStreams(v) {
725
729
  return v.streams.map(function (st) {
726
730
  var s = Object.assign({ name: fileLabel(v) }, st);
727
- return Stremio.youTubeSource ? Stremio.youTubeSource(s) : s;
731
+ return Stremio.torrentSource(Stremio.youTubeSource(s));
728
732
  });
729
733
  }
730
734
 
@@ -1395,6 +1399,8 @@
1395
1399
  uncached: function (a, b) { return (a.info.cache === -1 ? 1 : 0) - (b.info.cache === -1 ? 1 : 0); },
1396
1400
  kind: function (a, b) { return (KIND_SCORE[b.info.kind] || 0) - (KIND_SCORE[a.info.kind] || 0); },
1397
1401
  hdr: function (a, b) { return (b.info.hdr ? 1 : 0) - (a.info.hdr ? 1 : 0); },
1402
+ // (only two sources that both state theirs: the others keep their order)
1403
+ seeds: function (a, b) { return (a.info.seeds >= 0 && b.info.seeds >= 0) ? b.info.seeds - a.info.seeds : 0; },
1398
1404
  addon: function (a, b) { return (a.st._addonRank || 0) - (b.st._addonRank || 0); },
1399
1405
  // each source's place in its addon's own list (an addon's first ties with another's
1400
1406
  // first): a plain key, so it orders consistently wherever it sits among the criteria
@@ -1464,7 +1470,13 @@
1464
1470
  // again), as the player takes a source, or null. total: how many it could try in all.
1465
1471
  // avoid: [{ addon, tag }] — an addon's links through a debrid service that sent an account's
1466
1472
  // notice (access refused, a limit: the player's noticeOf): they'd say the same.
1473
+ // Torrent sources' links from the streaming server set now (Settings may have changed it since
1474
+ // they came: none when it's off).
1475
+ function serverNow(list) {
1476
+ (list || []).forEach(function (st) { if (st && st.infoHash) { Stremio.torrentSource(st); } });
1477
+ }
1467
1478
  function nextSourceFor(tried, avoid) {
1479
+ serverNow(_d && _d.streams);
1468
1480
  var rows = (_d && _d.streams) ? filterRows(rankRowsFor(_d.streams)) : [], seen = {}, all = []; // (within the filter: Addon ▾ / Resolution ▾)
1469
1481
  rows.forEach(function (r) {
1470
1482
  var st = r.st, id = Stremio.sourceId(st), dt = Stremio.debridOf ? Stremio.debridOf(st) : null;
@@ -1847,6 +1859,7 @@
1847
1859
  // before, 4K, 1080p, …) starts with its heading.
1848
1860
  var heading = streamsHeading();
1849
1861
  if (heading) { listScroll.appendChild(U.el('div', 'stream-for', heading)); }
1862
+ serverNow(_d.streams); // (a page kept from before: its torrents follow the streaming server set now)
1850
1863
  var list = _d.streams, keepAt = -1, rows = rankRowsFor(list), lastGroup = null;
1851
1864
  // Direct links only (Settings › Source ranking): the sources the TV can't play left out
1852
1865
  // (torrents, links it can't open); where-to-watch apps stay (they open)
@@ -1915,10 +1928,11 @@
1915
1928
  if (row && row.sized) { lines.push('📏 Outside your size limits: ' + row.sized + ' (Settings › Playback › Source ranking)'); }
1916
1929
  if (row && row.blocked) { lines.push('⚠ ' + row.blocked + ' isn’t allowed (Settings › Playback › Formats)'); }
1917
1930
  if (row && row.bad.length) { lines.push('⚠ Marked “Not working” on this TV: ' + row.bad.join(', ') + ' (Settings › Playback › Formats)'); }
1931
+ if (stream._torrent) { lines.push('🧲 A torrent, through your streaming server (Settings › Content › Streaming server)'); }
1918
1932
  if (opens) {
1919
1933
  lines.push('▶ OK opens ' + (opens.data ? 'it in ' : '') + opens.app.name);
1920
1934
  } else if (!playable) {
1921
- if (stream.infoHash) { lines.push('— torrent, needs a debrid/torrent client'); }
1935
+ if (stream.infoHash) { lines.push('— torrent: plays through a streaming server (Settings › Content › Streaming server)'); }
1922
1936
  else if (stream.externalUrl) {
1923
1937
  lines.push('— another app' + ((typeof TVApps !== 'undefined' && TVApps.available())
1924
1938
  ? ': add it in Settings › Content › Streaming apps to open it from here' : ' (not playable here)'));
@@ -1936,7 +1950,7 @@
1936
1950
  }
1937
1951
  node.__onselect = function () {
1938
1952
  if (opens) { openWhere(opens); return; }
1939
- if (!playable) { U.toast('This source can’t play directly on the TV.'); return; }
1953
+ if (!playable) { U.toast(stream.infoHash ? 'Torrents play through a streaming server: Settings › Content › Streaming server' : 'This source can’t play directly on the TV.', stream.infoHash ? 6000 : undefined); return; }
1940
1954
  startPlayback(stream, _d.streamId, _d.streamLabel || _d.meta.name, howHere());
1941
1955
  };
1942
1956
  return node;
@@ -1985,6 +1999,7 @@
1985
1999
  // The player's "Select source" list: each source with the request headers it needs
1986
2000
  // (IPTV), so switching to it sends its own (or none).
1987
2001
  function playableSources(list) {
2002
+ serverNow(list || _d.streams);
1988
2003
  return (list || _d.streams || []).filter(Stremio.isPlayable).map(function (st) {
1989
2004
  var sz = Stremio.streamSize(st);
1990
2005
  var desc = Stremio.streamDescription(st).replace(/\n/g, ' ');
@@ -2001,6 +2016,10 @@
2001
2016
 
2002
2017
  // how (optional): { fromStart } — Play from start (Continue Watching): from 0:00.
2003
2018
  function startPlayback(stream, videoId, label, how) {
2019
+ if (stream && stream.infoHash) { // (a torrent: its link from the streaming server set now)
2020
+ Stremio.torrentSource(stream);
2021
+ if (!stream.url) { U.toast('Torrents play through a streaming server: Settings › Content › Streaming server', 6000); return null; }
2022
+ }
2004
2023
  // VOD changes progress/watched state. Live playback does not, and keeping Home
2005
2024
  // untouched lets us restore the exact originating card and scroll position.
2006
2025
  if (!(_d && _d.exitToBack)) { markHomeDirty(); }
@@ -2015,8 +2034,10 @@
2015
2034
  var titleId = _d && _d.id, series = _d && _d.type === 'series', vtype = _d && _d.type;
2016
2035
  var remembers = !!(_d && !_d.live);
2017
2036
  // (also for another source picked in the player: what's learned is that one's)
2018
- function learnFor(st) {
2019
- var info = Stremio.streamInfo(st), fkeys = Stremio.formatKeys(info);
2037
+ // how.converted: played converted by the streaming server, so its formats aren't learned
2038
+ // (they didn't play on the TV); Played before and the kept source still are.
2039
+ function learnFor(st, how) {
2040
+ var info = Stremio.streamInfo(st), fkeys = (how && how.converted) ? [] : Stremio.formatKeys(info);
2020
2041
  return {
2021
2042
  // (private viewing: nothing about this playback remembered or forgotten — its own
2022
2043
  // flag, set when it started, see player-store; what plays on this TV is still learned)
@@ -2039,7 +2060,7 @@
2039
2060
  if (_d) { _d.direct = false; _d.directList = false; _d.directFailed = false; } // (only this once: played again from the list, back to the list)
2040
2061
  var onExit = function (how) {
2041
2062
  // (after CH+/CH−, back on the card of the channel you ended on)
2042
- if (exitToBack) { App.returnFromLive(_d.channels ? _d.channels.list[_d.channels.idx].card : null); return; }
2063
+ if (exitToBack) { var ch = _d.channels ? _d.channels.list[_d.channels.idx] : null; App.returnFromLive(ch ? ch.card : null, ch ? ch.meta.id : null); return; }
2043
2064
  // played straight away (a single YouTube video): back where it was opened from
2044
2065
  if (direct && !page.directFailed && _d === page) { App.back(); return; } // (Home redrawn if Continue Watching changed)
2045
2066
  // a channel's video played straight away: back on the channel's videos, on it
@@ -2199,13 +2220,20 @@
2199
2220
  }
2200
2221
 
2201
2222
  // Load a specific episode's sources and auto-play the first playable one.
2223
+ // The next / previous episode (from the player): its sources asked for, the best like the
2224
+ // last one played. An answer once you've left the page, or asked for another episode since
2225
+ // (_epGen), does nothing: no playback, no page drawn, no spinner change, no note.
2226
+ var _epGen = 0;
2202
2227
  function playEpisode(v) {
2228
+ var still = pageNow(), gen = ++_epGen;
2229
+ function current() { return still() && gen === _epGen; }
2203
2230
  _d.mode = 'streams';
2204
2231
  _d.streamId = v.id;
2205
2232
  _d.season = (v.season === undefined ? 1 : v.season);
2206
2233
  _d.streamLabel = epName(v);
2207
2234
  U.spinner(true);
2208
2235
  Stremio.getStreams(addonType(), v.id).then(function (list) {
2236
+ if (!current()) { return; }
2209
2237
  _d.streams = sortStreams(Stremio.dedupeStreams(list));
2210
2238
  U.spinner(false);
2211
2239
  var pick = pickLikeLast(_d.id, _d.streams);
@@ -2215,7 +2243,7 @@
2215
2243
  return;
2216
2244
  }
2217
2245
  startPlayback(pick, v.id, _d.streamLabel);
2218
- }, function () { U.spinner(false); U.toast('Failed to load that episode'); });
2246
+ }, function () { if (!current()) { return; } U.spinner(false); U.toast('Failed to load that episode'); });
2219
2247
  }
2220
2248
 
2221
2249
  // ----- binge: the next episode plays a source like the one you've been watching -----
@@ -334,7 +334,7 @@
334
334
  }
335
335
  function rowHeight() {
336
336
  var els = _dv.cardEls, c = cols();
337
- return (els.length > c) ? (els[c].getBoundingClientRect().top - els[0].getBoundingClientRect().top) : 0;
337
+ return (els.length > c) ? (els[c].getBoundingClientRect().top - els[0].getBoundingClientRect().top) / U.zoom() : 0; // (in the grid's own pixels)
338
338
  }
339
339
  function drawGrid() {
340
340
  var grid = gridEl();
package/js/view-guide.js CHANGED
@@ -7,10 +7,13 @@
7
7
  an edge, never back before the current half hour for one still on), ↑ / ↓ the channel above /
8
8
  below, at the same time. Home's details strip describes the focused program; OK on what's on
9
9
  now plays the channel, on another says what it is. A guide-only channel's (tvg-type="guide":
10
- watched elsewhere) any program: the film looked up in your addons (findInAddons). */
10
+ watched elsewhere) any program: the film looked up in your addons (findInAddons). Hold OK on
11
+ a program: Watch now, the channel's favorites, Find in your addons (hold). */
11
12
  (function (V) {
12
13
  'use strict';
13
14
  function scr() { return V.scr.apply(null, arguments); } // core
15
+ function pickList() { return V.pickList.apply(null, arguments); } // core
16
+ function refreshAddonRows() { return V.refreshAddonRows.apply(null, arguments); } // home
14
17
 
15
18
  var SPAN = 150 * 60, STEP = 30 * 60, ROWS = 10, AHEAD = 10;
16
19
  var _g = null; // the grid on screen: { el, start, chans, lists: { id: programs }, asked: { id: 1 }, top, nodes: [row: cells], box }
@@ -165,6 +168,7 @@
165
168
  wait.__t = want && want.id === c.id && typeof want.t === 'number' ? want.t : Math.max(now, g.start);
166
169
  wait.__meta = { name: c.name, releaseInfo: 'Reading its TV guide…' };
167
170
  hooks(wait, -1);
171
+ wait.__onlongpress = function () { hold(g, r, null); };
168
172
  track.appendChild(wait);
169
173
  nodes.push(wait);
170
174
  if (want && want.id === c.id) { found = wait; }
@@ -177,6 +181,7 @@
177
181
  n.__meta = { name: p.title, releaseInfo: c.name + ' · ' + EPG.dayTime(p.start) + '–' + EPG.hhmm(p.stop) + (c.guideOnly ? ' · OK: find it in your addons' : ''), description: p.desc, picture: p.picture };
178
182
  n.__p = p; n.__i = i;
179
183
  n.__onselect = function () { pick(g, r, p); };
184
+ n.__onlongpress = function () { hold(g, r, p); };
180
185
  hooks(n, i);
181
186
  track.appendChild(n);
182
187
  nodes.push(n);
@@ -301,6 +306,23 @@
301
306
  (p.stop <= now ? ' (already shown)' : ''), 8000);
302
307
  }
303
308
 
309
+ // Hold OK on a program: ▶ Watch now (what's on now, a channel that plays), ★ Add to / Remove
310
+ // from favorites (its channel), Find in your addons (that program, as a guide-only channel's
311
+ // OK). p: null for a channel whose guide hasn't come (its favorites only).
312
+ function hold(g, r, p) {
313
+ var c = g.chans[r], now = Date.now() / 1000, fav = IPTV.isFavorite(c.id), items = [];
314
+ if (p && !c.guideOnly && p.start <= now && now < p.stop) { items.push(['play', '▶ Watch now']); }
315
+ items.push(['fav', fav ? '★ Remove from favorites' : '★ Add to favorites']);
316
+ if (p && p.title) { items.push(['find', '🔍 Find in your addons']); }
317
+ pickList(c.name + (p && p.title ? ' · ' + p.title : ''), items, items[0][0], function (v) {
318
+ if (v === 'play') { pick(g, r, p); return; }
319
+ if (v === 'find') { findInAddons(p); return; }
320
+ IPTV.setFavorite(c.id, !fav);
321
+ U.toast(fav ? 'Removed from favorites' : 'Added to favorites: ★ Favorites, first on the IPTV and Guide tabs', 3000);
322
+ refreshAddonRows('local:iptv');
323
+ });
324
+ }
325
+
304
326
  // A guide-only channel's program: a film found by the guide's original title and year (else
305
327
  // its title) opens its page with your addons' sources, as Discover does; a show, or a film
306
328
  // without one sure match, opens Search with its name.
@@ -328,5 +350,22 @@
328
350
  Stremio.findFilm({ title: p.title, orig: p.orig, year: p.year }, cancel).then(done, function () { done(null); });
329
351
  }
330
352
 
331
- V.guideGrid = guideGrid;
353
+ // Back on the Guide from a channel played from it (BACK, or its stream gave up), after CH+ /
354
+ // CH−: the hours at the present again, the row of the channel ended on in view (read if it
355
+ // wasn't), and what's on now on it. Returns that cell; null when the Guide isn't on screen or
356
+ // the channel isn't on it.
357
+ function guideBackOn(id) {
358
+ var g = _g, r = -1;
359
+ if (!id || !g || !g.box || !document.documentElement.contains(g.box)) { return null; }
360
+ for (var i = 0; i < g.chans.length; i++) { if (g.chans[i].id === id) { r = i; break; } }
361
+ if (r < 0) { return null; }
362
+ var now = Date.now() / 1000;
363
+ g.start = floorStep(now); g.live = true;
364
+ if (r < g.top) { g.top = r; } else if (r >= g.top + ROWS) { g.top = r - ROWS + 1; }
365
+ var n = draw(g, { id: id, t: now });
366
+ ensure(g).then(function (got) { if (got) { refresh(g); } }); // (its programs, when they weren't read: then onto what's on)
367
+ return n;
368
+ }
369
+
370
+ V.guideGrid = guideGrid; V.guideBackOn = guideBackOn;
332
371
  })(ViewParts);
package/js/view-home.js CHANGED
@@ -164,18 +164,32 @@
164
164
  // reordered): reload just its rows; every other row keeps what it has, with no request.
165
165
  // url: the addon ('local:iptv' for IPTV); none: nothing to fetch (only the set / order
166
166
  // of rows changed). Calls before the reload runs add up.
167
+ // An addon Home had no rows from (IPTV read after your addons, a first playlist) is one too:
168
+ // its rows asked for, its tabs added to the menu when they come.
167
169
  var _reloadOnly = null; // { addon url: 1 } for the next reload, else a full one
168
- var _homeAddons = {}; // { addon url: 1 } the addons Home's last load had catalogs from
169
170
  function refreshAddonRows(url) {
170
171
  if (_homeCatalogs === null) { clearCatalogCache(); return; } // nothing loaded yet: a full load anyway
171
- // an addon Home didn't load rows from (the first playlist added: IPTV): a full load, its tab with it
172
- if (url && !_homeAddons[url]) { clearCatalogCache(); return; }
173
172
  _reloadOnly = _reloadOnly || {};
174
173
  if (url) { _reloadOnly[url] = 1; }
175
174
  _homeDirty = true;
176
175
  if (_current() === scr('home') && !isModalOpen() && !Player.isActive()) { loadHomeCatalogs(); }
177
176
  }
178
177
 
178
+ // Auto-refresh (Settings › Content › IPTV): every minute, whether the playlists are due —
179
+ // their oldest download (a saved copy's own date) and the last try both that long ago — then
180
+ // all of them and their guides read again, as Reload does, and their rows with them. Not
181
+ // while something plays or StremioK is in the background: at the next minute after.
182
+ var _iptvTriedAt = 0, _iptvReading = false;
183
+ function iptvRefreshTick() {
184
+ var h = Stremio.iptvRefreshEvery ? Stremio.iptvRefreshEvery() : 0;
185
+ if (!h || _iptvReading || document.hidden || Player.isActive()) { return; }
186
+ var due = h * 3600000, now = Date.now(), at = Stremio.iptvReadAt();
187
+ if (at === Infinity || now - at < due || now - _iptvTriedAt < due) { return; }
188
+ _iptvTriedAt = now; _iptvReading = true;
189
+ Stremio.iptvReload().then(function () { _iptvReading = false; refreshAddonRows('local:iptv'); }, function () { _iptvReading = false; });
190
+ }
191
+ setInterval(iptvRefreshTick, 60000);
192
+
179
193
  // The Home rows are also saved on the TV, trimmed to what a card and the details strip
180
194
  // use, so a launch shows the last rows at once and refreshes them in the background
181
195
  // (as the 15-minute refresh does). Saved rows are only used with the same addons.
@@ -1179,7 +1193,10 @@
1179
1193
  }
1180
1194
 
1181
1195
  // Switch the active home tab (from the shared top menu) and show it.
1182
- function setHomeTab(key) { _homeTab = key || _homeTab; _focusFirstCard = 'new'; }
1196
+ function setHomeTab(key) { _homeTab = key || _homeTab; _focusFirstCard = 'new'; _wantTab = null; }
1197
+ // A tab asked for whose rows are still coming (a first playlist's IPTV tab): shown when they
1198
+ // come, if Home is still on the tab it showed instead. { key, instead }
1199
+ var _wantTab = null;
1183
1200
 
1184
1201
  // prevFocus: a Home card to put focus back on after the rebuild (the card a title
1185
1202
  // was opened from). Otherwise the card currently focused on Home is kept, so rows
@@ -1206,7 +1223,9 @@
1206
1223
  // Keep the active tab valid for what's actually available.
1207
1224
  var avail = availableTabs();
1208
1225
  if (!_homeTab || !avail.some(function (t) { return t.key === _homeTab; })) {
1226
+ var asked = _homeTab;
1209
1227
  _homeTab = avail.length ? avail[0].key : 'movie';
1228
+ if (asked && (_homeLoading || _reloadOnly)) { _wantTab = { key: asked, instead: _homeTab }; }
1210
1229
  }
1211
1230
 
1212
1231
  s.appendChild(buildTopMenu(_homeTab));
@@ -1416,12 +1435,12 @@
1416
1435
  _homeLoading = false;
1417
1436
  _homeFirstLoad = false;
1418
1437
  if (first) { U.spinner(false); }
1419
- if (_reloadOnly && onHome()) { setTimeout(loadHomeCatalogs, 0); } // (rows asked for meanwhile: the IPTV ones, read after the addons)
1438
+ // (rows asked for meanwhile: the IPTV ones, read after the addons; unless the redraw
1439
+ // that follows has asked for them already)
1440
+ if (_reloadOnly && onHome()) { setTimeout(function () { if (_reloadOnly) { loadHomeCatalogs(); } }, 0); }
1420
1441
  }
1421
1442
  function onHome() { return _current() === s && !isModalOpen() && !Player.isActive(); }
1422
1443
  Stremio.catalogsForHome().then(function (cats) {
1423
- _homeAddons = {};
1424
- cats.forEach(function (cat) { if (cat.addon && cat.addon.url) { _homeAddons[cat.addon.url] = 1; } });
1425
1444
  var acc = cats.map(function (cat) { return copyCat({ name: cat.name, type: cat.type, yt: isYouTubeCat(cat) }, []); });
1426
1445
  var arrived = cats.map(function () { return false; }), shown = 0, partialTimer = null;
1427
1446
  // First load: publish each row as soon as it arrives, in its own place (a slower one above
@@ -1458,11 +1477,16 @@
1458
1477
  U.settleAll(tasks, 3).then(function () {
1459
1478
  _homeCatalogs = splitOtherCatalogs(acc).filter(function (c) { return c.metas && c.metas.length; });
1460
1479
  done();
1480
+ var want = _wantTab;
1481
+ _wantTab = null;
1482
+ if (want && _homeTab === want.instead && onHome() && availableTabs().some(function (t) { return t.key === want.key; })) {
1483
+ setHomeTab(want.key);
1484
+ }
1461
1485
  saveHomeCache();
1462
1486
  // The tab on screen is redrawn only if what it shows changed: redrawing rebuilds
1463
1487
  // every card (and reloads its artwork), which shows as a flicker. Continue
1464
1488
  // Watching doesn't come from catalogs at all. Other tabs rebuild when opened.
1465
- var same = (before !== null && shownSig() === before) || ownTabStill();
1489
+ var same = (before !== null && shownSig() === before && menuSig() === _renderedMenu) || ownTabStill();
1466
1490
  var keep = {};
1467
1491
  if (same && _tabRows[_homeTab]) { keep[_homeTab] = _tabRows[_homeTab]; }
1468
1492
  _tabRows = keep;
@@ -1473,6 +1497,7 @@
1473
1497
  });
1474
1498
  }, function () {
1475
1499
  done();
1500
+ _wantTab = null;
1476
1501
  if (first) { U.toast('Failed to load addons. Check network / Settings.'); }
1477
1502
  });
1478
1503
  }