stremiok 2.1.0 β 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/README.md +10 -9
- package/config.xml +1 -1
- package/css/style.css +27 -0
- package/index.html +34 -31
- package/js/app.js +25 -10
- package/js/epg.js +320 -0
- package/js/iptv.js +198 -25
- package/js/keys.js +2 -0
- package/js/player-controls.js +50 -8
- package/js/player-core.js +112 -12
- package/js/player-lang.js +66 -5
- package/js/player-store.js +23 -2
- package/js/player-subs.js +57 -13
- package/js/stremio.js +70 -6
- package/js/trakt.js +1 -0
- package/js/util.js +24 -13
- package/js/view-core.js +128 -55
- package/js/view-detail.js +1 -1
- package/js/view-guide.js +371 -0
- package/js/view-home.js +148 -33
- package/js/view-settings.js +241 -90
- package/js/views.js +4 -3
- package/js/ytplay.js +53 -5
- package/package.json +2 -1
- package/service/index.js +863 -46
package/js/stremio.js
CHANGED
|
@@ -175,7 +175,8 @@ var Stremio = (function () {
|
|
|
175
175
|
posterRatings: p.posterRatings === true, // Settings > Appearance: IMDb rating on cards
|
|
176
176
|
homePreview: p.homePreview !== false, // Settings > Appearance: Continue Watching on the TV's home bar (js/hubpreview.js)
|
|
177
177
|
homePreviewOrder: p.homePreviewOrder === 'watching' ? 'watching' : 'recent', // ...its tiles: the latest first, or as in Watching
|
|
178
|
-
hiddenTabs: wordList(p.hiddenTabs, 20, 20), // Settings > Appearance >
|
|
178
|
+
hiddenTabs: wordList(p.hiddenTabs, 20, 20), // Settings > Appearance > Menu: tabs hidden
|
|
179
|
+
menuOrder: wordList(p.menuOrder, 20, 20), // ...and the menu's order (see ViewParts.menuOrder)
|
|
179
180
|
hideWords: wordList(p.hideWords, 30, 40), // Settings > Source ranking: sources with these words at the bottom
|
|
180
181
|
watchRegion: /^[A-Z]{2}$/.test(p.watchRegion || '') ? p.watchRegion : '' }; // Settings > Integrations ('' = automatic)
|
|
181
182
|
}
|
|
@@ -481,7 +482,18 @@ var Stremio = (function () {
|
|
|
481
482
|
if (_hasIptv) { IPTV.removePlaylist(id); }
|
|
482
483
|
configChanged();
|
|
483
484
|
}
|
|
485
|
+
function iptvAddXtream(base, user, pass) { // (as any playlist added: the addons' list made again)
|
|
486
|
+
var id = _hasIptv ? IPTV.addXtream(base, user, pass) : false;
|
|
487
|
+
configChanged();
|
|
488
|
+
return id;
|
|
489
|
+
}
|
|
490
|
+
function iptvSetXtreamFormat(id, fmt) {
|
|
491
|
+
var changed = _hasIptv ? IPTV.setXtreamFormat(id, fmt) : false;
|
|
492
|
+
configChanged();
|
|
493
|
+
return changed;
|
|
494
|
+
}
|
|
484
495
|
function iptvRenamePlaylist(id, name) { return _hasIptv ? IPTV.renamePlaylist(id, name) : false; }
|
|
496
|
+
function iptvSetPlaylistGuide(id, url) { return _hasIptv && IPTV.setPlaylistGuide ? IPTV.setPlaylistGuide(id, url) : false; }
|
|
485
497
|
function iptvSetPlaylistOff(id, off) { var ok = _hasIptv ? IPTV.setPlaylistOff(id, off) : false; configChanged(); return ok; }
|
|
486
498
|
function iptvMovePlaylist(id, dir) { var ok = _hasIptv ? IPTV.movePlaylist(id, dir) : false; configChanged(); return ok; }
|
|
487
499
|
function iptvLabel(id) { return _hasIptv ? IPTV.label(id) : ''; }
|
|
@@ -685,7 +697,8 @@ var Stremio = (function () {
|
|
|
685
697
|
// ---- Feed lists: order + show / hide, as the user sets them ----
|
|
686
698
|
// Home feeds (the Home rows: every catalog) and search sources (the Search rows:
|
|
687
699
|
// every catalog that can search) each keep their own { order: [keys], hidden: [keys] }.
|
|
688
|
-
// Anything new (an addon added later) goes after the ordered ones, in addon order
|
|
700
|
+
// Anything new (an addon added later) goes after the ordered ones, in addon order, but for a
|
|
701
|
+
// catalog that asks to come first (def._first: IPTV's Favorites) until you place it.
|
|
689
702
|
var FEEDS_KEY = 'stremiok.feeds', SEARCH_FEEDS_KEY = 'stremiok.searchfeeds';
|
|
690
703
|
function catalogKey(cat) {
|
|
691
704
|
var aid = (cat.addon.manifest && cat.addon.manifest.id) || cat.addon.url;
|
|
@@ -698,8 +711,8 @@ var Stremio = (function () {
|
|
|
698
711
|
var pos = {};
|
|
699
712
|
cfg().order.forEach(function (k, i) { pos[k] = i; });
|
|
700
713
|
return items.map(function (c, i) { return { c: c, i: i, k: keyOf(c) }; }).sort(function (a, b) {
|
|
701
|
-
var pa = (pos[a.k] !== undefined) ? pos[a.k] : (100000 + a.i);
|
|
702
|
-
var pb = (pos[b.k] !== undefined) ? pos[b.k] : (100000 + b.i);
|
|
714
|
+
var pa = (pos[a.k] !== undefined) ? pos[a.k] : (a.c && a.c.def && a.c.def._first) ? -1 : (100000 + a.i);
|
|
715
|
+
var pb = (pos[b.k] !== undefined) ? pos[b.k] : (b.c && b.c.def && b.c.def._first) ? -1 : (100000 + b.i);
|
|
703
716
|
return pa - pb;
|
|
704
717
|
}).map(function (x) { return x.c; });
|
|
705
718
|
}
|
|
@@ -1455,6 +1468,57 @@ var Stremio = (function () {
|
|
|
1455
1468
|
return tmdbGet('/' + kind + '/' + tmdbId + '/external_ids', '', cancel).then(function (r) { return (r && r.imdb_id) || null; });
|
|
1456
1469
|
}
|
|
1457
1470
|
|
|
1471
|
+
// A film known only by a TV guide's program (the Guide's guide-only channels): { title (as
|
|
1472
|
+
// shown, Spanish say), orig (its original title), year } -> Promise of { type, id (IMDb), name,
|
|
1473
|
+
// poster } when exactly one film has that name (and that year, when known), else null.
|
|
1474
|
+
// TMDB with your key (the original title, else the shown one in Spanish), else Cinemeta
|
|
1475
|
+
// (original titles only).
|
|
1476
|
+
function plainTitle(s) { // ("El SeΓ±or de los Anillos:" -> "elsenordelosanillos")
|
|
1477
|
+
var t = ('' + (s || '')).toLowerCase();
|
|
1478
|
+
if (t.normalize) { try { t = t.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); } catch (e) {} }
|
|
1479
|
+
return t.replace(/[^a-z0-9]+/g, '');
|
|
1480
|
+
}
|
|
1481
|
+
function findFilm(p, cancel) {
|
|
1482
|
+
var year = +p.year || 0;
|
|
1483
|
+
function near(y) { return !year || (y && Math.abs(y - year) <= 1); }
|
|
1484
|
+
function one(list, names) {
|
|
1485
|
+
var want = names.map(plainTitle).filter(Boolean), hits = (list || []).filter(function (r) {
|
|
1486
|
+
return near(r.year) && want.some(function (w) { return w === plainTitle(r.name) || w === plainTitle(r.orig); });
|
|
1487
|
+
});
|
|
1488
|
+
return hits.length === 1 ? hits[0] : null;
|
|
1489
|
+
}
|
|
1490
|
+
function tmdb(query, spanish) {
|
|
1491
|
+
var extra = '&query=' + encodeURIComponent(query) + (year ? '&year=' + year : '') + (spanish ? '&language=es-AR' : '');
|
|
1492
|
+
return tmdbGet('/search/movie', extra, cancel).then(function (r) {
|
|
1493
|
+
return ((r && r.results) || []).map(function (x) {
|
|
1494
|
+
return { tmdbId: x.id, name: x.title || '', orig: x.original_title || '', year: +('' + (x.release_date || '')).slice(0, 4) || 0, poster: tmdbImg(x.poster_path, 'w342') };
|
|
1495
|
+
});
|
|
1496
|
+
}, function () { return []; });
|
|
1497
|
+
}
|
|
1498
|
+
function viaTmdb() {
|
|
1499
|
+
if (!tmdbKey()) { return Promise.resolve(null); }
|
|
1500
|
+
var first = p.orig ? tmdb(p.orig, false).then(function (l) { return one(l, [p.orig]); }) : Promise.resolve(null);
|
|
1501
|
+
return first.then(function (hit) {
|
|
1502
|
+
return hit || (p.title ? tmdb(p.title, true).then(function (l) { return one(l, [p.title]); }) : null);
|
|
1503
|
+
}).then(function (hit) {
|
|
1504
|
+
if (!hit) { return null; }
|
|
1505
|
+
return tmdbImdbId('movie', hit.tmdbId, cancel).then(function (imdb) {
|
|
1506
|
+
return imdb ? { type: 'movie', id: imdb, name: hit.name || p.title, poster: hit.poster } : null;
|
|
1507
|
+
}, function () { return null; });
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
function viaCinemeta() {
|
|
1511
|
+
if (!p.orig) { return Promise.resolve(null); }
|
|
1512
|
+
return U.getJSON('https://v3-cinemeta.strem.io/catalog/movie/top/search=' + encodeURIComponent(p.orig) + '.json', 10000, null, cancel).then(function (r) {
|
|
1513
|
+
var hit = one(((r && r.metas) || []).map(function (m) {
|
|
1514
|
+
return { id: m.id, name: m.name || '', year: parseInt(m.releaseInfo || m.year || '', 10) || 0, poster: m.poster || '' };
|
|
1515
|
+
}), [p.orig]);
|
|
1516
|
+
return hit && /^tt\d+$/.test(hit.id) ? { type: 'movie', id: hit.id, name: p.title || hit.name, poster: hit.poster } : null;
|
|
1517
|
+
}, function () { return null; });
|
|
1518
|
+
}
|
|
1519
|
+
return viaTmdb().then(function (hit) { return hit || viaCinemeta(); });
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1458
1522
|
// ---- MDBList ratings (Settings > Integrations > MDBList) ----
|
|
1459
1523
|
// -> [{ key, label, text }] in a fixed order, only the ones the title has.
|
|
1460
1524
|
var MDB_SOURCES = [['imdb', 'IMDb'], ['tomatoes', 'Rotten Tomatoes'], ['popcorn', 'Audience'],
|
|
@@ -2012,7 +2076,7 @@ var Stremio = (function () {
|
|
|
2012
2076
|
getIntroSegments: getIntroSegments,
|
|
2013
2077
|
tmdbValidate: tmdbValidate, mdblistValidate: mdblistValidate, omdbValidate: omdbValidate,
|
|
2014
2078
|
tmdbFindTvId: tmdbFindTvId, tmdbSeasonEpisodes: tmdbSeasonEpisodes, tmdbImg: tmdbImg,
|
|
2015
|
-
tmdbTitleExtras: tmdbTitleExtras, tmdbExtrasById: tmdbExtrasById, tmdbEpisodeCast: tmdbEpisodeCast, tmdbEpisodeTrailer: tmdbEpisodeTrailer, setHideWords: setHideWords, hideWordHit: hideWordHit, normalizeAddonUrl: normalizeManifestUrl, debridOf: debridOf, addonOrigin: addonOrigin, setAddonOrigin: setAddonOrigin, parseSetupList: parseSetupList, applySetupEntries: applySetupEntries, sizeLimitHit: sizeLimitHit, setSizeLimit: setSizeLimit, gbText: gbText, runtimeMinutes: runtimeMinutes, tmdbPeople: tmdbPeople, tmdbPerson: tmdbPerson, tmdbBrowse: tmdbBrowse, tmdbTitleTrailers: tmdbTitleTrailers, tmdbProviders: tmdbProviders, tmdbProvidersKept: tmdbProvidersKept, tmdbGenres: tmdbGenres, tmdbWhereToWatch: tmdbWhereToWatch, watchRegion: watchRegion, autoWatchRegion: autoWatchRegion, tmdbImdbId: tmdbImdbId, hasTmdb: function () { return !!tmdbKey(); },
|
|
2079
|
+
tmdbTitleExtras: tmdbTitleExtras, tmdbExtrasById: tmdbExtrasById, tmdbEpisodeCast: tmdbEpisodeCast, tmdbEpisodeTrailer: tmdbEpisodeTrailer, setHideWords: setHideWords, hideWordHit: hideWordHit, normalizeAddonUrl: normalizeManifestUrl, debridOf: debridOf, addonOrigin: addonOrigin, setAddonOrigin: setAddonOrigin, parseSetupList: parseSetupList, applySetupEntries: applySetupEntries, sizeLimitHit: sizeLimitHit, setSizeLimit: setSizeLimit, gbText: gbText, runtimeMinutes: runtimeMinutes, tmdbPeople: tmdbPeople, tmdbPerson: tmdbPerson, tmdbBrowse: tmdbBrowse, tmdbTitleTrailers: tmdbTitleTrailers, tmdbProviders: tmdbProviders, tmdbProvidersKept: tmdbProvidersKept, tmdbGenres: tmdbGenres, tmdbWhereToWatch: tmdbWhereToWatch, watchRegion: watchRegion, autoWatchRegion: autoWatchRegion, tmdbImdbId: tmdbImdbId, findFilm: findFilm, hasTmdb: function () { return !!tmdbKey(); },
|
|
2016
2080
|
mdblistRatings: mdblistRatings,
|
|
2017
2081
|
omdbSeasonRatings: omdbSeasonRatings,
|
|
2018
2082
|
loadAddons: loadAddons, listCatalogs: listCatalogs, getCatalog: getCatalog,
|
|
@@ -2042,7 +2106,7 @@ var Stremio = (function () {
|
|
|
2042
2106
|
importSetupList: importSetupList, savedManifest: savedManifest, isAddonOff: isAddonOff, setAddonOff: setAddonOff, onIptvReady: onIptvReady, streamListenerCount: function (type, id) { var e = _streamCache[type + ':' + id]; return e ? e.listeners.length : 0; },
|
|
2043
2107
|
iptvGetPlaylists: iptvGetPlaylists, iptvAddPlaylist: iptvAddPlaylist,
|
|
2044
2108
|
iptvUpdatePlaylist: iptvUpdatePlaylist, iptvRemovePlaylist: iptvRemovePlaylist,
|
|
2045
|
-
iptvRenamePlaylist: iptvRenamePlaylist, iptvSetPlaylistOff: iptvSetPlaylistOff, iptvMovePlaylist: iptvMovePlaylist, iptvLabel: iptvLabel,
|
|
2109
|
+
iptvRenamePlaylist: iptvRenamePlaylist, iptvSetPlaylistGuide: iptvSetPlaylistGuide, iptvAddXtream: iptvAddXtream, iptvSetXtreamFormat: iptvSetXtreamFormat, iptvSetPlaylistOff: iptvSetPlaylistOff, iptvMovePlaylist: iptvMovePlaylist, iptvLabel: iptvLabel,
|
|
2046
2110
|
iptvLoad: iptvLoad, iptvStatus: iptvStatus, iptvReload: iptvReload
|
|
2047
2111
|
};
|
|
2048
2112
|
})();
|
package/js/trakt.js
CHANGED
|
@@ -375,6 +375,7 @@ var Trakt = (function () {
|
|
|
375
375
|
function next() {
|
|
376
376
|
return authed('GET', path + (path.indexOf('?') === -1 ? '?' : '&') + 'page=' + page + '&limit=' + perPage, null).then(function (r) {
|
|
377
377
|
live(g);
|
|
378
|
+
if (!Array.isArray(r.data)) { var bad = new Error('Trakt sent a page that couldn\'t be read (' + path.split('?')[0] + ', page ' + page + ')'); bad.status = r.status || 0; throw bad; } // (not the last page: the refresh fails, the copy kept)
|
|
378
379
|
var items = list(r.data, perPage), first = items.length ? JSON.stringify(items[0]) : '';
|
|
379
380
|
if (page > 1 && first && first === before) { return out.slice(0, max); }
|
|
380
381
|
before = first;
|
package/js/util.js
CHANGED
|
@@ -430,15 +430,20 @@ var U = (function () {
|
|
|
430
430
|
/* GET via XHR with a timeout. Returns a Promise of the response text.
|
|
431
431
|
maxBytes: give up as soon as more than that has arrived ("Too large (over β¦)");
|
|
432
432
|
TEXT_MAX when not given. */
|
|
433
|
-
// A failed download, in Diagnostics.
|
|
434
|
-
//
|
|
433
|
+
// A failed download, in Diagnostics. One asked during a private playback (when it started:
|
|
434
|
+
// it may fail after the playback ended) says only that, what kind of failure, and its
|
|
435
|
+
// server: not its link nor what the server answered (a title's name can be in either), in
|
|
436
|
+
// the log or the copy saved for the next launch.
|
|
435
437
|
function privateNow() { try { return typeof Player !== 'undefined' && !!Player.privateNow && Player.privateNow(); } catch (e) { return false; } }
|
|
436
|
-
function logged(err) {
|
|
438
|
+
function logged(err, priv) {
|
|
437
439
|
var msg = '' + err.message;
|
|
438
|
-
if (privateNow()) {
|
|
440
|
+
if (priv || privateNow()) {
|
|
441
|
+
var kind = /^(HTTP \d+|Timeout|Network error|Too large|Bad JSON|Redirected into the home network)/.exec(msg), host = /(https?:\/\/[^\/\s?#]+)/.exec(msg);
|
|
442
|
+
msg = 'Private viewing: a download' + (host ? ' from ' + host[1] : '') + ' failed' + (kind ? ' (' + kind[1] + ')' : '');
|
|
443
|
+
}
|
|
439
444
|
log('network', msg); err.logged = true; return err;
|
|
440
445
|
}
|
|
441
|
-
function noteNetwork(err) { return logged(err); }
|
|
446
|
+
function noteNetwork(err, priv) { return logged(err, priv); }
|
|
442
447
|
// What a server said when it refused (an error answer's own message, e.g. AIOStreams'
|
|
443
448
|
// { error: { message: "Invalid stream expression β¦ Expected :" } }, or short plain text),
|
|
444
449
|
// on one line, links in it masked, a long one kept to its start and end (where the what
|
|
@@ -464,8 +469,10 @@ var U = (function () {
|
|
|
464
469
|
// quiet: a failure isn't noted in Diagnostics (a check that's expected to fail at times, like
|
|
465
470
|
// the YouTube service's while it starts; the caller says so if it matters).
|
|
466
471
|
// headers (optional): { name: value } sent with it.
|
|
467
|
-
|
|
468
|
-
|
|
472
|
+
// body (optional): sent as a POST (text), else it's a GET.
|
|
473
|
+
function getText(url, timeoutMs, maxBytes, cancel, quiet, headers, body) {
|
|
474
|
+
var priv = privateNow(); // (asked during a private playback: logged as one, however late it fails)
|
|
475
|
+
function logged(err) { return quiet ? err : noteNetwork(err, priv); }
|
|
469
476
|
maxBytes = maxBytes || TEXT_MAX;
|
|
470
477
|
return new Promise(function (resolveP, rejectP) {
|
|
471
478
|
if (cancel && cancel.cancelled) { rejectP(cancelledError(url)); return; } // (nothing started)
|
|
@@ -482,7 +489,7 @@ var U = (function () {
|
|
|
482
489
|
});
|
|
483
490
|
}
|
|
484
491
|
try {
|
|
485
|
-
xhr.open('GET', url, true);
|
|
492
|
+
xhr.open(body == null ? 'GET' : 'POST', url, true);
|
|
486
493
|
if (headers) { Object.keys(headers).forEach(function (h) { xhr.setRequestHeader(h, headers[h]); }); }
|
|
487
494
|
} catch (e) { reject(e); return; }
|
|
488
495
|
xhr.timeout = timeoutMs || 15000;
|
|
@@ -516,7 +523,7 @@ var U = (function () {
|
|
|
516
523
|
};
|
|
517
524
|
xhr.ontimeout = function () { if (!done) { done = true; reject(logged(new Error('Timeout: ' + redactUrl(url)))); } };
|
|
518
525
|
xhr.onerror = function () { if (!done) { done = true; reject(logged(new Error('Network error: ' + redactUrl(url)))); } };
|
|
519
|
-
try { xhr.send(); } catch (e3) { reject(e3); }
|
|
526
|
+
try { if (body == null) { xhr.send(); } else { xhr.send('' + body); } } catch (e3) { reject(e3); }
|
|
520
527
|
});
|
|
521
528
|
}
|
|
522
529
|
|
|
@@ -557,10 +564,13 @@ var U = (function () {
|
|
|
557
564
|
|
|
558
565
|
/* GET JSON via XHR with a timeout. Returns a Promise of the parsed body (JSON_MAX
|
|
559
566
|
unless maxBytes says otherwise). cancel, headers: see getText. */
|
|
560
|
-
|
|
561
|
-
|
|
567
|
+
// A POST (body: text), its answer JSON.
|
|
568
|
+
function postJSON(url, body, timeoutMs, maxBytes, headers) { return getJSON(url, timeoutMs, maxBytes, null, headers, body); }
|
|
569
|
+
function getJSON(url, timeoutMs, maxBytes, cancel, headers, body) {
|
|
570
|
+
var priv = privateNow(); // (asked during a private playback: its answer unreadable later is logged as one too)
|
|
571
|
+
return getText(url, timeoutMs, maxBytes || JSON_MAX, cancel, false, headers, body).then(function (text) {
|
|
562
572
|
try { return JSON.parse(text); }
|
|
563
|
-
catch (e) { throw logged(new Error('Bad JSON from ' + redactUrl(url))); }
|
|
573
|
+
catch (e) { throw logged(new Error('Bad JSON from ' + redactUrl(url)), priv); }
|
|
564
574
|
});
|
|
565
575
|
}
|
|
566
576
|
|
|
@@ -1491,7 +1501,8 @@ var U = (function () {
|
|
|
1491
1501
|
}
|
|
1492
1502
|
|
|
1493
1503
|
return {
|
|
1494
|
-
byId: byId, el: el, clear: clear, moveIn: moveIn, getJSON: getJSON, getText: getText,
|
|
1504
|
+
byId: byId, el: el, clear: clear, moveIn: moveIn, getJSON: getJSON, getText: getText,
|
|
1505
|
+
postJSON: postJSON, redactUrl: redactUrl, problem: problem, problemDetail: problemDetail, listOf: listOf, lru: lru, cachedJSON: cachedJSON,
|
|
1495
1506
|
parseUrl: parseUrl, urlProblem: urlProblem, isPlaylistUrl: isPlaylistUrl, isManifestUrl: isManifestUrl,
|
|
1496
1507
|
isPrivateHost: isPrivateHost, urlIsPrivate: urlIsPrivate, setLocalPolicy: setLocalPolicy, sanitizeHeaders: sanitizeHeaders, drawable: drawable,
|
|
1497
1508
|
log: log, logEntries: logEntries, clearLog: clearLog, bigStore: bigStore, diskInfo: diskInfo,
|
package/js/view-core.js
CHANGED
|
@@ -8,6 +8,7 @@ var ViewParts = {};
|
|
|
8
8
|
|
|
9
9
|
// ----- from the other parts -----
|
|
10
10
|
function availableTabs() { return V.availableTabs.apply(null, arguments); } // home
|
|
11
|
+
function menuItems() { return V.menuItems.apply(null, arguments); } // home
|
|
11
12
|
function clearCatalogCache() { return V.clearCatalogCache.apply(null, arguments); } // home
|
|
12
13
|
function refreshAddonRows() { return V.refreshAddonRows.apply(null, arguments); } // home
|
|
13
14
|
function epCode() { return V.epCode.apply(null, arguments); } // detail
|
|
@@ -271,6 +272,16 @@ var ViewParts = {};
|
|
|
271
272
|
var rename = U.el('div', 'pill focusable modal-btn', 'Rename');
|
|
272
273
|
var onoff = U.el('div', 'pill focusable modal-btn', playlist.off ? 'Turn on' : 'Turn off');
|
|
273
274
|
var edit = U.el('div', 'pill focusable modal-btn', 'Edit link');
|
|
275
|
+
var guide = U.el('div', 'pill focusable modal-btn', 'Guide link');
|
|
276
|
+
var fmt = IPTV.xtreamFormat ? IPTV.xtreamFormat(playlist.id) : ''; // (an Xtream account: MPEG-TS or HLS)
|
|
277
|
+
var format = U.el('div', 'pill focusable modal-btn', 'Format: ' + (fmt === 'hls' ? 'HLS' : 'MPEG-TS'));
|
|
278
|
+
format.__onselect = function () {
|
|
279
|
+
closeModal();
|
|
280
|
+
Stremio.iptvSetXtreamFormat(playlist.id, fmt === 'hls' ? 'ts' : 'hls');
|
|
281
|
+
refreshAddonRows('local:iptv');
|
|
282
|
+
U.toast('Its channels now play as ' + (fmt === 'hls' ? 'MPEG-TS' : 'HLS') + ' (reading the account againβ¦)', 4000);
|
|
283
|
+
renderSettings();
|
|
284
|
+
};
|
|
274
285
|
var reload = U.el('div', 'pill focusable modal-btn', 'Reload');
|
|
275
286
|
var remove = U.el('div', 'pill focusable modal-btn danger', 'Remove');
|
|
276
287
|
|
|
@@ -312,6 +323,20 @@ var ViewParts = {};
|
|
|
312
323
|
}
|
|
313
324
|
});
|
|
314
325
|
};
|
|
326
|
+
guide.__onselect = function () { // its TV guide: yours, or the playlist's own (empty)
|
|
327
|
+
closeModal();
|
|
328
|
+
App.openEntry({
|
|
329
|
+
title: 'TV guide link', phone: 'playlist',
|
|
330
|
+
hint: 'An XMLTV guide (.xml or .xml.gz), then Confirm. Empty uses the playlistβs own (the one it names, or an Xtream playlistβs). RETURN cancels.',
|
|
331
|
+
value: playlist.guide || '', placeholder: 'https://example.com/guide.xml.gz',
|
|
332
|
+
validate: function (v) { return ('' + v).trim() ? (U.urlProblem(v) || null) : null; },
|
|
333
|
+
onConfirm: function (v) {
|
|
334
|
+
Stremio.iptvSetPlaylistGuide(playlist.id, v);
|
|
335
|
+
U.toast(('' + v).trim() ? 'Guide link set: reading itβ¦' : 'Back to the playlistβs own guide', 3000);
|
|
336
|
+
App.back(); renderSettings();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
};
|
|
315
340
|
reload.__onselect = function () {
|
|
316
341
|
closeModal();
|
|
317
342
|
U.toast('Reloading playlistβ¦', 4000);
|
|
@@ -334,6 +359,8 @@ var ViewParts = {};
|
|
|
334
359
|
row.appendChild(rename);
|
|
335
360
|
row.appendChild(onoff);
|
|
336
361
|
row.appendChild(edit);
|
|
362
|
+
if (!playlist.off && typeof EPG !== 'undefined' && EPG.available && EPG.available()) { row.appendChild(guide); }
|
|
363
|
+
if (!playlist.off && fmt) { row.appendChild(format); }
|
|
337
364
|
if (!playlist.off) { row.appendChild(reload); }
|
|
338
365
|
row.appendChild(remove);
|
|
339
366
|
box.appendChild(row);
|
|
@@ -373,33 +400,6 @@ var ViewParts = {};
|
|
|
373
400
|
showModal(overlay, pref);
|
|
374
401
|
}
|
|
375
402
|
|
|
376
|
-
// Switches in a popup (Settings βΊ Appearance βΊ Home tabs): OK on one turns it on or off and
|
|
377
|
-
// the list stays open (BACK closes it). items: [[key, label, on]]; words: { on, off } (the
|
|
378
|
-
// state shown after each label); onToggle(key, on).
|
|
379
|
-
function toggleList(title, items, words, onToggle) {
|
|
380
|
-
var existing = document.querySelector('.modal-overlay');
|
|
381
|
-
if (existing) { existing.parentNode.removeChild(existing); }
|
|
382
|
-
var overlay = U.el('div', 'modal-overlay');
|
|
383
|
-
var box = U.el('div', 'modal-box pick-box');
|
|
384
|
-
box.appendChild(U.el('div', 'modal-msg', title));
|
|
385
|
-
var listEl = U.el('div', 'pick-list'), first = null;
|
|
386
|
-
items.forEach(function (opt) {
|
|
387
|
-
var on = !!opt[2], it = U.el('div', 'pick-item focusable');
|
|
388
|
-
function draw() {
|
|
389
|
-
it.textContent = opt[1] + ': ' + (on ? words.on : words.off);
|
|
390
|
-
if (on) { it.classList.add('sel'); } else { it.classList.remove('sel'); }
|
|
391
|
-
}
|
|
392
|
-
draw();
|
|
393
|
-
it.__onselect = function () { on = !on; draw(); onToggle(opt[0], on); };
|
|
394
|
-
if (!first) { first = it; }
|
|
395
|
-
listEl.appendChild(it);
|
|
396
|
-
});
|
|
397
|
-
box.appendChild(listEl);
|
|
398
|
-
overlay.appendChild(box);
|
|
399
|
-
document.body.appendChild(overlay);
|
|
400
|
-
showModal(overlay, first);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
403
|
// Ticks in a popup (what to import): nothing ticked at first; OK on one ticks or unticks it,
|
|
404
404
|
// Select all ticks every one that can be (again: none); the button counts what's ticked.
|
|
405
405
|
// items: [{ key, label, have (already yours: shown as such, can't be ticked), note }];
|
|
@@ -482,7 +482,8 @@ var ViewParts = {};
|
|
|
482
482
|
}
|
|
483
483
|
|
|
484
484
|
// The shared top menu, used on Home / Search / Settings so they look and navigate
|
|
485
|
-
// the same:
|
|
485
|
+
// the same: Watching, then the tabs, Discover and Search in your order (menuItems), then
|
|
486
|
+
// Settings, on the left; StremioK on the right.
|
|
486
487
|
// `active` is the key of the current view: a tab key, 'search' or 'settings'.
|
|
487
488
|
function buildTopMenu(active) {
|
|
488
489
|
var head = U.el('div', 'header home-header');
|
|
@@ -495,34 +496,23 @@ var ViewParts = {};
|
|
|
495
496
|
};
|
|
496
497
|
function down() { return focusBelowTop(head); }
|
|
497
498
|
var tabbar = U.el('div', 'tabbar-inline');
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
499
|
+
// a pill: a Home tab, Discover (browse any catalog in full, its own screen), Search;
|
|
500
|
+
// Settings a chip like them (name, then icon), after them
|
|
501
|
+
function item(cls, key, label, icon, open, into) {
|
|
502
|
+
var p = U.el('div', cls + ' focusable' + (key === active ? ' active' : ''));
|
|
503
|
+
p.appendChild(document.createTextNode(label));
|
|
504
|
+
p.appendChild(U.el('span', 'tab-ic', icon));
|
|
505
|
+
p.__onselect = open;
|
|
506
|
+
p.__navDOWN = down;
|
|
507
|
+
into.appendChild(p);
|
|
508
|
+
}
|
|
509
|
+
menuItems().forEach(function (m) {
|
|
510
|
+
if (m.tab) { item('tab-pill', m.key, m.tab.label, m.tab.icon, function () { App.goHome(m.key); }, tabbar); }
|
|
511
|
+
else if (m.key === 'discover') { item('tab-pill', 'discover', 'Discover', 'π', function () { App.openDiscover(); }, tabbar); }
|
|
512
|
+
else if (m.key === 'search') { item('hchip', 'search', 'Search', 'π', function () { App.openSearch(); }, tabbar); }
|
|
505
513
|
});
|
|
506
|
-
// Discover: browse any catalog in full, by genre / year (its own screen)
|
|
507
|
-
var disc = U.el('div', 'tab-pill focusable' + (active === 'discover' ? ' active' : ''));
|
|
508
|
-
disc.appendChild(document.createTextNode('Discover'));
|
|
509
|
-
disc.appendChild(U.el('span', 'tab-ic', 'π'));
|
|
510
|
-
disc.__onselect = function () { App.openDiscover(); };
|
|
511
|
-
disc.__navDOWN = down;
|
|
512
|
-
tabbar.appendChild(disc);
|
|
513
514
|
head.appendChild(tabbar);
|
|
514
|
-
|
|
515
|
-
// Search and Settings: pills like the tabs (name, then icon), after them
|
|
516
|
-
function chip(key, label, icon, open) {
|
|
517
|
-
var c = U.el('div', 'hchip focusable' + (active === key ? ' active' : ''));
|
|
518
|
-
c.appendChild(document.createTextNode(label));
|
|
519
|
-
c.appendChild(U.el('span', 'tab-ic', icon));
|
|
520
|
-
c.__onselect = open;
|
|
521
|
-
c.__navDOWN = down;
|
|
522
|
-
head.appendChild(c);
|
|
523
|
-
}
|
|
524
|
-
chip('search', 'Search', 'π', function () { App.openSearch(); });
|
|
525
|
-
chip('settings', 'Settings', 'β', function () { App.openSettings(); });
|
|
515
|
+
item('hchip', 'settings', 'Settings', 'β', function () { App.openSettings(); }, head);
|
|
526
516
|
|
|
527
517
|
head.appendChild(U.el('div', 'spacer'));
|
|
528
518
|
var brand = U.el('div', 'brand');
|
|
@@ -579,6 +569,10 @@ var ViewParts = {};
|
|
|
579
569
|
if (rating > 0 && rating <= 10 && Stremio.getPrefs().posterRatings) { c.appendChild(U.el('div', 'card-rating', 'β
' + rating.toFixed(1))); } // (a series: marked watched whole, see setTitleWatched / the title page)
|
|
580
570
|
if (kind === 'movie' || kind === 'series') { c.__onlongpress = function () { posterActions(meta, kind, c); }; }
|
|
581
571
|
else if (youTubeList(meta.id)) { c.__onlongpress = function () { addYouTubeCatalog(meta); }; } // (a channel / playlist: to your YouTube)
|
|
572
|
+
else if (/^iptv:/.test(meta.id || '') && typeof EPG !== 'undefined') { // an IPTV channel: what's on, and its schedule
|
|
573
|
+
guideOnCard(c, meta.id);
|
|
574
|
+
c.__onlongpress = function () { liveSchedule(meta, c); };
|
|
575
|
+
}
|
|
582
576
|
c.__onselect = function () {
|
|
583
577
|
var t = titleType(meta.type || fallbackType);
|
|
584
578
|
// Live TV catalogs (anything that isn't movies/series) skip the info page and
|
|
@@ -597,6 +591,86 @@ var ViewParts = {};
|
|
|
597
591
|
return c;
|
|
598
592
|
}
|
|
599
593
|
|
|
594
|
+
// ---- TV guide (js/epg.js) on IPTV channel cards ----
|
|
595
|
+
// What's on now, under the channel's name, with how far along it is; drawn again when the
|
|
596
|
+
// guide brings more (the cards still on a page; the most recent GUIDE_CARDS).
|
|
597
|
+
var _guideCards = [], GUIDE_CARDS = 3000;
|
|
598
|
+
function guideOnCard(card, id) {
|
|
599
|
+
drawGuide(card, id);
|
|
600
|
+
_guideCards.push(card);
|
|
601
|
+
if (_guideCards.length > GUIDE_CARDS) { _guideCards = _guideCards.slice(-GUIDE_CARDS); }
|
|
602
|
+
}
|
|
603
|
+
function drawGuide(card, id) {
|
|
604
|
+
guideLogo(card, id);
|
|
605
|
+
var g = EPG.now(id), old = card.querySelector('.card-epg');
|
|
606
|
+
if (!g || !g.now) { if (old) { card.removeChild(old); } return; }
|
|
607
|
+
var t = Date.now() / 1000, pct = Math.max(0, Math.min(100, Math.round((t - g.now.start) / (g.now.stop - g.now.start) * 100)));
|
|
608
|
+
var box = old || U.el('div', 'card-epg');
|
|
609
|
+
U.clear(box);
|
|
610
|
+
box.appendChild(U.el('div', 'card-epg-t', g.now.title));
|
|
611
|
+
var bar = U.el('div', 'card-epg-bar'), fill = U.el('i', 'card-epg-fill');
|
|
612
|
+
fill.setAttribute('style', 'width: ' + pct + '%');
|
|
613
|
+
bar.appendChild(fill);
|
|
614
|
+
box.appendChild(bar);
|
|
615
|
+
if (!old) { card.appendChild(box); }
|
|
616
|
+
}
|
|
617
|
+
// A channel card without a picture (no tvg-logo): its guide's logo, once it's known; the
|
|
618
|
+
// name stays until it has loaded.
|
|
619
|
+
function guideLogo(card, id) {
|
|
620
|
+
var src = !card.__guideLogo && !card.querySelector('img.poster') && EPG.logo(id);
|
|
621
|
+
if (!src) { return; }
|
|
622
|
+
card.__guideLogo = true;
|
|
623
|
+
var img = U.el('img', 'poster'), fb = card.querySelector('.fallback');
|
|
624
|
+
img.style.visibility = 'hidden';
|
|
625
|
+
img.onload = function () { img.style.visibility = ''; if (fb && fb.parentNode) { fb.parentNode.removeChild(fb); } };
|
|
626
|
+
img.onerror = function () { if (img.parentNode) { img.parentNode.removeChild(img); } };
|
|
627
|
+
card.insertBefore(img, card.firstChild);
|
|
628
|
+
U.deferImage(img, U.cardImage(src, 'landscape'));
|
|
629
|
+
}
|
|
630
|
+
function redrawGuideCards() {
|
|
631
|
+
_guideCards = _guideCards.filter(function (c) { return document.documentElement.contains(c); });
|
|
632
|
+
_guideCards.forEach(function (c) { drawGuide(c, c.getAttribute('data-id')); });
|
|
633
|
+
}
|
|
634
|
+
if (typeof EPG !== 'undefined') {
|
|
635
|
+
EPG.onChange(redrawGuideCards);
|
|
636
|
+
// every minute: the cards' progress bars (and what's on, once it changed) along with the time
|
|
637
|
+
setInterval(function () {
|
|
638
|
+
if (!_guideCards.length || document.hidden || (typeof Player !== 'undefined' && Player.isActive())) { return; }
|
|
639
|
+
redrawGuideCards();
|
|
640
|
+
}, 60000);
|
|
641
|
+
}
|
|
642
|
+
// Hold OK on a channel card: βΆ Watch now, β
Add to / Remove from favorites, and its schedule
|
|
643
|
+
// from 2 hours ago to tomorrow (what's on now marked, and picked); OK on a program says
|
|
644
|
+
// what it is. A channel without a guide: the first two.
|
|
645
|
+
var _schedGen = 0;
|
|
646
|
+
function liveSchedule(meta, card) {
|
|
647
|
+
var gen = ++_schedGen;
|
|
648
|
+
U.spinner(true);
|
|
649
|
+
EPG.schedule(meta.id).then(null, function () { return []; }).then(function (list) {
|
|
650
|
+
if (gen !== _schedGen) { return; } // (held on another card since: its own)
|
|
651
|
+
U.spinner(false);
|
|
652
|
+
if (!document.documentElement.contains(card) || (typeof Player !== 'undefined' && Player.isActive())) { return; } // (left meanwhile)
|
|
653
|
+
var fav = IPTV.isFavorite(meta.id), t = Date.now() / 1000, at = 'play';
|
|
654
|
+
var items = [['play', 'βΆ Watch now'], ['fav', fav ? 'β
Remove from favorites' : 'β
Add to favorites']];
|
|
655
|
+
list.forEach(function (p, i) {
|
|
656
|
+
var on = p.start <= t && t < p.stop;
|
|
657
|
+
if (on) { at = 'p' + i; }
|
|
658
|
+
items.push(['p' + i, (on ? 'β ' : '') + EPG.dayTime(p.start) + ' ' + p.title]);
|
|
659
|
+
});
|
|
660
|
+
pickList((meta.name || '') + (list.length ? ' Β· TV guide' : ''), items, at, function (v) {
|
|
661
|
+
if (v === 'play') { card.__onselect(); return; }
|
|
662
|
+
if (v === 'fav') {
|
|
663
|
+
IPTV.setFavorite(meta.id, !fav);
|
|
664
|
+
U.toast(fav ? 'Removed from favorites' : 'Added to favorites: β
Favorites, first on the IPTV tab', 3000);
|
|
665
|
+
refreshAddonRows('local:iptv');
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
var p = list[+v.slice(1)];
|
|
669
|
+
if (p) { U.toast(EPG.dayTime(p.start) + 'β' + EPG.hhmm(p.stop) + ' ' + p.title + (p.desc ? ': ' + p.desc : ''), 8000); }
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
600
674
|
// The channels of the row a live card sits in, for CH+/CHβ while it plays.
|
|
601
675
|
function rowChannels(card) {
|
|
602
676
|
var sib = card.parentNode ? card.parentNode.children : [card], list = [], idx = 0;
|
|
@@ -750,7 +824,6 @@ var ViewParts = {};
|
|
|
750
824
|
V.inNowPlaying = inNowPlaying;
|
|
751
825
|
V.onModalClose = onModalClose;
|
|
752
826
|
V.pickList = pickList;
|
|
753
|
-
V.toggleList = toggleList;
|
|
754
827
|
V.pickMany = pickMany;
|
|
755
828
|
V.playlistActions = playlistActions;
|
|
756
829
|
V.posterCard = posterCard;
|
package/js/view-detail.js
CHANGED
|
@@ -2039,7 +2039,7 @@
|
|
|
2039
2039
|
if (_d) { _d.direct = false; _d.directList = false; _d.directFailed = false; } // (only this once: played again from the list, back to the list)
|
|
2040
2040
|
var onExit = function (how) {
|
|
2041
2041
|
// (after CH+/CHβ, back on the card of the channel you ended on)
|
|
2042
|
-
if (exitToBack) {
|
|
2042
|
+
if (exitToBack) { var ch = _d.channels ? _d.channels.list[_d.channels.idx] : null; App.returnFromLive(ch ? ch.card : null, ch ? ch.meta.id : null); return; }
|
|
2043
2043
|
// played straight away (a single YouTube video): back where it was opened from
|
|
2044
2044
|
if (direct && !page.directFailed && _d === page) { App.back(); return; } // (Home redrawn if Continue Watching changed)
|
|
2045
2045
|
// a channel's video played straight away: back on the channel's videos, on it
|