stremiok 2.1.0 → 2.2.1

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-home.js CHANGED
@@ -26,49 +26,116 @@
26
26
  { key: 'movie', icon: '🎬', label: 'Movies' },
27
27
  { key: 'series', icon: '📺', label: 'Series' },
28
28
  { key: 'tv', icon: '📡', label: 'IPTV' },
29
+ { key: 'guide', icon: '☰', label: 'Guide' }, // (the IPTV channels' TV guide as a grid, once one is read: see js/view-guide.js)
29
30
  { key: 'radio', icon: '📻', label: 'Radio' },
30
31
  { key: 'sport', icon: '⚽', label: 'Sports' },
31
32
  { key: 'youtube', icon: '📹', label: 'YouTube' }, // (StremioK's YouTube catalogs, an addon's YouTube / channel ones: see isYouTubeCat)
32
33
  { key: 'local', icon: '💾', label: 'Local' } // (a USB drive plugged in: see LocalMedia)
33
34
  ];
34
35
  function hasLocal() { return typeof LocalMedia !== 'undefined' && LocalMedia.present(); }
36
+ function hasGuide() { return typeof EPG !== 'undefined' && !!EPG.hasAny && EPG.hasAny(); }
37
+
38
+ // The top menu's order (Settings › Appearance › Menu): Watching first and Settings last, the
39
+ // rest (the tabs, Discover, Search) in yours. A saved order is kept as far as it goes: an
40
+ // item it doesn't have (one added since) goes after the one it usually follows, and one
41
+ // that no longer exists is dropped.
42
+ var MENU_ITEMS = HOME_TABS.filter(function (t) { return t.key !== 'continue'; }).map(function (t) { return t.key; }).concat(['discover', 'search']);
43
+ function menuOrder() {
44
+ var out = [];
45
+ (Stremio.getPrefs().menuOrder || []).forEach(function (k) { if (MENU_ITEMS.indexOf(k) !== -1 && out.indexOf(k) === -1) { out.push(k); } });
46
+ MENU_ITEMS.forEach(function (k, i) {
47
+ if (out.indexOf(k) !== -1) { return; }
48
+ var at = 0;
49
+ for (var j = i - 1; j >= 0; j--) { var p = out.indexOf(MENU_ITEMS[j]); if (p !== -1) { at = p + 1; break; } }
50
+ out.splice(at, 0, k);
51
+ });
52
+ return out;
53
+ }
54
+ function moveMenuItem(key, dir) {
55
+ var o = menuOrder();
56
+ if (!U.moveIn(o, o.indexOf(key), dir)) { return false; }
57
+ Stremio.setPref('menuOrder', o);
58
+ return true;
59
+ }
60
+ // What the top menu shows, left to right, before Settings: Watching, then in your order the
61
+ // tabs showing (availableTabs), Discover and Search. [{ key, tab }] (tab: a Home tab's entry).
62
+ function menuItems() {
63
+ var tabs = availableTabs(), byKey = {}, out = [];
64
+ tabs.forEach(function (t) { byKey[t.key] = t; });
65
+ if (byKey['continue']) { out.push({ key: 'continue', tab: byKey['continue'] }); }
66
+ menuOrder().forEach(function (k) {
67
+ if (k === 'discover' || k === 'search') { out.push({ key: k }); }
68
+ else if (byKey[k]) { out.push({ key: k, tab: byKey[k] }); }
69
+ });
70
+ return out;
71
+ }
35
72
 
36
73
  // Which tabs to show in the top menu (needs the home catalogs; falls back to the
37
74
  // standard set before they've loaded so the menu is consistent everywhere).
38
- // Tabs you hid (Settings › Appearance › Home tabs) aren't shown; Watching always is.
75
+ // Tabs you hid (Settings › Appearance › Menu) aren't shown; Watching always is.
39
76
  function availableTabs() {
40
77
  var hidden = Stremio.getPrefs().hiddenTabs || [];
41
- function shown(t) { return t.key === 'continue' || hidden.indexOf(t.key) === -1; }
78
+ function shown(t) { return t.key === 'continue' || (hidden.indexOf(t.key) === -1 && (t.key !== 'guide' || hasGuide())); }
42
79
  if (_homeCatalogs === null || _homeFirstLoad) { // first load still running
43
80
  return HOME_TABS.filter(function (t) { return shown(t) && (t.key !== 'local' || hasLocal()); });
44
81
  }
45
82
  return HOME_TABS.filter(function (t) {
46
- return shown(t) && (t.key === 'continue' || t.key === 'calendar' || (t.key === 'local' ? hasLocal() : catsForTab(t.key).length > 0));
83
+ return shown(t) && (t.key === 'continue' || t.key === 'calendar' || t.key === 'guide' || (t.key === 'local' ? hasLocal() : catsForTab(t.key).length > 0));
47
84
  });
48
85
  }
49
86
 
50
87
  // YouTube's tab: StremioK's own YouTube catalogs (whatever type you gave one: "Podcasts"),
51
88
  // and another addon's YouTube or channel catalogs (YouTubio's "YouTube", Stremio's "channel").
89
+ // (Home's copies of a catalog, and the ones saved for the next launch, keep only its name,
90
+ // type and titles: a YouTube one carries yt, as its addon isn't kept with them)
52
91
  function isYouTubeCat(c) {
53
92
  var m = c.addon && c.addon.manifest;
54
- return !!(m && m.id === 'org.stremiok.youtube') || /^(youtube|channel)$/i.test('' + c.type);
93
+ return !!c.yt || !!(m && m.id === 'org.stremiok.youtube') || /^(youtube|channel)$/i.test('' + c.type);
55
94
  }
56
- // Which cached catalogs belong to a tab. Sports, radio and YouTube have dedicated tabs;
57
- // IPTV is the catch-all for every other live source (tv, …).
58
- function catsForTab(key) {
59
- return (_homeCatalogs || []).filter(function (c) {
60
- if (key === 'youtube') { return isYouTubeCat(c); }
61
- if (isYouTubeCat(c)) { return false; }
62
- if (key === 'movie') { return c.type === 'movie'; }
63
- if (key === 'series') { return c.type === 'series'; }
64
- if (key === 'sport') { return c.type === 'sport'; }
65
- if (key === 'radio') { return c.type === 'radio'; }
66
- if (key === 'tv') {
67
- return c.type !== 'movie' && c.type !== 'series' &&
68
- c.type !== 'sport' && c.type !== 'radio';
69
- }
70
- return false;
95
+ function copyCat(c, metas, more) { // (name, type, titles: and yt when it's one)
96
+ var o = { name: c.name, type: c.type, metas: metas };
97
+ if (c.yt) { o.yt = true; }
98
+ if (more) { Object.keys(more).forEach(function (k) { o[k] = more[k]; }); }
99
+ return o;
100
+ }
101
+ // Which cached catalogs belong to a tab (tabOf). Movies, series, sports, radio and YouTube go
102
+ // to their own tabs, live TV (LIVE_TYPES, IPTV playlists) to IPTV. Another type (a debrid
103
+ // library's "RealDebrid", "other") goes by what it lists (byItems); anything still unclear
104
+ // isn't on Home ('': Discover has it). ("anime" never gets here: see Stremio's discoverOnly.)
105
+ var LIVE_TYPES = { tv: 1, events: 1, event: 1, live: 1, livetv: 1, iptv: 1 };
106
+ var TITLE_IDS = /^(tt\d+|tmdb:|tvdb:|kitsu:|mal:|anilist:|anidb:)/i;
107
+ function tabOf(c) {
108
+ var n = (c.metas || []).length;
109
+ if (c._tab !== undefined && c._tabN === n) { return c._tab; } // (worked out again once it has titles)
110
+ var t = ('' + (c.type || '')).toLowerCase(), tab;
111
+ if (isYouTubeCat(c)) { tab = 'youtube'; }
112
+ else if (t === 'movie' || t === 'series' || t === 'radio') { tab = t; }
113
+ else if (t === 'sport' || t === 'sports') { tab = 'sport'; }
114
+ else if (LIVE_TYPES[t]) { tab = 'tv'; }
115
+ else { tab = byItems(c); }
116
+ c._tab = tab; c._tabN = n;
117
+ return tab;
118
+ }
119
+ // Mostly films or series (60% of its first 50 titles): that tab. Title ids, and "anime",
120
+ // "series" or "show" in its name or type: Series ("movie" / "film": Movies). Else ''.
121
+ function byItems(c) {
122
+ var ms = (c.metas || []).slice(0, 50), mv = 0, sr = 0, ids = 0;
123
+ if (!ms.length) { return ''; }
124
+ ms.forEach(function (m) {
125
+ var mt = ('' + ((m && m.type) || '')).toLowerCase();
126
+ if (mt === 'movie') { mv++; } else if (mt === 'series') { sr++; }
127
+ if (TITLE_IDS.test('' + ((m && m.id) || ''))) { ids++; }
71
128
  });
129
+ if (mv + sr >= ms.length * 0.6) { return mv > sr ? 'movie' : 'series'; }
130
+ if (ids >= ms.length * 0.6) {
131
+ var hint = (c.type + ' ' + (c.name || '') + ' ' + ((c.def && c.def.name) || '')).toLowerCase();
132
+ if (/movie|film|pel[ií]cula/.test(hint)) { return 'movie'; }
133
+ if (/anime|series|show|serie/.test(hint)) { return 'series'; }
134
+ }
135
+ return '';
136
+ }
137
+ function catsForTab(key) {
138
+ return (_homeCatalogs || []).filter(function (c) { return tabOf(c) === key; });
72
139
  }
73
140
  var _homeCatalogs = null; // [{name,type,metas}] — fetched once per session, filtered per tab
74
141
  var _homeTab = null; // active tab key
@@ -98,8 +165,11 @@
98
165
  // url: the addon ('local:iptv' for IPTV); none: nothing to fetch (only the set / order
99
166
  // of rows changed). Calls before the reload runs add up.
100
167
  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
101
169
  function refreshAddonRows(url) {
102
170
  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; }
103
173
  _reloadOnly = _reloadOnly || {};
104
174
  if (url) { _reloadOnly[url] = 1; }
105
175
  _homeDirty = true;
@@ -148,17 +218,17 @@
148
218
  if (bytes > _homeBytesMax) { return; }
149
219
  var metas = c.metas.map(trimMeta);
150
220
  metas.forEach(function (m) { bytes += sizeOf(m); });
151
- if (bytes <= _homeBytesMax) { cats.push({ name: c.name, type: c.type, metas: metas }); }
221
+ if (bytes <= _homeBytesMax) { cats.push(copyCat(c, metas)); }
152
222
  });
153
223
  if (U.bigStore && !_homeSaveFailed) {
154
224
  U.bigStore.set(HOME_CACHE_KEY, { sig: sig, cats: cats }).then(function (ok) { if (ok === false) { _homeSaveFailed = true; } });
155
225
  }
156
226
  U.store.set(HOME_SNAP_KEY, { sig: sig, cats: cats.slice(0, SNAP_ROWS).map(function (c) {
157
- return { name: c.name, type: c.type, metas: c.metas.slice(0, SNAP_CARDS).map(function (m) {
227
+ return copyCat(c, c.metas.slice(0, SNAP_CARDS).map(function (m) {
158
228
  var o = { id: m.id, type: m.type, name: m.name };
159
229
  SNAP_FIELDS.forEach(function (k) { if (m[k] != null) { o[k] = m[k]; } });
160
230
  return o;
161
- }) };
231
+ }));
162
232
  }) });
163
233
  }
164
234
  function useHomeCache() {
@@ -1150,7 +1220,7 @@
1150
1220
  // Rows for the active tab, from the once-per-session catalog cache. Catalog tabs
1151
1221
  // keep their built rows, so switching back doesn't rebuild every card; Continue
1152
1222
  // Watching is small and changes after every playback, so it's always rebuilt.
1153
- var rows = (_homeTab !== 'continue' && _homeTab !== 'local' && _homeTab !== 'calendar') ? _tabRows[_homeTab] : null;
1223
+ var rows = (_homeTab !== 'continue' && _homeTab !== 'local' && _homeTab !== 'calendar' && _homeTab !== 'guide') ? _tabRows[_homeTab] : null;
1154
1224
  if (_homeTab !== 'calendar' && _calCancel) { _calCancel.cancel(); _calCancel = null; } // (its titles' details still coming: not wanted)
1155
1225
  if (rows) {
1156
1226
  s.appendChild(rows);
@@ -1162,6 +1232,8 @@
1162
1232
  localRows(rows);
1163
1233
  } else if (_homeTab === 'calendar') {
1164
1234
  calendarRows(rows);
1235
+ } else if (_homeTab === 'guide') {
1236
+ V.guideGrid(rows);
1165
1237
  } else if (_homeTab === 'continue') {
1166
1238
  var entries = continueEntries();
1167
1239
  if (entries.length) {
@@ -1194,12 +1266,15 @@
1194
1266
  var again = keep ? findCard(s, keep) : null;
1195
1267
  var landed = false;
1196
1268
  var sameItem = (!firstCard && inMenu) ? menuItem(s, cur.textContent) : null; // the menu item focus was on
1197
- if (again || sameItem) { Focus.setScope(s, again || sameItem); } else { landed = focusHome(s, !firstCard && inMenu); }
1269
+ var rowsEl = s.querySelector('.rows'), fc = Focus.current();
1270
+ if (_homeTab === 'guide' && fc && rowsEl && rowsEl.contains(fc)) { landed = true; } // (the Guide drawn from what it had put focus back on its program)
1271
+ else if (again || sameItem) { Focus.setScope(s, again || sameItem); } else { landed = focusHome(s, !firstCard && inMenu); }
1198
1272
  if (_homeCatalogs === null || homeStale() || _reloadOnly) { loadHomeCatalogs(); }
1199
1273
  // No title to focus yet: try again when the rows arrive (Continue Watching and a
1200
1274
  // tab with no catalogs have nothing coming, so focus simply stays on the pill).
1201
- _focusFirstCard = (firstCard && !landed && _homeLoading && _homeTab !== 'continue' && _homeTab !== 'calendar') ? 'pending' : false;
1275
+ _focusFirstCard = (firstCard && !landed && _homeLoading && _homeTab !== 'continue' && _homeTab !== 'calendar' && _homeTab !== 'guide') ? 'pending' : false;
1202
1276
  _renderedSig = tabSig();
1277
+ _renderedMenu = menuSig();
1203
1278
  if (s.querySelector('.rows .card')) { U.mark('firstCard'); }
1204
1279
  // the strip describes the focused title only (empty while focus is in the menu)
1205
1280
  clearTimeout(_homeInfoTimer); _homeInfoTimer = null;
@@ -1214,6 +1289,7 @@
1214
1289
  function fillInfoBox(box, m) {
1215
1290
  if (!box) { return; }
1216
1291
  m = m || {};
1292
+ if (!m.logo && /^iptv:/.test(m.id || '') && typeof EPG !== 'undefined' && EPG.logo && EPG.logo(m.id)) { m = Object.assign({}, m, { logo: EPG.logo(m.id) }); } // (a channel: its guide's logo)
1217
1293
  var bits = [];
1218
1294
  if (m.releaseInfo) { bits.push(m.releaseInfo); } else if (m.year) { bits.push('' + m.year); }
1219
1295
  if (m.runtime) { bits.push(m.runtime); }
@@ -1231,8 +1307,34 @@
1231
1307
  } else {
1232
1308
  t.textContent = name;
1233
1309
  }
1310
+ var desc = m.description || '';
1311
+ var g = /^iptv:/.test(m.id || '') && typeof EPG !== 'undefined' ? EPG.now(m.id) : null; // a channel: what's on, from its TV guide
1312
+ if (g && g.now) {
1313
+ bits = ['Now ' + EPG.hhmm(g.now.start) + '–' + EPG.hhmm(g.now.stop) + ' ' + g.now.title];
1314
+ desc = (g.now.desc ? g.now.desc + ' ' : '') + (g.next ? 'Next ' + EPG.hhmm(g.next.start) + ' ' + g.next.title : '');
1315
+ }
1234
1316
  box.children[1].textContent = bits.join(' · ');
1235
- box.children[2].textContent = m.description || '';
1317
+ box.children[2].textContent = desc;
1318
+ // a program's picture from the TV guide (the Guide tab's focused one, a channel's now)
1319
+ var picSrc = m.picture || (g && g.now && g.now.picture) || '', pic = box.querySelector('.home-info-pic');
1320
+ if (!picSrc) { if (pic) { pic.className = 'home-info-pic hidden'; } box.classList.remove('has-pic'); return; }
1321
+ if (!pic) { pic = U.el('img', 'home-info-pic'); pic.onerror = function () { pic.className = 'home-info-pic hidden'; box.classList.remove('has-pic'); }; box.appendChild(pic); }
1322
+ pic.className = 'home-info-pic';
1323
+ box.classList.add('has-pic');
1324
+ if (pic.__src !== picSrc) { pic.__src = picSrc; U.deferImage(pic, U.cardImage(picSrc, 'still'), true); }
1325
+ }
1326
+ if (typeof EPG !== 'undefined') { // (the focused channel's: drawn again when its guide arrives)
1327
+ var _hadGuide = false;
1328
+ EPG.onChange(function () {
1329
+ var n = Focus.current();
1330
+ if (n && n.__meta && /^iptv:/.test(n.__meta.id || '') && scr('home').contains(n)) { fillHomeInfo(n.__meta); }
1331
+ // a guide read (or gone): the Guide tab comes (or goes) in the menu, at once
1332
+ var has = hasGuide();
1333
+ if (has !== _hadGuide) {
1334
+ _hadGuide = has;
1335
+ if (_current() === scr('home') && !isModalOpen() && !Player.isActive()) { renderHome(); }
1336
+ }
1337
+ });
1236
1338
  }
1237
1339
  Focus.onFocus(function (node) {
1238
1340
  if (!node || !scr('home').contains(node)) { return; }
@@ -1263,12 +1365,18 @@
1263
1365
  // Everything the tab on screen shows from the catalogs (which titles, in which rows),
1264
1366
  // to tell whether a refresh changed it. Continue Watching shows none of it.
1265
1367
  function shownSig() {
1266
- if (_homeTab === 'continue' || _homeTab === 'calendar') { return _homeTab; }
1368
+ if (_homeTab === 'continue' || _homeTab === 'calendar' || _homeTab === 'guide') { return _homeTab; }
1267
1369
  return _homeTab + '#' + catsForTab(_homeTab).map(function (c) {
1268
1370
  return c.name + ':' + c.metas.map(function (m) { return m.id + '/' + (m.poster || ''); }).join(',');
1269
1371
  }).join('|');
1270
1372
  }
1271
- var _renderedSig = '';
1373
+ var _renderedSig = '', _renderedMenu = '';
1374
+ // The tabs that aren't drawn from catalogs (Watching, Calendar, Guide, Local): catalogs that
1375
+ // arrive don't change them, only the top menu might (a tab coming). Redrawing one rebuilds it
1376
+ // whole (the Guide asks for its programs again): a flicker, so not unless the menu changed.
1377
+ function ownTab(t) { return t === 'continue' || t === 'calendar' || t === 'guide' || t === 'local'; }
1378
+ function menuSig() { return availableTabs().map(function (t) { return t.key; }).join(','); }
1379
+ function ownTabStill() { return ownTab(_homeTab) && menuSig() === _renderedMenu; }
1272
1380
  function tabSig() {
1273
1381
  return _homeTab + '#' + catsForTab(_homeTab).map(function (c) { return c.name + ':' + c.metas.length; }).join('|');
1274
1382
  }
@@ -1312,7 +1420,9 @@
1312
1420
  }
1313
1421
  function onHome() { return _current() === s && !isModalOpen() && !Player.isActive(); }
1314
1422
  Stremio.catalogsForHome().then(function (cats) {
1315
- var acc = cats.map(function (cat) { return { name: cat.name, type: cat.type, metas: [] }; });
1423
+ _homeAddons = {};
1424
+ cats.forEach(function (cat) { if (cat.addon && cat.addon.url) { _homeAddons[cat.addon.url] = 1; } });
1425
+ var acc = cats.map(function (cat) { return copyCat({ name: cat.name, type: cat.type, yt: isYouTubeCat(cat) }, []); });
1316
1426
  var arrived = cats.map(function () { return false; }), shown = 0, partialTimer = null;
1317
1427
  // First load: publish each row as soon as it arrives, in its own place (a slower one above
1318
1428
  // it held by a "Loading…" placeholder, so nothing jumps when it comes), and redraw when
@@ -1321,12 +1431,13 @@
1321
1431
  var n = arrived.filter(Boolean).length, last = arrived.lastIndexOf(true);
1322
1432
  if (!first || n <= shown) { return; }
1323
1433
  shown = n;
1324
- var now = acc.slice(0, last + 1).map(function (c, i) { return arrived[i] ? c : { name: c.name, type: c.type, metas: [], loading: true }; });
1434
+ var now = acc.slice(0, last + 1).map(function (c, i) { return arrived[i] ? c : copyCat(c, [], { loading: true }); });
1325
1435
  _homeCatalogs = splitOtherCatalogs(now).filter(function (c) { return c.loading || (c.metas && c.metas.length); });
1326
1436
  if (partialTimer) { return; }
1327
1437
  partialTimer = setTimeout(function () {
1328
1438
  partialTimer = null;
1329
1439
  if (!_homeFirstLoad) { return; } // everything arrived meanwhile: drawn below
1440
+ if (ownTabStill()) { _tabRows = {}; return; } // (Watching, the Guide…: as they are)
1330
1441
  var keep = {};
1331
1442
  if (tabSig() === _renderedSig) { keep[_homeTab] = _tabRows[_homeTab]; }
1332
1443
  _tabRows = keep; // other tabs rebuild from the new rows when opened
@@ -1351,7 +1462,7 @@
1351
1462
  // The tab on screen is redrawn only if what it shows changed: redrawing rebuilds
1352
1463
  // every card (and reloads its artwork), which shows as a flicker. Continue
1353
1464
  // Watching doesn't come from catalogs at all. Other tabs rebuild when opened.
1354
- var same = before !== null && shownSig() === before;
1465
+ var same = (before !== null && shownSig() === before) || ownTabStill();
1355
1466
  var keep = {};
1356
1467
  if (same && _tabRows[_homeTab]) { keep[_homeTab] = _tabRows[_homeTab]; }
1357
1468
  _tabRows = keep;
@@ -1375,8 +1486,8 @@
1375
1486
  function splitOtherCatalogs(list) {
1376
1487
  var out = [];
1377
1488
  list.forEach(function (c) {
1378
- if (c.loading) { if (c.type !== 'other') { out.push({ name: c.name, type: c.type, metas: [], loading: true }); } return; } // (a placeholder: still loading)
1379
- if (c.type !== 'other') { out.push({ name: c.name, type: c.type, metas: c.metas.slice(0, ROW_CARDS) }); return; }
1489
+ if (c.loading) { if (c.type !== 'other' || c.yt) { out.push(copyCat(c, [], { loading: true })); } return; } // (a placeholder: still loading)
1490
+ if (c.type !== 'other' || c.yt) { out.push(copyCat(c, c.metas.slice(0, ROW_CARDS))); return; } // (a YouTube one as it is, whatever its type)
1380
1491
  var movies = [], series = [];
1381
1492
  c.metas.forEach(function (m) { (SERIES_NAME.test(m.name || '') ? series : movies).push(m); });
1382
1493
  out.push({ name: otherRowName('Movies', c.name), type: 'movie', metas: movies.slice(0, ROW_CARDS) });
@@ -1435,6 +1546,10 @@
1435
1546
  // ----- published for the other parts (and js/views.js) -----
1436
1547
  V.fillInfoBox = fillInfoBox;
1437
1548
  V.availableTabs = availableTabs;
1549
+ V.menuOrder = menuOrder;
1550
+ V.moveMenuItem = moveMenuItem;
1551
+ V.menuItems = menuItems;
1552
+ V.MENU_ITEMS = MENU_ITEMS;
1438
1553
  V.HOME_TABS = HOME_TABS;
1439
1554
  V.buildRow = buildRow;
1440
1555
  V.clearCatalogCache = clearCatalogCache;