nuvio-tizen 1.3.0 β 1.5.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 +12 -2
- package/css/style.css +4 -1
- package/js/app.js +8 -3
- package/js/player.js +192 -40
- package/js/stremio.js +44 -2
- package/js/views.js +60 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,8 +15,18 @@ 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**):
|
|
24
|
+
- *Select source* β switch stream mid-playback (keeps your position), each source shown
|
|
25
|
+
on two lines (name + size / seeds / languages).
|
|
26
|
+
- *Audio track* β shown only when the stream has more than one audio track
|
|
27
|
+
(AVPlay `getTotalTrackInfo` / `setSelectTrack`).
|
|
28
|
+
- *Configure subtitles* β *Language* (2 levels: language β the tracks within it),
|
|
29
|
+
*Size*, *Style*.
|
|
20
30
|
- **Subtitles from addons**: fetched per title/episode, SRT + VTT parsed and rendered as a
|
|
21
31
|
timed overlay; press **UP** during playback to pick a track (or turn them Off).
|
|
22
32
|
Configurable in Settings: **default language** (auto-loads a matching track),
|
package/css/style.css
CHANGED
|
@@ -116,8 +116,11 @@ html, body {
|
|
|
116
116
|
#sub-menu { position: absolute; right: 60px; bottom: 250px; width: 560px; max-height: 620px; overflow: hidden; z-index: 30; background: rgba(20,25,36,0.98); border-radius: 12px; padding: 18px; }
|
|
117
117
|
#sub-menu .sm-title { font-size: 24px; font-weight: bold; margin-bottom: 12px; color: #c7cbd4; }
|
|
118
118
|
#sub-menu .sm-list { max-height: 520px; overflow: hidden; }
|
|
119
|
-
#sub-menu .sm-item { padding: 12px 16px;
|
|
119
|
+
#sub-menu .sm-item { padding: 12px 16px; border-radius: 8px; border: 3px solid transparent; }
|
|
120
|
+
#sub-menu .sm-item .sm-main { font-size: 22px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
121
|
+
#sub-menu .sm-item .sm-detail { font-size: 16px; color: #9aa2b1; margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
120
122
|
#sub-menu .sm-item.sel { background: #818cf8; color: #0e1117; }
|
|
123
|
+
#sub-menu .sm-item.sel .sm-detail { color: #26304a; }
|
|
121
124
|
|
|
122
125
|
/* --- Search --- */
|
|
123
126
|
.search-wrap { position: absolute; top: 120px; left: 60px; right: 60px; bottom: 40px; }
|
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
|
@@ -43,13 +43,16 @@ var Player = (function () {
|
|
|
43
43
|
var _osdTimer = null;
|
|
44
44
|
var _seekPending = 0;
|
|
45
45
|
|
|
46
|
+
// audio
|
|
47
|
+
var _audioIdx = null; // currently selected audio track index
|
|
48
|
+
|
|
46
49
|
// subtitles
|
|
47
50
|
var _subTracks = []; // [{lang,url,_addon}]
|
|
48
51
|
var _cues = null; // [{start,end,text}]
|
|
49
52
|
var _subOn = false;
|
|
50
53
|
var _subLabel = '';
|
|
54
|
+
var _subTrackUrl = '';
|
|
51
55
|
var _menuOpen = false;
|
|
52
|
-
var _menuIdx = 0;
|
|
53
56
|
|
|
54
57
|
function init() {
|
|
55
58
|
objEl = U.byId('av-player');
|
|
@@ -124,9 +127,10 @@ var Player = (function () {
|
|
|
124
127
|
_isLive = (ctx.type === 'tv') || !!ctx.isLive;
|
|
125
128
|
_ctx.resumeSec = ctx.resumeSec || getResume(ctx.type, ctx.id);
|
|
126
129
|
|
|
127
|
-
// reset subtitle state
|
|
128
|
-
_subTracks = []; _cues = null; _subOn = false; _subLabel = '';
|
|
129
|
-
|
|
130
|
+
// reset subtitle + audio state
|
|
131
|
+
_subTracks = []; _cues = null; _subOn = false; _subLabel = ''; _subTrackUrl = '';
|
|
132
|
+
_audioIdx = null;
|
|
133
|
+
_menuOpen = false;
|
|
130
134
|
subsEl.className = 'hidden'; U.clear(subsEl);
|
|
131
135
|
subMenuEl.className = 'hidden';
|
|
132
136
|
updateHint();
|
|
@@ -159,7 +163,7 @@ var Player = (function () {
|
|
|
159
163
|
|
|
160
164
|
function updateHint() {
|
|
161
165
|
var base = _isLive ? 'OK: Play/Pause RETURN: Back' : 'OK: Play/Pause β/β: Seek RETURN: Back';
|
|
162
|
-
|
|
166
|
+
base += ' UP: Menu';
|
|
163
167
|
var h = U.byId('osd-hint');
|
|
164
168
|
if (h) { h.textContent = base; }
|
|
165
169
|
}
|
|
@@ -192,6 +196,7 @@ var Player = (function () {
|
|
|
192
196
|
try { _dur = avplay.getDuration() / 1000; } catch (e3) { _dur = 0; }
|
|
193
197
|
markLiveFromDuration();
|
|
194
198
|
updateHint();
|
|
199
|
+
try { var atr = getAudioTracks(); _audioIdx = atr.length ? atr[0].index : null; } catch (eA) {}
|
|
195
200
|
if (!_isLive && _ctx.resumeSec > 5) { try { avplay.seekTo(_ctx.resumeSec * 1000); } catch (e4) {} }
|
|
196
201
|
avplay.play();
|
|
197
202
|
renderOSD();
|
|
@@ -309,66 +314,213 @@ var Player = (function () {
|
|
|
309
314
|
U.getText(track.url, 15000).then(function (text) {
|
|
310
315
|
var cues = parseCues(text);
|
|
311
316
|
if (!cues.length) { U.toast('Could not parse subtitles'); return; }
|
|
312
|
-
_cues = cues; _subOn = true; _subLabel = track.lang || 'on';
|
|
317
|
+
_cues = cues; _subOn = true; _subLabel = track.lang || 'on'; _subTrackUrl = track.url;
|
|
313
318
|
renderSubs();
|
|
314
319
|
U.toast('Subtitles: ' + (track.lang || 'on'));
|
|
315
320
|
}, function () { U.toast('Failed to download subtitles'); });
|
|
316
321
|
}
|
|
317
322
|
|
|
318
323
|
function subsOff() {
|
|
319
|
-
_subOn = false; _cues = null; _subLabel = '';
|
|
324
|
+
_subOn = false; _cues = null; _subLabel = ''; _subTrackUrl = '';
|
|
320
325
|
subsEl.className = 'hidden'; U.clear(subsEl);
|
|
321
326
|
}
|
|
322
327
|
|
|
323
|
-
// ----------
|
|
324
|
-
function
|
|
325
|
-
if (
|
|
328
|
+
// ---------- audio tracks ----------
|
|
329
|
+
function getAudioTracks() {
|
|
330
|
+
if (useAV) {
|
|
331
|
+
try {
|
|
332
|
+
var all = avplay.getTotalTrackInfo() || [];
|
|
333
|
+
var out = [];
|
|
334
|
+
for (var i = 0; i < all.length; i++) {
|
|
335
|
+
if (('' + all[i].type).toUpperCase() === 'AUDIO') { out.push(all[i]); }
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
} catch (e) { return []; }
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
var at = videoEl.audioTracks, arr = [];
|
|
342
|
+
if (at) {
|
|
343
|
+
for (var j = 0; j < at.length; j++) {
|
|
344
|
+
arr.push({ index: j, extra_info: JSON.stringify({ language: at[j].language || at[j].label }) });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return arr;
|
|
348
|
+
} catch (e2) { return []; }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function audioLabel(tr, n) {
|
|
352
|
+
var info = tr.extra_info;
|
|
353
|
+
try { if (typeof info === 'string') { info = JSON.parse(info); } } catch (e) { info = null; }
|
|
354
|
+
var lang = info && (info.language || info.lang);
|
|
355
|
+
var ch = info && (info.channels || info.channel);
|
|
356
|
+
var parts = [];
|
|
357
|
+
parts.push((lang && lang !== 'und' && lang !== '') ? lang : ('Audio ' + (n + 1)));
|
|
358
|
+
if (ch) { parts.push(ch + 'ch'); }
|
|
359
|
+
return parts.join(' Β· ');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function selectAudio(index) {
|
|
363
|
+
if (useAV) {
|
|
364
|
+
try { avplay.setSelectTrack('AUDIO', index); }
|
|
365
|
+
catch (e) { try { avplay.setSelectTrack('AUDIO', '' + index); } catch (e2) {} }
|
|
366
|
+
} else {
|
|
367
|
+
try { var at = videoEl.audioTracks; for (var i = 0; i < at.length; i++) { at[i].enabled = (i === index); } } catch (e3) {}
|
|
368
|
+
}
|
|
369
|
+
_audioIdx = index;
|
|
370
|
+
U.toast('Audio track changed');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function applyStyleNow() {
|
|
374
|
+
var span = subsEl.firstChild;
|
|
375
|
+
if (span) { styleSpan(span); }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Switch to a different source mid-playback, keeping position (VOD).
|
|
379
|
+
function switchSource(s) {
|
|
380
|
+
if (!s || !s.url) { return; }
|
|
381
|
+
_ctx.url = s.url;
|
|
382
|
+
_ctx.resumeSec = _isLive ? 0 : _cur;
|
|
383
|
+
U.toast('Switching sourceβ¦', 1500);
|
|
384
|
+
U.spinner(true);
|
|
385
|
+
if (useAV) { startAV(s.url); } else { startHTML5(s.url); }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---------- in-player menu (stack of levels) ----------
|
|
389
|
+
var _menuStack = [];
|
|
390
|
+
|
|
391
|
+
function topMenu() { return _menuStack[_menuStack.length - 1]; }
|
|
392
|
+
|
|
393
|
+
function openMenu() {
|
|
326
394
|
_menuOpen = true;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
renderSubMenu();
|
|
395
|
+
_menuStack = [rootMenu()];
|
|
396
|
+
renderMenu();
|
|
330
397
|
subMenuEl.className = '';
|
|
331
398
|
showOSD(true);
|
|
332
399
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
_menuOpen = false;
|
|
400
|
+
function closeMenu() {
|
|
401
|
+
_menuOpen = false; _menuStack = [];
|
|
336
402
|
subMenuEl.className = 'hidden';
|
|
337
403
|
showOSD(false);
|
|
338
404
|
}
|
|
405
|
+
function pushMenu(desc) { _menuStack.push(desc); renderMenu(); }
|
|
406
|
+
function popMenu() { _menuStack.pop(); if (!_menuStack.length) { closeMenu(); } else { renderMenu(); } }
|
|
407
|
+
function replaceTop(desc) { _menuStack[_menuStack.length - 1] = desc; renderMenu(); }
|
|
339
408
|
|
|
340
|
-
function
|
|
409
|
+
function renderMenu() {
|
|
410
|
+
var m = topMenu();
|
|
341
411
|
U.clear(subMenuEl);
|
|
342
|
-
subMenuEl.appendChild(U.el('div', 'sm-title',
|
|
412
|
+
subMenuEl.appendChild(U.el('div', 'sm-title', m.title));
|
|
343
413
|
var list = U.el('div', 'sm-list');
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
});
|
|
349
|
-
_menuItems = items;
|
|
350
|
-
items.forEach(function (it, i) {
|
|
351
|
-
var node = U.el('div', 'sm-item' + (i === _menuIdx ? ' sel' : ''), it.label);
|
|
414
|
+
m.items.forEach(function (it, i) {
|
|
415
|
+
var node = U.el('div', 'sm-item' + (i === m.idx ? ' sel' : ''));
|
|
416
|
+
node.appendChild(U.el('div', 'sm-main', it.label + (it.sel ? ' β' : '')));
|
|
417
|
+
if (it.detail) { node.appendChild(U.el('div', 'sm-detail', it.detail)); }
|
|
352
418
|
list.appendChild(node);
|
|
353
419
|
});
|
|
354
420
|
subMenuEl.appendChild(list);
|
|
355
|
-
|
|
356
|
-
var sel = list.children[_menuIdx];
|
|
421
|
+
var sel = list.children[m.idx];
|
|
357
422
|
if (sel && sel.scrollIntoView) { try { sel.scrollIntoView(false); } catch (e) {} }
|
|
358
423
|
}
|
|
359
424
|
|
|
360
|
-
|
|
425
|
+
function rootMenu() {
|
|
426
|
+
var items = [];
|
|
427
|
+
if (_ctx.streams && _ctx.streams.length) {
|
|
428
|
+
items.push({ label: 'Select source', run: function () { pushMenu(sourceMenu()); } });
|
|
429
|
+
}
|
|
430
|
+
if (getAudioTracks().length > 1) {
|
|
431
|
+
items.push({ label: 'Audio track', run: function () { pushMenu(audioMenu()); } });
|
|
432
|
+
}
|
|
433
|
+
items.push({ label: 'Configure subtitles', run: function () { pushMenu(subConfigMenu()); } });
|
|
434
|
+
return { title: 'Menu', items: items, idx: 0 };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function sourceMenu() {
|
|
438
|
+
var idx = 0;
|
|
439
|
+
var items = _ctx.streams.map(function (s, i) {
|
|
440
|
+
if (s.url === _ctx.url) { idx = i; }
|
|
441
|
+
return { label: s.label || 'Source', detail: s.detail || '', sel: (s.url === _ctx.url),
|
|
442
|
+
run: function () { switchSource(s); closeMenu(); } };
|
|
443
|
+
});
|
|
444
|
+
return { title: 'Select source', items: items, idx: idx };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function audioMenu() {
|
|
448
|
+
var tracks = getAudioTracks();
|
|
449
|
+
var idx = 0;
|
|
450
|
+
var items = tracks.map(function (tr, i) {
|
|
451
|
+
if (tr.index === _audioIdx) { idx = i; }
|
|
452
|
+
return { label: audioLabel(tr, i), sel: (tr.index === _audioIdx),
|
|
453
|
+
run: function () { selectAudio(tr.index); closeMenu(); } };
|
|
454
|
+
});
|
|
455
|
+
return { title: 'Audio track', items: items, idx: idx };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function subConfigMenu() {
|
|
459
|
+
return { title: 'Configure subtitles', idx: 0, items: [
|
|
460
|
+
{ label: 'Language', run: function () { pushMenu(langMenu()); } },
|
|
461
|
+
{ label: 'Size', run: function () { pushMenu(sizeMenu()); } },
|
|
462
|
+
{ label: 'Style', run: function () { pushMenu(styleMenu()); } }
|
|
463
|
+
] };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Level 1: Off + one row per language (with option count).
|
|
467
|
+
function langMenu() {
|
|
468
|
+
var byLang = {}, order = [];
|
|
469
|
+
_subTracks.forEach(function (tr) {
|
|
470
|
+
var l = tr.lang || 'unknown';
|
|
471
|
+
if (!byLang[l]) { byLang[l] = []; order.push(l); }
|
|
472
|
+
byLang[l].push(tr);
|
|
473
|
+
});
|
|
474
|
+
var items = [{ label: 'Off', sel: !_subOn, run: function () { subsOff(); closeMenu(); } }];
|
|
475
|
+
order.forEach(function (l) {
|
|
476
|
+
var n = byLang[l].length;
|
|
477
|
+
items.push({ label: l, detail: n + ' option' + (n > 1 ? 's' : ''),
|
|
478
|
+
sel: (_subOn && _subLabel === l),
|
|
479
|
+
run: function () { pushMenu(langTracksMenu(l, byLang[l])); } });
|
|
480
|
+
});
|
|
481
|
+
return { title: 'Subtitle language', items: items, idx: 0 };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Level 2: the individual tracks within one language.
|
|
485
|
+
function langTracksMenu(lang, tracks) {
|
|
486
|
+
var idx = 0;
|
|
487
|
+
var items = tracks.map(function (tr, i) {
|
|
488
|
+
if (_subTrackUrl === tr.url) { idx = i; }
|
|
489
|
+
return { label: (tr._addon || 'Source') + ' Β· option ' + (i + 1),
|
|
490
|
+
sel: (_subTrackUrl === tr.url),
|
|
491
|
+
run: function () { loadTrack(tr); closeMenu(); } };
|
|
492
|
+
});
|
|
493
|
+
return { title: lang + ' subtitles', items: items, idx: idx };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function optionMenu(title, key, opts) {
|
|
497
|
+
var cfg = getSubCfg();
|
|
498
|
+
var idx = 0;
|
|
499
|
+
var items = opts.map(function (p, i) {
|
|
500
|
+
if (cfg[key] === p[0]) { idx = i; }
|
|
501
|
+
return { label: p[1], sel: (cfg[key] === p[0]), run: function () {
|
|
502
|
+
var c = getSubCfg(); c[key] = p[0]; setSubCfg(c); applyStyleNow();
|
|
503
|
+
replaceTop(optionMenu(title, key, opts)); // refresh checkmark
|
|
504
|
+
} };
|
|
505
|
+
});
|
|
506
|
+
return { title: title, items: items, idx: idx };
|
|
507
|
+
}
|
|
508
|
+
function sizeMenu() {
|
|
509
|
+
return optionMenu('Subtitle size', 'size',
|
|
510
|
+
[['small', 'Small'], ['medium', 'Medium'], ['large', 'Large'], ['xlarge', 'Extra Large']]);
|
|
511
|
+
}
|
|
512
|
+
function styleMenu() {
|
|
513
|
+
return optionMenu('Subtitle style', 'style',
|
|
514
|
+
[['shadow', 'White + shadow'], ['box', 'White on black box'], ['yellow', 'Yellow']]);
|
|
515
|
+
}
|
|
361
516
|
|
|
362
517
|
function menuKey(action) {
|
|
363
|
-
|
|
364
|
-
if (action === '
|
|
365
|
-
if (action === '
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (action === 'BACK' || action === 'STOP') { closeSubMenu(); return; }
|
|
518
|
+
var m = topMenu();
|
|
519
|
+
if (action === 'UP') { m.idx = Math.max(0, m.idx - 1); renderMenu(); return; }
|
|
520
|
+
if (action === 'DOWN') { m.idx = Math.min(m.items.length - 1, m.idx + 1); renderMenu(); return; }
|
|
521
|
+
if (action === 'OK') { var it = m.items[m.idx]; if (it && it.run) { it.run(); } return; }
|
|
522
|
+
if (action === 'LEFT' || action === 'BACK') { popMenu(); return; }
|
|
523
|
+
if (action === 'STOP') { closeMenu(); return; }
|
|
372
524
|
}
|
|
373
525
|
|
|
374
526
|
function handleKey(action) {
|
|
@@ -380,7 +532,7 @@ var Player = (function () {
|
|
|
380
532
|
case 'PAUSE': if (useAV) { try { avplay.pause(); } catch (e) {} } else { videoEl.pause(); } return true;
|
|
381
533
|
case 'RIGHT': case 'FF': seekBy(action === 'FF' ? 30 : 10); return true;
|
|
382
534
|
case 'LEFT': case 'REW': seekBy(action === 'REW' ? -30 : -10); return true;
|
|
383
|
-
case 'UP':
|
|
535
|
+
case 'UP': openMenu(); return true;
|
|
384
536
|
case 'DOWN': showOSD(false); return true;
|
|
385
537
|
case 'STOP': case 'BACK': exit(); return true;
|
|
386
538
|
}
|
|
@@ -395,7 +547,7 @@ var Player = (function () {
|
|
|
395
547
|
if (_osdTimer) { clearTimeout(_osdTimer); _osdTimer = null; }
|
|
396
548
|
hideOSD();
|
|
397
549
|
subsEl.className = 'hidden'; U.clear(subsEl);
|
|
398
|
-
subMenuEl.className = 'hidden'; _menuOpen = false;
|
|
550
|
+
subMenuEl.className = 'hidden'; _menuOpen = false; _menuStack = [];
|
|
399
551
|
_subOn = false; _cues = null; _subTracks = [];
|
|
400
552
|
if (useAV) {
|
|
401
553
|
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,42 @@ 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
|
+
var desc = Stremio.streamDescription(st).replace(/\n/g, ' ');
|
|
364
|
+
var dparts = [];
|
|
365
|
+
if (sz && !/(TB|GB|MB)\b/i.test(desc)) { dparts.push('πΎ ' + sz); }
|
|
366
|
+
if (desc) { dparts.push(desc); }
|
|
367
|
+
return {
|
|
368
|
+
url: st.url,
|
|
369
|
+
label: (st._addon ? st._addon + ' Β· ' : '') + Stremio.streamName(st),
|
|
370
|
+
detail: dparts.join(' ')
|
|
371
|
+
};
|
|
372
|
+
});
|
|
325
373
|
Player.play({
|
|
326
374
|
url: stream.url, type: _d.type, id: _d.streamId,
|
|
327
|
-
name: _d.streamLabel || _d.meta.name, poster: _d.meta.poster
|
|
375
|
+
name: _d.streamLabel || _d.meta.name, poster: _d.meta.poster,
|
|
376
|
+
streams: sources
|
|
328
377
|
}, function () { drawDetail(); });
|
|
329
378
|
};
|
|
330
379
|
listScroll.appendChild(node);
|