stremiok 2.0.0 → 2.2.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/CHANGELOG.md +82 -0
- package/README.md +10 -8
- package/config.xml +1 -1
- package/css/style.css +37 -2
- package/index.html +34 -31
- package/js/app.js +19 -7
- package/js/downloads.js +17 -5
- package/js/epg.js +320 -0
- package/js/hubpreview.js +15 -5
- package/js/iptv.js +202 -29
- package/js/keys.js +2 -0
- package/js/player-controls.js +55 -8
- package/js/player-core.js +204 -31
- package/js/player-lang.js +1 -1
- package/js/player-quality.js +15 -7
- package/js/player-store.js +28 -4
- package/js/player-subs.js +45 -11
- package/js/player.js +1 -1
- package/js/stremio-account.js +3 -0
- package/js/stremio.js +157 -28
- package/js/trakt.js +61 -9
- package/js/util.js +36 -10
- package/js/view-core.js +139 -55
- package/js/view-detail.js +5 -4
- package/js/view-guide.js +332 -0
- package/js/view-home.js +168 -36
- package/js/view-settings.js +170 -80
- package/js/views.js +3 -2
- package/js/youtube.js +1 -3
- package/js/ytplay.js +62 -5
- package/package.json +2 -1
- package/service/index.js +1131 -108
- package/service/preview.js +14 -4
package/js/view-settings.js
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
function markHomeDirty() { return V.markHomeDirty.apply(null, arguments); } // home
|
|
18
18
|
function onModalClose() { return V.onModalClose.apply(null, arguments); } // core
|
|
19
19
|
function pickList() { return V.pickList.apply(null, arguments); } // core
|
|
20
|
-
function toggleList() { return V.toggleList.apply(null, arguments); } // core
|
|
21
20
|
function playlistActions() { return V.playlistActions.apply(null, arguments); } // core
|
|
22
21
|
function scr() { return V.scr.apply(null, arguments); } // core
|
|
23
22
|
function showModal() { return V.showModal.apply(null, arguments); } // core
|
|
@@ -207,14 +206,16 @@
|
|
|
207
206
|
get: function () { return { spoilers: Stremio.getPrefs().spoilers }; },
|
|
208
207
|
set: function (cfg) { Stremio.setPref('spoilers', cfg.spoilers); }
|
|
209
208
|
}));
|
|
210
|
-
|
|
209
|
+
var menu = sectionRow('Menu ›', 'Order the top menu, and show or hide its tabs', 'menuorder');
|
|
210
|
+
menu.className += ' cfg-row';
|
|
211
|
+
wrap.appendChild(menu);
|
|
211
212
|
var feeds = sectionRow('Home feeds ›', 'Order the Home rows, and show or hide each one', 'feeds');
|
|
212
213
|
feeds.className += ' cfg-row';
|
|
213
214
|
wrap.appendChild(feeds);
|
|
214
215
|
var sfeeds = sectionRow('Search results ›', 'Order the Search rows (one per catalog that can search), and show or hide each one', 'searchfeeds');
|
|
215
216
|
sfeeds.className += ' cfg-row';
|
|
216
217
|
wrap.appendChild(sfeeds);
|
|
217
|
-
var back = _focusChild === 'feeds' ? feeds : _focusChild === 'searchfeeds' ? sfeeds : null;
|
|
218
|
+
var back = _focusChild === 'menuorder' ? menu : _focusChild === 'feeds' ? feeds : _focusChild === 'searchfeeds' ? sfeeds : null;
|
|
218
219
|
_focusChild = null;
|
|
219
220
|
if (back) { Focus.setScope(scr('settings'), back); } else { focusSettings(); }
|
|
220
221
|
}
|
|
@@ -323,7 +324,7 @@
|
|
|
323
324
|
['Audio', 'Preferred and second-choice audio language', 'audio'],
|
|
324
325
|
['Subtitles', 'Default language, size, color, background, position, font', 'subtitles'],
|
|
325
326
|
['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'],
|
|
327
|
+
['Seeking & buffering', 'How far ◀ ▶ jump; a bigger buffer for slow connections; reconnecting live channels', 'seeking'],
|
|
327
328
|
['Source ranking', 'How sources are ordered (played before, resolution, release type…), and your addons: their order, on or off', 'ranking'],
|
|
328
329
|
['Formats', 'Allowed resolutions, and what plays on this TV, learned as you watch', 'formats'],
|
|
329
330
|
['Private viewing', 'Play without anything recorded: no progress, Continue Watching, watched marks or Trakt', 'private']] },
|
|
@@ -337,7 +338,7 @@
|
|
|
337
338
|
// BACK goes up one level, onto the row it came from: from a section to its group (on
|
|
338
339
|
// that section, not the first one), from Home feeds / Search results to Appearance,
|
|
339
340
|
// from a group to the Settings list (on that group).
|
|
340
|
-
var SECTION_PARENT = { feeds: 'appearance', searchfeeds: 'appearance', aiomanager: 'addons' };
|
|
341
|
+
var SECTION_PARENT = { menuorder: 'appearance', feeds: 'appearance', searchfeeds: 'appearance', aiomanager: 'addons' };
|
|
341
342
|
Object.keys(SETTINGS_GROUPS).forEach(function (g) {
|
|
342
343
|
SETTINGS_GROUPS[g].rows.forEach(function (row) { SECTION_PARENT[row[2]] = g; });
|
|
343
344
|
});
|
|
@@ -366,6 +367,7 @@
|
|
|
366
367
|
else if (_settingsSection === 'subtitles') { settingsSubtitles(wrap); }
|
|
367
368
|
else if (_settingsSection === 'skipping') { settingsSkipping(wrap); }
|
|
368
369
|
else if (_settingsSection === 'seeking') { settingsSeeking(wrap); }
|
|
370
|
+
else if (_settingsSection === 'menuorder') { settingsFeeds(wrap, 'menu'); }
|
|
369
371
|
else if (_settingsSection === 'feeds') { settingsFeeds(wrap, 'home'); }
|
|
370
372
|
else if (_settingsSection === 'searchfeeds') { settingsFeeds(wrap, 'search'); }
|
|
371
373
|
else if (_settingsSection === 'ranking') { settingsFeeds(wrap, 'ranking'); }
|
|
@@ -470,6 +472,24 @@
|
|
|
470
472
|
});
|
|
471
473
|
return 'Gives: ' + (bits.length ? bits.join(' · ') : 'nothing StremioK uses') + (types.length ? ' · ' + types.slice(0, 8).join(', ') : '');
|
|
472
474
|
}
|
|
475
|
+
// An addon's logo tile: its initial (the first character, an emoji whole; "?" for one that
|
|
476
|
+
// didn't load; drawn by the CSS from data-initial, so the row's text is still its name),
|
|
477
|
+
// and over it the logo from its manifest (Stremio.addonLogo), loaded with the other
|
|
478
|
+
// artwork; one that can't load is taken away, leaving the initial.
|
|
479
|
+
function addonTile(m, url) {
|
|
480
|
+
var name = ('' + ((m && m.name) || '')).replace(/^\s+/, ''), c = name.charCodeAt(0);
|
|
481
|
+
var tile = U.el('div', 'addon-logo');
|
|
482
|
+
tile.setAttribute('data-initial', !m ? '?' : name ? name.slice(0, c >= 0xD800 && c <= 0xDBFF ? 2 : 1).toUpperCase() : 'A');
|
|
483
|
+
var src = Stremio.addonLogo ? Stremio.addonLogo(m, url) : null;
|
|
484
|
+
if (src) {
|
|
485
|
+
var img = document.createElement('img');
|
|
486
|
+
img.onload = function () { tile.className = 'addon-logo loaded'; };
|
|
487
|
+
img.onerror = function () { if (img.parentNode) { img.parentNode.removeChild(img); } tile.className = 'addon-logo'; };
|
|
488
|
+
tile.appendChild(img);
|
|
489
|
+
U.deferImage(img, src);
|
|
490
|
+
}
|
|
491
|
+
return tile;
|
|
492
|
+
}
|
|
473
493
|
function settingsAddons(wrap) {
|
|
474
494
|
wrap.appendChild(U.el('h2', null, 'Addons'));
|
|
475
495
|
// Home-network links from addons on the internet (see Stremio.blocksLocal)
|
|
@@ -489,16 +509,14 @@
|
|
|
489
509
|
var urls = Stremio.getAddonUrls(), want = null;
|
|
490
510
|
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
511
|
urls.forEach(function (url) {
|
|
492
|
-
var
|
|
493
|
-
var
|
|
494
|
-
|
|
495
|
-
var
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
down.__onselect = move(1, 'down');
|
|
499
|
-
var item = U.el('div', 'addon-item focusable playlist-item');
|
|
512
|
+
var off = !!(Stremio.isAddonOff && Stremio.isAddonOff(url)), kept = off && Stremio.savedManifest ? Stremio.savedManifest(url) : null;
|
|
513
|
+
var loaded = addons.find(function (a) { return a.url === url; }) || (kept ? { url: url, manifest: kept } : null); // (one that's off: its saved manifest)
|
|
514
|
+
var frow = U.el('div', 'feed-row playlist-row' + (off ? ' feed-off' : ''));
|
|
515
|
+
var btns = orderButtons(function (dir, ctl) { if (Stremio.moveAddon(url, dir)) { _addonFocus = { url: url, ctl: ctl }; refreshAddonRows(); renderSettings(); } });
|
|
516
|
+
var item = U.el('div', 'addon-item focusable playlist-item has-logo');
|
|
517
|
+
item.appendChild(addonTile(loaded && loaded.manifest, url));
|
|
500
518
|
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 : '') + ')');
|
|
519
|
+
var nm = U.el('div', 'a-name', (loaded ? (loaded.manifest.name || 'Addon') : off ? 'Addon' : '(failed to load' + (why ? ': ' + why : '') + ')') + (off ? ' (off)' : ''));
|
|
502
520
|
var origin = Stremio.addonOrigin ? Stremio.addonOrigin(url) : '', ob = originBadge(origin);
|
|
503
521
|
if (ob) { nm.appendChild(ob); }
|
|
504
522
|
item.appendChild(nm);
|
|
@@ -512,10 +530,10 @@
|
|
|
512
530
|
item.__onselect = function () {
|
|
513
531
|
addonActions(name, url);
|
|
514
532
|
};
|
|
515
|
-
if (urls.length > 1) {
|
|
533
|
+
if (urls.length > 1) { btns.forEach(function (b) { frow.appendChild(b); }); }
|
|
516
534
|
frow.appendChild(item);
|
|
517
535
|
wrap.appendChild(frow);
|
|
518
|
-
if (_addonFocus && _addonFocus.url === url) { want = _addonFocus.ctl
|
|
536
|
+
if (_addonFocus && _addonFocus.url === url) { want = btns[ctlIndex(_addonFocus.ctl)]; }
|
|
519
537
|
});
|
|
520
538
|
|
|
521
539
|
var addRow = U.el('div', 'addon-item focusable add-row');
|
|
@@ -731,9 +749,12 @@
|
|
|
731
749
|
var byNorm = {};
|
|
732
750
|
there.forEach(function (x) { byNorm[norm(x.url)] = x; });
|
|
733
751
|
var mine = Stremio.getAddonUrls(), mineNorm = mine.map(norm);
|
|
734
|
-
|
|
752
|
+
// Only what came from AIOManager is its to refresh or remove (am.synced: the ones a sync
|
|
753
|
+
// added); an addon you added yourself stays yours, even when AIOManager has it too.
|
|
754
|
+
function fromIt(u) { return am.synced.indexOf(norm(u)) !== -1; }
|
|
755
|
+
var shared = mine.filter(function (u) { var x = byNorm[norm(u)]; return fromIt(u) && x && x.enabled !== false; });
|
|
735
756
|
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
|
|
757
|
+
var gone = mine.filter(function (u) { var x = byNorm[norm(u)]; return fromIt(u) && (!x || x.enabled === false); });
|
|
737
758
|
var items = [];
|
|
738
759
|
if (shared.length) { items.push({ key: 'refresh', label: '↻ Refresh the ' + shared.length + ' addon' + (shared.length === 1 ? '' : 's') + ' you have from AIOManager' }); }
|
|
739
760
|
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 +762,18 @@
|
|
|
741
762
|
var x = byNorm[norm(u)];
|
|
742
763
|
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
764
|
});
|
|
744
|
-
function done(n) {
|
|
745
|
-
var
|
|
746
|
-
keptUrls.
|
|
765
|
+
function done(n, addedUrls) { // what came from it: as before, and what this sync added (still in your list)
|
|
766
|
+
var now = (addedUrls || []).map(norm);
|
|
767
|
+
var keptUrls = Stremio.getAddonUrls().filter(function (u) { return fromIt(u) || now.indexOf(norm(u)) !== -1; });
|
|
747
768
|
aiomSave({ synced: keptUrls.map(norm), at: Date.now(), count: there.length });
|
|
748
769
|
if (n) { clearCatalogCache(); refreshAddonRows(null); }
|
|
749
770
|
renderSettings();
|
|
750
771
|
}
|
|
751
772
|
if (!items.length) { U.toast('Nothing to sync: AIOManager has no addons StremioK can use', 4000); done(0); return; }
|
|
752
773
|
pickMany('Sync with AIOManager: tick what to do', items, { button: function (n) { return 'Apply ' + n; } }, function (keys) {
|
|
753
|
-
var added = 0, removed = 0,
|
|
774
|
+
var added = 0, removed = 0, addedUrls = [];
|
|
754
775
|
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'); } }
|
|
776
|
+
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
777
|
else if (k.indexOf('rm:') === 0) { Stremio.removeAddon(gone[+k.slice(3)]); removed++; }
|
|
757
778
|
});
|
|
758
779
|
var refreshing = keys.indexOf('refresh') !== -1 ? shared.filter(function (u) { return Stremio.getAddonUrls().indexOf(u) !== -1; }) : [];
|
|
@@ -762,7 +783,7 @@
|
|
|
762
783
|
if (removed) { parts.push(removed + ' removed'); }
|
|
763
784
|
if (refreshing.length) { parts.push(refreshed + ' refreshed' + (refreshed < refreshing.length ? ' (' + (refreshing.length - refreshed) + ' didn’t answer)' : '')); }
|
|
764
785
|
U.toast('Synced: ' + (parts.join(', ') || 'nothing changed'), 4000);
|
|
765
|
-
done(added + removed + refreshed);
|
|
786
|
+
done(added + removed + refreshed, addedUrls);
|
|
766
787
|
});
|
|
767
788
|
});
|
|
768
789
|
}, function (e) {
|
|
@@ -848,6 +869,15 @@
|
|
|
848
869
|
var _iptvFocus = null; // { id, ctl } to focus once the page is drawn again
|
|
849
870
|
function two(n) { return (n < 10 ? '0' : '') + n; }
|
|
850
871
|
function thousands(n) { return ('' + n).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
|
|
872
|
+
// The playlist rows IPTV Settings drew last: their guide lines kept current by one listener
|
|
873
|
+
// (one added each time it was drawn used to pile up, each holding its rows).
|
|
874
|
+
var _guideRows = [];
|
|
875
|
+
if (typeof EPG !== 'undefined' && EPG.onChange) {
|
|
876
|
+
EPG.onChange(function () {
|
|
877
|
+
_guideRows = _guideRows.filter(function (r) { return document.documentElement.contains(r.info); });
|
|
878
|
+
_guideRows.forEach(function (r) { r.info.textContent = playlistInfo(r.p); });
|
|
879
|
+
});
|
|
880
|
+
}
|
|
851
881
|
function playlistInfo(p) {
|
|
852
882
|
if (p.off) { return 'Off — its channels aren’t shown'; }
|
|
853
883
|
var st = Stremio.iptvStatus(p.id), parts = [];
|
|
@@ -856,13 +886,65 @@
|
|
|
856
886
|
if (st.fetchedAt) { var d = new Date(st.fetchedAt); parts.push('downloaded ' + U.dayLabel(st.fetchedAt) + ' ' + two(d.getHours()) + ':' + two(d.getMinutes())); }
|
|
857
887
|
if (st.dropped) { parts.push(thousands(st.dropped) + ' more left out (a playlist keeps its first 30,000)'); }
|
|
858
888
|
if (st.uncached) { parts.push('too large to save on the TV: downloaded each launch'); }
|
|
889
|
+
var g = guideInfo(p);
|
|
890
|
+
if (g) { parts.push(g); }
|
|
859
891
|
return parts.join(' · ');
|
|
860
892
|
}
|
|
893
|
+
// Its TV guide (js/epg.js): being read, read (for how many of its channels), or why not.
|
|
894
|
+
function guideInfo(p) {
|
|
895
|
+
var g = typeof EPG !== 'undefined' && EPG.status ? EPG.status(p.id) : null;
|
|
896
|
+
if (!g) { return typeof EPG !== 'undefined' && EPG.available && EPG.available() ? 'no TV guide' : ''; }
|
|
897
|
+
if (g.state === 'ready') {
|
|
898
|
+
var d = new Date(g.at);
|
|
899
|
+
return 'TV guide: ' + thousands(g.channels) + ' channel' + (g.channels === 1 ? '' : 's') + ', ' + thousands(g.programmes) + ' programs' + (g.partial ? ' (part of it: too large)' : '') +
|
|
900
|
+
(g.at ? ', read ' + U.dayLabel(g.at) + ' ' + two(d.getHours()) + ':' + two(d.getMinutes()) : '') + (g.error ? ' (the last read failed: ' + g.error + ')' : '');
|
|
901
|
+
}
|
|
902
|
+
if (g.state === 'failed') { return '⚠ TV guide: ' + (g.error || 'couldn’t be read'); }
|
|
903
|
+
return 'TV guide: reading…';
|
|
904
|
+
}
|
|
861
905
|
function playlistProblem(p) {
|
|
862
906
|
var st = Stremio.iptvStatus(p.id);
|
|
863
907
|
if (p.off || !st.error) { return ''; }
|
|
864
908
|
return '⚠ ' + (st.stale ? 'Couldn’t refresh it (showing the saved copy): ' : '') + st.error;
|
|
865
909
|
}
|
|
910
|
+
// + Add Xtream account: its server, username and password, a page each (a whole Xtream link
|
|
911
|
+
// in the server's fills the rest); checked with the server, then added (Stremio.iptvAddXtream).
|
|
912
|
+
function addXtream() {
|
|
913
|
+
function done(base, user, pass) {
|
|
914
|
+
U.toast('Checking the account…', 15000);
|
|
915
|
+
IPTV.xtreamCheck(base, user, pass).then(function (r) {
|
|
916
|
+
if (r.unreachable) { U.toast('Couldn’t reach the server: check its address (nothing was saved)', 5000); return; }
|
|
917
|
+
if (r.refused) { U.toast('The server refused that username and password' + (r.status ? ' (' + r.status + ')' : '') + ': nothing was saved', 5000); return; }
|
|
918
|
+
Stremio.iptvAddXtream(base, user, pass);
|
|
919
|
+
refreshAddonRows('local:iptv');
|
|
920
|
+
U.toast('Xtream account added: reading its channels and TV guide…', 4000);
|
|
921
|
+
renderSettings();
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
App.openEntry({
|
|
925
|
+
title: 'Xtream account: server', phone: 'playlist',
|
|
926
|
+
hint: 'The server address your provider gave you (http://line.example.com:8080), or a whole Xtream link. RETURN cancels.',
|
|
927
|
+
value: 'http://', placeholder: 'http://line.example.com:8080',
|
|
928
|
+
validate: function (v) { return IPTV.xtreamParts(v) ? null : 'That isn’t a server address'; },
|
|
929
|
+
onConfirm: function (v) {
|
|
930
|
+
var x = IPTV.xtreamParts(v);
|
|
931
|
+
App.back();
|
|
932
|
+
if (x.user && x.pass) { done(x.base, x.user, x.pass); return; }
|
|
933
|
+
App.openEntry({
|
|
934
|
+
title: 'Xtream account: username', hint: 'Your account’s username. RETURN cancels.', value: x.user, placeholder: 'username',
|
|
935
|
+
validate: function (u) { return ('' + u).trim() ? null : 'Type the username'; },
|
|
936
|
+
onConfirm: function (u) {
|
|
937
|
+
App.back();
|
|
938
|
+
App.openEntry({
|
|
939
|
+
title: 'Xtream account: password', hint: 'Your account’s password (kept on the TV, in the playlist’s link). RETURN cancels.', value: '', placeholder: 'password', secret: true,
|
|
940
|
+
validate: function (p) { return ('' + p) ? null : 'Type the password'; },
|
|
941
|
+
onConfirm: function (p) { App.back(); done(x.base, ('' + u).trim(), '' + p); }
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
}
|
|
866
948
|
function settingsIptv(wrap) {
|
|
867
949
|
wrap.appendChild(U.el('h2', null, 'IPTV'));
|
|
868
950
|
var playlists = Stremio.iptvGetPlaylists ? Stremio.iptvGetPlaylists() : [];
|
|
@@ -888,10 +970,7 @@
|
|
|
888
970
|
renderSettings();
|
|
889
971
|
};
|
|
890
972
|
}
|
|
891
|
-
var
|
|
892
|
-
up.__onselect = move(-1, 'up');
|
|
893
|
-
var down = ctl(U.el('div', 'feed-btn focusable', '▼'), 'down');
|
|
894
|
-
down.__onselect = move(1, 'down');
|
|
973
|
+
var btns = orderButtons(function (dir, name) { move(dir, name)(); }, ctl);
|
|
895
974
|
var item = ctl(U.el('div', 'addon-item focusable playlist-item'), 'item');
|
|
896
975
|
item.appendChild(U.el('div', 'a-name', label + (playlist.off ? ' (off)' : '')));
|
|
897
976
|
item.appendChild(U.el('div', 'a-url', U.redactUrl(playlist.url))); // (credentials masked)
|
|
@@ -903,13 +982,13 @@
|
|
|
903
982
|
_iptvFocus = { id: playlist.id, ctl: 'item' };
|
|
904
983
|
playlistActions(playlist, label);
|
|
905
984
|
};
|
|
906
|
-
frow.appendChild(
|
|
907
|
-
frow.appendChild(down);
|
|
985
|
+
btns.forEach(function (b) { frow.appendChild(b); });
|
|
908
986
|
frow.appendChild(item);
|
|
909
987
|
wrap.appendChild(frow);
|
|
910
988
|
rows.push({ p: playlist, info: info, problem: problem, idx: idx });
|
|
911
989
|
});
|
|
912
990
|
|
|
991
|
+
_guideRows = rows; // (their guide lines kept current as guides are read: _guideRows)
|
|
913
992
|
if (statusEl) {
|
|
914
993
|
Stremio.iptvLoad().then(function () {
|
|
915
994
|
var channels = 0, groups = 0, failed = 0, on = 0;
|
|
@@ -947,6 +1026,11 @@
|
|
|
947
1026
|
});
|
|
948
1027
|
};
|
|
949
1028
|
wrap.appendChild(add);
|
|
1029
|
+
var xt = U.el('div', 'addon-item focusable add-row');
|
|
1030
|
+
xt.appendChild(U.el('div', 'a-name', '+ Add Xtream account'));
|
|
1031
|
+
xt.appendChild(U.el('div', 'a-url', 'Its server, username and password: its channels and TV guide added'));
|
|
1032
|
+
xt.__onselect = addXtream;
|
|
1033
|
+
wrap.appendChild(xt);
|
|
950
1034
|
|
|
951
1035
|
wrap.appendChild(U.el('div', 'hint',
|
|
952
1036
|
'Channels are grouped into IPTV and Radio rows on Home. ▲ / ▼ set the playlists\' order: a channel in two playlists comes from the first.'));
|
|
@@ -1090,15 +1174,12 @@
|
|
|
1090
1174
|
list.forEach(function (x) {
|
|
1091
1175
|
var frow = U.el('div', 'feed-row playlist-row' + (x.off ? ' feed-off' : ''));
|
|
1092
1176
|
function ctl(node, name) { node.setAttribute('data-pl', x.id); node.setAttribute('data-ctl', name); return node; }
|
|
1093
|
-
var
|
|
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' }); } };
|
|
1177
|
+
var btns = orderButtons(function (dir, name) { if (YouTube.move(x.id, dir)) { ytChanged({ id: x.id, ctl: name }); } }, ctl);
|
|
1097
1178
|
var item = ctl(U.el('div', 'addon-item focusable playlist-item yt-catalog'), 'item');
|
|
1098
1179
|
item.appendChild(U.el('div', 'a-name', x.name + (x.off ? ' (off)' : '')));
|
|
1099
1180
|
item.appendChild(U.el('div', 'a-url', ytDescribe(x)));
|
|
1100
1181
|
item.__onselect = function () { ytActions(x); };
|
|
1101
|
-
|
|
1182
|
+
btns.forEach(function (b) { frow.appendChild(b); }); frow.appendChild(item);
|
|
1102
1183
|
wrap.appendChild(frow);
|
|
1103
1184
|
});
|
|
1104
1185
|
|
|
@@ -1347,14 +1428,17 @@
|
|
|
1347
1428
|
// unused fails the first time while it wakes up).
|
|
1348
1429
|
var ADDON_WAITS = [['0', 'Normal (12 s; sources 15, search 25)'], ['20', '20 seconds'], ['30', '30 seconds'], ['45', '45 seconds'], ['60', '60 seconds']];
|
|
1349
1430
|
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
|
|
1431
|
+
// Seeking & buffering: how far LEFT / RIGHT jump; a bigger buffer for slow connections; how
|
|
1432
|
+
// often a live channel that drops is opened again.
|
|
1433
|
+
var RECONNECTS = [['off', 'Off'], ['few', 'A few times (3 in 5 minutes)'], ['always', 'Keep trying']];
|
|
1351
1434
|
function settingsSeeking(wrap) {
|
|
1352
1435
|
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 }); } };
|
|
1436
|
+
var store = { get: function () { var c = Player.playerCfg(); return { seek: '' + c.seek, buffer: c.buffer, reconnect: c.reconnect }; },
|
|
1437
|
+
set: function (cfg) { Player.setPlayerCfg({ seek: +cfg.seek || 10, buffer: cfg.buffer, reconnect: cfg.reconnect }); } };
|
|
1355
1438
|
wrap.appendChild(subPickRow('Seek step', [['5', '5 s'], ['10', '10 s'], ['15', '15 s'], ['30', '30 s']], 'seek', store));
|
|
1356
1439
|
wrap.appendChild(subPickRow('Buffer', [['normal', 'Normal (the TV’s own)'], ['more', 'More'], ['most', 'Most']], 'buffer', store));
|
|
1357
|
-
wrap.appendChild(
|
|
1440
|
+
wrap.appendChild(subPickRow('Reconnect live channels', RECONNECTS, 'reconnect', store));
|
|
1441
|
+
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
1442
|
focusSettings();
|
|
1359
1443
|
}
|
|
1360
1444
|
function settingsSkipping(wrap) {
|
|
@@ -1371,34 +1455,6 @@
|
|
|
1371
1455
|
focusSettings();
|
|
1372
1456
|
}
|
|
1373
1457
|
|
|
1374
|
-
// Home tabs: hide the ones you never use (Watching always stays: it's where Home opens). A
|
|
1375
|
-
// row like the ones above it; OK opens the tabs, OK on one shows or hides it (the list
|
|
1376
|
-
// stays open). A hidden tab's rows are back when it's shown again.
|
|
1377
|
-
function homeTabsRow() {
|
|
1378
|
-
function hiddenNow() { return Stremio.getPrefs().hiddenTabs || []; }
|
|
1379
|
-
var r = U.el('div', 'addon-item focusable cfg-row'), name = U.el('div', 'a-name');
|
|
1380
|
-
var tabs = (V.HOME_TABS || []).filter(function (t) { return t.key !== 'continue'; });
|
|
1381
|
-
function draw() {
|
|
1382
|
-
var hidden = hiddenNow().filter(function (k) { return tabs.some(function (t) { return t.key === k; }); });
|
|
1383
|
-
name.textContent = 'Home tabs: ' + (hidden.length ? hidden.length + ' hidden' : 'All shown');
|
|
1384
|
-
}
|
|
1385
|
-
draw();
|
|
1386
|
-
r.appendChild(name);
|
|
1387
|
-
r.appendChild(U.el('div', 'a-url', 'Press OK to choose'));
|
|
1388
|
-
r.__onselect = function () {
|
|
1389
|
-
var hidden = hiddenNow();
|
|
1390
|
-
toggleList('Home tabs', tabs.map(function (t) { return [t.key, t.label + (t.key === 'local' ? ' (with a USB drive in)' : ''), hidden.indexOf(t.key) === -1]; }),
|
|
1391
|
-
{ on: 'Shown', off: 'Hidden' }, function (key, on) {
|
|
1392
|
-
var now = hiddenNow().filter(function (k) { return k !== key; });
|
|
1393
|
-
if (!on) { now.push(key); }
|
|
1394
|
-
Stremio.setPref('hiddenTabs', now);
|
|
1395
|
-
markHomeDirty();
|
|
1396
|
-
draw();
|
|
1397
|
-
});
|
|
1398
|
-
};
|
|
1399
|
-
return r;
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
1458
|
var POSTER_SHAPES = [['portrait', 'Portrait'], ['landscape', 'Landscape']];
|
|
1403
1459
|
// ----- Diagnostics › Logs: the recent problems U.log kept (in memory, URLs masked), and
|
|
1404
1460
|
// how long launches took -----
|
|
@@ -1615,14 +1671,17 @@
|
|
|
1615
1671
|
if (!inf.entries.length) { line(dbOn ? 'Nothing in the TV’s database.' : 'The TV’s database isn’t used (turned off above).'); }
|
|
1616
1672
|
line('Settings storage: ' + fmtBytes(inf.ls) + ' (progress, watched marks, settings…)' + (inf.lsTop.length ? '; the largest:' : ''));
|
|
1617
1673
|
inf.lsTop.forEach(function (e) { line(' ' + fmtBytes(e.bytes) + ' ' + e.key); });
|
|
1674
|
+
if (inf.guides && inf.guides.count) { line('Saved TV guides (in StremioK’s folder): ' + fmtBytes(inf.guides.bytes) + ' in ' + inf.guides.count + (inf.guides.count === 1 ? ' file' : ' files')); }
|
|
1618
1675
|
if (inf.disk) { line('The TV’s own disk: ' + fmtBytes(inf.disk.free) + ' free of ' + fmtBytes(inf.disk.total) + ' (what the TV allows StremioK follows it: freeing space, unused apps for one, raises it)'); }
|
|
1619
1676
|
}
|
|
1620
1677
|
r.__onselect = function () {
|
|
1621
1678
|
if (!U.bigStore || !U.bigStore.info || _storageInfo === 'busy') { return; }
|
|
1622
1679
|
_storageInfo = 'busy'; renderSettings();
|
|
1623
|
-
|
|
1680
|
+
var guides = (typeof YTPlay !== 'undefined' && YTPlay.epgFiles) ? YTPlay.epgFiles().then(null, function () { return null; }) : null;
|
|
1681
|
+
Promise.all([U.bigStore.info(), U.diskInfo ? U.diskInfo() : null, guides]).then(function (both) {
|
|
1624
1682
|
var inf = both[0], ls = 0, lsAll = [];
|
|
1625
1683
|
inf.disk = both[1];
|
|
1684
|
+
inf.guides = both[2]; // ({ count, bytes }: the TV guides StremioK's service saved)
|
|
1626
1685
|
inf.lsKeys = {};
|
|
1627
1686
|
try {
|
|
1628
1687
|
for (var i = 0; i < window.localStorage.length; i++) {
|
|
@@ -1654,12 +1713,15 @@
|
|
|
1654
1713
|
}
|
|
1655
1714
|
var clr = U.el('div', 'addon-item focusable');
|
|
1656
1715
|
clr.appendChild(U.el('div', 'a-name', 'Clear saved copies'));
|
|
1657
|
-
clr.appendChild(U.el('div', 'a-url', 'Home’s saved rows, IPTV playlist copies, the USB index: made again when needed (playlists download again)'));
|
|
1716
|
+
clr.appendChild(U.el('div', 'a-url', 'Home’s saved rows, IPTV playlist copies, saved TV guides, the USB index: made again when needed (playlists and guides download again)'));
|
|
1658
1717
|
clr.__onselect = function () {
|
|
1659
|
-
confirm('Clear the copies StremioK can make again (Home rows, playlist copies, USB index)?', 'Clear', function () {
|
|
1718
|
+
confirm('Clear the copies StremioK can make again (Home rows, playlist copies, TV guides, USB index)?', 'Clear', function () {
|
|
1660
1719
|
U.bigStore.keys().then(function (ks) {
|
|
1661
1720
|
return Promise.all(ks.filter(function (k) { return /^stremiok\.(homecache|local\.index|iptv\.cache\.)/.test(k); }).map(function (k) { return U.bigStore.remove(k); }));
|
|
1662
1721
|
}).then(function () {
|
|
1722
|
+
return (typeof YTPlay !== 'undefined' && YTPlay.epgClear) ? YTPlay.epgClear().then(null, function () {}) : null; // (the saved guides: read again as they're needed)
|
|
1723
|
+
}).then(function () {
|
|
1724
|
+
if (typeof EPG !== 'undefined' && EPG.reset) { EPG.reset(); }
|
|
1663
1725
|
if (typeof LocalMedia !== 'undefined') { LocalMedia.clearIndex(); }
|
|
1664
1726
|
_storageInfo = null;
|
|
1665
1727
|
U.toast('Saved copies cleared');
|
|
@@ -1831,7 +1893,28 @@
|
|
|
1831
1893
|
// ----- Home feeds and Search results (Settings > Appearance): order, show / hide -----
|
|
1832
1894
|
// kind 'home': the Home rows (every catalog); 'search': the Search rows (every catalog
|
|
1833
1895
|
// that can search, IPTV as one per type). ▲/▼ moves one; OK on its name shows / hides it.
|
|
1896
|
+
// 'menu': the top menu between Watching and Settings (the tabs, Discover, Search: ViewParts'
|
|
1897
|
+
// menuOrder); OK on a tab shows / hides it, Discover and Search always show.
|
|
1898
|
+
var MENU_FIXED = { discover: 'Discover', search: 'Search' };
|
|
1899
|
+
function menuHidden(k) { return !MENU_FIXED[k] && (Stremio.getPrefs().hiddenTabs || []).indexOf(k) !== -1; }
|
|
1834
1900
|
var FEED_KINDS = {
|
|
1901
|
+
menu: { section: 'menuorder', title: 'Appearance › Menu', hint: 'Watching comes first and Settings last: ⇈ ▲ ▼ ⇊ put the rest in your order · OK on a tab shows/hides it.',
|
|
1902
|
+
list: function () {
|
|
1903
|
+
return Promise.resolve(V.menuOrder().map(function (k) {
|
|
1904
|
+
var t = (V.HOME_TABS || []).filter(function (x) { return x.key === k; })[0];
|
|
1905
|
+
return { key: k, name: MENU_FIXED[k] || (t ? t.label : k) + (k === 'local' ? ' (with a USB drive in)' : k === 'guide' ? ' (once a TV guide is read)' : '') };
|
|
1906
|
+
}));
|
|
1907
|
+
},
|
|
1908
|
+
keyOf: function (x) { return x.key; },
|
|
1909
|
+
move: function (k, d) { V.moveMenuItem(k, d); markHomeDirty(); return Promise.resolve(); },
|
|
1910
|
+
toggle: function (k) {
|
|
1911
|
+
if (MENU_FIXED[k]) { U.toast(MENU_FIXED[k] + ' is always in the menu'); return; }
|
|
1912
|
+
var now = (Stremio.getPrefs().hiddenTabs || []).filter(function (x) { return x !== k; });
|
|
1913
|
+
if (!menuHidden(k)) { now.push(k); }
|
|
1914
|
+
Stremio.setPref('hiddenTabs', now);
|
|
1915
|
+
markHomeDirty();
|
|
1916
|
+
},
|
|
1917
|
+
hidden: menuHidden },
|
|
1835
1918
|
home: { section: 'feeds', title: 'Appearance › Home feeds', hint: '▲/▼ reorder a feed · OK on the name shows/hides it on Home.',
|
|
1836
1919
|
list: function () { return Stremio.listCatalogsOrdered(); }, keyOf: function (c) { return Stremio.catalogKey(c); },
|
|
1837
1920
|
move: function (k, d) { return Stremio.moveFeed(k, d); }, toggle: function (k) { Stremio.toggleFeed(k); },
|
|
@@ -1962,7 +2045,20 @@
|
|
|
1962
2045
|
var SOURCE_ROWS = { prefix: 'src:', off: 'off', keyOf: function (x) { return x.key; },
|
|
1963
2046
|
move: function (k, d) { return Stremio.moveFeedSource(k, d); }, toggle: function (k) { Stremio.toggleFeedSource(k); },
|
|
1964
2047
|
hidden: function (k) { return Stremio.isFeedSourceOff(k); } };
|
|
1965
|
-
// ▲
|
|
2048
|
+
// ⇈ ▲ ▼ ⇊ for a row of a list you put in order: to the top, one place up, one down, to the
|
|
2049
|
+
// bottom. move(dir, ctl) moves it (dir -Infinity / -1 / 1 / Infinity: see U.moveIn); mark(node,
|
|
2050
|
+
// ctl) tags each button, so the page can focus the one used once it's drawn again.
|
|
2051
|
+
var ORDER_BTNS = [['top', '⇈', -Infinity], ['up', '▲', -1], ['down', '▼', 1], ['bottom', '⇊', Infinity]];
|
|
2052
|
+
function orderButtons(move, mark) {
|
|
2053
|
+
return ORDER_BTNS.map(function (b) {
|
|
2054
|
+
var n = U.el('div', 'feed-btn focusable', b[1]);
|
|
2055
|
+
if (mark) { mark(n, b[0]); }
|
|
2056
|
+
n.__onselect = function () { move(b[2], b[0]); };
|
|
2057
|
+
return n;
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
function ctlIndex(ctl) { for (var i = 0; i < ORDER_BTNS.length; i++) { if (ORDER_BTNS[i][0] === ctl) { return i; } } return 1; }
|
|
2061
|
+
// ⇈ ▲ ▼ ⇊ / name rows for an ordered list (L: keyOf, hidden, move, toggle, off, prefix).
|
|
1966
2062
|
// After a change the page is drawn again, focused on the control used (refeed).
|
|
1967
2063
|
function feedRows(wrap, kind, items, L) {
|
|
1968
2064
|
items.forEach(function (item) {
|
|
@@ -1970,20 +2066,14 @@
|
|
|
1970
2066
|
var hidden = L.hidden(key);
|
|
1971
2067
|
var frow = U.el('div', 'feed-row' + (hidden ? ' feed-off' : ''));
|
|
1972
2068
|
|
|
1973
|
-
var
|
|
1974
|
-
|
|
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'); }); };
|
|
2069
|
+
var btns = orderButtons(function (dir, ctl) { L.move(key, dir).then(function () { refeed(kind, fk, ctl); }); },
|
|
2070
|
+
function (n, ctl) { n.setAttribute('data-fk', fk); n.setAttribute('data-ctl', ctl); });
|
|
1980
2071
|
|
|
1981
2072
|
var name = U.el('div', 'feed-name focusable', item.name + (hidden ? ' (' + (L.off || 'hidden') + ')' : ''));
|
|
1982
2073
|
name.setAttribute('data-fk', fk); name.setAttribute('data-ctl', 'name');
|
|
1983
2074
|
name.__onselect = function () { L.toggle(key); refeed(kind, fk, 'name'); };
|
|
1984
2075
|
|
|
1985
|
-
frow.appendChild(
|
|
1986
|
-
frow.appendChild(down);
|
|
2076
|
+
btns.forEach(function (b) { frow.appendChild(b); });
|
|
1987
2077
|
frow.appendChild(name);
|
|
1988
2078
|
wrap.appendChild(frow);
|
|
1989
2079
|
});
|
package/js/views.js
CHANGED
|
@@ -12,8 +12,9 @@ var Views = (function (V) {
|
|
|
12
12
|
// pages, a search's rows) stopped
|
|
13
13
|
leaveScreen: function (name) { var f = { detail: V.leaveDetail, discover: V.leaveDiscover, search: V.leaveSearch }[name]; if (f) { f(); } }, backInLocal: V.backInLocal, setHomeTab: V.setHomeTab,
|
|
14
14
|
renderLive: V.renderLive,
|
|
15
|
-
renderSearch: V.renderSearch, renderDiscover: V.renderDiscover, renderEntry: V.renderEntry, backInDetail: V.backInDetail,
|
|
15
|
+
renderSearch: V.renderSearch, searchFor: function (q) { V.searchFor(q); }, renderDiscover: V.renderDiscover, renderEntry: V.renderEntry, backInDetail: V.backInDetail,
|
|
16
16
|
homeDirty: V.homeDirty, refreshHomeIfStale: V.refreshHomeIfStale, alertNewEpisodes: V.alertNewEpisodes, previewTiles: V.previewTiles, withShowInfo: V.withShowInfo,
|
|
17
|
-
isModalOpen: V.isModalOpen, closeModal: V.closeModal, moveKey: V.moveKey, availableTabs: V.availableTabs
|
|
17
|
+
isModalOpen: V.isModalOpen, closeModal: V.closeModal, moveKey: V.moveKey, availableTabs: V.availableTabs,
|
|
18
|
+
menuItems: V.menuItems, menuOrder: V.menuOrder, moveMenuItem: V.moveMenuItem
|
|
18
19
|
};
|
|
19
20
|
})(ViewParts);
|
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
|
-
|
|
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
|
@@ -62,15 +62,28 @@ var YTPlay = (function () {
|
|
|
62
62
|
}
|
|
63
63
|
// A live HLS channel whose sound the TV's player stutters on (its server cuts the AAC sound
|
|
64
64
|
// mid-frame): the service looks at it and, if so, gives a relay that repacks the sound:
|
|
65
|
-
// { fix, sure, why, url
|
|
66
|
-
// h: the channel's own User-Agent /
|
|
67
|
-
|
|
65
|
+
// { fix, sure, why, url, cc: [{ ch, lang }] (its closed captions), ccSure }. force: the relay
|
|
66
|
+
// at once (the channel's already known to need it). h: the channel's own User-Agent /
|
|
67
|
+
// Referer ({ ua, ref }). opts (with force): { fix: false (the sound as it is), cc: the caption
|
|
68
|
+
// channels to add as subtitle tracks }.
|
|
69
|
+
function liveOpen(url, h, force, opts) {
|
|
68
70
|
if (MODE === 'browser') { return Promise.reject(new Error('no service')); }
|
|
71
|
+
var cc = opts && opts.cc ? opts.cc.map(function (c) { return c.ch + (c.lang ? '-' + c.lang : ''); }).join(',') : '';
|
|
69
72
|
return ensureService().then(entryAuth).then(function (a) {
|
|
70
|
-
var q = '/live/open?u=' + encodeURIComponent(url) + (h && h.ua ? '&ua=' + encodeURIComponent(h.ua) : '') + (h && h.ref ? '&ref=' + encodeURIComponent(h.ref) : '') + (force ? '&force=1' : '')
|
|
73
|
+
var q = '/live/open?u=' + encodeURIComponent(url) + (h && h.ua ? '&ua=' + encodeURIComponent(h.ua) : '') + (h && h.ref ? '&ref=' + encodeURIComponent(h.ref) : '') + (force ? '&force=1' : '') +
|
|
74
|
+
(force && opts && opts.fix === false ? '&fix=0' : '') + (force && cc ? '&cc=' + encodeURIComponent(cc) : '');
|
|
71
75
|
return U.getJSON(PROXY + q + a.q, force ? 5000 : 30000, 65536, null, a.h);
|
|
72
76
|
});
|
|
73
77
|
}
|
|
78
|
+
// An HLS stream's quality list, read by the service with the stream's own User-Agent /
|
|
79
|
+
// Referer (h: { ua, ref }), which a web page can't send: its text.
|
|
80
|
+
function hlsMaster(url, h) {
|
|
81
|
+
if (MODE === 'browser') { return Promise.reject(new Error('no service')); }
|
|
82
|
+
return ensureService().then(entryAuth).then(function (a) {
|
|
83
|
+
var q = '/live/master?u=' + encodeURIComponent(url) + (h && h.ua ? '&ua=' + encodeURIComponent(h.ua) : '') + (h && h.ref ? '&ref=' + encodeURIComponent(h.ref) : '');
|
|
84
|
+
return U.getText(PROXY + q + a.q, 12000, 524288, null, false, a.h);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
74
87
|
// An AIOManager account's addons (its Hydra API, through the service: an instance only
|
|
75
88
|
// answers the web origins it's set to): { addons: [{ url, name, enabled }] }. base: its
|
|
76
89
|
// address; key: the account's API key (a header, never in the link).
|
|
@@ -82,6 +95,50 @@ var YTPlay = (function () {
|
|
|
82
95
|
return U.getJSON(PROXY + '/aiom/addons?base=' + encodeURIComponent(base) + a.q, 40000, 1048576, null, h);
|
|
83
96
|
});
|
|
84
97
|
}
|
|
98
|
+
// A playlist's TV guide, read and kept by the service (see service/index.js, epgControl):
|
|
99
|
+
// epgLoad asks it to read it for these channels (k: their keys as a bitset, EPG.bitsFor) ->
|
|
100
|
+
// { state, at, channels, programmes, partial, error }; epgGet: their programs -> { <key>:
|
|
101
|
+
// [[start, stop, title, desc]] }. Both POSTs, what's asked in the body: a newer TV's service
|
|
102
|
+
// reads 8 KB of a request's address and headers, less than thousands of channels' keys.
|
|
103
|
+
// (a POST refused, a service from before it took them or one without the token: asked
|
|
104
|
+
// again as a GET, when it fits in the address)
|
|
105
|
+
// An app that can't read the service's token (its header instead): the guide ticket, asked
|
|
106
|
+
// for once, given with the POSTs (the TV adds an Origin to them, which the header alone doesn't pass).
|
|
107
|
+
var _ticket = null;
|
|
108
|
+
function epgTicket(a) {
|
|
109
|
+
if (a.q || MODE === 'tizenbrew') { return Promise.resolve(a.q); } // (the token; TizenBrew's origin passes as it is)
|
|
110
|
+
if (!_ticket) {
|
|
111
|
+
_ticket = U.getJSON(PROXY + '/epg/ticket', 8000, 4096, null, a.h).then(function (r) {
|
|
112
|
+
return r && /^[0-9a-f]{48}$/.test(r.ticket || '') ? '&ticket=' + r.ticket : '';
|
|
113
|
+
}, function () { _ticket = null; return ''; });
|
|
114
|
+
}
|
|
115
|
+
return _ticket;
|
|
116
|
+
}
|
|
117
|
+
function epgAsk(path, body, maxBytes) {
|
|
118
|
+
if (MODE === 'browser') { return Promise.reject(new Error('no service')); }
|
|
119
|
+
return ensureService().then(entryAuth).then(function (a) {
|
|
120
|
+
return epgTicket(a).then(function (cred) { return { a: a, cred: cred }; });
|
|
121
|
+
}).then(function (x) {
|
|
122
|
+
var a = x.a, q = x.cred ? '?' + x.cred.replace(/^&/, '') : '';
|
|
123
|
+
return U.postJSON(PROXY + path + q, body, 15000, maxBytes, a.h).then(null, function (err) {
|
|
124
|
+
if (err && /^HTTP 403\b/.test(err.message || '') && /ticket=/.test(q)) { _ticket = null; } // (the service started again since: a new ticket next time)
|
|
125
|
+
if (!(err && /^HTTP (403|405)\b/.test(err.message || '') && body.length < 6000)) { throw err; }
|
|
126
|
+
return U.getJSON(PROXY + path + (q ? q + '&' : '?') + body, 15000, maxBytes, null, a.h);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// Settings › Storage: the saved guides ({ count, bytes }), and deleting them all.
|
|
131
|
+
function epgFiles() {
|
|
132
|
+
if (MODE === 'browser') { return Promise.resolve(null); }
|
|
133
|
+
return ensureService().then(entryAuth).then(function (a) { return U.getJSON(PROXY + '/epg/files' + (a.q ? '?' + a.q.replace(/^&/, '') : ''), 8000, 4096, null, a.h); });
|
|
134
|
+
}
|
|
135
|
+
function epgClear() { return epgAsk('/epg/clear', '', 4096); }
|
|
136
|
+
function epgLoad(u, k, force) {
|
|
137
|
+
return epgAsk('/epg/load', 'u=' + encodeURIComponent(u) + '&k=' + k + (force ? '&force=1' : ''), 65536);
|
|
138
|
+
}
|
|
139
|
+
function epgGet(u, keys, from, to, max) {
|
|
140
|
+
return epgAsk('/epg/get', 'u=' + encodeURIComponent(u) + '&ids=' + encodeURIComponent(keys.join('|')) + '&from=' + from + '&to=' + to + (max ? '&max=' + max : ''), 4194304);
|
|
141
|
+
}
|
|
85
142
|
// Whether YouTube gives the video out to the service (it refuses some uploads to the player
|
|
86
143
|
// the service asks as): false when it says no; true when it does, or no answer came (the
|
|
87
144
|
// player tries). The service keeps what it found, so playing it next asks YouTube nothing new.
|
|
@@ -172,6 +229,6 @@ var YTPlay = (function () {
|
|
|
172
229
|
return MODE === 'tizenbrew' ? 'TizenBrew module' : MODE === 'wgt' ? 'Sideloaded app (.wgt)' : 'Browser (development)';
|
|
173
230
|
}
|
|
174
231
|
|
|
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; },
|
|
232
|
+
return { ensureService: ensureService, resolverUrl: resolverUrl, infoUrl: infoUrl, canPlay: canPlay, liveOpen: liveOpen, hlsMaster: hlsMaster, aiomAddons: aiomAddons, epgLoad: epgLoad, epgGet: epgGet, epgFiles: epgFiles, epgClear: epgClear, entryOpen: entryOpen, entryPoll: entryPoll, entryClose: entryClose, subsUrl: subsUrl, audioLang: audioLang, setAudioLang: setAudioLang, installMode: function () { return MODE; },
|
|
176
233
|
installLabel: installLabel, unavailableMessage: unavailableMessage, isRunning: isRunning, port: PORT };
|
|
177
234
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stremiok",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "StremioK streaming client for Tizen 3.0+ Samsung TVs (TizenBrew app module).",
|
|
5
5
|
"packageType": "app",
|
|
6
6
|
"appName": "StremioK",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"icon.png",
|
|
15
15
|
"config.xml",
|
|
16
16
|
"README.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
17
18
|
"LICENSE"
|
|
18
19
|
],
|
|
19
20
|
"keys": [
|