stremiok 2.2.1 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +7 -6
- package/config.xml +1 -1
- package/css/style.css +11 -8
- package/index.html +33 -33
- package/js/app.js +18 -19
- package/js/focus.js +5 -5
- package/js/iptv.js +15 -2
- package/js/player-core.js +71 -9
- package/js/player-store.js +2 -2
- package/js/player-subs.js +47 -7
- package/js/stremio.js +154 -23
- package/js/util.js +55 -13
- package/js/view-core.js +43 -13
- package/js/view-detail.js +35 -7
- package/js/view-discover.js +1 -1
- package/js/view-home.js +33 -8
- package/js/view-settings.js +69 -1
- package/js/views.js +1 -1
- package/js/ytplay.js +4 -3
- package/package.json +2 -1
- package/service/index.js +33 -9
package/js/view-core.js
CHANGED
|
@@ -24,8 +24,9 @@ var ViewParts = {};
|
|
|
24
24
|
var _modalPrevFocus = null;
|
|
25
25
|
var _modalScope = null;
|
|
26
26
|
|
|
27
|
-
// opts (optional): { noLabel, onNo, plain } — another label and an action for the
|
|
28
|
-
// button; plain: the confirm button not marked as destructive
|
|
27
|
+
// opts (optional): { noLabel, onNo, plain, focusYes } — another label and an action for the
|
|
28
|
+
// other button; plain: the confirm button not marked as destructive; focusYes: focus starts on
|
|
29
|
+
// it (not on Cancel).
|
|
29
30
|
function confirm(message, confirmLabel, onYes, opts) {
|
|
30
31
|
opts = opts || {};
|
|
31
32
|
// defensive: never stack modals
|
|
@@ -45,7 +46,7 @@ var ViewParts = {};
|
|
|
45
46
|
overlay.appendChild(box);
|
|
46
47
|
document.body.appendChild(overlay);
|
|
47
48
|
|
|
48
|
-
showModal(overlay, no); // default to Cancel (safe)
|
|
49
|
+
showModal(overlay, opts.focusYes ? yes : no); // default to Cancel (safe)
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
// A QR code for the phone (an addon's link or setup page): the code holds the whole link;
|
|
@@ -511,10 +512,13 @@ var ViewParts = {};
|
|
|
511
512
|
else if (m.key === 'discover') { item('tab-pill', 'discover', 'Discover', '🔭', function () { App.openDiscover(); }, tabbar); }
|
|
512
513
|
else if (m.key === 'search') { item('hchip', 'search', 'Search', '🔍', function () { App.openSearch(); }, tabbar); }
|
|
513
514
|
});
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
515
|
+
// The items in one strip that scrolls sideways when they don't all fit (Focus keeps the
|
|
516
|
+
// focused one in view); the StremioK name outside it, at the right.
|
|
517
|
+
var strip = U.el('div', 'menu-scroll');
|
|
518
|
+
strip.appendChild(tabbar);
|
|
519
|
+
item('hchip', 'settings', 'Settings', '⚙', function () { App.openSettings(); }, strip);
|
|
520
|
+
head.appendChild(strip);
|
|
521
|
+
setTimeout(function () { showActiveItem(strip); }, 0); // (once it's on screen)
|
|
518
522
|
var brand = U.el('div', 'brand');
|
|
519
523
|
brand.innerHTML = 'Stremio<span class="dot">K</span>';
|
|
520
524
|
if (Player.isPrivate && Player.isPrivate()) { brand.className += ' private'; } // (private viewing on: drawn as an outline)
|
|
@@ -522,6 +526,15 @@ var ViewParts = {};
|
|
|
522
526
|
return head;
|
|
523
527
|
}
|
|
524
528
|
|
|
529
|
+
// The page's own item scrolled into view (one picked by number, far to the right), with
|
|
530
|
+
// room for a neighbour on either side; focus may be on the page below.
|
|
531
|
+
function showActiveItem(strip) {
|
|
532
|
+
var a = strip.querySelector('.active'), w = strip.clientWidth, mx = 90;
|
|
533
|
+
if (!a || !w) { return; }
|
|
534
|
+
if (a.offsetLeft < strip.scrollLeft + mx) { strip.scrollLeft = Math.max(0, a.offsetLeft - mx); }
|
|
535
|
+
else if (a.offsetLeft + a.offsetWidth > strip.scrollLeft + w - mx) { strip.scrollLeft = a.offsetLeft + a.offsetWidth - w + mx; }
|
|
536
|
+
}
|
|
537
|
+
|
|
525
538
|
// DOWN from the top menu returns to where focus was before going UP (if it's still
|
|
526
539
|
// on screen), else the first focusable in the content area below the menu.
|
|
527
540
|
function focusBelowTop(head) {
|
|
@@ -534,7 +547,7 @@ var ViewParts = {};
|
|
|
534
547
|
var all = s.querySelectorAll('.focusable');
|
|
535
548
|
for (var i = 0; i < all.length; i++) {
|
|
536
549
|
var r = all[i].getBoundingClientRect();
|
|
537
|
-
if (r.height > 0 && r.top > 80) { Focus.setFocus(all[i]); return true; }
|
|
550
|
+
if (r.height > 0 && r.top > 80 * U.zoom()) { Focus.setFocus(all[i]); return true; } // (below the menu: on screen, zoomed)
|
|
538
551
|
}
|
|
539
552
|
return false;
|
|
540
553
|
}
|
|
@@ -639,17 +652,32 @@ var ViewParts = {};
|
|
|
639
652
|
redrawGuideCards();
|
|
640
653
|
}, 60000);
|
|
641
654
|
}
|
|
655
|
+
// A hold-OK menu that waits for something first (a channel's schedule, a title's details):
|
|
656
|
+
// shown only if, when it comes, you're still where you held it: the card in the page, its
|
|
657
|
+
// screen not hidden (Settings or a title opened meanwhile; Home stays in the page, hidden),
|
|
658
|
+
// no player, no other menu up, and no hold since (that one's). -> { still(), latest() }
|
|
659
|
+
var _holdGen = 0;
|
|
660
|
+
function holdNow(card) {
|
|
661
|
+
var gen = ++_holdGen, screen = null;
|
|
662
|
+
for (var n = card; n && !screen; n = n.parentNode) { if (/\bscreen\b/.test(n.className || '')) { screen = n; } }
|
|
663
|
+
return {
|
|
664
|
+
latest: function () { return gen === _holdGen; },
|
|
665
|
+
still: function () {
|
|
666
|
+
return gen === _holdGen && !!card && document.documentElement.contains(card) && (!screen || !/\bhidden\b/.test(screen.className)) &&
|
|
667
|
+
!(typeof Player !== 'undefined' && Player.isActive()) && !isModalOpen();
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
642
671
|
// Hold OK on a channel card: ▶ Watch now, ★ Add to / Remove from favorites, and its schedule
|
|
643
672
|
// from 2 hours ago to tomorrow (what's on now marked, and picked); OK on a program says
|
|
644
673
|
// what it is. A channel without a guide: the first two.
|
|
645
|
-
var _schedGen = 0;
|
|
646
674
|
function liveSchedule(meta, card) {
|
|
647
|
-
var
|
|
675
|
+
var hold = holdNow(card);
|
|
648
676
|
U.spinner(true);
|
|
649
677
|
EPG.schedule(meta.id).then(null, function () { return []; }).then(function (list) {
|
|
650
|
-
if (
|
|
678
|
+
if (!hold.latest()) { return; } // (held on another card since: its own)
|
|
651
679
|
U.spinner(false);
|
|
652
|
-
if (!
|
|
680
|
+
if (!hold.still()) { return; } // (left meanwhile)
|
|
653
681
|
var fav = IPTV.isFavorite(meta.id), t = Date.now() / 1000, at = 'play';
|
|
654
682
|
var items = [['play', '▶ Watch now'], ['fav', fav ? '★ Remove from favorites' : '★ Add to favorites']];
|
|
655
683
|
list.forEach(function (p, i) {
|
|
@@ -686,10 +714,12 @@ var ViewParts = {};
|
|
|
686
714
|
// Playing, or mark it watched / unwatched (a series as a whole). The title's details
|
|
687
715
|
// are fetched first (usually cached) so the choices match its current state.
|
|
688
716
|
function posterActions(meta, type, card) {
|
|
717
|
+
var hold = holdNow(card);
|
|
689
718
|
U.spinner(true);
|
|
690
719
|
titleActions(meta, type).then(function (a) {
|
|
720
|
+
if (!hold.latest()) { return; } // (held on another card since: its own)
|
|
691
721
|
U.spinner(false);
|
|
692
|
-
pickList(a.name, a.items, null, a.run);
|
|
722
|
+
if (hold.still()) { pickList(a.name, a.items, null, a.run); } // (not over a screen opened meanwhile)
|
|
693
723
|
});
|
|
694
724
|
}
|
|
695
725
|
// What a title's hold-OK menu offers, wherever it's held (a poster; the player's Cast & more,
|
package/js/view-detail.js
CHANGED
|
@@ -203,6 +203,10 @@
|
|
|
203
203
|
if (local) { resumeSources(startPlayback(local, p.id, p.name, how), p.id); return; }
|
|
204
204
|
var st = { url: p.url, name: p.name, _headers: U.sanitizeHeaders(p.headers) || undefined,
|
|
205
205
|
behaviorHints: (p.fileHints && typeof p.fileHints === 'object') ? p.fileHints : undefined }; // (its file: for subtitles made for it)
|
|
206
|
+
if (p.torrent && typeof p.torrent.infoHash === 'string') { // (a torrent: its link from the streaming server set now)
|
|
207
|
+
st = Stremio.torrentSource({ infoHash: p.torrent.infoHash, fileIdx: p.torrent.fileIdx, sources: p.torrent.sources, name: p.name, behaviorHints: st.behaviorHints });
|
|
208
|
+
if (!st.url) { U.toast('This one plays through a streaming server: Settings › Content › Streaming server', 6000); return; }
|
|
209
|
+
}
|
|
206
210
|
if (/^https:\/\/www\.youtube\.com\/watch\?v=[A-Za-z0-9_-]{11}$/.test(p.url || '')) { st._yt = true; st.ytPage = p.url; } // (a YouTube video: through the service again)
|
|
207
211
|
resumeSources(startPlayback(st, p.id, p.name, how), p.id);
|
|
208
212
|
}
|
|
@@ -321,7 +325,7 @@
|
|
|
321
325
|
if (!panel || (' ' + body.className + ' ').indexOf(' series-body ') === -1) { return; }
|
|
322
326
|
var sr = s.getBoundingClientRect(), br = body.getBoundingClientRect();
|
|
323
327
|
if (!(br.height > 0)) { return; }
|
|
324
|
-
var top = Math.round(br.bottom - sr.top) + PANEL_GAP;
|
|
328
|
+
var top = Math.round((br.bottom - sr.top) / U.zoom()) + PANEL_GAP; // (measured on screen, zoomed)
|
|
325
329
|
panel.style.top = Math.max(PANEL_MIN, Math.min(PANEL_MAX, top)) + 'px';
|
|
326
330
|
}
|
|
327
331
|
|
|
@@ -724,7 +728,7 @@
|
|
|
724
728
|
function fileStreams(v) {
|
|
725
729
|
return v.streams.map(function (st) {
|
|
726
730
|
var s = Object.assign({ name: fileLabel(v) }, st);
|
|
727
|
-
return Stremio.
|
|
731
|
+
return Stremio.torrentSource(Stremio.youTubeSource(s));
|
|
728
732
|
});
|
|
729
733
|
}
|
|
730
734
|
|
|
@@ -1395,6 +1399,8 @@
|
|
|
1395
1399
|
uncached: function (a, b) { return (a.info.cache === -1 ? 1 : 0) - (b.info.cache === -1 ? 1 : 0); },
|
|
1396
1400
|
kind: function (a, b) { return (KIND_SCORE[b.info.kind] || 0) - (KIND_SCORE[a.info.kind] || 0); },
|
|
1397
1401
|
hdr: function (a, b) { return (b.info.hdr ? 1 : 0) - (a.info.hdr ? 1 : 0); },
|
|
1402
|
+
// (only two sources that both state theirs: the others keep their order)
|
|
1403
|
+
seeds: function (a, b) { return (a.info.seeds >= 0 && b.info.seeds >= 0) ? b.info.seeds - a.info.seeds : 0; },
|
|
1398
1404
|
addon: function (a, b) { return (a.st._addonRank || 0) - (b.st._addonRank || 0); },
|
|
1399
1405
|
// each source's place in its addon's own list (an addon's first ties with another's
|
|
1400
1406
|
// first): a plain key, so it orders consistently wherever it sits among the criteria
|
|
@@ -1464,7 +1470,13 @@
|
|
|
1464
1470
|
// again), as the player takes a source, or null. total: how many it could try in all.
|
|
1465
1471
|
// avoid: [{ addon, tag }] — an addon's links through a debrid service that sent an account's
|
|
1466
1472
|
// notice (access refused, a limit: the player's noticeOf): they'd say the same.
|
|
1473
|
+
// Torrent sources' links from the streaming server set now (Settings may have changed it since
|
|
1474
|
+
// they came: none when it's off).
|
|
1475
|
+
function serverNow(list) {
|
|
1476
|
+
(list || []).forEach(function (st) { if (st && st.infoHash) { Stremio.torrentSource(st); } });
|
|
1477
|
+
}
|
|
1467
1478
|
function nextSourceFor(tried, avoid) {
|
|
1479
|
+
serverNow(_d && _d.streams);
|
|
1468
1480
|
var rows = (_d && _d.streams) ? filterRows(rankRowsFor(_d.streams)) : [], seen = {}, all = []; // (within the filter: Addon ▾ / Resolution ▾)
|
|
1469
1481
|
rows.forEach(function (r) {
|
|
1470
1482
|
var st = r.st, id = Stremio.sourceId(st), dt = Stremio.debridOf ? Stremio.debridOf(st) : null;
|
|
@@ -1847,6 +1859,7 @@
|
|
|
1847
1859
|
// before, 4K, 1080p, …) starts with its heading.
|
|
1848
1860
|
var heading = streamsHeading();
|
|
1849
1861
|
if (heading) { listScroll.appendChild(U.el('div', 'stream-for', heading)); }
|
|
1862
|
+
serverNow(_d.streams); // (a page kept from before: its torrents follow the streaming server set now)
|
|
1850
1863
|
var list = _d.streams, keepAt = -1, rows = rankRowsFor(list), lastGroup = null;
|
|
1851
1864
|
// Direct links only (Settings › Source ranking): the sources the TV can't play left out
|
|
1852
1865
|
// (torrents, links it can't open); where-to-watch apps stay (they open)
|
|
@@ -1915,10 +1928,11 @@
|
|
|
1915
1928
|
if (row && row.sized) { lines.push('📏 Outside your size limits: ' + row.sized + ' (Settings › Playback › Source ranking)'); }
|
|
1916
1929
|
if (row && row.blocked) { lines.push('⚠ ' + row.blocked + ' isn’t allowed (Settings › Playback › Formats)'); }
|
|
1917
1930
|
if (row && row.bad.length) { lines.push('⚠ Marked “Not working” on this TV: ' + row.bad.join(', ') + ' (Settings › Playback › Formats)'); }
|
|
1931
|
+
if (stream._torrent) { lines.push('🧲 A torrent, through your streaming server (Settings › Content › Streaming server)'); }
|
|
1918
1932
|
if (opens) {
|
|
1919
1933
|
lines.push('▶ OK opens ' + (opens.data ? 'it in ' : '') + opens.app.name);
|
|
1920
1934
|
} else if (!playable) {
|
|
1921
|
-
if (stream.infoHash) { lines.push('— torrent
|
|
1935
|
+
if (stream.infoHash) { lines.push('— torrent: plays through a streaming server (Settings › Content › Streaming server)'); }
|
|
1922
1936
|
else if (stream.externalUrl) {
|
|
1923
1937
|
lines.push('— another app' + ((typeof TVApps !== 'undefined' && TVApps.available())
|
|
1924
1938
|
? ': add it in Settings › Content › Streaming apps to open it from here' : ' (not playable here)'));
|
|
@@ -1936,7 +1950,7 @@
|
|
|
1936
1950
|
}
|
|
1937
1951
|
node.__onselect = function () {
|
|
1938
1952
|
if (opens) { openWhere(opens); return; }
|
|
1939
|
-
if (!playable) { U.toast('This source can’t play directly on the TV.'); return; }
|
|
1953
|
+
if (!playable) { U.toast(stream.infoHash ? 'Torrents play through a streaming server: Settings › Content › Streaming server' : 'This source can’t play directly on the TV.', stream.infoHash ? 6000 : undefined); return; }
|
|
1940
1954
|
startPlayback(stream, _d.streamId, _d.streamLabel || _d.meta.name, howHere());
|
|
1941
1955
|
};
|
|
1942
1956
|
return node;
|
|
@@ -1985,6 +1999,7 @@
|
|
|
1985
1999
|
// The player's "Select source" list: each source with the request headers it needs
|
|
1986
2000
|
// (IPTV), so switching to it sends its own (or none).
|
|
1987
2001
|
function playableSources(list) {
|
|
2002
|
+
serverNow(list || _d.streams);
|
|
1988
2003
|
return (list || _d.streams || []).filter(Stremio.isPlayable).map(function (st) {
|
|
1989
2004
|
var sz = Stremio.streamSize(st);
|
|
1990
2005
|
var desc = Stremio.streamDescription(st).replace(/\n/g, ' ');
|
|
@@ -2001,6 +2016,10 @@
|
|
|
2001
2016
|
|
|
2002
2017
|
// how (optional): { fromStart } — Play from start (Continue Watching): from 0:00.
|
|
2003
2018
|
function startPlayback(stream, videoId, label, how) {
|
|
2019
|
+
if (stream && stream.infoHash) { // (a torrent: its link from the streaming server set now)
|
|
2020
|
+
Stremio.torrentSource(stream);
|
|
2021
|
+
if (!stream.url) { U.toast('Torrents play through a streaming server: Settings › Content › Streaming server', 6000); return null; }
|
|
2022
|
+
}
|
|
2004
2023
|
// VOD changes progress/watched state. Live playback does not, and keeping Home
|
|
2005
2024
|
// untouched lets us restore the exact originating card and scroll position.
|
|
2006
2025
|
if (!(_d && _d.exitToBack)) { markHomeDirty(); }
|
|
@@ -2015,8 +2034,10 @@
|
|
|
2015
2034
|
var titleId = _d && _d.id, series = _d && _d.type === 'series', vtype = _d && _d.type;
|
|
2016
2035
|
var remembers = !!(_d && !_d.live);
|
|
2017
2036
|
// (also for another source picked in the player: what's learned is that one's)
|
|
2018
|
-
|
|
2019
|
-
|
|
2037
|
+
// how.converted: played converted by the streaming server, so its formats aren't learned
|
|
2038
|
+
// (they didn't play on the TV); Played before and the kept source still are.
|
|
2039
|
+
function learnFor(st, how) {
|
|
2040
|
+
var info = Stremio.streamInfo(st), fkeys = (how && how.converted) ? [] : Stremio.formatKeys(info);
|
|
2020
2041
|
return {
|
|
2021
2042
|
// (private viewing: nothing about this playback remembered or forgotten — its own
|
|
2022
2043
|
// flag, set when it started, see player-store; what plays on this TV is still learned)
|
|
@@ -2199,13 +2220,20 @@
|
|
|
2199
2220
|
}
|
|
2200
2221
|
|
|
2201
2222
|
// Load a specific episode's sources and auto-play the first playable one.
|
|
2223
|
+
// The next / previous episode (from the player): its sources asked for, the best like the
|
|
2224
|
+
// last one played. An answer once you've left the page, or asked for another episode since
|
|
2225
|
+
// (_epGen), does nothing: no playback, no page drawn, no spinner change, no note.
|
|
2226
|
+
var _epGen = 0;
|
|
2202
2227
|
function playEpisode(v) {
|
|
2228
|
+
var still = pageNow(), gen = ++_epGen;
|
|
2229
|
+
function current() { return still() && gen === _epGen; }
|
|
2203
2230
|
_d.mode = 'streams';
|
|
2204
2231
|
_d.streamId = v.id;
|
|
2205
2232
|
_d.season = (v.season === undefined ? 1 : v.season);
|
|
2206
2233
|
_d.streamLabel = epName(v);
|
|
2207
2234
|
U.spinner(true);
|
|
2208
2235
|
Stremio.getStreams(addonType(), v.id).then(function (list) {
|
|
2236
|
+
if (!current()) { return; }
|
|
2209
2237
|
_d.streams = sortStreams(Stremio.dedupeStreams(list));
|
|
2210
2238
|
U.spinner(false);
|
|
2211
2239
|
var pick = pickLikeLast(_d.id, _d.streams);
|
|
@@ -2215,7 +2243,7 @@
|
|
|
2215
2243
|
return;
|
|
2216
2244
|
}
|
|
2217
2245
|
startPlayback(pick, v.id, _d.streamLabel);
|
|
2218
|
-
}, function () { U.spinner(false); U.toast('Failed to load that episode'); });
|
|
2246
|
+
}, function () { if (!current()) { return; } U.spinner(false); U.toast('Failed to load that episode'); });
|
|
2219
2247
|
}
|
|
2220
2248
|
|
|
2221
2249
|
// ----- binge: the next episode plays a source like the one you've been watching -----
|
package/js/view-discover.js
CHANGED
|
@@ -334,7 +334,7 @@
|
|
|
334
334
|
}
|
|
335
335
|
function rowHeight() {
|
|
336
336
|
var els = _dv.cardEls, c = cols();
|
|
337
|
-
return (els.length > c) ? (els[c].getBoundingClientRect().top - els[0].getBoundingClientRect().top) : 0;
|
|
337
|
+
return (els.length > c) ? (els[c].getBoundingClientRect().top - els[0].getBoundingClientRect().top) / U.zoom() : 0; // (in the grid's own pixels)
|
|
338
338
|
}
|
|
339
339
|
function drawGrid() {
|
|
340
340
|
var grid = gridEl();
|
package/js/view-home.js
CHANGED
|
@@ -164,18 +164,32 @@
|
|
|
164
164
|
// reordered): reload just its rows; every other row keeps what it has, with no request.
|
|
165
165
|
// url: the addon ('local:iptv' for IPTV); none: nothing to fetch (only the set / order
|
|
166
166
|
// of rows changed). Calls before the reload runs add up.
|
|
167
|
+
// An addon Home had no rows from (IPTV read after your addons, a first playlist) is one too:
|
|
168
|
+
// its rows asked for, its tabs added to the menu when they come.
|
|
167
169
|
var _reloadOnly = null; // { addon url: 1 } for the next reload, else a full one
|
|
168
|
-
var _homeAddons = {}; // { addon url: 1 } the addons Home's last load had catalogs from
|
|
169
170
|
function refreshAddonRows(url) {
|
|
170
171
|
if (_homeCatalogs === null) { clearCatalogCache(); return; } // nothing loaded yet: a full load anyway
|
|
171
|
-
// an addon Home didn't load rows from (the first playlist added: IPTV): a full load, its tab with it
|
|
172
|
-
if (url && !_homeAddons[url]) { clearCatalogCache(); return; }
|
|
173
172
|
_reloadOnly = _reloadOnly || {};
|
|
174
173
|
if (url) { _reloadOnly[url] = 1; }
|
|
175
174
|
_homeDirty = true;
|
|
176
175
|
if (_current() === scr('home') && !isModalOpen() && !Player.isActive()) { loadHomeCatalogs(); }
|
|
177
176
|
}
|
|
178
177
|
|
|
178
|
+
// Auto-refresh (Settings › Content › IPTV): every minute, whether the playlists are due —
|
|
179
|
+
// their oldest download (a saved copy's own date) and the last try both that long ago — then
|
|
180
|
+
// all of them and their guides read again, as Reload does, and their rows with them. Not
|
|
181
|
+
// while something plays or StremioK is in the background: at the next minute after.
|
|
182
|
+
var _iptvTriedAt = 0, _iptvReading = false;
|
|
183
|
+
function iptvRefreshTick() {
|
|
184
|
+
var h = Stremio.iptvRefreshEvery ? Stremio.iptvRefreshEvery() : 0;
|
|
185
|
+
if (!h || _iptvReading || document.hidden || Player.isActive()) { return; }
|
|
186
|
+
var due = h * 3600000, now = Date.now(), at = Stremio.iptvReadAt();
|
|
187
|
+
if (at === Infinity || now - at < due || now - _iptvTriedAt < due) { return; }
|
|
188
|
+
_iptvTriedAt = now; _iptvReading = true;
|
|
189
|
+
Stremio.iptvReload().then(function () { _iptvReading = false; refreshAddonRows('local:iptv'); }, function () { _iptvReading = false; });
|
|
190
|
+
}
|
|
191
|
+
setInterval(iptvRefreshTick, 60000);
|
|
192
|
+
|
|
179
193
|
// The Home rows are also saved on the TV, trimmed to what a card and the details strip
|
|
180
194
|
// use, so a launch shows the last rows at once and refreshes them in the background
|
|
181
195
|
// (as the 15-minute refresh does). Saved rows are only used with the same addons.
|
|
@@ -1179,7 +1193,10 @@
|
|
|
1179
1193
|
}
|
|
1180
1194
|
|
|
1181
1195
|
// Switch the active home tab (from the shared top menu) and show it.
|
|
1182
|
-
function setHomeTab(key) { _homeTab = key || _homeTab; _focusFirstCard = 'new'; }
|
|
1196
|
+
function setHomeTab(key) { _homeTab = key || _homeTab; _focusFirstCard = 'new'; _wantTab = null; }
|
|
1197
|
+
// A tab asked for whose rows are still coming (a first playlist's IPTV tab): shown when they
|
|
1198
|
+
// come, if Home is still on the tab it showed instead. { key, instead }
|
|
1199
|
+
var _wantTab = null;
|
|
1183
1200
|
|
|
1184
1201
|
// prevFocus: a Home card to put focus back on after the rebuild (the card a title
|
|
1185
1202
|
// was opened from). Otherwise the card currently focused on Home is kept, so rows
|
|
@@ -1206,7 +1223,9 @@
|
|
|
1206
1223
|
// Keep the active tab valid for what's actually available.
|
|
1207
1224
|
var avail = availableTabs();
|
|
1208
1225
|
if (!_homeTab || !avail.some(function (t) { return t.key === _homeTab; })) {
|
|
1226
|
+
var asked = _homeTab;
|
|
1209
1227
|
_homeTab = avail.length ? avail[0].key : 'movie';
|
|
1228
|
+
if (asked && (_homeLoading || _reloadOnly)) { _wantTab = { key: asked, instead: _homeTab }; }
|
|
1210
1229
|
}
|
|
1211
1230
|
|
|
1212
1231
|
s.appendChild(buildTopMenu(_homeTab));
|
|
@@ -1416,12 +1435,12 @@
|
|
|
1416
1435
|
_homeLoading = false;
|
|
1417
1436
|
_homeFirstLoad = false;
|
|
1418
1437
|
if (first) { U.spinner(false); }
|
|
1419
|
-
|
|
1438
|
+
// (rows asked for meanwhile: the IPTV ones, read after the addons; unless the redraw
|
|
1439
|
+
// that follows has asked for them already)
|
|
1440
|
+
if (_reloadOnly && onHome()) { setTimeout(function () { if (_reloadOnly) { loadHomeCatalogs(); } }, 0); }
|
|
1420
1441
|
}
|
|
1421
1442
|
function onHome() { return _current() === s && !isModalOpen() && !Player.isActive(); }
|
|
1422
1443
|
Stremio.catalogsForHome().then(function (cats) {
|
|
1423
|
-
_homeAddons = {};
|
|
1424
|
-
cats.forEach(function (cat) { if (cat.addon && cat.addon.url) { _homeAddons[cat.addon.url] = 1; } });
|
|
1425
1444
|
var acc = cats.map(function (cat) { return copyCat({ name: cat.name, type: cat.type, yt: isYouTubeCat(cat) }, []); });
|
|
1426
1445
|
var arrived = cats.map(function () { return false; }), shown = 0, partialTimer = null;
|
|
1427
1446
|
// First load: publish each row as soon as it arrives, in its own place (a slower one above
|
|
@@ -1458,11 +1477,16 @@
|
|
|
1458
1477
|
U.settleAll(tasks, 3).then(function () {
|
|
1459
1478
|
_homeCatalogs = splitOtherCatalogs(acc).filter(function (c) { return c.metas && c.metas.length; });
|
|
1460
1479
|
done();
|
|
1480
|
+
var want = _wantTab;
|
|
1481
|
+
_wantTab = null;
|
|
1482
|
+
if (want && _homeTab === want.instead && onHome() && availableTabs().some(function (t) { return t.key === want.key; })) {
|
|
1483
|
+
setHomeTab(want.key);
|
|
1484
|
+
}
|
|
1461
1485
|
saveHomeCache();
|
|
1462
1486
|
// The tab on screen is redrawn only if what it shows changed: redrawing rebuilds
|
|
1463
1487
|
// every card (and reloads its artwork), which shows as a flicker. Continue
|
|
1464
1488
|
// Watching doesn't come from catalogs at all. Other tabs rebuild when opened.
|
|
1465
|
-
var same = (before !== null && shownSig() === before) || ownTabStill();
|
|
1489
|
+
var same = (before !== null && shownSig() === before && menuSig() === _renderedMenu) || ownTabStill();
|
|
1466
1490
|
var keep = {};
|
|
1467
1491
|
if (same && _tabRows[_homeTab]) { keep[_homeTab] = _tabRows[_homeTab]; }
|
|
1468
1492
|
_tabRows = keep;
|
|
@@ -1473,6 +1497,7 @@
|
|
|
1473
1497
|
});
|
|
1474
1498
|
}, function () {
|
|
1475
1499
|
done();
|
|
1500
|
+
_wantTab = null;
|
|
1476
1501
|
if (first) { U.toast('Failed to load addons. Check network / Settings.'); }
|
|
1477
1502
|
});
|
|
1478
1503
|
}
|
package/js/view-settings.js
CHANGED
|
@@ -193,9 +193,13 @@
|
|
|
193
193
|
var bgs = { dark: 1, black: 1, gray: 1, white: 1 };
|
|
194
194
|
return { accent: ACCENT_NAMES[accent] ? accent : 'indigo', bg: bgs[bg] ? bg : 'dark' };
|
|
195
195
|
}
|
|
196
|
+
// Zoom: everything bigger (see U.setZoom), in percent.
|
|
197
|
+
var ZOOMS = [['100', '100%'], ['110', '110%'], ['120', '120%'], ['130', '130%'], ['140', '140%']];
|
|
198
|
+
function zoomPct() { return Stremio.getPrefs().zoom; }
|
|
196
199
|
function applyAppearance() {
|
|
197
200
|
var a = appearance();
|
|
198
201
|
U.applyTheme(a.accent, a.bg);
|
|
202
|
+
U.setZoom(zoomPct() / 100);
|
|
199
203
|
}
|
|
200
204
|
function settingsAppearance(wrap) {
|
|
201
205
|
var store = { get: appearance, set: function (cfg) {
|
|
@@ -207,6 +211,10 @@
|
|
|
207
211
|
wrap.appendChild(U.el('h2', null, 'Appearance'));
|
|
208
212
|
wrap.appendChild(subPickRow('Background', BACKGROUNDS, 'bg', store));
|
|
209
213
|
wrap.appendChild(subPickRow('Accent color', accents, 'accent', store));
|
|
214
|
+
wrap.appendChild(subPickRow('Zoom', ZOOMS, 'zoom', {
|
|
215
|
+
get: function () { return { zoom: '' + zoomPct() }; },
|
|
216
|
+
set: function (cfg) { Stremio.setPref('zoom', +cfg.zoom); applyAppearance(); }
|
|
217
|
+
}));
|
|
210
218
|
wrap.appendChild(subPickRow('Poster orientation', POSTER_SHAPES, 'posterShape', {
|
|
211
219
|
get: function () { return { posterShape: Stremio.getPrefs().posterShape }; },
|
|
212
220
|
set: function (cfg) { Stremio.setPref('posterShape', cfg.posterShape); markHomeDirty(); } // Home redraws on return
|
|
@@ -338,6 +346,7 @@
|
|
|
338
346
|
var SETTINGS_GROUPS = {
|
|
339
347
|
content: { title: 'Content', rows: [
|
|
340
348
|
['Addons', 'Catalog, stream & subtitle addons', 'addons'],
|
|
349
|
+
['Streaming server', 'Stremio’s streaming server or TorrServer on your network: torrents, and videos the TV can’t play', 'server'],
|
|
341
350
|
['Integrations', 'API keys (OMDb, TMDB, MDBList), where-to-watch country', 'integrations'],
|
|
342
351
|
['IPTV', 'Manage M3U playlist URLs', 'iptv'],
|
|
343
352
|
['YouTube', 'Your YouTube channels, playlists and searches; YouTube in Search', 'youtube'],
|
|
@@ -407,6 +416,7 @@
|
|
|
407
416
|
else if (_settingsSection === 'appshortcuts') { settingsAppShortcuts(wrap); }
|
|
408
417
|
else if (_settingsSection === 'import') { settingsImport(wrap); }
|
|
409
418
|
else if (_settingsSection === 'aiomanager') { settingsAiom(wrap); }
|
|
419
|
+
else if (_settingsSection === 'server') { settingsServer(wrap); }
|
|
410
420
|
else if (_settingsSection === 'appearance') { settingsAppearance(wrap); }
|
|
411
421
|
else if (_settingsSection === 'formats') { settingsFormats(wrap); }
|
|
412
422
|
else { settingsMenu(wrap); }
|
|
@@ -449,7 +459,7 @@
|
|
|
449
459
|
wrap.appendChild(U.el('h2', null, 'Settings'));
|
|
450
460
|
wrap.appendChild(sectionRow('Content', 'Addons, API keys, IPTV playlists, streaming apps, importing a configuration file', 'content'));
|
|
451
461
|
wrap.appendChild(sectionRow('Playback', 'Audio and subtitles, source ranking, feed sources, formats', 'playback'));
|
|
452
|
-
wrap.appendChild(sectionRow('Appearance', 'Background, accent color, poster orientation, Home feeds, Search results', 'appearance'));
|
|
462
|
+
wrap.appendChild(sectionRow('Appearance', 'Background, accent color, zoom, poster orientation, Home feeds, Search results', 'appearance'));
|
|
453
463
|
wrap.appendChild(sectionRow('Diagnostics', 'Logs, storage, installed apps; how StremioK was installed', 'diagnostics'));
|
|
454
464
|
if (typeof YTPlay !== 'undefined' && YTPlay.installMode && YTPlay.installMode() === 'tizenbrew') { wrap.appendChild(U.el('div', 'hint', TIZENBREW_NOTE)); }
|
|
455
465
|
focusBack(wrap);
|
|
@@ -972,6 +982,60 @@
|
|
|
972
982
|
}
|
|
973
983
|
});
|
|
974
984
|
}
|
|
985
|
+
// Streaming server: Stremio's or TorrServer, on your network (see Stremio.server). Picking one
|
|
986
|
+
// asks for its address; Test says whether it answers.
|
|
987
|
+
var SERVERS = [['', 'Off'], ['stremio', 'Stremio server'], ['torrserver', 'TorrServer']];
|
|
988
|
+
var SERVER_PORT = { stremio: 11470, torrserver: 8090 };
|
|
989
|
+
var TORRENT_WAITS = [['45', '45 seconds'], ['60', '1 minute'], ['120', '2 minutes'], ['180', '3 minutes'], ['300', '5 minutes']];
|
|
990
|
+
function askServerAddress(kind, url) {
|
|
991
|
+
var eg = 'http://192.168.0.10:' + SERVER_PORT[kind];
|
|
992
|
+
App.openEntry({
|
|
993
|
+
title: (kind === 'stremio' ? 'Stremio server' : 'TorrServer') + ': address',
|
|
994
|
+
hint: 'Its address on your network, like ' + eg + '. RETURN cancels.',
|
|
995
|
+
value: url || 'http://', placeholder: eg, phone: 'server',
|
|
996
|
+
validate: function (v) { return Stremio.serverAddressProblem(v); },
|
|
997
|
+
onConfirm: function (v) { Stremio.setServer({ kind: kind, url: v }); App.back(); renderSettings(); }
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
function settingsServer(wrap) {
|
|
1001
|
+
wrap.appendChild(U.el('h2', null, 'Streaming server'));
|
|
1002
|
+
wrap.appendChild(U.el('div', 'hint', 'Torrent-only sources play through it; Stremio’s can also convert a video the TV can’t play.'));
|
|
1003
|
+
var sv = Stremio.server();
|
|
1004
|
+
wrap.appendChild(subPickRow('Server', SERVERS, 'kind', {
|
|
1005
|
+
get: function () { return { kind: Stremio.server().kind }; },
|
|
1006
|
+
set: function (cfg) {
|
|
1007
|
+
var cur = Stremio.server();
|
|
1008
|
+
if (!cfg.kind) { Stremio.setServer({ kind: '' }); renderSettings(); return; }
|
|
1009
|
+
askServerAddress(cfg.kind, cur.kind === cfg.kind ? cur.url : '');
|
|
1010
|
+
}
|
|
1011
|
+
}));
|
|
1012
|
+
if (!sv.kind) { return; }
|
|
1013
|
+
var addr = U.el('div', 'addon-item focusable cfg-row');
|
|
1014
|
+
addr.appendChild(U.el('div', 'a-name', 'Address: ' + sv.url));
|
|
1015
|
+
addr.appendChild(U.el('div', 'a-url', 'Press OK to change'));
|
|
1016
|
+
addr.__onselect = function () { askServerAddress(sv.kind, sv.url); };
|
|
1017
|
+
wrap.appendChild(addr);
|
|
1018
|
+
var test = U.el('div', 'addon-item focusable cfg-row'), said = U.el('div', 'a-url', 'Press OK to see whether it answers');
|
|
1019
|
+
test.appendChild(U.el('div', 'a-name', 'Test'));
|
|
1020
|
+
test.appendChild(said);
|
|
1021
|
+
test.__onselect = function () {
|
|
1022
|
+
said.textContent = 'Asking…';
|
|
1023
|
+
Stremio.testServer().then(function (r) { said.textContent = (r.ok ? '✓ ' : '✗ ') + r.text; });
|
|
1024
|
+
};
|
|
1025
|
+
wrap.appendChild(test);
|
|
1026
|
+
wrap.appendChild(subPickRow('Wait for a torrent to start', TORRENT_WAITS, 'wait', {
|
|
1027
|
+
get: function () { return { wait: '' + Stremio.server().wait }; },
|
|
1028
|
+
set: function (cfg) { Stremio.setServer({ kind: Stremio.server().kind, wait: +cfg.wait }); }
|
|
1029
|
+
}));
|
|
1030
|
+
if (sv.kind === 'stremio') {
|
|
1031
|
+
wrap.appendChild(subPickRow('Convert videos the TV can’t play', [['on', 'On'], ['off', 'Off']], 'convert', {
|
|
1032
|
+
get: function () { return { convert: Stremio.server().convert ? 'on' : 'off' }; },
|
|
1033
|
+
set: function (cfg) { Stremio.setServer({ kind: 'stremio', convert: cfg.convert === 'on' }); }
|
|
1034
|
+
}));
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
var IPTV_EVERY = [['0', 'Off'], ['1', 'Every hour'], ['3', 'Every 3 hours'], ['6', 'Every 6 hours'], ['12', 'Every 12 hours']];
|
|
975
1039
|
function settingsIptv(wrap) {
|
|
976
1040
|
wrap.appendChild(U.el('h2', null, 'IPTV'));
|
|
977
1041
|
var playlists = Stremio.iptvGetPlaylists ? Stremio.iptvGetPlaylists() : [];
|
|
@@ -1058,6 +1122,10 @@
|
|
|
1058
1122
|
xt.appendChild(U.el('div', 'a-url', 'Its server, username and password: its channels and TV guide added'));
|
|
1059
1123
|
xt.__onselect = addXtream;
|
|
1060
1124
|
wrap.appendChild(xt);
|
|
1125
|
+
if (playlists.length && Stremio.iptvRefreshEvery) { // (all of them, with their guides, as Reload does)
|
|
1126
|
+
wrap.appendChild(subPickRow('Auto-refresh', IPTV_EVERY, 'every',
|
|
1127
|
+
{ get: function () { return { every: String(Stremio.iptvRefreshEvery()) }; }, set: function (c) { Stremio.iptvSetRefreshEvery(+c.every); } }));
|
|
1128
|
+
}
|
|
1061
1129
|
|
|
1062
1130
|
wrap.appendChild(U.el('div', 'hint',
|
|
1063
1131
|
'Channels are grouped into IPTV and Radio rows on Home. ▲ / ▼ set the playlists\' order: a channel in two playlists comes from the first.'));
|
package/js/views.js
CHANGED
|
@@ -14,7 +14,7 @@ var Views = (function (V) {
|
|
|
14
14
|
renderLive: V.renderLive,
|
|
15
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, confirm: V.confirm, moveKey: V.moveKey, availableTabs: V.availableTabs,
|
|
18
18
|
menuItems: V.menuItems, menuOrder: V.menuOrder, moveMenuItem: V.moveMenuItem
|
|
19
19
|
};
|
|
20
20
|
})(ViewParts);
|
package/js/ytplay.js
CHANGED
|
@@ -65,14 +65,15 @@ var YTPlay = (function () {
|
|
|
65
65
|
// { fix, sure, why, url, cc: [{ ch, lang }] (its closed captions), ccSure }. force: the relay
|
|
66
66
|
// at once (the channel's already known to need it). h: the channel's own User-Agent /
|
|
67
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
|
-
|
|
68
|
+
// channels to add as subtitle tracks }. cancel (a U.cancelToken): the look stopped, the
|
|
69
|
+
// service's downloads with it (another channel, the player closed).
|
|
70
|
+
function liveOpen(url, h, force, opts, cancel) {
|
|
70
71
|
if (MODE === 'browser') { return Promise.reject(new Error('no service')); }
|
|
71
72
|
var cc = opts && opts.cc ? opts.cc.map(function (c) { return c.ch + (c.lang ? '-' + c.lang : ''); }).join(',') : '';
|
|
72
73
|
return ensureService().then(entryAuth).then(function (a) {
|
|
73
74
|
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
75
|
(force && opts && opts.fix === false ? '&fix=0' : '') + (force && cc ? '&cc=' + encodeURIComponent(cc) : '');
|
|
75
|
-
return U.getJSON(PROXY + q + a.q, force ? 5000 : 30000, 65536, null, a.h);
|
|
76
|
+
return U.getJSON(PROXY + q + a.q, force ? 5000 : 30000, 65536, cancel || null, a.h);
|
|
76
77
|
});
|
|
77
78
|
}
|
|
78
79
|
// An HLS stream's quality list, read by the service with the stream's own User-Agent /
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stremiok",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "StremioK streaming client for Tizen 3.0+ Samsung TVs (TizenBrew app module).",
|
|
5
5
|
"packageType": "app",
|
|
6
6
|
"appName": "StremioK",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"MediaTrackNext",
|
|
29
29
|
"ChannelUp",
|
|
30
30
|
"ChannelDown",
|
|
31
|
+
"Guide",
|
|
31
32
|
"ColorF0Red",
|
|
32
33
|
"ColorF1Green",
|
|
33
34
|
"ColorF2Yellow",
|
package/service/index.js
CHANGED
|
@@ -1589,6 +1589,29 @@ function openPart(fs, part, append, cb) {
|
|
|
1589
1589
|
});
|
|
1590
1590
|
});
|
|
1591
1591
|
}
|
|
1592
|
+
// A partial download's version tag (ETag / Last-Modified), for resuming it after a restart: kept
|
|
1593
|
+
// in StremioK's own folder by the partial file's path (DL_VALIDATORS at most, the oldest dropped),
|
|
1594
|
+
// never on the drive, where a prepared file could be a link elsewhere (read: its contents sent
|
|
1595
|
+
// as If-Range; written: overwritten). None to keep it in (TizenBrew): downloads are closed there.
|
|
1596
|
+
var DL_VALIDATORS = 64;
|
|
1597
|
+
function dlValFile() { var d = tokenDir(); return d ? d + '/dl-validators.json' : null; }
|
|
1598
|
+
function dlValAll() {
|
|
1599
|
+
var f = dlValFile(), o = null;
|
|
1600
|
+
if (!f) { return {}; }
|
|
1601
|
+
try { o = JSON.parse(require('fs').readFileSync(f, 'utf8')); } catch (e) { o = null; }
|
|
1602
|
+
return o && typeof o === 'object' && !Array.isArray(o) ? o : {};
|
|
1603
|
+
}
|
|
1604
|
+
function dlValGet(part) { var v = dlValAll()[part]; return typeof v === 'string' ? v.slice(0, 300) : ''; }
|
|
1605
|
+
function dlValSet(part, v) {
|
|
1606
|
+
var f = dlValFile();
|
|
1607
|
+
if (!f) { return; }
|
|
1608
|
+
var all = dlValAll();
|
|
1609
|
+
delete all[part];
|
|
1610
|
+
if (v) { all[part] = String(v).slice(0, 300); }
|
|
1611
|
+
var keys = Object.keys(all);
|
|
1612
|
+
while (keys.length > DL_VALIDATORS) { delete all[keys.shift()]; }
|
|
1613
|
+
try { require('fs').writeFileSync(f, JSON.stringify(all), { mode: 384 }); } catch (e) { log('download version not kept: ' + (e && (e.code || e.message))); }
|
|
1614
|
+
}
|
|
1592
1615
|
function dlRun(e) {
|
|
1593
1616
|
var fs = require('fs'), part = e.dir + '/' + e.name + '.part';
|
|
1594
1617
|
e.state = 'downloading'; e.err = '';
|
|
@@ -1601,17 +1624,17 @@ function dlRun(e) {
|
|
|
1601
1624
|
fs.mkdir(e.dir, function () {
|
|
1602
1625
|
fsRealOk(e.dir, function (bad) {
|
|
1603
1626
|
if (bad) { dlEnd(e, 'failed', 'the folder: ' + bad.message); return; }
|
|
1604
|
-
fs.
|
|
1627
|
+
fs.lstat(part, function (se, st) { // (not through a link: a partial file is a regular one, see openPart)
|
|
1628
|
+
if (!se && !st.isFile()) { dlEnd(e, 'failed', 'the partial file isn\'t a regular file'); return; }
|
|
1605
1629
|
var have = se ? 0 : st.size;
|
|
1606
1630
|
if (!have || e.answered) { get(e.url, 0, have); return; } // (this one's own, from this run: picked up as it was)
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
if (e.validator) { get(e.url, 0, have); } else { dropPart(function () { get(e.url, 0, 0); }); }
|
|
1610
|
-
});
|
|
1631
|
+
e.validator = dlValGet(part); // (a partial file from before a restart: resumed only with its version kept)
|
|
1632
|
+
if (e.validator) { get(e.url, 0, have); } else { dropPart(function () { get(e.url, 0, 0); }); }
|
|
1611
1633
|
});
|
|
1612
1634
|
});
|
|
1613
1635
|
});
|
|
1614
|
-
|
|
1636
|
+
// (….part.v: where 2.1.17–2.2.1 kept the version, removed; a link removed, not what it points to)
|
|
1637
|
+
function dropPart(then) { dlValSet(part, ''); fs.unlink(part + '.v', function () { fs.unlink(part, function () { then(); }); }); }
|
|
1615
1638
|
function get(u, hops, have) {
|
|
1616
1639
|
if (e.state !== 'downloading') { return; }
|
|
1617
1640
|
var pu = urllib.parse(u), mod = pu.protocol === 'https:' ? https : http;
|
|
@@ -1647,7 +1670,7 @@ function dlRun(e) {
|
|
|
1647
1670
|
if (!resumed) { // (asked again on a resume: If-Range; kept beside the partial file, for one after a restart)
|
|
1648
1671
|
var et = resp.headers.etag;
|
|
1649
1672
|
e.validator = et && !/^W\//.test(et) ? et : (resp.headers['last-modified'] || '');
|
|
1650
|
-
|
|
1673
|
+
dlValSet(part, e.validator);
|
|
1651
1674
|
}
|
|
1652
1675
|
if (isPlaylistAnswer(resp.headers['content-type'], '')) { resp.resume(); closed = true; dlEnd(e, 'failed', 'a streamed playlist (HLS or DASH), not a file: it can\'t be downloaded'); return; }
|
|
1653
1676
|
var total = 0, cr = /\/(\d+)\s*$/.exec(resp.headers['content-range'] || '');
|
|
@@ -1715,7 +1738,7 @@ function dlRun(e) {
|
|
|
1715
1738
|
fs.rename(part, final, function (re) {
|
|
1716
1739
|
if (re) { dlEnd(e, 'failed', 'couldn\'t finish the file: ' + (re.code || re.message)); return; }
|
|
1717
1740
|
e.name = final.split('/').pop(); e.tries = 0;
|
|
1718
|
-
fs.unlink(part + '.v', function () {});
|
|
1741
|
+
dlValSet(part, ''); fs.unlink(part + '.v', function () {});
|
|
1719
1742
|
dlEnd(e, 'done');
|
|
1720
1743
|
});
|
|
1721
1744
|
});
|
|
@@ -3057,7 +3080,8 @@ function epgControl(req, res, path, query) {
|
|
|
3057
3080
|
var ENTRY_PORT = PORT + 10, ENTRY_MS = 5 * 60 * 1000, ENTRY_IDLE_MS = 30000, ENTRY_MAX = 8192;
|
|
3058
3081
|
var ENTRY_OPEN_MAX = 2, ENTRY_OPENS = 12, ENTRY_CONNECTIONS = 8, ENTRY_VISIT_MS = 20000, ENTRY_QUIET_MS = 10000;
|
|
3059
3082
|
var ENTRY_TITLES = { link: 'Play a link on StremioK', addon: 'Add an addon to StremioK', playlist: 'Add an IPTV playlist to StremioK', config: 'Import a configuration file into StremioK',
|
|
3060
|
-
search: 'Search on StremioK', youtube: 'Add a YouTube catalog to StremioK', aiom: 'Your AIOManager address for StremioK', aiomkey: 'Your AIOManager API key for StremioK'
|
|
3083
|
+
search: 'Search on StremioK', youtube: 'Add a YouTube catalog to StremioK', aiom: 'Your AIOManager address for StremioK', aiomkey: 'Your AIOManager API key for StremioK',
|
|
3084
|
+
server: 'Your streaming server’s address for StremioK' };
|
|
3061
3085
|
var ENTRY_TEXT = { search: 'Words to search for', youtube: 'A channel, a playlist link, or words to search YouTube for', aiomkey: 'Your account API key' }; // (plain text, not a link)
|
|
3062
3086
|
var _entries = Object.create(null), _entryServer = null, _entrySockets = [], _entryOpened = [];
|
|
3063
3087
|
function lanAddress() {
|