stremiok 2.0.0 → 2.1.0

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
@@ -28,6 +28,7 @@
28
28
  { key: 'tv', icon: '📡', label: 'IPTV' },
29
29
  { key: 'radio', icon: '📻', label: 'Radio' },
30
30
  { key: 'sport', icon: 'âš½', label: 'Sports' },
31
+ { key: 'youtube', icon: '📹', label: 'YouTube' }, // (StremioK's YouTube catalogs, an addon's YouTube / channel ones: see isYouTubeCat)
31
32
  { key: 'local', icon: '💾', label: 'Local' } // (a USB drive plugged in: see LocalMedia)
32
33
  ];
33
34
  function hasLocal() { return typeof LocalMedia !== 'undefined' && LocalMedia.present(); }
@@ -46,10 +47,18 @@
46
47
  });
47
48
  }
48
49
 
49
- // Which cached catalogs belong to a tab. Sports and radio have dedicated tabs;
50
- // Live TV is the catch-all for every other live source (tv, channel, …).
50
+ // YouTube's tab: StremioK's own YouTube catalogs (whatever type you gave one: "Podcasts"),
51
+ // and another addon's YouTube or channel catalogs (YouTubio's "YouTube", Stremio's "channel").
52
+ function isYouTubeCat(c) {
53
+ var m = c.addon && c.addon.manifest;
54
+ return !!(m && m.id === 'org.stremiok.youtube') || /^(youtube|channel)$/i.test('' + c.type);
55
+ }
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, …).
51
58
  function catsForTab(key) {
52
59
  return (_homeCatalogs || []).filter(function (c) {
60
+ if (key === 'youtube') { return isYouTubeCat(c); }
61
+ if (isYouTubeCat(c)) { return false; }
53
62
  if (key === 'movie') { return c.type === 'movie'; }
54
63
  if (key === 'series') { return c.type === 'series'; }
55
64
  if (key === 'sport') { return c.type === 'sport'; }
@@ -967,16 +976,18 @@
967
976
  showModal(overlay, details || watched);
968
977
  }
969
978
 
970
- // Local progress merged with Trakt playback (when connected), deduped, newest first.
979
+ // Local progress merged with Trakt playback (when connected), deduped, newest first; a
980
+ // place kept here that Trakt has you watching past since (another TV) left out.
971
981
  // One card per show: its furthest episode (highest season, then episode), placed by
972
982
  // the show's most recent activity. The others ride along as _dupes so Remove clears
973
983
  // the whole show.
974
984
  // byActivity: the most recent activity first, whatever order you gave the row (Move): the
975
985
  // home bar's tiles (previewTiles).
976
986
  function continueEntries(byActivity) {
977
- var local = Player.continueList();
987
+ var local = Player.continueList(), traktOn = typeof Trakt !== 'undefined' && Trakt.isConnected();
988
+ if (traktOn && Trakt.watchedPast) { local = local.filter(function (e) { return !Trakt.watchedPast(e); }); }
978
989
  var merged = local.slice();
979
- if (typeof Trakt !== 'undefined' && Trakt.isConnected()) {
990
+ if (traktOn) {
980
991
  var seen = {};
981
992
  local.forEach(function (e) { seen[e.type + ':' + e.id] = 1; });
982
993
  Trakt.continueList().forEach(function (e) {
@@ -1272,6 +1283,8 @@
1272
1283
  if (typeof IPTV !== 'undefined' && IPTV.onProgress) {
1273
1284
  IPTV.onProgress(function (n) { if (_homeFirstLoad) { U.spinner(true, channelsText(n)); } });
1274
1285
  }
1286
+ // The IPTV playlists read after your addons loaded (they aren't waited for): their rows join.
1287
+ if (typeof Stremio !== 'undefined' && Stremio.onIptvReady) { Stremio.onIptvReady(function () { refreshAddonRows('local:iptv'); }); }
1275
1288
  var PARTIAL_DELAY = 150; // coalesces catalogs that arrive together into one redraw
1276
1289
  function loadHomeCatalogs() {
1277
1290
  if (_homeLoading) { return; }
@@ -1295,19 +1308,21 @@
1295
1308
  _homeLoading = false;
1296
1309
  _homeFirstLoad = false;
1297
1310
  if (first) { U.spinner(false); }
1311
+ if (_reloadOnly && onHome()) { setTimeout(loadHomeCatalogs, 0); } // (rows asked for meanwhile: the IPTV ones, read after the addons)
1298
1312
  }
1299
1313
  function onHome() { return _current() === s && !isModalOpen() && !Player.isActive(); }
1300
1314
  Stremio.catalogsForHome().then(function (cats) {
1301
1315
  var acc = cats.map(function (cat) { return { name: cat.name, type: cat.type, metas: [] }; });
1302
1316
  var arrived = cats.map(function () { return false; }), shown = 0, partialTimer = null;
1303
- // First load: publish the rows that have arrived in order (row 1..n), and redraw
1304
- // when that changes the tab on screen.
1317
+ // First load: publish each row as soon as it arrives, in its own place (a slower one above
1318
+ // it held by a "Loading…" placeholder, so nothing jumps when it comes), and redraw when
1319
+ // that changes the tab on screen.
1305
1320
  function partial() {
1306
- var n = 0;
1307
- while (n < arrived.length && arrived[n]) { n++; }
1321
+ var n = arrived.filter(Boolean).length, last = arrived.lastIndexOf(true);
1308
1322
  if (!first || n <= shown) { return; }
1309
1323
  shown = n;
1310
- _homeCatalogs = splitOtherCatalogs(acc.slice(0, n)).filter(function (c) { return c.metas && c.metas.length; });
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 }; });
1325
+ _homeCatalogs = splitOtherCatalogs(now).filter(function (c) { return c.loading || (c.metas && c.metas.length); });
1311
1326
  if (partialTimer) { return; }
1312
1327
  partialTimer = setTimeout(function () {
1313
1328
  partialTimer = null;
@@ -1360,6 +1375,7 @@
1360
1375
  function splitOtherCatalogs(list) {
1361
1376
  var out = [];
1362
1377
  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)
1363
1379
  if (c.type !== 'other') { out.push({ name: c.name, type: c.type, metas: c.metas.slice(0, ROW_CARDS) }); return; }
1364
1380
  var movies = [], series = [];
1365
1381
  c.metas.forEach(function (m) { (SERIES_NAME.test(m.name || '') ? series : movies).push(m); });
@@ -1379,6 +1395,7 @@
1379
1395
  for (var i = from; i < to; i++) {
1380
1396
  var c = cats[i];
1381
1397
  var row = buildRow(c.name, c.metas.map(function (m) { return posterCard(m, c.type); }));
1398
+ if (c.loading) { row.className += ' row-loading'; row.appendChild(U.el('div', 'row-empty', 'Loading…')); } // (its place kept: nothing to focus)
1382
1399
  rows.appendChild(row);
1383
1400
  built.push(row);
1384
1401
  }
@@ -323,7 +323,7 @@
323
323
  ['Audio', 'Preferred and second-choice audio language', 'audio'],
324
324
  ['Subtitles', 'Default language, size, color, background, position, font', 'subtitles'],
325
325
  ['Skipping', 'Intros, recaps and previews, and the next episode at the credits: ask, or on their own after a countdown', 'skipping'],
326
- ['Seeking & buffering', 'How far â—€ â–¶ jump; a bigger buffer for slow connections', 'seeking'],
326
+ ['Seeking & buffering', 'How far â—€ â–¶ jump; a bigger buffer for slow connections; reconnecting live channels', 'seeking'],
327
327
  ['Source ranking', 'How sources are ordered (played before, resolution, release type…), and your addons: their order, on or off', 'ranking'],
328
328
  ['Formats', 'Allowed resolutions, and what plays on this TV, learned as you watch', 'formats'],
329
329
  ['Private viewing', 'Play without anything recorded: no progress, Continue Watching, watched marks or Trakt', 'private']] },
@@ -470,6 +470,24 @@
470
470
  });
471
471
  return 'Gives: ' + (bits.length ? bits.join(' · ') : 'nothing StremioK uses') + (types.length ? ' · ' + types.slice(0, 8).join(', ') : '');
472
472
  }
473
+ // An addon's logo tile: its initial (the first character, an emoji whole; "?" for one that
474
+ // didn't load; drawn by the CSS from data-initial, so the row's text is still its name),
475
+ // and over it the logo from its manifest (Stremio.addonLogo), loaded with the other
476
+ // artwork; one that can't load is taken away, leaving the initial.
477
+ function addonTile(m, url) {
478
+ var name = ('' + ((m && m.name) || '')).replace(/^\s+/, ''), c = name.charCodeAt(0);
479
+ var tile = U.el('div', 'addon-logo');
480
+ tile.setAttribute('data-initial', !m ? '?' : name ? name.slice(0, c >= 0xD800 && c <= 0xDBFF ? 2 : 1).toUpperCase() : 'A');
481
+ var src = Stremio.addonLogo ? Stremio.addonLogo(m, url) : null;
482
+ if (src) {
483
+ var img = document.createElement('img');
484
+ img.onload = function () { tile.className = 'addon-logo loaded'; };
485
+ img.onerror = function () { if (img.parentNode) { img.parentNode.removeChild(img); } tile.className = 'addon-logo'; };
486
+ tile.appendChild(img);
487
+ U.deferImage(img, src);
488
+ }
489
+ return tile;
490
+ }
473
491
  function settingsAddons(wrap) {
474
492
  wrap.appendChild(U.el('h2', null, 'Addons'));
475
493
  // Home-network links from addons on the internet (see Stremio.blocksLocal)
@@ -489,16 +507,14 @@
489
507
  var urls = Stremio.getAddonUrls(), want = null;
490
508
  if (urls.length > 1) { wrap.appendChild(U.el('div', 'hint', 'â–² / â–¼ set the order: a title\'s details come from the highest addon that has them (its page says which), and the higher addon comes first where several answer.')); }
491
509
  urls.forEach(function (url) {
492
- var loaded = addons.find(function (a) { return a.url === url; });
493
- var frow = U.el('div', 'feed-row playlist-row');
494
- function move(dir, ctl) { return function () { if (Stremio.moveAddon(url, dir)) { _addonFocus = { url: url, ctl: ctl }; refreshAddonRows(); renderSettings(); } }; }
495
- var up = U.el('div', 'feed-btn focusable', 'â–²');
496
- up.__onselect = move(-1, 'up');
497
- var down = U.el('div', 'feed-btn focusable', 'â–¼');
498
- down.__onselect = move(1, 'down');
499
- var item = U.el('div', 'addon-item focusable playlist-item');
510
+ var off = !!(Stremio.isAddonOff && Stremio.isAddonOff(url)), kept = off && Stremio.savedManifest ? Stremio.savedManifest(url) : null;
511
+ var loaded = addons.find(function (a) { return a.url === url; }) || (kept ? { url: url, manifest: kept } : null); // (one that's off: its saved manifest)
512
+ var frow = U.el('div', 'feed-row playlist-row' + (off ? ' feed-off' : ''));
513
+ var btns = orderButtons(function (dir, ctl) { if (Stremio.moveAddon(url, dir)) { _addonFocus = { url: url, ctl: ctl }; refreshAddonRows(); renderSettings(); } });
514
+ var item = U.el('div', 'addon-item focusable playlist-item has-logo');
515
+ item.appendChild(addonTile(loaded && loaded.manifest, url));
500
516
  var why = !loaded && Stremio.manifestProblem ? Stremio.manifestProblem(url) : ''; // "too large (over 2 MB)", "not an addon manifest"…
501
- var nm = U.el('div', 'a-name', loaded ? (loaded.manifest.name || 'Addon') : '(failed to load' + (why ? ': ' + why : '') + ')');
517
+ var nm = U.el('div', 'a-name', (loaded ? (loaded.manifest.name || 'Addon') : off ? 'Addon' : '(failed to load' + (why ? ': ' + why : '') + ')') + (off ? ' (off)' : ''));
502
518
  var origin = Stremio.addonOrigin ? Stremio.addonOrigin(url) : '', ob = originBadge(origin);
503
519
  if (ob) { nm.appendChild(ob); }
504
520
  item.appendChild(nm);
@@ -512,10 +528,10 @@
512
528
  item.__onselect = function () {
513
529
  addonActions(name, url);
514
530
  };
515
- if (urls.length > 1) { frow.appendChild(up); frow.appendChild(down); }
531
+ if (urls.length > 1) { btns.forEach(function (b) { frow.appendChild(b); }); }
516
532
  frow.appendChild(item);
517
533
  wrap.appendChild(frow);
518
- if (_addonFocus && _addonFocus.url === url) { want = _addonFocus.ctl === 'up' ? up : down; }
534
+ if (_addonFocus && _addonFocus.url === url) { want = btns[ctlIndex(_addonFocus.ctl)]; }
519
535
  });
520
536
 
521
537
  var addRow = U.el('div', 'addon-item focusable add-row');
@@ -731,9 +747,12 @@
731
747
  var byNorm = {};
732
748
  there.forEach(function (x) { byNorm[norm(x.url)] = x; });
733
749
  var mine = Stremio.getAddonUrls(), mineNorm = mine.map(norm);
734
- var shared = mine.filter(function (u) { var x = byNorm[norm(u)]; return x && x.enabled !== false; });
750
+ // Only what came from AIOManager is its to refresh or remove (am.synced: the ones a sync
751
+ // added); an addon you added yourself stays yours, even when AIOManager has it too.
752
+ function fromIt(u) { return am.synced.indexOf(norm(u)) !== -1; }
753
+ var shared = mine.filter(function (u) { var x = byNorm[norm(u)]; return fromIt(u) && x && x.enabled !== false; });
735
754
  var add = there.filter(function (x) { return x.enabled !== false && mineNorm.indexOf(norm(x.url)) === -1; });
736
- var gone = mine.filter(function (u) { var x = byNorm[norm(u)]; return am.synced.indexOf(norm(u)) !== -1 && (!x || x.enabled === false); });
755
+ var gone = mine.filter(function (u) { var x = byNorm[norm(u)]; return fromIt(u) && (!x || x.enabled === false); });
737
756
  var items = [];
738
757
  if (shared.length) { items.push({ key: 'refresh', label: '↻ Refresh the ' + shared.length + ' addon' + (shared.length === 1 ? '' : 's') + ' you have from AIOManager' }); }
739
758
  add.forEach(function (x, i) { items.push({ key: 'add:' + i, label: '+ Add · ' + U.drawable(x.name || shortHost(x.url)).slice(0, 50) + ' · ' + shortHost(x.url) }); });
@@ -741,18 +760,18 @@
741
760
  var x = byNorm[norm(u)];
742
761
  items.push({ key: 'rm:' + i, label: '− Remove · ' + (x && x.name ? U.drawable(x.name).slice(0, 50) + ' · ' : '') + shortHost(u), note: x ? 'off in AIOManager' : 'gone from AIOManager' });
743
762
  });
744
- function done(n) {
745
- var keptUrls = Stremio.getAddonUrls().filter(function (u) { var x = byNorm[norm(u)]; return x && x.enabled !== false; });
746
- keptUrls.forEach(function (u) { if (!Stremio.addonOrigin(u)) { Stremio.setAddonOrigin(u, 'aiomanager'); } }); // (yours that AIOManager has too: from it from now on)
763
+ function done(n, addedUrls) { // what came from it: as before, and what this sync added (still in your list)
764
+ var now = (addedUrls || []).map(norm);
765
+ var keptUrls = Stremio.getAddonUrls().filter(function (u) { return fromIt(u) || now.indexOf(norm(u)) !== -1; });
747
766
  aiomSave({ synced: keptUrls.map(norm), at: Date.now(), count: there.length });
748
767
  if (n) { clearCatalogCache(); refreshAddonRows(null); }
749
768
  renderSettings();
750
769
  }
751
770
  if (!items.length) { U.toast('Nothing to sync: AIOManager has no addons StremioK can use', 4000); done(0); return; }
752
771
  pickMany('Sync with AIOManager: tick what to do', items, { button: function (n) { return 'Apply ' + n; } }, function (keys) {
753
- var added = 0, removed = 0, jobs = [];
772
+ var added = 0, removed = 0, addedUrls = [];
754
773
  keys.forEach(function (k) {
755
- if (k.indexOf('add:') === 0) { var got = Stremio.addAddon(add[+k.slice(4)].url); if (got) { added++; Stremio.setAddonOrigin(got, 'aiomanager'); } }
774
+ if (k.indexOf('add:') === 0) { var got = Stremio.addAddon(add[+k.slice(4)].url); if (got) { added++; addedUrls.push(got); Stremio.setAddonOrigin(got, 'aiomanager'); } }
756
775
  else if (k.indexOf('rm:') === 0) { Stremio.removeAddon(gone[+k.slice(3)]); removed++; }
757
776
  });
758
777
  var refreshing = keys.indexOf('refresh') !== -1 ? shared.filter(function (u) { return Stremio.getAddonUrls().indexOf(u) !== -1; }) : [];
@@ -762,7 +781,7 @@
762
781
  if (removed) { parts.push(removed + ' removed'); }
763
782
  if (refreshing.length) { parts.push(refreshed + ' refreshed' + (refreshed < refreshing.length ? ' (' + (refreshing.length - refreshed) + ' didn’t answer)' : '')); }
764
783
  U.toast('Synced: ' + (parts.join(', ') || 'nothing changed'), 4000);
765
- done(added + removed + refreshed);
784
+ done(added + removed + refreshed, addedUrls);
766
785
  });
767
786
  });
768
787
  }, function (e) {
@@ -888,10 +907,7 @@
888
907
  renderSettings();
889
908
  };
890
909
  }
891
- var up = ctl(U.el('div', 'feed-btn focusable', 'â–²'), 'up');
892
- up.__onselect = move(-1, 'up');
893
- var down = ctl(U.el('div', 'feed-btn focusable', 'â–¼'), 'down');
894
- down.__onselect = move(1, 'down');
910
+ var btns = orderButtons(function (dir, name) { move(dir, name)(); }, ctl);
895
911
  var item = ctl(U.el('div', 'addon-item focusable playlist-item'), 'item');
896
912
  item.appendChild(U.el('div', 'a-name', label + (playlist.off ? ' (off)' : '')));
897
913
  item.appendChild(U.el('div', 'a-url', U.redactUrl(playlist.url))); // (credentials masked)
@@ -903,8 +919,7 @@
903
919
  _iptvFocus = { id: playlist.id, ctl: 'item' };
904
920
  playlistActions(playlist, label);
905
921
  };
906
- frow.appendChild(up);
907
- frow.appendChild(down);
922
+ btns.forEach(function (b) { frow.appendChild(b); });
908
923
  frow.appendChild(item);
909
924
  wrap.appendChild(frow);
910
925
  rows.push({ p: playlist, info: info, problem: problem, idx: idx });
@@ -1090,15 +1105,12 @@
1090
1105
  list.forEach(function (x) {
1091
1106
  var frow = U.el('div', 'feed-row playlist-row' + (x.off ? ' feed-off' : ''));
1092
1107
  function ctl(node, name) { node.setAttribute('data-pl', x.id); node.setAttribute('data-ctl', name); return node; }
1093
- var up = ctl(U.el('div', 'feed-btn focusable', 'â–²'), 'up');
1094
- up.__onselect = function () { if (YouTube.move(x.id, -1)) { ytChanged({ id: x.id, ctl: 'up' }); } };
1095
- var down = ctl(U.el('div', 'feed-btn focusable', 'â–¼'), 'down');
1096
- down.__onselect = function () { if (YouTube.move(x.id, 1)) { ytChanged({ id: x.id, ctl: 'down' }); } };
1108
+ var btns = orderButtons(function (dir, name) { if (YouTube.move(x.id, dir)) { ytChanged({ id: x.id, ctl: name }); } }, ctl);
1097
1109
  var item = ctl(U.el('div', 'addon-item focusable playlist-item yt-catalog'), 'item');
1098
1110
  item.appendChild(U.el('div', 'a-name', x.name + (x.off ? ' (off)' : '')));
1099
1111
  item.appendChild(U.el('div', 'a-url', ytDescribe(x)));
1100
1112
  item.__onselect = function () { ytActions(x); };
1101
- frow.appendChild(up); frow.appendChild(down); frow.appendChild(item);
1113
+ btns.forEach(function (b) { frow.appendChild(b); }); frow.appendChild(item);
1102
1114
  wrap.appendChild(frow);
1103
1115
  });
1104
1116
 
@@ -1347,14 +1359,17 @@
1347
1359
  // unused fails the first time while it wakes up).
1348
1360
  var ADDON_WAITS = [['0', 'Normal (12 s; sources 15, search 25)'], ['20', '20 seconds'], ['30', '30 seconds'], ['45', '45 seconds'], ['60', '60 seconds']];
1349
1361
  var NEXT_CHOICES = [['0', 'Ask'], ['5', 'After 5 seconds'], ['10', 'After 10 seconds'], ['15', 'After 15 seconds'], ['30', 'After 30 seconds']];
1350
- // Seeking & buffering: how far LEFT / RIGHT jump; a bigger buffer for slow connections.
1362
+ // Seeking & buffering: how far LEFT / RIGHT jump; a bigger buffer for slow connections; how
1363
+ // often a live channel that drops is opened again.
1364
+ var RECONNECTS = [['off', 'Off'], ['few', 'A few times (3 in 5 minutes)'], ['always', 'Keep trying']];
1351
1365
  function settingsSeeking(wrap) {
1352
1366
  wrap.appendChild(U.el('h2', null, 'Seeking & buffering'));
1353
- var store = { get: function () { var c = Player.playerCfg(); return { seek: '' + c.seek, buffer: c.buffer }; },
1354
- set: function (cfg) { Player.setPlayerCfg({ seek: +cfg.seek || 10, buffer: cfg.buffer }); } };
1367
+ var store = { get: function () { var c = Player.playerCfg(); return { seek: '' + c.seek, buffer: c.buffer, reconnect: c.reconnect }; },
1368
+ set: function (cfg) { Player.setPlayerCfg({ seek: +cfg.seek || 10, buffer: cfg.buffer, reconnect: cfg.reconnect }); } };
1355
1369
  wrap.appendChild(subPickRow('Seek step', [['5', '5 s'], ['10', '10 s'], ['15', '15 s'], ['30', '30 s']], 'seek', store));
1356
1370
  wrap.appendChild(subPickRow('Buffer', [['normal', 'Normal (the TV’s own)'], ['more', 'More'], ['most', 'Most']], 'buffer', store));
1357
- wrap.appendChild(U.el('div', 'hint', 'Seek step: how far LEFT / RIGHT jump (FF / REW jump three steps). Buffer: More or Most starts a little slower but stops less on a slow connection, from the next video you play.'));
1371
+ wrap.appendChild(subPickRow('Reconnect live channels', RECONNECTS, 'reconnect', store));
1372
+ wrap.appendChild(U.el('div', 'hint', 'Seek step: how far LEFT / RIGHT jump (FF / REW jump three steps). Buffer: More or Most starts a little slower but stops less on a slow connection, from the next video you play. Reconnect: a live channel that drops opens again; Keep trying waits a little longer each time (BACK stops it).'));
1358
1373
  focusSettings();
1359
1374
  }
1360
1375
  function settingsSkipping(wrap) {
@@ -1962,7 +1977,20 @@
1962
1977
  var SOURCE_ROWS = { prefix: 'src:', off: 'off', keyOf: function (x) { return x.key; },
1963
1978
  move: function (k, d) { return Stremio.moveFeedSource(k, d); }, toggle: function (k) { Stremio.toggleFeedSource(k); },
1964
1979
  hidden: function (k) { return Stremio.isFeedSourceOff(k); } };
1965
- // â–² / â–¼ / name rows for an ordered list (L: keyOf, hidden, move, toggle, off, prefix).
1980
+ // ⇈ ▲ ▼ ⇊ for a row of a list you put in order: to the top, one place up, one down, to the
1981
+ // bottom. move(dir, ctl) moves it (dir -Infinity / -1 / 1 / Infinity: see U.moveIn); mark(node,
1982
+ // ctl) tags each button, so the page can focus the one used once it's drawn again.
1983
+ var ORDER_BTNS = [['top', '⇈', -Infinity], ['up', '▲', -1], ['down', '▼', 1], ['bottom', '⇊', Infinity]];
1984
+ function orderButtons(move, mark) {
1985
+ return ORDER_BTNS.map(function (b) {
1986
+ var n = U.el('div', 'feed-btn focusable', b[1]);
1987
+ if (mark) { mark(n, b[0]); }
1988
+ n.__onselect = function () { move(b[2], b[0]); };
1989
+ return n;
1990
+ });
1991
+ }
1992
+ function ctlIndex(ctl) { for (var i = 0; i < ORDER_BTNS.length; i++) { if (ORDER_BTNS[i][0] === ctl) { return i; } } return 1; }
1993
+ // ⇈ ▲ ▼ ⇊ / name rows for an ordered list (L: keyOf, hidden, move, toggle, off, prefix).
1966
1994
  // After a change the page is drawn again, focused on the control used (refeed).
1967
1995
  function feedRows(wrap, kind, items, L) {
1968
1996
  items.forEach(function (item) {
@@ -1970,20 +1998,14 @@
1970
1998
  var hidden = L.hidden(key);
1971
1999
  var frow = U.el('div', 'feed-row' + (hidden ? ' feed-off' : ''));
1972
2000
 
1973
- var up = U.el('div', 'feed-btn focusable', 'â–²');
1974
- up.setAttribute('data-fk', fk); up.setAttribute('data-ctl', 'up');
1975
- up.__onselect = function () { L.move(key, -1).then(function () { refeed(kind, fk, 'up'); }); };
1976
-
1977
- var down = U.el('div', 'feed-btn focusable', 'â–¼');
1978
- down.setAttribute('data-fk', fk); down.setAttribute('data-ctl', 'down');
1979
- down.__onselect = function () { L.move(key, 1).then(function () { refeed(kind, fk, 'down'); }); };
2001
+ var btns = orderButtons(function (dir, ctl) { L.move(key, dir).then(function () { refeed(kind, fk, ctl); }); },
2002
+ function (n, ctl) { n.setAttribute('data-fk', fk); n.setAttribute('data-ctl', ctl); });
1980
2003
 
1981
2004
  var name = U.el('div', 'feed-name focusable', item.name + (hidden ? ' (' + (L.off || 'hidden') + ')' : ''));
1982
2005
  name.setAttribute('data-fk', fk); name.setAttribute('data-ctl', 'name');
1983
2006
  name.__onselect = function () { L.toggle(key); refeed(kind, fk, 'name'); };
1984
2007
 
1985
- frow.appendChild(up);
1986
- frow.appendChild(down);
2008
+ btns.forEach(function (b) { frow.appendChild(b); });
1987
2009
  frow.appendChild(name);
1988
2010
  wrap.appendChild(frow);
1989
2011
  });
package/js/youtube.js CHANGED
@@ -130,9 +130,7 @@ var YouTube = (function () {
130
130
  function move(id, dir) {
131
131
  var c = cfg(), i = -1;
132
132
  c.catalogs.forEach(function (x, n) { if (x.id === id) { i = n; } });
133
- var j = i + dir;
134
- if (i < 0 || j < 0 || j >= c.catalogs.length) { return false; }
135
- var t = c.catalogs[i]; c.catalogs[i] = c.catalogs[j]; c.catalogs[j] = t;
133
+ if (!U.moveIn(c.catalogs, i, dir)) { return false; } // (one place, or to the top / bottom)
136
134
  save(c);
137
135
  return true;
138
136
  }
package/js/ytplay.js CHANGED
@@ -71,6 +71,15 @@ var YTPlay = (function () {
71
71
  return U.getJSON(PROXY + q + a.q, force ? 5000 : 30000, 65536, null, a.h);
72
72
  });
73
73
  }
74
+ // An HLS stream's quality list, read by the service with the stream's own User-Agent /
75
+ // Referer (h: { ua, ref }), which a web page can't send: its text.
76
+ function hlsMaster(url, h) {
77
+ if (MODE === 'browser') { return Promise.reject(new Error('no service')); }
78
+ return ensureService().then(entryAuth).then(function (a) {
79
+ var q = '/live/master?u=' + encodeURIComponent(url) + (h && h.ua ? '&ua=' + encodeURIComponent(h.ua) : '') + (h && h.ref ? '&ref=' + encodeURIComponent(h.ref) : '');
80
+ return U.getText(PROXY + q + a.q, 12000, 524288, null, false, a.h);
81
+ });
82
+ }
74
83
  // An AIOManager account's addons (its Hydra API, through the service: an instance only
75
84
  // answers the web origins it's set to): { addons: [{ url, name, enabled }] }. base: its
76
85
  // address; key: the account's API key (a header, never in the link).
@@ -172,6 +181,6 @@ var YTPlay = (function () {
172
181
  return MODE === 'tizenbrew' ? 'TizenBrew module' : MODE === 'wgt' ? 'Sideloaded app (.wgt)' : 'Browser (development)';
173
182
  }
174
183
 
175
- return { ensureService: ensureService, resolverUrl: resolverUrl, infoUrl: infoUrl, canPlay: canPlay, liveOpen: liveOpen, aiomAddons: aiomAddons, entryOpen: entryOpen, entryPoll: entryPoll, entryClose: entryClose, subsUrl: subsUrl, audioLang: audioLang, setAudioLang: setAudioLang, installMode: function () { return MODE; },
184
+ return { ensureService: ensureService, resolverUrl: resolverUrl, infoUrl: infoUrl, canPlay: canPlay, liveOpen: liveOpen, hlsMaster: hlsMaster, aiomAddons: aiomAddons, entryOpen: entryOpen, entryPoll: entryPoll, entryClose: entryClose, subsUrl: subsUrl, audioLang: audioLang, setAudioLang: setAudioLang, installMode: function () { return MODE; },
176
185
  installLabel: installLabel, unavailableMessage: unavailableMessage, isRunning: isRunning, port: PORT };
177
186
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stremiok",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "StremioK streaming client for Tizen 3.0+ Samsung TVs (TizenBrew app module).",
5
5
  "packageType": "app",
6
6
  "appName": "StremioK",