stremiok 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.md +10 -8
- package/config.xml +1 -1
- package/css/style.css +37 -2
- package/index.html +34 -31
- package/js/app.js +19 -7
- package/js/downloads.js +17 -5
- package/js/epg.js +320 -0
- package/js/hubpreview.js +15 -5
- package/js/iptv.js +202 -29
- package/js/keys.js +2 -0
- package/js/player-controls.js +55 -8
- package/js/player-core.js +204 -31
- package/js/player-lang.js +1 -1
- package/js/player-quality.js +15 -7
- package/js/player-store.js +28 -4
- package/js/player-subs.js +45 -11
- package/js/player.js +1 -1
- package/js/stremio-account.js +3 -0
- package/js/stremio.js +157 -28
- package/js/trakt.js +61 -9
- package/js/util.js +36 -10
- package/js/view-core.js +139 -55
- package/js/view-detail.js +5 -4
- package/js/view-guide.js +332 -0
- package/js/view-home.js +168 -36
- package/js/view-settings.js +170 -80
- package/js/views.js +3 -2
- package/js/youtube.js +1 -3
- package/js/ytplay.js +62 -5
- package/package.json +2 -1
- package/service/index.js +1131 -108
- package/service/preview.js +14 -4
package/js/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; });
|
|
@@ -330,8 +350,12 @@
|
|
|
330
350
|
el.className = '';
|
|
331
351
|
var span = el.firstChild;
|
|
332
352
|
if (!span) { span = document.createElement('span'); el.appendChild(span); }
|
|
333
|
-
|
|
334
|
-
|
|
353
|
+
var ver = P.subCfgVersion ? P.subCfgVersion() : -1;
|
|
354
|
+
if (span.__styled !== ver || ver === -1) { // (styled when made, and when the subtitle settings change: not every tick)
|
|
355
|
+
styleSpan(span);
|
|
356
|
+
el.style.bottom = ''; // (styleSpan places the bottom one: this one stays at the top)
|
|
357
|
+
span.__styled = ver;
|
|
358
|
+
}
|
|
335
359
|
if (span.textContent !== show) { span.textContent = show; }
|
|
336
360
|
}
|
|
337
361
|
// Green › Second subtitle: Off, or a language the addons have (the same setting as Settings').
|
|
@@ -380,6 +404,7 @@
|
|
|
380
404
|
pick = tracks.find(function (t) { return t.index === w.index && (!key || langKey(textLang(t)) === key); }) || null;
|
|
381
405
|
}
|
|
382
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)
|
|
383
408
|
if (!pick && w.lang2) { // nothing in the default language anywhere: the second choice, a track to download first
|
|
384
409
|
var key2 = langKey(w.lang2);
|
|
385
410
|
var dl = _subTracks.find(function (t) { return langMatches(key2, t.lang); });
|
|
@@ -394,7 +419,7 @@
|
|
|
394
419
|
try { avplay.setSelectTrack('TEXT', tr.index); }
|
|
395
420
|
catch (e2) { try { avplay.setSelectTrack('TEXT', '' + tr.index); } catch (e3) {} }
|
|
396
421
|
_embSub = tr.index; _embLang = textLang(tr);
|
|
397
|
-
U.toast('Subtitles: ' + (_embLang || 'on') + ' (built-in)', 1800);
|
|
422
|
+
U.toast('Subtitles: ' + (_embLang || 'on') + (S.ctx && S.ctx._relayCc ? ' (closed captions)' : ' (built-in)'), 1800);
|
|
398
423
|
}
|
|
399
424
|
function embOff() {
|
|
400
425
|
if (_embSub != null) { try { avplay.setSilentSubtitle(true); } catch (e) {} }
|
|
@@ -442,7 +467,7 @@
|
|
|
442
467
|
function drawEmbedded(text, duration) {
|
|
443
468
|
if (_embSub == null || _subOn) { return; }
|
|
444
469
|
if (_embTimer) { clearTimeout(_embTimer); _embTimer = null; }
|
|
445
|
-
var t = ('' + (text || '')).replace(/<br\s*\/?>/gi, '\n')
|
|
470
|
+
var t = noTags(('' + (text || '')).replace(/<br\s*\/?>/gi, '\n')).replace(/\{[^}]+\}/g, '').trim();
|
|
446
471
|
if (!t) { S.subsEl.className = 'hidden'; return; }
|
|
447
472
|
S.subsEl.className = '';
|
|
448
473
|
var span = S.subsEl.firstChild;
|
|
@@ -511,7 +536,7 @@
|
|
|
511
536
|
replaceTop(timingMenu(topMenu().idx));
|
|
512
537
|
U.toast('Subtitles: ' + fmtDelay(_subDelay) + (_subDelay > 0 ? ' (later)' : _subDelay < 0 ? ' (earlier)' : ' (as in the file)'), 1800);
|
|
513
538
|
}
|
|
514
|
-
function lineText(c) { return '“' + ('' + c.text).replace(
|
|
539
|
+
function lineText(c) { return '“' + noTags('' + c.text).replace(/\s+/g, ' ').trim().slice(0, 70) + '”'; }
|
|
515
540
|
// The next line coming up (as timed now): OK the moment it's said.
|
|
516
541
|
function earMenu() {
|
|
517
542
|
var t = S.cur - _subDelay, next = null;
|
|
@@ -559,12 +584,14 @@
|
|
|
559
584
|
// group), named in full, with the file's own tracks in the same groups as the addons'.
|
|
560
585
|
// Order: the language showing now, the preferred language, then by name.
|
|
561
586
|
function langMenu() {
|
|
562
|
-
var groups =
|
|
587
|
+
var groups = Object.create(null), order = []; // (no inherited names: a language an addon calls "__proto__" is just a key)
|
|
563
588
|
function add(k, item) {
|
|
564
589
|
if (!groups[k]) { groups[k] = []; order.push(k); }
|
|
565
590
|
groups[k].push(item);
|
|
566
591
|
}
|
|
567
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 }); });
|
|
568
595
|
_subTracks.forEach(function (tr) { add(langKey(tr.lang), { track: tr }); });
|
|
569
596
|
var curKey = _subOn ? langKey(_subLabel) : (_embSub != null ? langKey(_embLang) : null);
|
|
570
597
|
var pref = getSubCfg().lang;
|
|
@@ -579,7 +606,7 @@
|
|
|
579
606
|
order.forEach(function (k) {
|
|
580
607
|
var list = groups[k], n = list.length, emb = list.filter(function (x) { return x.emb; }).length;
|
|
581
608
|
items.push({ label: langName(k),
|
|
582
|
-
detail: n + ' option' + (n > 1 ? 's' : '') + (emb ? (emb === n ? ',
|
|
609
|
+
detail: n + ' option' + (n > 1 ? 's' : '') + (emb ? (emb === n ? ', ' + inFile() : ', ' + emb + ' ' + inFile()) : ''),
|
|
583
610
|
sel: (k === curKey),
|
|
584
611
|
run: function () { pushMenu(langTracksMenu(k, list)); } });
|
|
585
612
|
});
|
|
@@ -590,6 +617,7 @@
|
|
|
590
617
|
return { title: 'Subtitle language', items: items, idx: idx, rebuild: langMenu };
|
|
591
618
|
}
|
|
592
619
|
|
|
620
|
+
function inFile() { return S.ctx && (S.ctx._relayCc || S.ctx._ccOffer) ? 'closed captions' : 'in the file'; }
|
|
593
621
|
// Level 2: the tracks in one language: the file's own first, then each addon's.
|
|
594
622
|
// Picking a track keeps the menu open, the checkmark on it (an addon's once it loads), so
|
|
595
623
|
// several can be tried in a row: the file's own the same as an addon's.
|
|
@@ -597,10 +625,15 @@
|
|
|
597
625
|
var idx = 0, nAddon = 0;
|
|
598
626
|
list = list.filter(function (x) { return x.emb; }).concat(list.filter(function (x) { return !x.emb; }));
|
|
599
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
|
+
}
|
|
600
633
|
if (x.emb) {
|
|
601
634
|
var tr = x.emb, l = textLang(tr);
|
|
602
635
|
if (_embSub === tr.index) { idx = i; }
|
|
603
|
-
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,
|
|
604
637
|
run: function () {
|
|
605
638
|
_embDelay = 0; _embQueue = []; _embNudge = null; selectEmbeddedSub(tr); rememberSub(S.ctx.id, { embedded: true, index: tr.index, lang: l });
|
|
606
639
|
if (menuOpen()) { replaceTop(langTracksMenu(key, list)); }
|
|
@@ -686,5 +719,6 @@
|
|
|
686
719
|
P.fileHints = fileHints; P.parseCues = parseCues; P.loadTrack = loadTrack; P.subsOff = subsOff;
|
|
687
720
|
P.resetSubs = resetSubs; P.closeSubs = closeSubs; P.startSubtitles = startSubtitles;
|
|
688
721
|
P.subsBeforeOpen = subsBeforeOpen; P.subsOnTick = subsOnTick; P.subtitleSummary = subtitleSummary;
|
|
722
|
+
P.wantEmbedded = wantEmbedded;
|
|
689
723
|
P.renderSubs = renderSubs; P.showEmbeddedText = showEmbeddedText; P.subConfigMenu = subConfigMenu; P.langMenu = langMenu;
|
|
690
724
|
})(PlayerParts);
|
package/js/player.js
CHANGED
|
@@ -11,7 +11,7 @@ var Player = (function (P) {
|
|
|
11
11
|
getSubCfg: P.getSubCfg, setSubCfg: P.setSubCfg, getAudioCfg: P.getAudioCfg, setAudioCfg: P.setAudioCfg,
|
|
12
12
|
isWatched: P.isWatched, watchedState: P.watchedState, watchedAt: P.watchedAt, watchedSince: P.watchedSince, progressFor: P.progressFor,
|
|
13
13
|
addToContinue: P.addToContinue, setWatched: P.setWatched, setWatchedMany: P.setWatchedMany, clearUnwatched: P.clearUnwatched,
|
|
14
|
-
isPrivate: P.isPrivate, setPrivate: P.setPrivate, playerCfg: P.playerCfg, setPlayerCfg: P.setPlayerCfg, langName: P.langName, lang2: P.lang2, langKey: P.langKey,
|
|
14
|
+
isPrivate: P.isPrivate, setPrivate: P.setPrivate, privateNow: P.privateNow, playerCfg: P.playerCfg, setPlayerCfg: P.setPlayerCfg, langName: P.langName, lang2: P.lang2, langKey: P.langKey,
|
|
15
15
|
SUB_FONTS: P.SUB_FONTS, SUB_COLORS: P.SUB_COLORS, SUB_BGS: P.SUB_BGS, SUB_EDGES: P.SUB_EDGES, SUB_POSITIONS: P.SUB_POSITIONS
|
|
16
16
|
};
|
|
17
17
|
})(PlayerParts);
|
package/js/stremio-account.js
CHANGED
|
@@ -42,6 +42,9 @@ var StremioAccount = (function () {
|
|
|
42
42
|
try { xhr.open('POST', API + method, true); } catch (e) { reject(new Error('couldn’t reach Stremio')); return; }
|
|
43
43
|
xhr.timeout = 20000;
|
|
44
44
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
45
|
+
xhr.onprogress = function (ev) { // (an answer over MAX_BYTES: stopped as it arrives, as every other request is)
|
|
46
|
+
if (!done && ev && ev.loaded > MAX_BYTES) { done = true; try { xhr.abort(); } catch (e) {} reject(new Error('too large an answer')); }
|
|
47
|
+
};
|
|
45
48
|
xhr.onreadystatechange = function () {
|
|
46
49
|
if (xhr.readyState !== 4 || done) { return; }
|
|
47
50
|
done = true;
|
package/js/stremio.js
CHANGED
|
@@ -77,18 +77,31 @@ var Stremio = (function () {
|
|
|
77
77
|
|
|
78
78
|
// Your order (Settings › Addons ▲▼): the higher addon is asked first wherever one answer
|
|
79
79
|
// is used (a title's details: the first that has them) and listed first where several are.
|
|
80
|
+
// dir: -1 / 1 one place, -Infinity / Infinity to the top / bottom (U.moveIn)
|
|
80
81
|
function moveAddon(url, dir) {
|
|
81
|
-
var list = getAddonUrls()
|
|
82
|
-
if (
|
|
83
|
-
list.splice(i, 1); list.splice(j, 0, url);
|
|
82
|
+
var list = getAddonUrls();
|
|
83
|
+
if (!U.moveIn(list, list.indexOf(url), dir)) { return false; }
|
|
84
84
|
saveAddonUrls(list);
|
|
85
85
|
configChanged(); // (rebuilt from the saved manifests: nothing fetched)
|
|
86
86
|
return true;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Addons you turned off (Settings › Content › Addons, OK on one): kept in your list, in
|
|
90
|
+
// their place, but not loaded, so nothing is asked of them (rows, details, sources,
|
|
91
|
+
// subtitles, search) until they're turned on again. A backup can wait there.
|
|
92
|
+
var OFF_KEY = 'stremiok.addonsoff';
|
|
93
|
+
function offUrls() { var l = U.store.get(OFF_KEY, []); return Array.isArray(l) ? l.filter(function (u) { return typeof u === 'string'; }) : []; }
|
|
94
|
+
function isAddonOff(url) { return offUrls().indexOf(url) !== -1; }
|
|
95
|
+
function setAddonOff(url, off) {
|
|
96
|
+
var l = offUrls().filter(function (u) { return u !== url; });
|
|
97
|
+
if (off) { l.push(url); }
|
|
98
|
+
U.store.set(OFF_KEY, l);
|
|
99
|
+
configChanged(); // (loaded again without it, or with it: from the saved manifests)
|
|
100
|
+
}
|
|
89
101
|
function removeAddon(url) {
|
|
90
102
|
var list = getAddonUrls().filter(function (u) { return u !== url; });
|
|
91
103
|
saveAddonUrls(list);
|
|
104
|
+
if (isAddonOff(url)) { U.store.set(OFF_KEY, offUrls().filter(function (u) { return u !== url; })); } // (added again: on)
|
|
92
105
|
var o = originsAll();
|
|
93
106
|
if (o[url]) { delete o[url]; U.store.set(ORIGIN_KEY, o); } // (added again by hand: none)
|
|
94
107
|
configChanged();
|
|
@@ -162,7 +175,8 @@ var Stremio = (function () {
|
|
|
162
175
|
posterRatings: p.posterRatings === true, // Settings > Appearance: IMDb rating on cards
|
|
163
176
|
homePreview: p.homePreview !== false, // Settings > Appearance: Continue Watching on the TV's home bar (js/hubpreview.js)
|
|
164
177
|
homePreviewOrder: p.homePreviewOrder === 'watching' ? 'watching' : 'recent', // ...its tiles: the latest first, or as in Watching
|
|
165
|
-
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)
|
|
166
180
|
hideWords: wordList(p.hideWords, 30, 40), // Settings > Source ranking: sources with these words at the bottom
|
|
167
181
|
watchRegion: /^[A-Z]{2}$/.test(p.watchRegion || '') ? p.watchRegion : '' }; // Settings > Integrations ('' = automatic)
|
|
168
182
|
}
|
|
@@ -329,6 +343,21 @@ var Stremio = (function () {
|
|
|
329
343
|
var MAX_METAS = 1000, MAX_STREAMS = 500, MAX_SUBTITLES = 300, MAX_EPISODES = 5000, MAX_CATALOGS = 200;
|
|
330
344
|
var _manifestProblem = {}; // url -> why it didn't load ("too large (over 2 MB)", "not an addon manifest"…)
|
|
331
345
|
var _manifestDetail = {}; // url -> what the addon itself said about it, if anything (its error message)
|
|
346
|
+
// An addon's logo (its manifest's "logo", else "icon"), for Settings › Content › Addons: an
|
|
347
|
+
// http(s) link by the artwork rules (U.cardImage), or a small base64 picture written into
|
|
348
|
+
// the manifest, whole (one as long as STR_MAX was cut short). From an internet addon (url:
|
|
349
|
+
// its link; none given, taken as one), never a link into your home network (blocksLocal).
|
|
350
|
+
// Else null: its initial shows instead.
|
|
351
|
+
var LOGO_DATA = /^data:image\/(?:png|jpeg|gif|webp|svg\+xml);base64,[A-Za-z0-9+\/]+=*$/i;
|
|
352
|
+
function addonLogo(m, url) {
|
|
353
|
+
if (!m || typeof m !== 'object') { return null; }
|
|
354
|
+
var u = typeof m.logo === 'string' && m.logo.trim() ? m.logo : typeof m.icon === 'string' ? m.icon : '';
|
|
355
|
+
u = u.trim();
|
|
356
|
+
if (!u) { return null; }
|
|
357
|
+
if (/^data:/i.test(u)) { return u.length < STR_MAX && LOGO_DATA.test(u) ? u : null; }
|
|
358
|
+
u = U.cardImage(u, 'logo');
|
|
359
|
+
return u && (url ? blocksLocal({ url: url }) : true) && U.urlIsPrivate(u) ? null : u;
|
|
360
|
+
}
|
|
332
361
|
// A manifest as the app can use it, or null: an id, and lists that are lists.
|
|
333
362
|
// Its lists too: resources (a name, or { name, types, idPrefixes }), types and id prefixes
|
|
334
363
|
// (text), catalogs (a type and id; extras each a name, their options text).
|
|
@@ -385,13 +414,14 @@ var Stremio = (function () {
|
|
|
385
414
|
function loadAddons() {
|
|
386
415
|
if (_loaded) { return Promise.resolve(_loaded); }
|
|
387
416
|
if (_loading) { return _loading; }
|
|
388
|
-
var urls = getAddonUrls();
|
|
417
|
+
var off = offUrls(), urls = getAddonUrls().filter(function (u) { return off.indexOf(u) === -1; }); // (the ones turned off: not loaded)
|
|
389
418
|
var saved = U.store.get(MANIFESTS_KEY, {}) || {};
|
|
390
419
|
var force = _forceNet; _forceNet = false;
|
|
391
420
|
var need = urls.filter(function (u) { return force || !saved[u]; }), rev = _rev, p;
|
|
392
|
-
// IPTV playlists
|
|
393
|
-
|
|
394
|
-
|
|
421
|
+
// The IPTV playlists aren't waited for (a big one takes a while to read): the synthetic
|
|
422
|
+
// addon joins once they're read (iptvJoin), and onIptvReady's listeners redraw its rows.
|
|
423
|
+
var iptvOn = _hasIptv && IPTV.isEnabled(), iptvNow = iptvOn && (!IPTV.isReady || IPTV.isReady());
|
|
424
|
+
p = Promise.all([null, need.length ? fetchManifests(need) : Promise.resolve({})]).then(function (res) {
|
|
395
425
|
if (_loading === p) { _loading = null; }
|
|
396
426
|
if (rev !== _rev) { return loadAddons(); } // (your addons changed meanwhile: loaded as they are now)
|
|
397
427
|
var got = res[1], addons = [];
|
|
@@ -401,7 +431,8 @@ var Stremio = (function () {
|
|
|
401
431
|
});
|
|
402
432
|
// Prepend so the IPTV rows sort first on Home (reorder them in Settings >
|
|
403
433
|
// Appearance > Home feeds; there's no cap on how many rows or feeds).
|
|
404
|
-
if (
|
|
434
|
+
if (iptvNow) { addons.unshift(IPTV.synthAddon()); }
|
|
435
|
+
else if (iptvOn) { iptvJoin(addons, rev); }
|
|
405
436
|
if (hasYouTube()) { addons.push(YouTube.synthAddon()); } // (after your addons: its rows last on Home)
|
|
406
437
|
if (typeof Trakt !== 'undefined' && Trakt.isConnected()) { addons.push(Trakt.synthAddon()); } // (your Watchlist and Up next)
|
|
407
438
|
_loaded = addons;
|
|
@@ -414,6 +445,17 @@ var Stremio = (function () {
|
|
|
414
445
|
return p;
|
|
415
446
|
}
|
|
416
447
|
|
|
448
|
+
// The IPTV playlists, read after the addons loaded: their synthetic addon put first among them
|
|
449
|
+
// (its rows sort first on Home), as if it had been there, and onIptvReady's listeners told.
|
|
450
|
+
var _iptvReadyFns = [];
|
|
451
|
+
function onIptvReady(fn) { _iptvReadyFns.push(fn); }
|
|
452
|
+
function iptvJoin(addons, rev) {
|
|
453
|
+
IPTV.load().then(function () {
|
|
454
|
+
if (rev !== _rev || _loaded !== addons) { return; } // (your addons changed meanwhile: loaded afresh, with it)
|
|
455
|
+
if (!addons.some(function (a) { return a.url === 'local:iptv'; })) { addons.unshift(IPTV.synthAddon()); }
|
|
456
|
+
_iptvReadyFns.forEach(function (fn) { try { fn(); } catch (e) {} });
|
|
457
|
+
}, function () {});
|
|
458
|
+
}
|
|
417
459
|
// Background refresh of manifests that came from the TV's saved copies. Updates the
|
|
418
460
|
// loaded addons in place, so anything holding them sees the new manifest.
|
|
419
461
|
function refreshSavedManifests(urls) {
|
|
@@ -440,7 +482,18 @@ var Stremio = (function () {
|
|
|
440
482
|
if (_hasIptv) { IPTV.removePlaylist(id); }
|
|
441
483
|
configChanged();
|
|
442
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
|
+
}
|
|
443
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; }
|
|
444
497
|
function iptvSetPlaylistOff(id, off) { var ok = _hasIptv ? IPTV.setPlaylistOff(id, off) : false; configChanged(); return ok; }
|
|
445
498
|
function iptvMovePlaylist(id, dir) { var ok = _hasIptv ? IPTV.movePlaylist(id, dir) : false; configChanged(); return ok; }
|
|
446
499
|
function iptvLabel(id) { return _hasIptv ? IPTV.label(id) : ''; }
|
|
@@ -644,7 +697,8 @@ var Stremio = (function () {
|
|
|
644
697
|
// ---- Feed lists: order + show / hide, as the user sets them ----
|
|
645
698
|
// Home feeds (the Home rows: every catalog) and search sources (the Search rows:
|
|
646
699
|
// every catalog that can search) each keep their own { order: [keys], hidden: [keys] }.
|
|
647
|
-
// 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.
|
|
648
702
|
var FEEDS_KEY = 'stremiok.feeds', SEARCH_FEEDS_KEY = 'stremiok.searchfeeds';
|
|
649
703
|
function catalogKey(cat) {
|
|
650
704
|
var aid = (cat.addon.manifest && cat.addon.manifest.id) || cat.addon.url;
|
|
@@ -657,8 +711,8 @@ var Stremio = (function () {
|
|
|
657
711
|
var pos = {};
|
|
658
712
|
cfg().order.forEach(function (k, i) { pos[k] = i; });
|
|
659
713
|
return items.map(function (c, i) { return { c: c, i: i, k: keyOf(c) }; }).sort(function (a, b) {
|
|
660
|
-
var pa = (pos[a.k] !== undefined) ? pos[a.k] : (100000 + a.i);
|
|
661
|
-
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);
|
|
662
716
|
return pa - pb;
|
|
663
717
|
}).map(function (x) { return x.c; });
|
|
664
718
|
}
|
|
@@ -668,9 +722,8 @@ var Stremio = (function () {
|
|
|
668
722
|
visible: function () { return list().then(function (items) { return sort(items).filter(function (c) { return !isHidden(keyOf(c)); }); }); },
|
|
669
723
|
move: function (key, dir) {
|
|
670
724
|
return list().then(function (items) {
|
|
671
|
-
var ordered = sort(items).map(keyOf)
|
|
672
|
-
if (
|
|
673
|
-
var t = ordered[i]; ordered[i] = ordered[j]; ordered[j] = t;
|
|
725
|
+
var ordered = sort(items).map(keyOf);
|
|
726
|
+
if (!U.moveIn(ordered, ordered.indexOf(key), dir)) { return; }
|
|
674
727
|
var c = cfg(); c.order = ordered; U.store.set(storeKey, c);
|
|
675
728
|
});
|
|
676
729
|
},
|
|
@@ -747,9 +800,7 @@ var Stremio = (function () {
|
|
|
747
800
|
function moveRanking(key, dir) {
|
|
748
801
|
var g = getRanking(), i = -1;
|
|
749
802
|
g.criteria.forEach(function (c, n) { if (c.key === key) { i = n; } });
|
|
750
|
-
|
|
751
|
-
if (i < 0 || j < 0 || j >= g.criteria.length) { return; }
|
|
752
|
-
var t = g.criteria[i]; g.criteria[i] = g.criteria[j]; g.criteria[j] = t;
|
|
803
|
+
if (!U.moveIn(g.criteria, i, dir)) { return; }
|
|
753
804
|
saveRanking(g);
|
|
754
805
|
}
|
|
755
806
|
function toggleRanking(key) {
|
|
@@ -802,6 +853,10 @@ var Stremio = (function () {
|
|
|
802
853
|
return out.filter(Boolean);
|
|
803
854
|
}
|
|
804
855
|
|
|
856
|
+
// An addon's error sent as a title's details (AIOStreams: "aiostreamserror.…", its name
|
|
857
|
+
// "[❌] <the addon that failed>", the error as its description; AIOMetadata: "aiom.error.…"):
|
|
858
|
+
// not the title, so the next addon is asked.
|
|
859
|
+
function isErrorMeta(m) { return /^(aiostreamserror\.|aiom\.error\.)/.test('' + (m && m.id)); }
|
|
805
860
|
function getMeta(type, id, cancel) {
|
|
806
861
|
if (/^ytk:/.test(id) && typeof YouTube !== 'undefined') {
|
|
807
862
|
return YouTube.meta(id).then(function (m) { if (!m) { throw new Error('Not found on YouTube'); } return m; });
|
|
@@ -832,6 +887,11 @@ var Stremio = (function () {
|
|
|
832
887
|
var url = a.base + '/meta/' + encodeURIComponent(asType[a.url] || type) + '/' + encodeURIComponent(id) + '.json';
|
|
833
888
|
return U.getJSON(url, addonMs(12000), META_MAX, cancel).then(function (res) {
|
|
834
889
|
var meta = fromAddon(a, res, 'title details').meta;
|
|
890
|
+
if (meta && typeof meta === 'object' && isErrorMeta(meta)) {
|
|
891
|
+
U.log('addon', ((a.manifest && a.manifest.name) || 'An addon') + ' sent an error instead of details for ' + id + ': ' +
|
|
892
|
+
String(meta.name || '').slice(0, 80) + (meta.description ? ' (' + String(meta.description).slice(0, 120) + ')' : ''));
|
|
893
|
+
return tryNext();
|
|
894
|
+
}
|
|
835
895
|
if (meta && typeof meta === 'object') {
|
|
836
896
|
if (meta.videos !== undefined) { meta.videos = oneEach(U.listOf(meta.videos, MAX_EPISODES), meta.id || id); }
|
|
837
897
|
meta._from = (a.manifest && a.manifest.name) || ''; // (the title page says which addon described it)
|
|
@@ -872,8 +932,8 @@ var Stremio = (function () {
|
|
|
872
932
|
if (e && !force && (e.pending > 0 || Date.now() - e.at < STREAM_TTL)) {
|
|
873
933
|
syncLocal(e, type, id); // (the addons' answers kept; the USB drive's matches as they are now)
|
|
874
934
|
if (onUpdate) {
|
|
875
|
-
e.listeners.push(onUpdate);
|
|
876
|
-
if (e.started) { onUpdate(e.list, e.pending); }
|
|
935
|
+
if (!e.done) { e.listeners.push(onUpdate); } // (still loading: told as each addon answers)
|
|
936
|
+
if (e.started) { onUpdate(e.list, e.pending); } // (done: told once, now, and not kept)
|
|
877
937
|
}
|
|
878
938
|
return e.promise.then(function () { syncLocal(e, type, id); return e.list; });
|
|
879
939
|
}
|
|
@@ -902,7 +962,7 @@ var Stremio = (function () {
|
|
|
902
962
|
return function () {
|
|
903
963
|
var url = a.base + '/stream/' + encodeURIComponent(type) + '/' + encodeURIComponent(id) + '.json';
|
|
904
964
|
return U.getJSON(url, addonMs(15000)).then(function (res) {
|
|
905
|
-
var arr = U.listOf(fromAddon(a, res, 'sources').streams, MAX_STREAMS).map(youTubeSource);
|
|
965
|
+
var arr = U.listOf(fromAddon(a, res, 'sources').streams, MAX_STREAMS).map(youTubeSource), said = notes.length;
|
|
906
966
|
arr = arr.filter(function (s) { // (an addon's notices: what it removed and why, its errors; not sources)
|
|
907
967
|
if (!isNotice(s)) { return true; }
|
|
908
968
|
notes.push({ addon: a.manifest.name || 'Addon', title: ('' + (s.name || '')).replace(/\s+/g, ' ').trim().slice(0, 120),
|
|
@@ -916,18 +976,23 @@ var Stremio = (function () {
|
|
|
916
976
|
if (h) { s._headers = h; }
|
|
917
977
|
});
|
|
918
978
|
slots[i] = arr; okCount++;
|
|
979
|
+
if (!arr.length && notes.length === said) { note(a, i, 'no sources for this'); } // (its notices say why, when it sent any)
|
|
919
980
|
}).then(function () { done(); }, function (err) {
|
|
920
981
|
// (no answer in time: what you played from it before, marked so — see keptSources)
|
|
921
982
|
if (isTimeout(err) && !slots[i].length) { slots[i] = keptSources(type, id, a, i); }
|
|
983
|
+
note(a, i, whyNone(err));
|
|
922
984
|
done(); throw err;
|
|
923
985
|
});
|
|
924
986
|
};
|
|
925
987
|
}), 4);
|
|
926
988
|
}
|
|
989
|
+
// Why an addon gave nothing ("No sources found" shows it), in your addon order
|
|
990
|
+
function note(a, i, why) { notes.push({ addon: a.manifest.name || 'Addon', title: why, text: '', rank: i }); }
|
|
927
991
|
function done() {
|
|
928
992
|
e.pending--;
|
|
929
993
|
e.list = local.concat([].concat.apply([], slots));
|
|
930
|
-
|
|
994
|
+
notes.sort(function (x, y) { return (x.rank === undefined ? -1 : x.rank) - (y.rank === undefined ? -1 : y.rank); });
|
|
995
|
+
_streamNotes.set(key, notes.slice(0, 10));
|
|
931
996
|
notify();
|
|
932
997
|
}
|
|
933
998
|
// Addons that gather from others (AIOStreams…) can answer nothing while those are
|
|
@@ -942,6 +1007,7 @@ var Stremio = (function () {
|
|
|
942
1007
|
return new Promise(function (r) { setTimeout(r, RETRY_EMPTY_STREAMS_MS); }).then(ask);
|
|
943
1008
|
}).then(function () {
|
|
944
1009
|
e.at = Date.now();
|
|
1010
|
+
e.done = true; e.listeners = []; // (nothing more to tell: none kept)
|
|
945
1011
|
// Nothing (no answer, or no sources): not kept, so opening it again asks again.
|
|
946
1012
|
if (providers.length && (!okCount || fresh() <= local.length) && _streamCache[key] === e) { delete _streamCache[key]; }
|
|
947
1013
|
return e.list;
|
|
@@ -954,6 +1020,16 @@ var Stremio = (function () {
|
|
|
954
1020
|
}
|
|
955
1021
|
|
|
956
1022
|
function isTimeout(err) { return /^Timeout/.test((err && err.message) || ''); }
|
|
1023
|
+
// Why an addon's sources didn't come, in a few words (its link, and any token in it, left out):
|
|
1024
|
+
// "refused (HTTP 429 · rate_limited)", "didn't answer in time", "couldn't be reached"…
|
|
1025
|
+
function whyNone(err) {
|
|
1026
|
+
var m = String((err && err.message) || ''), h = /^HTTP (\d{3}) for \S+(?::\s+(.+))?$/.exec(m); // (its message after ": ", not the link's "://")
|
|
1027
|
+
if (h) { return 'refused (HTTP ' + h[1] + (h[2] ? ' · ' + h[2].replace(/https?:\/\/\S+/g, '…').slice(0, 80) : '') + ')'; }
|
|
1028
|
+
if (isTimeout(err)) { return 'didn’t answer in time'; }
|
|
1029
|
+
if (/^Network error/.test(m)) { return 'couldn’t be reached'; }
|
|
1030
|
+
if (/^Too large/.test(m)) { return 'sent too much (' + (/\(([^)]+)\)/.exec(m) || ['', 'too large'])[1] + ')'; }
|
|
1031
|
+
return 'failed (' + m.replace(/https?:\/\/\S+/g, '…').slice(0, 80) + ')';
|
|
1032
|
+
}
|
|
957
1033
|
|
|
958
1034
|
// The debrid service a source comes through, from its tag ("[TB⚡]", "[RD+]"): { tag, name } or null.
|
|
959
1035
|
var DEBRIDS = { TB: 'TorBox', RD: 'Real-Debrid', AD: 'AllDebrid', PM: 'Premiumize', DL: 'Debrid-Link', ED: 'EasyDebrid', OC: 'Offcloud', PP: 'PikPak' };
|
|
@@ -1392,6 +1468,57 @@ var Stremio = (function () {
|
|
|
1392
1468
|
return tmdbGet('/' + kind + '/' + tmdbId + '/external_ids', '', cancel).then(function (r) { return (r && r.imdb_id) || null; });
|
|
1393
1469
|
}
|
|
1394
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
|
+
|
|
1395
1522
|
// ---- MDBList ratings (Settings > Integrations > MDBList) ----
|
|
1396
1523
|
// -> [{ key, label, text }] in a fixed order, only the ones the title has.
|
|
1397
1524
|
var MDB_SOURCES = [['imdb', 'IMDb'], ['tomatoes', 'Rotten Tomatoes'], ['popcorn', 'Audience'],
|
|
@@ -1488,9 +1615,11 @@ var Stremio = (function () {
|
|
|
1488
1615
|
var ep = parts.length >= 3 ? { season: parts[1], episode: parts[2] } : null;
|
|
1489
1616
|
return { imdb: parts[0], ep: ep };
|
|
1490
1617
|
}
|
|
1491
|
-
// GET https://api.theintrodb.org/
|
|
1618
|
+
// GET https://api.theintrodb.org/v4/media?imdb_id=tt..[&season=&episode=][&duration_ms=]
|
|
1619
|
+
// (v4: answers keyed by the show's TheTVDB id, episodes in TheTVDB's default order; the
|
|
1620
|
+
// segments as v3 gave them. v2 and v3 stop on 18 Jan 2027.)
|
|
1492
1621
|
function theIntroDb(q, durationMs) {
|
|
1493
|
-
var url = 'https://api.theintrodb.org/
|
|
1622
|
+
var url = 'https://api.theintrodb.org/v4/media?imdb_id=' + encodeURIComponent(q.imdb);
|
|
1494
1623
|
if (q.ep) { url += '&season=' + encodeURIComponent(q.ep.season) + '&episode=' + encodeURIComponent(q.ep.episode); }
|
|
1495
1624
|
if (durationMs && durationMs > 0) { url += '&duration_ms=' + Math.floor(durationMs); }
|
|
1496
1625
|
return U.getJSON(url, 10000).then(function (r) {
|
|
@@ -1943,11 +2072,11 @@ var Stremio = (function () {
|
|
|
1943
2072
|
DEFAULT_ADDONS: DEFAULT_ADDONS, prettyType: prettyType,
|
|
1944
2073
|
addonUrlProblem: function (v) { return U.urlProblem(v); },
|
|
1945
2074
|
getAddonUrls: getAddonUrls, manifestProblem: function (url) { return _manifestProblem[url] || ''; }, manifestDetail: function (url) { return _manifestDetail[url] || ''; }, addAddon: addAddon, removeAddon: removeAddon, moveAddon: moveAddon, streamNotes: streamNotes, keepSource: keepSource, forgetSource: forgetSource, sourceId: sourceId, hasStreamAddons: hasStreamAddons, thirdPartyNote: thirdPartyNote, refreshAddons: refreshAddons, refreshAddon: refreshAddon,
|
|
1946
|
-
getIntegrations: getIntegrations, setIntegration: setIntegration, getPrefs: getPrefs, setPref: setPref,
|
|
2075
|
+
getIntegrations: getIntegrations, setIntegration: setIntegration, getPrefs: getPrefs, setPref: setPref, addonLogo: addonLogo,
|
|
1947
2076
|
getIntroSegments: getIntroSegments,
|
|
1948
2077
|
tmdbValidate: tmdbValidate, mdblistValidate: mdblistValidate, omdbValidate: omdbValidate,
|
|
1949
2078
|
tmdbFindTvId: tmdbFindTvId, tmdbSeasonEpisodes: tmdbSeasonEpisodes, tmdbImg: tmdbImg,
|
|
1950
|
-
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(); },
|
|
1951
2080
|
mdblistRatings: mdblistRatings,
|
|
1952
2081
|
omdbSeasonRatings: omdbSeasonRatings,
|
|
1953
2082
|
loadAddons: loadAddons, listCatalogs: listCatalogs, getCatalog: getCatalog,
|
|
@@ -1974,10 +2103,10 @@ var Stremio = (function () {
|
|
|
1974
2103
|
streamInfo: streamInfo, formatTags: formatTags, formatKeys: formatKeys, recordFormats: recordFormats, setFormat: setFormat,
|
|
1975
2104
|
resetFormats: resetFormats, badFormats: badFormats, formatList: formatList,
|
|
1976
2105
|
resList: resList, setResAllowed: setResAllowed, blockedRes: blockedRes,
|
|
1977
|
-
importSetupList: importSetupList, savedManifest: savedManifest,
|
|
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; },
|
|
1978
2107
|
iptvGetPlaylists: iptvGetPlaylists, iptvAddPlaylist: iptvAddPlaylist,
|
|
1979
2108
|
iptvUpdatePlaylist: iptvUpdatePlaylist, iptvRemovePlaylist: iptvRemovePlaylist,
|
|
1980
|
-
iptvRenamePlaylist: iptvRenamePlaylist, iptvSetPlaylistOff: iptvSetPlaylistOff, iptvMovePlaylist: iptvMovePlaylist, iptvLabel: iptvLabel,
|
|
2109
|
+
iptvRenamePlaylist: iptvRenamePlaylist, iptvSetPlaylistGuide: iptvSetPlaylistGuide, iptvAddXtream: iptvAddXtream, iptvSetXtreamFormat: iptvSetXtreamFormat, iptvSetPlaylistOff: iptvSetPlaylistOff, iptvMovePlaylist: iptvMovePlaylist, iptvLabel: iptvLabel,
|
|
1981
2110
|
iptvLoad: iptvLoad, iptvStatus: iptvStatus, iptvReload: iptvReload
|
|
1982
2111
|
};
|
|
1983
2112
|
})();
|