stremiok 2.1.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 +9 -9
- package/config.xml +1 -1
- package/css/style.css +27 -0
- package/index.html +34 -31
- package/js/app.js +19 -7
- 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-store.js +21 -1
- package/js/player-subs.js +38 -8
- 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-guide.js +332 -0
- package/js/view-home.js +148 -33
- package/js/view-settings.js +103 -35
- package/js/views.js +3 -2
- package/js/ytplay.js +53 -5
- package/package.json +2 -1
- package/service/index.js +863 -46
package/js/player-core.js
CHANGED
|
@@ -299,15 +299,29 @@ var PlayerParts = {};
|
|
|
299
299
|
} catch (er2) {}
|
|
300
300
|
return out;
|
|
301
301
|
}
|
|
302
|
+
// An IPTV channel's TV guide (js/epg.js): { now, next } or null.
|
|
303
|
+
function guideNow() { return (typeof EPG !== 'undefined' && S.ctx && /^iptv:/.test(S.ctx.id || '')) ? EPG.now(S.ctx.id) : null; }
|
|
304
|
+
function clock(sec) { var d = new Date(sec * 1000); return (d.getHours() < 10 ? '0' : '') + d.getHours() + ':' + (d.getMinutes() < 10 ? '0' : '') + d.getMinutes(); }
|
|
305
|
+
if (typeof EPG !== 'undefined' && EPG.onChange) { EPG.onChange(function () { if (S.active && S.isLive) { renderOSD(); } }); }
|
|
302
306
|
function renderOSD() {
|
|
303
307
|
U.byId('osd-title').textContent = S.ctx ? S.ctx.name : '';
|
|
308
|
+
var ck = U.byId('osd-clock'); // (the time of day, top right while the bar is on screen)
|
|
309
|
+
if (ck) { ck.textContent = clock(Date.now() / 1000); }
|
|
304
310
|
renderTags();
|
|
305
311
|
renderChapters();
|
|
306
312
|
if (S.isLive) {
|
|
313
|
+
var cur = U.byId('osd-cur'), g = guideNow();
|
|
314
|
+
cur.className = 'live';
|
|
315
|
+
if (g && g.now) { // an IPTV channel with a TV guide: what's on, how far along, and next
|
|
316
|
+
var t = Date.now() / 1000;
|
|
317
|
+
U.byId('osd-bar').style.display = '';
|
|
318
|
+
U.byId('osd-progress').style.width = Math.max(0, Math.min(100, Math.round((t - g.now.start) / (g.now.stop - g.now.start) * 100))) + '%';
|
|
319
|
+
cur.textContent = '● LIVE ' + clock(g.now.start) + '–' + clock(g.now.stop) + ' ' + g.now.title;
|
|
320
|
+
U.byId('osd-dur').textContent = g.next ? 'Next ' + clock(g.next.start) + ' ' + g.next.title : '';
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
307
323
|
U.byId('osd-bar').style.display = 'none';
|
|
308
|
-
var cur = U.byId('osd-cur');
|
|
309
324
|
cur.textContent = '● LIVE';
|
|
310
|
-
cur.className = 'live';
|
|
311
325
|
U.byId('osd-dur').textContent = '';
|
|
312
326
|
return;
|
|
313
327
|
}
|
|
@@ -331,6 +345,41 @@ var PlayerParts = {};
|
|
|
331
345
|
yt: page, ytLang: lang, type: 'YouTube', id: 'yt_id:' + link.id, parent: 'yt_id:' + link.id, name: link.title || 'YouTube',
|
|
332
346
|
poster: 'https://i.ytimg.com/vi/' + link.id + '/hqdefault.jpg', streams: [], private: !!(S.ctx && S.ctx.private) || undefined }, _onExit); // (from a private one: private too)
|
|
333
347
|
}
|
|
348
|
+
// ---------- a live Xtream channel's format ----------
|
|
349
|
+
// An Xtream server sends a channel as MPEG-TS (…/live/USER/PASS/123.ts) or as HLS (….m3u8).
|
|
350
|
+
// The menu's Format plays it the other way and keeps that for the channel (FORMAT_KEY, by its
|
|
351
|
+
// id, the last FORMAT_KEEP), so it starts that way next time; its playlist's own way isn't
|
|
352
|
+
// kept. ctx._ownUrl: the link as its playlist gives it.
|
|
353
|
+
var FORMAT_KEY = 'stremiok.liveformat', FORMAT_KEEP = 300;
|
|
354
|
+
var XT_LIVE = /^(https?:\/\/[^?#]+\/live\/[^\/?#]+\/[^\/?#]+\/[^\/?#.]+)\.(ts|m3u8)([?#].*)?$/i;
|
|
355
|
+
function liveFormat(url) { var m = XT_LIVE.exec(url || ''); return m ? (m[2].toLowerCase() === 'm3u8' ? 'hls' : 'ts') : ''; }
|
|
356
|
+
function withFormat(url, fmt) { var m = XT_LIVE.exec(url || ''); return m ? m[1] + '.' + (fmt === 'hls' ? 'm3u8' : 'ts') + (m[3] || '') : url; }
|
|
357
|
+
function formatStart(ctx) {
|
|
358
|
+
if (!S.isLive || !ctx.id || !liveFormat(ctx._ownUrl || ctx.url)) { return; }
|
|
359
|
+
if (!ctx._ownUrl) { ctx._ownUrl = ctx.url; }
|
|
360
|
+
var want = (U.store.get(FORMAT_KEY, {}) || {})[ctx.id];
|
|
361
|
+
ctx.url = (want === 'ts' || want === 'hls') ? withFormat(ctx._ownUrl, want) : ctx._ownUrl;
|
|
362
|
+
}
|
|
363
|
+
function setLiveFormat(fmt) {
|
|
364
|
+
var old = S.ctx;
|
|
365
|
+
if (!S.active || !old || !old._ownUrl || !liveFormat(old.url) || liveFormat(old.url) === fmt) { return; }
|
|
366
|
+
var all = U.store.get(FORMAT_KEY, {}) || {};
|
|
367
|
+
delete all[old.id];
|
|
368
|
+
if (fmt !== liveFormat(old._ownUrl)) {
|
|
369
|
+
all[old.id] = fmt;
|
|
370
|
+
var keys = Object.keys(all);
|
|
371
|
+
keys.slice(0, Math.max(0, keys.length - FORMAT_KEEP)).forEach(function (k) { delete all[k]; }); // (the oldest picked)
|
|
372
|
+
}
|
|
373
|
+
U.store.set(FORMAT_KEY, all);
|
|
374
|
+
var ctx = {}; // (the channel again, without what was learned from the other link: its relay, its look)
|
|
375
|
+
Object.keys(old).forEach(function (k) { if (!/^_(relay|relayFor|relayCc|ccOffer|liveChecked)$/.test(k)) { ctx[k] = old[k]; } });
|
|
376
|
+
ctx.switching = true;
|
|
377
|
+
play(ctx, _onExit);
|
|
378
|
+
U.spinner(true, 'Opening it as ' + (fmt === 'hls' ? 'HLS' : 'MPEG-TS') + '…');
|
|
379
|
+
}
|
|
380
|
+
P.liveFormat = function () { return S.active && S.ctx && S.isLive && S.ctx._ownUrl ? liveFormat(S.ctx.url) : ''; };
|
|
381
|
+
P.setLiveFormat = setLiveFormat;
|
|
382
|
+
|
|
334
383
|
// Another source of what's playing (the player's source list): played in its place, at
|
|
335
384
|
// the same point, as a playback of its own — its quality ladder, YouTube details,
|
|
336
385
|
// captions, SponsorBlock sections, headers and file details are the new source's, and so
|
|
@@ -375,6 +424,7 @@ var PlayerParts = {};
|
|
|
375
424
|
S.active = true; S.cur = 0; S.dur = 0; _bg = null; S.actual = null;
|
|
376
425
|
// Stremio "tv" and "radio" types are strong live hints; refined after prepare.
|
|
377
426
|
S.isLive = (ctx.type === 'tv' || ctx.type === 'radio') || /^iptv:/.test(ctx.id || '') || !!ctx.isLive; // (an IPTV channel: live, a sports one too)
|
|
427
|
+
formatStart(ctx);
|
|
378
428
|
S.ctx.resumeSec = (ctx.trailer || ctx.fromStart) ? 0 : (ctx.resumeSec || getResume(ctx.type, ctx.id)); // (Play from start: 0:00)
|
|
379
429
|
|
|
380
430
|
// reset subtitle + audio + quality + on-screen controls state
|
|
@@ -1041,6 +1091,11 @@ var PlayerParts = {};
|
|
|
1041
1091
|
// cut that way, the channel restarts through the service's relay, which repacks the sound.
|
|
1042
1092
|
// The answer is kept per link (LIVE_KEY, LIVE_DAYS): a channel known to need it starts
|
|
1043
1093
|
// through the relay (startLive), one known not to isn't asked about again.
|
|
1094
|
+
// The same look says which closed-caption channels (CC1–CC4) are in its picture (cc, kept
|
|
1095
|
+
// with it once sure). They're offered in the subtitle menu (ctx._ccOffer); turned on (picked
|
|
1096
|
+
// there, remembered for the channel, or in your default subtitle language), the channel plays
|
|
1097
|
+
// through the relay with them as subtitle tracks (ctx._relayCc), the sound as it is unless it
|
|
1098
|
+
// needs repacking too.
|
|
1044
1099
|
var LIVE_KEY = 'stremiok.livefix', LIVE_KEEP = 300, LIVE_DAYS = 14;
|
|
1045
1100
|
function liveFixable(ctx) {
|
|
1046
1101
|
return useAV && S.isLive && !!ctx && !ctx.yt && !ctx.trailer && /^https?:\/\//i.test(ctx.url || '') &&
|
|
@@ -1050,22 +1105,45 @@ var PlayerParts = {};
|
|
|
1050
1105
|
var k = (U.store.get(LIVE_KEY, {}) || {})[url];
|
|
1051
1106
|
return (k && Date.now() - k.at < LIVE_DAYS * 86400000) ? k : null;
|
|
1052
1107
|
}
|
|
1053
|
-
|
|
1108
|
+
// The caption channels kept for a link: [{ ch: 'CC1', lang: 'en' }], or null (not known yet).
|
|
1109
|
+
function ccOf(k) {
|
|
1110
|
+
if (!k || !Array.isArray(k.cc)) { return null; }
|
|
1111
|
+
return k.cc.filter(function (c) { return c && /^CC[1-4]$/.test(c.ch) && (!c.lang || /^[a-z]{2,3}$/.test(c.lang)); }).slice(0, 4);
|
|
1112
|
+
}
|
|
1113
|
+
function liveKeep(url, fix, cc) {
|
|
1054
1114
|
var all = U.store.get(LIVE_KEY, {}) || {};
|
|
1055
1115
|
all[url] = { fix: fix ? 1 : 0, at: Date.now() };
|
|
1116
|
+
if (cc) { all[url].cc = cc; }
|
|
1056
1117
|
var keys = Object.keys(all);
|
|
1057
1118
|
if (keys.length > LIVE_KEEP) {
|
|
1058
1119
|
keys.sort(function (a, b) { return all[a].at - all[b].at; }).slice(0, keys.length - LIVE_KEEP).forEach(function (k) { delete all[k]; });
|
|
1059
1120
|
}
|
|
1060
1121
|
U.store.set(LIVE_KEY, all);
|
|
1061
1122
|
}
|
|
1123
|
+
// Captions on from the start: your choice for this channel (a closed-caption track picked
|
|
1124
|
+
// there last), else one in your default subtitle language (or second choice).
|
|
1125
|
+
function ccWanted(ctx, cc) {
|
|
1126
|
+
if (!cc || !cc.length) { return false; }
|
|
1127
|
+
var mem = P.recallSub(ctx.id);
|
|
1128
|
+
if (mem) { return !!mem.embedded; }
|
|
1129
|
+
var cfg = P.getSubCfg(), langs = [cfg.lang, cfg.lang2].filter(function (l) { return l && l !== 'off' && l !== 'none'; });
|
|
1130
|
+
return cc.some(function (c) { return c.lang && langs.some(function (l) { return P.langMatches(l, c.lang); }); });
|
|
1131
|
+
}
|
|
1132
|
+
function ccIndex(cc, w) { for (var i = 0; i < cc.length; i++) { if (cc[i].ch === w.ch) { return i; } } return 0; }
|
|
1133
|
+
function relayed(ctx, r, cc) {
|
|
1134
|
+
if (!r || typeof r.url !== 'string' || !/^http:\/\/localhost:\d+\/live\//.test(r.url)) { return false; }
|
|
1135
|
+
ctx._relay = r.url; ctx._relayFor = ctx.url; ctx._relayCc = cc || null; ctx._ccOffer = cc ? null : ctx._ccOffer;
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1062
1138
|
function startLive(ctx, settle) {
|
|
1063
|
-
var known = liveFixable(ctx) ? liveKnown(ctx.url) : null;
|
|
1064
|
-
if (
|
|
1065
|
-
|
|
1066
|
-
|
|
1139
|
+
var known = liveFixable(ctx) ? liveKnown(ctx.url) : null, cc = ccOf(known);
|
|
1140
|
+
if (cc && cc.length) { ctx._ccOffer = cc; }
|
|
1141
|
+
var withCc = ccWanted(ctx, cc) ? cc : null;
|
|
1142
|
+
if (cc) { ctx._liveChecked = true; } // (both answers in: not asked again)
|
|
1143
|
+
if (!known || (!known.fix && !withCc)) { startAV(ctx.url, settle); return; }
|
|
1144
|
+
YTPlay.liveOpen(ctx.url, ctx._headers, true, { fix: !!known.fix, cc: withCc }).then(function (r) {
|
|
1067
1145
|
if (S.ctx !== ctx || !S.active) { return; }
|
|
1068
|
-
|
|
1146
|
+
relayed(ctx, r, withCc);
|
|
1069
1147
|
startAV(ctx.url, settle);
|
|
1070
1148
|
}, function () { if (S.ctx === ctx && S.active) { startAV(ctx.url, settle); } });
|
|
1071
1149
|
}
|
|
@@ -1073,17 +1151,37 @@ var PlayerParts = {};
|
|
|
1073
1151
|
var ctx = S.ctx;
|
|
1074
1152
|
if (!ctx || ctx._liveChecked || !liveFixable(ctx)) { return; }
|
|
1075
1153
|
ctx._liveChecked = true;
|
|
1076
|
-
|
|
1154
|
+
var known = liveKnown(ctx.url);
|
|
1155
|
+
if (known && ccOf(known)) { return; }
|
|
1077
1156
|
var url = ctx.url;
|
|
1078
1157
|
YTPlay.liveOpen(url, ctx._headers, false).then(function (r) {
|
|
1079
1158
|
if (!r || !r.sure) { return; } // (couldn't tell: asked again next time)
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
ctx
|
|
1159
|
+
var cc = (r.ccSure || r.cc === undefined) ? (ccOf(r) || []) : null, fix = known ? !!known.fix : !!r.fix; // (captions with no text yet: asked again next time; the sound's answer: as first found)
|
|
1160
|
+
liveKeep(url, fix, cc);
|
|
1161
|
+
if (S.ctx !== ctx || !S.active || ctx.url !== url) { return; }
|
|
1162
|
+
if (cc && cc.length && !ctx._relayCc) { ctx._ccOffer = cc; }
|
|
1163
|
+
if (cc && cc.length && ccWanted(ctx, cc)) { ccOn(ctx, 'Adding this channel’s captions…'); return; }
|
|
1164
|
+
if (known || !fix || ctx._relay || !relayed(ctx, r, null)) { return; } // (the sound: restarted for on its first look only)
|
|
1083
1165
|
U.log('player', 'Live sound repacked by StremioK\'s service (' + (r.why || '') + ')');
|
|
1084
1166
|
reopen('Fixing this channel’s sound…');
|
|
1085
1167
|
}, function () {});
|
|
1086
1168
|
}
|
|
1169
|
+
// The channel again through the relay with its closed captions as subtitle tracks (the sound
|
|
1170
|
+
// repacked too when it needs it); then: true once it's reopening. want: a caption track to
|
|
1171
|
+
// select once it plays ({ ch, lang }).
|
|
1172
|
+
function ccOn(ctx, message, want) {
|
|
1173
|
+
var known = liveKnown(ctx.url), cc = ccOf(known) || ctx._ccOffer;
|
|
1174
|
+
if (!cc || !cc.length || !liveFixable(ctx)) { return Promise.resolve(false); }
|
|
1175
|
+
return YTPlay.liveOpen(ctx.url, ctx._headers, true, { fix: !!(known && known.fix), cc: cc }).then(function (r) {
|
|
1176
|
+
if (S.ctx !== ctx || !S.active || !relayed(ctx, r, cc)) { return false; }
|
|
1177
|
+
var w = want || cc.filter(function (c) { return ccWanted(ctx, [c]); })[0] || null;
|
|
1178
|
+
U.log('player', 'Live captions through StremioK\'s service (' + cc.map(function (c) { return c.ch; }).join(', ') + ')');
|
|
1179
|
+
reopen(message);
|
|
1180
|
+
if (w && P.wantEmbedded) { P.wantEmbedded({ nth: ccIndex(cc, w), lang: w.lang }); } // (after the reopen: on its tracks)
|
|
1181
|
+
return true;
|
|
1182
|
+
}, function () { return false; });
|
|
1183
|
+
}
|
|
1184
|
+
P.ccOn = function (want) { return S.ctx ? ccOn(S.ctx, 'Adding this channel’s captions…', want) : Promise.resolve(false); };
|
|
1087
1185
|
// The relay didn't start (StremioK's service gone): the channel as it is.
|
|
1088
1186
|
function relayFailed() {
|
|
1089
1187
|
if (S.playStarted || !S.ctx || !S.ctx._relay) { return false; }
|
|
@@ -1181,6 +1279,7 @@ var PlayerParts = {};
|
|
|
1181
1279
|
function exit() {
|
|
1182
1280
|
if (!S.active) { return; }
|
|
1183
1281
|
if (U.crumb) { U.crumb('player closed'); }
|
|
1282
|
+
if (P.sleepClear) { P.sleepClear(); } // (the sleep timer: this playback's)
|
|
1184
1283
|
saveProgress();
|
|
1185
1284
|
// Record partial progress to Trakt (unfinished VOD) so it shows in Continue Watching. Past
|
|
1186
1285
|
// 85% (Trakt connected): finished, Trakt's way (it marks it watched), and here as at the credits.
|
|
@@ -1227,4 +1326,5 @@ var PlayerParts = {};
|
|
|
1227
1326
|
P.sponsorHighlight = sponsorHighlight;
|
|
1228
1327
|
P.chapters = chapters; P.chapterAt = chapterAt; P.chapterLabel = chapterLabel;
|
|
1229
1328
|
P.seekTo = seekTo; P.doAVSeek = doAVSeek; P.seekBy = seekBy; P.exit = exit;
|
|
1329
|
+
P.reload = function () { if (S.active && S.ctx) { reopen(S.isLive ? 'Reloading the channel…' : 'Reloading…'); } }; // (the menu's Reload)
|
|
1230
1330
|
})(PlayerParts);
|
package/js/player-store.js
CHANGED
|
@@ -255,6 +255,26 @@
|
|
|
255
255
|
U.store.set(PLAYERCFG_KEY, d);
|
|
256
256
|
}
|
|
257
257
|
function aspectDef(id) { for (var i = 0; i < ASPECTS.length; i++) { if (ASPECTS[i][0] === id) { return ASPECTS[i]; } } return ASPECTS[0]; }
|
|
258
|
+
// A live channel's own picture mode (picked while it played): { channel id: mode }, the last
|
|
259
|
+
// ASPECTS_KEEP; one set back to the usual mode isn't kept apart. Anything else: the usual one.
|
|
260
|
+
var ASPECTS_KEY = 'stremiok.aspects', ASPECTS_KEEP = 300;
|
|
261
|
+
function aspectChannel() { var S = P.S; return (S && S.isLive && S.ctx && S.ctx.id) ? S.ctx.id : ''; }
|
|
262
|
+
function aspectNow() {
|
|
263
|
+
var ch = aspectChannel(), own = ch ? (U.store.get(ASPECTS_KEY, {}) || {})[ch] : '';
|
|
264
|
+
return own && aspectDef(own)[0] === own ? own : playerCfg().aspect;
|
|
265
|
+
}
|
|
266
|
+
function setAspectNow(mode) {
|
|
267
|
+
var ch = aspectChannel();
|
|
268
|
+
if (!ch) { setPlayerCfg({ aspect: mode }); return; }
|
|
269
|
+
var all = U.store.get(ASPECTS_KEY, {}) || {};
|
|
270
|
+
delete all[ch];
|
|
271
|
+
if (mode !== playerCfg().aspect) {
|
|
272
|
+
all[ch] = mode;
|
|
273
|
+
var keys = Object.keys(all);
|
|
274
|
+
keys.slice(0, Math.max(0, keys.length - ASPECTS_KEEP)).forEach(function (k) { delete all[k]; }); // (the oldest picked)
|
|
275
|
+
}
|
|
276
|
+
U.store.set(ASPECTS_KEY, all);
|
|
277
|
+
}
|
|
258
278
|
|
|
259
279
|
// ----- for the other parts (and js/player.js) -----
|
|
260
280
|
P.watchedState = watchedState; P.isWatched = isWatched; P.watchedAt = watchedAt; P.watchedSince = watchedSince;
|
|
@@ -265,7 +285,7 @@
|
|
|
265
285
|
P.getResume = getResume; P.removeProgress = removeProgress; P.continueList = continueList;
|
|
266
286
|
P.rememberSub = rememberSub; P.recallSub = recallSub; P.rememberAudio = rememberAudio;
|
|
267
287
|
P.recallAudio = recallAudio; P.playerCfg = playerCfg; P.setPlayerCfg = setPlayerCfg;
|
|
268
|
-
P.aspectDef = aspectDef; P.SUB_FONTS = SUB_FONTS; P.SUB_COLORS = SUB_COLORS; P.SUB_BGS = SUB_BGS;
|
|
288
|
+
P.aspectDef = aspectDef; P.aspectNow = aspectNow; P.setAspectNow = setAspectNow; P.SUB_FONTS = SUB_FONTS; P.SUB_COLORS = SUB_COLORS; P.SUB_BGS = SUB_BGS;
|
|
269
289
|
P.SUB_EDGES = SUB_EDGES; P.SUB_POSITIONS = SUB_POSITIONS; P.ASPECTS = ASPECTS;
|
|
270
290
|
P.isPrivate = isPrivate; P.setPrivate = setPrivate; P.privateNow = privateNow; P.subCfgVersion = subCfgVersion;
|
|
271
291
|
})(PlayerParts);
|
package/js/player-subs.js
CHANGED
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
if (mem.off) { return true; }
|
|
114
114
|
if (mem.embedded) {
|
|
115
115
|
if (partial) { return false; }
|
|
116
|
-
wantEmbedded({ index: mem.index, lang: mem.lang }); _embDelay = Math.max(0, +mem.delay || 0); return true;
|
|
116
|
+
wantEmbedded({ index: mem.index, lang: mem.lang, nth: mem.nth }); _embDelay = Math.max(0, +mem.delay || 0); return true;
|
|
117
117
|
}
|
|
118
118
|
var exact =
|
|
119
119
|
(mem.trackId != null ? _subTracks.find(function (t) { return t.id === mem.trackId; }) : null) ||
|
|
@@ -202,6 +202,21 @@
|
|
|
202
202
|
var WEBVTT_ASIDE = /^(NOTE|STYLE|REGION)([ \t]|$)/;
|
|
203
203
|
// Character codes (& > — …) as the characters they stand for.
|
|
204
204
|
var CHAR_CODES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: '\u00a0', lrm: '\u200e', rlm: '\u200f' };
|
|
205
|
+
// A subtitle's tags taken out, a stray < with them: one pass, in time with the text's length
|
|
206
|
+
// however it's made (a run of < with no > after it used to hold the remote for seconds).
|
|
207
|
+
// Its text is only ever shown as text (textContent): this is for how it reads.
|
|
208
|
+
function noTags(t) {
|
|
209
|
+
var out = '', i = 0, n = t.length;
|
|
210
|
+
while (i < n) {
|
|
211
|
+
var lt = t.indexOf('<', i);
|
|
212
|
+
if (lt === -1) { out += t.slice(i); break; }
|
|
213
|
+
out += t.slice(i, lt);
|
|
214
|
+
var gt = t.indexOf('>', lt + 1);
|
|
215
|
+
if (gt === -1) { out += t.slice(lt + 1).split('<').join(''); break; } // (no tag ends after it: the rest, its < gone)
|
|
216
|
+
i = gt + 1;
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
205
220
|
function charCodes(t) {
|
|
206
221
|
if (t.indexOf('&') === -1) { return t; }
|
|
207
222
|
return t.replace(/&(#[xX][0-9a-fA-F]{1,6}|#[0-9]{1,7}|[a-zA-Z]+);/g, function (all, c) {
|
|
@@ -213,13 +228,16 @@
|
|
|
213
228
|
return CHAR_CODES.hasOwnProperty(c) ? CHAR_CODES[c] : all;
|
|
214
229
|
});
|
|
215
230
|
}
|
|
231
|
+
// A file's cues: CUES_MAX at most, CUE_TEXT_MAX characters of text each (a broken or hostile
|
|
232
|
+
// file can't hold the remote: the whole parse stays in time with the file's length).
|
|
233
|
+
var CUES_MAX = 20000, CUE_TEXT_MAX = 1000;
|
|
216
234
|
function parseCues(text) {
|
|
217
235
|
text = ('' + text).replace(/\r/g, '');
|
|
218
236
|
var blocks = text.split(/\n\n+/);
|
|
219
237
|
var cues = [], skip = skipper(); // (Skip lines with: your words)
|
|
220
238
|
var re = /(?:(\d{1,3}):)?(\d{2}):(\d{2})[,.](\d{1,3})\s*-->\s*(?:(\d{1,3}):)?(\d{2}):(\d{2})[,.](\d{1,3})/;
|
|
221
239
|
function ms(x) { return +((x + '00').slice(0, 3)) / 1000; } // (".5" is half a second)
|
|
222
|
-
for (var i = 0; i < blocks.length; i++) {
|
|
240
|
+
for (var i = 0; i < blocks.length && cues.length < CUES_MAX; i++) {
|
|
223
241
|
var lines = blocks[i].replace(/^\n+/, '').split('\n');
|
|
224
242
|
if (WEBVTT_ASIDE.test(lines[0])) { continue; } // (a WebVTT comment, style or region: not shown)
|
|
225
243
|
var ti = -1;
|
|
@@ -229,7 +247,9 @@
|
|
|
229
247
|
if (!m) { continue; }
|
|
230
248
|
var start = (+(m[1] || 0)) * 3600 + (+m[2]) * 60 + (+m[3]) + ms(m[4]);
|
|
231
249
|
var end = (+(m[5] || 0)) * 3600 + (+m[6]) * 60 + (+m[7]) + ms(m[8]);
|
|
232
|
-
|
|
250
|
+
// (tags out, a stray < with them, in one pass; then entities read: one written as < is the file's own text,
|
|
251
|
+
// "<b>" shown as written. Never a tag: subtitles reach the screen as text only, textContent)
|
|
252
|
+
var txt = skip(charCodes(noTags(lines.slice(ti + 1).join('\n').slice(0, CUE_TEXT_MAX)).replace(/\{[^}]+\}/g, '')).replace(/\s+$/, ''));
|
|
233
253
|
if (txt) { cues.push({ start: start, end: end, text: txt }); }
|
|
234
254
|
}
|
|
235
255
|
cues.sort(function (a, b) { return a.start - b.start; });
|
|
@@ -384,6 +404,7 @@
|
|
|
384
404
|
pick = tracks.find(function (t) { return t.index === w.index && (!key || langKey(textLang(t)) === key); }) || null;
|
|
385
405
|
}
|
|
386
406
|
if (!pick && key) { pick = tracks.find(function (t) { return langMatches(key, textLang(t)); }) || null; }
|
|
407
|
+
if (!pick && w.nth != null && S.ctx && S.ctx._relayCc) { pick = tracks[w.nth] || null; } // (a live channel's closed captions: in the order asked for)
|
|
387
408
|
if (!pick && w.lang2) { // nothing in the default language anywhere: the second choice, a track to download first
|
|
388
409
|
var key2 = langKey(w.lang2);
|
|
389
410
|
var dl = _subTracks.find(function (t) { return langMatches(key2, t.lang); });
|
|
@@ -398,7 +419,7 @@
|
|
|
398
419
|
try { avplay.setSelectTrack('TEXT', tr.index); }
|
|
399
420
|
catch (e2) { try { avplay.setSelectTrack('TEXT', '' + tr.index); } catch (e3) {} }
|
|
400
421
|
_embSub = tr.index; _embLang = textLang(tr);
|
|
401
|
-
U.toast('Subtitles: ' + (_embLang || 'on') + ' (built-in)', 1800);
|
|
422
|
+
U.toast('Subtitles: ' + (_embLang || 'on') + (S.ctx && S.ctx._relayCc ? ' (closed captions)' : ' (built-in)'), 1800);
|
|
402
423
|
}
|
|
403
424
|
function embOff() {
|
|
404
425
|
if (_embSub != null) { try { avplay.setSilentSubtitle(true); } catch (e) {} }
|
|
@@ -446,7 +467,7 @@
|
|
|
446
467
|
function drawEmbedded(text, duration) {
|
|
447
468
|
if (_embSub == null || _subOn) { return; }
|
|
448
469
|
if (_embTimer) { clearTimeout(_embTimer); _embTimer = null; }
|
|
449
|
-
var t = ('' + (text || '')).replace(/<br\s*\/?>/gi, '\n')
|
|
470
|
+
var t = noTags(('' + (text || '')).replace(/<br\s*\/?>/gi, '\n')).replace(/\{[^}]+\}/g, '').trim();
|
|
450
471
|
if (!t) { S.subsEl.className = 'hidden'; return; }
|
|
451
472
|
S.subsEl.className = '';
|
|
452
473
|
var span = S.subsEl.firstChild;
|
|
@@ -515,7 +536,7 @@
|
|
|
515
536
|
replaceTop(timingMenu(topMenu().idx));
|
|
516
537
|
U.toast('Subtitles: ' + fmtDelay(_subDelay) + (_subDelay > 0 ? ' (later)' : _subDelay < 0 ? ' (earlier)' : ' (as in the file)'), 1800);
|
|
517
538
|
}
|
|
518
|
-
function lineText(c) { return '“' + ('' + c.text).replace(
|
|
539
|
+
function lineText(c) { return '“' + noTags('' + c.text).replace(/\s+/g, ' ').trim().slice(0, 70) + '”'; }
|
|
519
540
|
// The next line coming up (as timed now): OK the moment it's said.
|
|
520
541
|
function earMenu() {
|
|
521
542
|
var t = S.cur - _subDelay, next = null;
|
|
@@ -569,6 +590,8 @@
|
|
|
569
590
|
groups[k].push(item);
|
|
570
591
|
}
|
|
571
592
|
getTextTracks().forEach(function (tr, i) { add(langKey(textLang(tr)), { emb: tr, n: i }); });
|
|
593
|
+
var offer = S.ctx && !S.ctx._relayCc ? S.ctx._ccOffer : null; // (a live channel's closed captions, not on yet)
|
|
594
|
+
(offer || []).forEach(function (c, i) { add(langKey(c.lang || ''), { cc: c, n: i, emb: true }); });
|
|
572
595
|
_subTracks.forEach(function (tr) { add(langKey(tr.lang), { track: tr }); });
|
|
573
596
|
var curKey = _subOn ? langKey(_subLabel) : (_embSub != null ? langKey(_embLang) : null);
|
|
574
597
|
var pref = getSubCfg().lang;
|
|
@@ -583,7 +606,7 @@
|
|
|
583
606
|
order.forEach(function (k) {
|
|
584
607
|
var list = groups[k], n = list.length, emb = list.filter(function (x) { return x.emb; }).length;
|
|
585
608
|
items.push({ label: langName(k),
|
|
586
|
-
detail: n + ' option' + (n > 1 ? 's' : '') + (emb ? (emb === n ? ',
|
|
609
|
+
detail: n + ' option' + (n > 1 ? 's' : '') + (emb ? (emb === n ? ', ' + inFile() : ', ' + emb + ' ' + inFile()) : ''),
|
|
587
610
|
sel: (k === curKey),
|
|
588
611
|
run: function () { pushMenu(langTracksMenu(k, list)); } });
|
|
589
612
|
});
|
|
@@ -594,6 +617,7 @@
|
|
|
594
617
|
return { title: 'Subtitle language', items: items, idx: idx, rebuild: langMenu };
|
|
595
618
|
}
|
|
596
619
|
|
|
620
|
+
function inFile() { return S.ctx && (S.ctx._relayCc || S.ctx._ccOffer) ? 'closed captions' : 'in the file'; }
|
|
597
621
|
// Level 2: the tracks in one language: the file's own first, then each addon's.
|
|
598
622
|
// Picking a track keeps the menu open, the checkmark on it (an addon's once it loads), so
|
|
599
623
|
// several can be tried in a row: the file's own the same as an addon's.
|
|
@@ -601,10 +625,15 @@
|
|
|
601
625
|
var idx = 0, nAddon = 0;
|
|
602
626
|
list = list.filter(function (x) { return x.emb; }).concat(list.filter(function (x) { return !x.emb; }));
|
|
603
627
|
var items = list.map(function (x, i) {
|
|
628
|
+
if (x.cc) { // (turning a live channel's closed captions on: it restarts with them)
|
|
629
|
+
var c = x.cc;
|
|
630
|
+
return { label: 'Closed captions (' + c.ch + ')', detail: 'The channel restarts with them',
|
|
631
|
+
run: function () { closeMenu(); rememberSub(S.ctx.id, { embedded: true, nth: x.n, lang: c.lang }); P.ccOn(c); } };
|
|
632
|
+
}
|
|
604
633
|
if (x.emb) {
|
|
605
634
|
var tr = x.emb, l = textLang(tr);
|
|
606
635
|
if (_embSub === tr.index) { idx = i; }
|
|
607
|
-
return { label: 'Built into the file' + (l ? ' (' + l + ')' : ' · track ' + (x.n + 1)), sel: _embSub === tr.index,
|
|
636
|
+
return { label: (S.ctx && S.ctx._relayCc ? 'Closed captions' : 'Built into the file') + (l ? ' (' + l + ')' : ' · track ' + (x.n + 1)), sel: _embSub === tr.index,
|
|
608
637
|
run: function () {
|
|
609
638
|
_embDelay = 0; _embQueue = []; _embNudge = null; selectEmbeddedSub(tr); rememberSub(S.ctx.id, { embedded: true, index: tr.index, lang: l });
|
|
610
639
|
if (menuOpen()) { replaceTop(langTracksMenu(key, list)); }
|
|
@@ -690,5 +719,6 @@
|
|
|
690
719
|
P.fileHints = fileHints; P.parseCues = parseCues; P.loadTrack = loadTrack; P.subsOff = subsOff;
|
|
691
720
|
P.resetSubs = resetSubs; P.closeSubs = closeSubs; P.startSubtitles = startSubtitles;
|
|
692
721
|
P.subsBeforeOpen = subsBeforeOpen; P.subsOnTick = subsOnTick; P.subtitleSummary = subtitleSummary;
|
|
722
|
+
P.wantEmbedded = wantEmbedded;
|
|
693
723
|
P.renderSubs = renderSubs; P.showEmbeddedText = showEmbeddedText; P.subConfigMenu = subConfigMenu; P.langMenu = langMenu;
|
|
694
724
|
})(PlayerParts);
|
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,
|