nuvio-tizen 1.3.0 β 1.4.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/README.md +7 -2
- package/js/app.js +8 -3
- package/js/player.js +99 -35
- package/js/stremio.js +44 -2
- package/js/views.js +52 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,8 +15,13 @@ core behaviour, targeting 2017 Samsung TVs (Tizen 3.0, ~Chromium 47).
|
|
|
15
15
|
**Cinemeta** (catalogs + metadata), **WatchHub** (streams), **OpenSubtitles v3** (subtitles).
|
|
16
16
|
- Home screen with catalog rows + **Continue Watching**.
|
|
17
17
|
- **Search** (π in the header) across every addon catalog that supports the `search` extra.
|
|
18
|
-
- Detail pages: movies (Play) and series (season/episode browser).
|
|
19
|
-
|
|
18
|
+
- Detail pages: movies (Play) and series (season/episode browser). **Continue Watching**
|
|
19
|
+
resumes a series on the exact season + episode you left off.
|
|
20
|
+
- **Stream aggregation** across all installed stream addons, each source showing
|
|
21
|
+
**size + languages/description** (from `behaviorHints.videoSize` and the addon's own text,
|
|
22
|
+
e.g. MediaFusion / Torrentio).
|
|
23
|
+
- **In-player menu** (press **UP**): *Select source* (switch stream mid-playback, keeping
|
|
24
|
+
your position) and *Configure subtitles* β Language / Size / Style.
|
|
20
25
|
- **Subtitles from addons**: fetched per title/episode, SRT + VTT parsed and rendered as a
|
|
21
26
|
timed overlay; press **UP** during playback to pick a track (or turn them Off).
|
|
22
27
|
Configurable in Settings: **default language** (auto-loads a matching track),
|
package/js/app.js
CHANGED
|
@@ -16,11 +16,11 @@ var App = (function () {
|
|
|
16
16
|
|
|
17
17
|
function pushFrame() { _stack.push({ screen: _current, focus: Focus.current() }); }
|
|
18
18
|
|
|
19
|
-
function openDetail(type, id, preview) {
|
|
19
|
+
function openDetail(type, id, preview, target) {
|
|
20
20
|
if (!id) { return; }
|
|
21
21
|
pushFrame();
|
|
22
22
|
showScreen('detail');
|
|
23
|
-
Views.renderDetail(type, id, preview);
|
|
23
|
+
Views.renderDetail(type, id, preview, target);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function openSettings() {
|
|
@@ -67,7 +67,12 @@ var App = (function () {
|
|
|
67
67
|
case 'LEFT': Focus.move('LEFT'); return true;
|
|
68
68
|
case 'RIGHT': Focus.move('RIGHT'); return true;
|
|
69
69
|
case 'UP': Focus.move('UP'); return true;
|
|
70
|
-
case 'DOWN':
|
|
70
|
+
case 'DOWN': {
|
|
71
|
+
var c = Focus.current();
|
|
72
|
+
if (c && c.__navDOWN && c.__navDOWN() !== false) { return true; }
|
|
73
|
+
Focus.move('DOWN');
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
71
76
|
case 'OK': Focus.clickCurrent(); return true;
|
|
72
77
|
case 'BACK': back(); return true;
|
|
73
78
|
default: return true;
|
package/js/player.js
CHANGED
|
@@ -159,7 +159,7 @@ var Player = (function () {
|
|
|
159
159
|
|
|
160
160
|
function updateHint() {
|
|
161
161
|
var base = _isLive ? 'OK: Play/Pause RETURN: Back' : 'OK: Play/Pause β/β: Seek RETURN: Back';
|
|
162
|
-
|
|
162
|
+
base += ' UP: Menu';
|
|
163
163
|
var h = U.byId('osd-hint');
|
|
164
164
|
if (h) { h.textContent = base; }
|
|
165
165
|
}
|
|
@@ -320,55 +320,119 @@ var Player = (function () {
|
|
|
320
320
|
subsEl.className = 'hidden'; U.clear(subsEl);
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (
|
|
323
|
+
function applyStyleNow() {
|
|
324
|
+
var span = subsEl.firstChild;
|
|
325
|
+
if (span) { styleSpan(span); }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Switch to a different source mid-playback, keeping position (VOD).
|
|
329
|
+
function switchSource(s) {
|
|
330
|
+
if (!s || !s.url) { return; }
|
|
331
|
+
_ctx.url = s.url;
|
|
332
|
+
_ctx.resumeSec = _isLive ? 0 : _cur;
|
|
333
|
+
U.toast('Switching sourceβ¦', 1500);
|
|
334
|
+
U.spinner(true);
|
|
335
|
+
if (useAV) { startAV(s.url); } else { startHTML5(s.url); }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---------- in-player menu (stack of levels) ----------
|
|
339
|
+
var _menuStack = [];
|
|
340
|
+
|
|
341
|
+
function topMenu() { return _menuStack[_menuStack.length - 1]; }
|
|
342
|
+
|
|
343
|
+
function openMenu() {
|
|
326
344
|
_menuOpen = true;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
renderSubMenu();
|
|
345
|
+
_menuStack = [rootMenu()];
|
|
346
|
+
renderMenu();
|
|
330
347
|
subMenuEl.className = '';
|
|
331
348
|
showOSD(true);
|
|
332
349
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
_menuOpen = false;
|
|
350
|
+
function closeMenu() {
|
|
351
|
+
_menuOpen = false; _menuStack = [];
|
|
336
352
|
subMenuEl.className = 'hidden';
|
|
337
353
|
showOSD(false);
|
|
338
354
|
}
|
|
355
|
+
function pushMenu(desc) { _menuStack.push(desc); renderMenu(); }
|
|
356
|
+
function popMenu() { _menuStack.pop(); if (!_menuStack.length) { closeMenu(); } else { renderMenu(); } }
|
|
357
|
+
function replaceTop(desc) { _menuStack[_menuStack.length - 1] = desc; renderMenu(); }
|
|
339
358
|
|
|
340
|
-
function
|
|
359
|
+
function renderMenu() {
|
|
360
|
+
var m = topMenu();
|
|
341
361
|
U.clear(subMenuEl);
|
|
342
|
-
subMenuEl.appendChild(U.el('div', 'sm-title',
|
|
362
|
+
subMenuEl.appendChild(U.el('div', 'sm-title', m.title));
|
|
343
363
|
var list = U.el('div', 'sm-list');
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
items.push({ label: (tr.lang || 'unknown') + ' Β· ' + (tr._addon || '') + mark, track: tr });
|
|
348
|
-
});
|
|
349
|
-
_menuItems = items;
|
|
350
|
-
items.forEach(function (it, i) {
|
|
351
|
-
var node = U.el('div', 'sm-item' + (i === _menuIdx ? ' sel' : ''), it.label);
|
|
352
|
-
list.appendChild(node);
|
|
364
|
+
m.items.forEach(function (it, i) {
|
|
365
|
+
var label = it.label + (it.sel ? ' β' : '');
|
|
366
|
+
list.appendChild(U.el('div', 'sm-item' + (i === m.idx ? ' sel' : ''), label));
|
|
353
367
|
});
|
|
354
368
|
subMenuEl.appendChild(list);
|
|
355
|
-
|
|
356
|
-
var sel = list.children[_menuIdx];
|
|
369
|
+
var sel = list.children[m.idx];
|
|
357
370
|
if (sel && sel.scrollIntoView) { try { sel.scrollIntoView(false); } catch (e) {} }
|
|
358
371
|
}
|
|
359
372
|
|
|
360
|
-
|
|
373
|
+
function rootMenu() {
|
|
374
|
+
var items = [];
|
|
375
|
+
if (_ctx.streams && _ctx.streams.length) {
|
|
376
|
+
items.push({ label: 'Select source', run: function () { pushMenu(sourceMenu()); } });
|
|
377
|
+
}
|
|
378
|
+
items.push({ label: 'Configure subtitles', run: function () { pushMenu(subConfigMenu()); } });
|
|
379
|
+
return { title: 'Menu', items: items, idx: 0 };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function sourceMenu() {
|
|
383
|
+
var items = _ctx.streams.map(function (s) {
|
|
384
|
+
return { label: s.label || 'Source', sel: (s.url === _ctx.url),
|
|
385
|
+
run: function () { switchSource(s); closeMenu(); } };
|
|
386
|
+
});
|
|
387
|
+
return { title: 'Select source', items: items, idx: 0 };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function subConfigMenu() {
|
|
391
|
+
return { title: 'Configure subtitles', idx: 0, items: [
|
|
392
|
+
{ label: 'Language', run: function () { pushMenu(langMenu()); } },
|
|
393
|
+
{ label: 'Size', run: function () { pushMenu(sizeMenu()); } },
|
|
394
|
+
{ label: 'Style', run: function () { pushMenu(styleMenu()); } }
|
|
395
|
+
] };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function langMenu() {
|
|
399
|
+
var items = [{ label: 'Off', sel: !_subOn, run: function () { subsOff(); closeMenu(); } }];
|
|
400
|
+
_subTracks.forEach(function (tr) {
|
|
401
|
+
items.push({ label: (tr.lang || 'unknown') + ' Β· ' + (tr._addon || ''),
|
|
402
|
+
sel: (_subOn && _subLabel === (tr.lang || 'on')),
|
|
403
|
+
run: function () { loadTrack(tr); closeMenu(); } });
|
|
404
|
+
});
|
|
405
|
+
return { title: 'Subtitle language', items: items, idx: 0 };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function optionMenu(title, key, opts) {
|
|
409
|
+
var cfg = getSubCfg();
|
|
410
|
+
var idx = 0;
|
|
411
|
+
var items = opts.map(function (p, i) {
|
|
412
|
+
if (cfg[key] === p[0]) { idx = i; }
|
|
413
|
+
return { label: p[1], sel: (cfg[key] === p[0]), run: function () {
|
|
414
|
+
var c = getSubCfg(); c[key] = p[0]; setSubCfg(c); applyStyleNow();
|
|
415
|
+
replaceTop(optionMenu(title, key, opts)); // refresh checkmark
|
|
416
|
+
} };
|
|
417
|
+
});
|
|
418
|
+
return { title: title, items: items, idx: idx };
|
|
419
|
+
}
|
|
420
|
+
function sizeMenu() {
|
|
421
|
+
return optionMenu('Subtitle size', 'size',
|
|
422
|
+
[['small', 'Small'], ['medium', 'Medium'], ['large', 'Large'], ['xlarge', 'Extra Large']]);
|
|
423
|
+
}
|
|
424
|
+
function styleMenu() {
|
|
425
|
+
return optionMenu('Subtitle style', 'style',
|
|
426
|
+
[['shadow', 'White + shadow'], ['box', 'White on black box'], ['yellow', 'Yellow']]);
|
|
427
|
+
}
|
|
361
428
|
|
|
362
429
|
function menuKey(action) {
|
|
363
|
-
|
|
364
|
-
if (action === '
|
|
365
|
-
if (action === '
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (action === 'BACK' || action === 'STOP') { closeSubMenu(); return; }
|
|
430
|
+
var m = topMenu();
|
|
431
|
+
if (action === 'UP') { m.idx = Math.max(0, m.idx - 1); renderMenu(); return; }
|
|
432
|
+
if (action === 'DOWN') { m.idx = Math.min(m.items.length - 1, m.idx + 1); renderMenu(); return; }
|
|
433
|
+
if (action === 'OK') { var it = m.items[m.idx]; if (it && it.run) { it.run(); } return; }
|
|
434
|
+
if (action === 'LEFT' || action === 'BACK') { popMenu(); return; }
|
|
435
|
+
if (action === 'STOP') { closeMenu(); return; }
|
|
372
436
|
}
|
|
373
437
|
|
|
374
438
|
function handleKey(action) {
|
|
@@ -380,7 +444,7 @@ var Player = (function () {
|
|
|
380
444
|
case 'PAUSE': if (useAV) { try { avplay.pause(); } catch (e) {} } else { videoEl.pause(); } return true;
|
|
381
445
|
case 'RIGHT': case 'FF': seekBy(action === 'FF' ? 30 : 10); return true;
|
|
382
446
|
case 'LEFT': case 'REW': seekBy(action === 'REW' ? -30 : -10); return true;
|
|
383
|
-
case 'UP':
|
|
447
|
+
case 'UP': openMenu(); return true;
|
|
384
448
|
case 'DOWN': showOSD(false); return true;
|
|
385
449
|
case 'STOP': case 'BACK': exit(); return true;
|
|
386
450
|
}
|
|
@@ -395,7 +459,7 @@ var Player = (function () {
|
|
|
395
459
|
if (_osdTimer) { clearTimeout(_osdTimer); _osdTimer = null; }
|
|
396
460
|
hideOSD();
|
|
397
461
|
subsEl.className = 'hidden'; U.clear(subsEl);
|
|
398
|
-
subMenuEl.className = 'hidden'; _menuOpen = false;
|
|
462
|
+
subMenuEl.className = 'hidden'; _menuOpen = false; _menuStack = [];
|
|
399
463
|
_subOn = false; _cues = null; _subTracks = [];
|
|
400
464
|
if (useAV) {
|
|
401
465
|
try { avplay.stop(); } catch (e) {}
|
package/js/stremio.js
CHANGED
|
@@ -112,6 +112,12 @@ var Stremio = (function () {
|
|
|
112
112
|
|
|
113
113
|
// ---- Catalogs ----
|
|
114
114
|
// Returns list of {addon, type, id, name} across all addons.
|
|
115
|
+
var TYPE_LABELS = { movie: 'Movies', series: 'Series', tv: 'Live TV', channel: 'Channels' };
|
|
116
|
+
function prettyType(t) {
|
|
117
|
+
if (TYPE_LABELS[t]) { return TYPE_LABELS[t]; }
|
|
118
|
+
return t ? (t.charAt(0).toUpperCase() + t.slice(1)) : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
function listCatalogs() {
|
|
116
122
|
return loadAddons().then(function (addons) {
|
|
117
123
|
var cats = [];
|
|
@@ -120,8 +126,10 @@ var Stremio = (function () {
|
|
|
120
126
|
cs.forEach(function (c) {
|
|
121
127
|
if (!c.type || !c.id) { return; }
|
|
122
128
|
var label = c.name || c.id;
|
|
129
|
+
// Include the type so per-type catalogs sharing a name (e.g. Cinemeta's
|
|
130
|
+
// "Popular" for both movie & series) don't look like duplicates.
|
|
123
131
|
cats.push({ addon: addon, type: c.type, id: c.id,
|
|
124
|
-
name:
|
|
132
|
+
name: prettyType(c.type) + ' Β· ' + label + ' Β· ' + (addon.manifest.name || 'Addon') });
|
|
125
133
|
});
|
|
126
134
|
});
|
|
127
135
|
return cats;
|
|
@@ -272,12 +280,46 @@ var Stremio = (function () {
|
|
|
272
280
|
return ('' + t).replace(/\n/g, ' ');
|
|
273
281
|
}
|
|
274
282
|
|
|
283
|
+
// Short first-line label (quality / addon tag), e.g. "MediaFusion 2160p".
|
|
284
|
+
function streamName(stream) {
|
|
285
|
+
var t = stream.name || stream.title || stream.description || 'Source';
|
|
286
|
+
return ('' + t).split('\n')[0].trim();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function humanSize(bytes) {
|
|
290
|
+
if (!bytes || bytes <= 0) { return ''; }
|
|
291
|
+
var u = ['B', 'KB', 'MB', 'GB', 'TB'], i = 0, n = bytes;
|
|
292
|
+
while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
|
|
293
|
+
return (n >= 10 ? Math.round(n) : n.toFixed(1)) + ' ' + u[i];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// File size from behaviorHints, or parsed from the title/description text.
|
|
297
|
+
function streamSize(stream) {
|
|
298
|
+
var b = stream.behaviorHints && stream.behaviorHints.videoSize;
|
|
299
|
+
if (b) { return humanSize(b); }
|
|
300
|
+
var txt = ('' + (stream.title || '') + ' ' + (stream.description || ''));
|
|
301
|
+
var m = txt.match(/(\d+(?:[.,]\d+)?)\s?(TB|GB|MB)\b/i);
|
|
302
|
+
return m ? (m[1].replace(',', '.') + ' ' + m[2].toUpperCase()) : '';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// The descriptive block (size, languages, seeds, etc. as the addon formats it).
|
|
306
|
+
// Kept multi-line so details like MediaFusion's language list survive.
|
|
307
|
+
function streamDescription(stream) {
|
|
308
|
+
var d = '' + (stream.description || stream.title || '');
|
|
309
|
+
if (!stream.name) {
|
|
310
|
+
var parts = d.split('\n');
|
|
311
|
+
if (parts.length > 1) { d = parts.slice(1).join('\n'); }
|
|
312
|
+
}
|
|
313
|
+
return d.trim();
|
|
314
|
+
}
|
|
315
|
+
|
|
275
316
|
return {
|
|
276
317
|
DEFAULT_ADDONS: DEFAULT_ADDONS,
|
|
277
318
|
getAddonUrls: getAddonUrls, addAddon: addAddon, removeAddon: removeAddon, resetAddons: resetAddons,
|
|
278
319
|
loadAddons: loadAddons, listCatalogs: listCatalogs, getCatalog: getCatalog,
|
|
279
320
|
getMeta: getMeta, getStreams: getStreams, getSubtitles: getSubtitles,
|
|
280
321
|
getSearchCatalogs: getSearchCatalogs, search: search,
|
|
281
|
-
isPlayable: isPlayable, streamLabel: streamLabel
|
|
322
|
+
isPlayable: isPlayable, streamLabel: streamLabel, streamName: streamName,
|
|
323
|
+
streamSize: streamSize, streamDescription: streamDescription
|
|
282
324
|
};
|
|
283
325
|
})();
|
package/js/views.js
CHANGED
|
@@ -62,16 +62,28 @@ var Views = (function () {
|
|
|
62
62
|
var srch = U.el('div', 'gear focusable', 'π');
|
|
63
63
|
srch.style.marginRight = '24px';
|
|
64
64
|
srch.__onselect = function () { App.openSearch(); };
|
|
65
|
+
srch.__navDOWN = focusFirstRowItem;
|
|
65
66
|
h.appendChild(srch);
|
|
66
67
|
}
|
|
67
68
|
if (withGear) {
|
|
68
69
|
var g = U.el('div', 'gear focusable', 'β');
|
|
69
70
|
g.__onselect = function () { App.openSettings(); };
|
|
71
|
+
g.__navDOWN = focusFirstRowItem;
|
|
70
72
|
h.appendChild(g);
|
|
71
73
|
}
|
|
72
74
|
return h;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
// DOWN from the header lands on the first item of the first row
|
|
78
|
+
// (Continue Watching when present), not the geometrically-nearest card.
|
|
79
|
+
function focusFirstRowItem() {
|
|
80
|
+
var rows = scr('home').querySelector('.rows');
|
|
81
|
+
if (!rows) { return false; }
|
|
82
|
+
var first = rows.querySelector('.focusable');
|
|
83
|
+
if (first) { Focus.setFocus(first); return true; }
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
function posterCard(meta, fallbackType) {
|
|
76
88
|
var c = U.el('div', 'card focusable');
|
|
77
89
|
var poster = meta.poster || '';
|
|
@@ -101,6 +113,13 @@ var Views = (function () {
|
|
|
101
113
|
if (cont.length) {
|
|
102
114
|
rows.appendChild(buildRow('Continue Watching', cont.map(function (e) {
|
|
103
115
|
var c = posterCard({ id: e.id.split(':')[0], type: e.type, name: e.name, poster: e.poster }, e.type);
|
|
116
|
+
// For a series, keep the full episode id so detail opens on the right
|
|
117
|
+
// season and focuses the exact episode being resumed.
|
|
118
|
+
c.__onselect = function () {
|
|
119
|
+
App.openDetail(e.type, e.id.split(':')[0],
|
|
120
|
+
{ name: e.name, poster: e.poster, type: e.type },
|
|
121
|
+
e.type === 'series' ? e.id : null);
|
|
122
|
+
};
|
|
104
123
|
if (e.duration) {
|
|
105
124
|
var p = U.el('div', 'prog');
|
|
106
125
|
p.style.width = Math.min(100, (e.position / e.duration * 100)) + '%';
|
|
@@ -162,8 +181,8 @@ var Views = (function () {
|
|
|
162
181
|
// ================= DETAIL =================
|
|
163
182
|
var _d = null; // { type, id, meta, mode, season }
|
|
164
183
|
|
|
165
|
-
function renderDetail(type, id, preview) {
|
|
166
|
-
_d = { type: type, id: id, meta: null, mode: 'info', season: null, preview: preview || null };
|
|
184
|
+
function renderDetail(type, id, preview, target) {
|
|
185
|
+
_d = { type: type, id: id, meta: null, mode: 'info', season: null, preview: preview || null, target: target || null };
|
|
167
186
|
var s = scr('detail');
|
|
168
187
|
U.clear(s);
|
|
169
188
|
s.appendChild(U.el('div', 'detail-hero'));
|
|
@@ -209,6 +228,13 @@ var Views = (function () {
|
|
|
209
228
|
body.appendChild(U.el('div', 'detail-desc', meta.description || ''));
|
|
210
229
|
s.appendChild(body);
|
|
211
230
|
|
|
231
|
+
// If resuming a series episode, pre-select its season.
|
|
232
|
+
if (_d.type === 'series' && _d.target && _d.season === null && meta.videos) {
|
|
233
|
+
var tv = meta.videos.find(function (v) { return v.id === _d.target; });
|
|
234
|
+
if (tv) { _d.season = (tv.season === undefined ? 1 : tv.season); }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_d._epFocus = null;
|
|
212
238
|
if (_d.mode === 'streams') {
|
|
213
239
|
body.appendChild(streamActionsBar());
|
|
214
240
|
s.appendChild(streamsPanel());
|
|
@@ -217,7 +243,8 @@ var Views = (function () {
|
|
|
217
243
|
} else {
|
|
218
244
|
body.appendChild(movieActions());
|
|
219
245
|
}
|
|
220
|
-
Focus.setScope(s);
|
|
246
|
+
Focus.setScope(s, _d._epFocus);
|
|
247
|
+
_d.target = null; // consume: don't re-jump after the user navigates
|
|
221
248
|
}
|
|
222
249
|
|
|
223
250
|
function movieActions() {
|
|
@@ -265,6 +292,7 @@ var Views = (function () {
|
|
|
265
292
|
e.__onselect = function () {
|
|
266
293
|
openStreams(v.id, _d.meta.name + ' S' + _d.season + 'E' + (v.episode || ''));
|
|
267
294
|
};
|
|
295
|
+
if (_d.target && v.id === _d.target) { _d._epFocus = e; }
|
|
268
296
|
listScroll.appendChild(e);
|
|
269
297
|
});
|
|
270
298
|
panel.appendChild(listScroll);
|
|
@@ -310,21 +338,34 @@ var Views = (function () {
|
|
|
310
338
|
_d.streams.forEach(function (stream) {
|
|
311
339
|
var playable = Stremio.isPlayable(stream);
|
|
312
340
|
var node = U.el('div', 'stream focusable');
|
|
313
|
-
node.appendChild(document.createTextNode(Stremio.
|
|
314
|
-
|
|
341
|
+
node.appendChild(document.createTextNode(Stremio.streamName(stream)));
|
|
342
|
+
|
|
343
|
+
// Detail block: size + the addon's own description (languages, seeds, etc.)
|
|
344
|
+
var lines = [];
|
|
345
|
+
var size = Stremio.streamSize(stream);
|
|
346
|
+
var desc = Stremio.streamDescription(stream);
|
|
347
|
+
if (size && !/(TB|GB|MB)\b/i.test(desc)) { lines.push('πΎ ' + size); }
|
|
348
|
+
if (desc) { lines.push(desc); }
|
|
349
|
+
if (stream._addon) { lines.push('βοΈ ' + stream._addon); }
|
|
315
350
|
if (!playable) {
|
|
316
|
-
if (stream.infoHash) {
|
|
317
|
-
else if (stream.externalUrl) {
|
|
318
|
-
else {
|
|
351
|
+
if (stream.infoHash) { lines.push('β torrent, needs a debrid/torrent client'); }
|
|
352
|
+
else if (stream.externalUrl) { lines.push('β opens in an external app (not playable here)'); }
|
|
353
|
+
else { lines.push('β not directly playable on the TV'); }
|
|
319
354
|
}
|
|
320
|
-
var sub = (
|
|
321
|
-
|
|
355
|
+
var sub = U.el('span', 's-sub', lines.join('\n'));
|
|
356
|
+
sub.style.whiteSpace = 'pre-line';
|
|
357
|
+
node.appendChild(sub);
|
|
322
358
|
if (!playable) { node.style.opacity = '0.55'; }
|
|
323
359
|
node.__onselect = function () {
|
|
324
360
|
if (!playable) { U.toast('This source canβt play directly on the TV.'); return; }
|
|
361
|
+
var sources = _d.streams.filter(Stremio.isPlayable).map(function (st) {
|
|
362
|
+
var sz = Stremio.streamSize(st);
|
|
363
|
+
return { url: st.url, label: (st._addon ? st._addon + ' Β· ' : '') + Stremio.streamName(st) + (sz ? ' Β· ' + sz : '') };
|
|
364
|
+
});
|
|
325
365
|
Player.play({
|
|
326
366
|
url: stream.url, type: _d.type, id: _d.streamId,
|
|
327
|
-
name: _d.streamLabel || _d.meta.name, poster: _d.meta.poster
|
|
367
|
+
name: _d.streamLabel || _d.meta.name, poster: _d.meta.poster,
|
|
368
|
+
streams: sources
|
|
328
369
|
}, function () { drawDetail(); });
|
|
329
370
|
};
|
|
330
371
|
listScroll.appendChild(node);
|