stremiok 2.2.0 → 2.2.2

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.
@@ -98,11 +98,35 @@
98
98
  }
99
99
 
100
100
  // ================= SUBTITLE CONFIG =================
101
- var SUB_LANGS = [['off', 'Off'], ['eng', 'English'], ['spa', 'Spanish'], ['fre', 'French'],
102
- ['ger', 'German'], ['ita', 'Italian'], ['por', 'Portuguese'], ['dut', 'Dutch'],
103
- ['rus', 'Russian'], ['ara', 'Arabic'], ['tur', 'Turkish'], ['pol', 'Polish']];
101
+ // Every language player-lang recognizes on a track (its LANG_ALIASES), by name.
102
+ var SUB_LANGS = [['off', 'Off'], ['afr', 'Afrikaans'], ['alb', 'Albanian'], ['ara', 'Arabic'],
103
+ ['arg', 'Aragonese'], ['arm', 'Armenian'], ['ast', 'Asturian'], ['aze', 'Azerbaijani'],
104
+ ['baq', 'Basque'], ['bel', 'Belarusian'], ['ben', 'Bengali'], ['bos', 'Bosnian'],
105
+ ['bre', 'Breton'], ['bul', 'Bulgarian'], ['bur', 'Burmese'], ['cat', 'Catalan'],
106
+ ['chi', 'Chinese'], ['hrv', 'Croatian'], ['cze', 'Czech'], ['dan', 'Danish'], ['prs', 'Dari'],
107
+ ['dut', 'Dutch'], ['eng', 'English'], ['epo', 'Esperanto'], ['est', 'Estonian'],
108
+ ['ext', 'Extremaduran'], ['fil', 'Filipino'], ['fin', 'Finnish'], ['fre', 'French'],
109
+ ['glg', 'Galician'], ['geo', 'Georgian'], ['ger', 'German'], ['gre', 'Greek'],
110
+ ['heb', 'Hebrew'], ['hin', 'Hindi'], ['hun', 'Hungarian'], ['ice', 'Icelandic'],
111
+ ['ibo', 'Igbo'], ['ind', 'Indonesian'], ['ina', 'Interlingua'], ['gle', 'Irish'],
112
+ ['ita', 'Italian'], ['jpn', 'Japanese'], ['kan', 'Kannada'], ['kaz', 'Kazakh'],
113
+ ['khm', 'Khmer'], ['kor', 'Korean'], ['kur', 'Kurdish'], ['lav', 'Latvian'],
114
+ ['lit', 'Lithuanian'], ['ltz', 'Luxembourgish'], ['mac', 'Macedonian'], ['may', 'Malay'],
115
+ ['mal', 'Malayalam'], ['mni', 'Manipuri'], ['mar', 'Marathi'], ['mon', 'Mongolian'],
116
+ ['mne', 'Montenegrin'], ['nav', 'Navajo'], ['nep', 'Nepali'], ['sme', 'Northern Sami'],
117
+ ['nor', 'Norwegian'], ['oci', 'Occitan'], ['ori', 'Odia'], ['pus', 'Pashto'],
118
+ ['per', 'Persian'], ['pol', 'Polish'], ['por', 'Portuguese'], ['pan', 'Punjabi'],
119
+ ['rum', 'Romanian'], ['rus', 'Russian'], ['sat', 'Santali'], ['gla', 'Scottish Gaelic'],
120
+ ['srp', 'Serbian'], ['snd', 'Sindhi'], ['sin', 'Sinhala'], ['slo', 'Slovak'],
121
+ ['slv', 'Slovenian'], ['som', 'Somali'], ['spa', 'Spanish'], ['swa', 'Swahili'],
122
+ ['swe', 'Swedish'], ['syr', 'Syriac'], ['tam', 'Tamil'], ['tat', 'Tatar'], ['tel', 'Telugu'],
123
+ ['tha', 'Thai'], ['tok', 'Toki Pona'], ['tur', 'Turkish'], ['tuk', 'Turkmen'],
124
+ ['ukr', 'Ukrainian'], ['urd', 'Urdu'], ['uzb', 'Uzbek'], ['vie', 'Vietnamese'],
125
+ ['wel', 'Welsh']];
104
126
  // A second choice, turned on when nothing is in the default language (from any source).
105
127
  var SUB_LANGS2 = [['none', 'None']].concat(SUB_LANGS.slice(1));
128
+ // The subtitle menu with only the three languages above (and tracks of no known language).
129
+ var SUB_ONLY = [['off', 'Off'], ['on', 'On']];
106
130
  var SUB_SIZES = [['small', 'Small'], ['medium', 'Medium'], ['large', 'Large'], ['xlarge', 'Extra Large']];
107
131
 
108
132
  function labelFor(list, v) {
@@ -169,9 +193,13 @@
169
193
  var bgs = { dark: 1, black: 1, gray: 1, white: 1 };
170
194
  return { accent: ACCENT_NAMES[accent] ? accent : 'indigo', bg: bgs[bg] ? bg : 'dark' };
171
195
  }
196
+ // Zoom: everything bigger (see U.setZoom), in percent.
197
+ var ZOOMS = [['100', '100%'], ['110', '110%'], ['120', '120%'], ['130', '130%'], ['140', '140%']];
198
+ function zoomPct() { return Stremio.getPrefs().zoom; }
172
199
  function applyAppearance() {
173
200
  var a = appearance();
174
201
  U.applyTheme(a.accent, a.bg);
202
+ U.setZoom(zoomPct() / 100);
175
203
  }
176
204
  function settingsAppearance(wrap) {
177
205
  var store = { get: appearance, set: function (cfg) {
@@ -183,6 +211,10 @@
183
211
  wrap.appendChild(U.el('h2', null, 'Appearance'));
184
212
  wrap.appendChild(subPickRow('Background', BACKGROUNDS, 'bg', store));
185
213
  wrap.appendChild(subPickRow('Accent color', accents, 'accent', store));
214
+ wrap.appendChild(subPickRow('Zoom', ZOOMS, 'zoom', {
215
+ get: function () { return { zoom: '' + zoomPct() }; },
216
+ set: function (cfg) { Stremio.setPref('zoom', +cfg.zoom); applyAppearance(); }
217
+ }));
186
218
  wrap.appendChild(subPickRow('Poster orientation', POSTER_SHAPES, 'posterShape', {
187
219
  get: function () { return { posterShape: Stremio.getPrefs().posterShape }; },
188
220
  set: function (cfg) { Stremio.setPref('posterShape', cfg.posterShape); markHomeDirty(); } // Home redraws on return
@@ -314,6 +346,7 @@
314
346
  var SETTINGS_GROUPS = {
315
347
  content: { title: 'Content', rows: [
316
348
  ['Addons', 'Catalog, stream & subtitle addons', 'addons'],
349
+ ['Streaming server', 'Stremio’s streaming server or TorrServer on your network: torrents, and videos the TV can’t play', 'server'],
317
350
  ['Integrations', 'API keys (OMDb, TMDB, MDBList), where-to-watch country', 'integrations'],
318
351
  ['IPTV', 'Manage M3U playlist URLs', 'iptv'],
319
352
  ['YouTube', 'Your YouTube channels, playlists and searches; YouTube in Search', 'youtube'],
@@ -338,7 +371,7 @@
338
371
  // BACK goes up one level, onto the row it came from: from a section to its group (on
339
372
  // that section, not the first one), from Home feeds / Search results to Appearance,
340
373
  // from a group to the Settings list (on that group).
341
- var SECTION_PARENT = { menuorder: 'appearance', feeds: 'appearance', searchfeeds: 'appearance', aiomanager: 'addons' };
374
+ var SECTION_PARENT = { menuorder: 'appearance', feeds: 'appearance', searchfeeds: 'appearance', aiomanager: 'addons', subtitlelook: 'subtitles', rankfilters: 'ranking', rankplay: 'ranking' };
342
375
  Object.keys(SETTINGS_GROUPS).forEach(function (g) {
343
376
  SETTINGS_GROUPS[g].rows.forEach(function (row) { SECTION_PARENT[row[2]] = g; });
344
377
  });
@@ -365,12 +398,15 @@
365
398
  else if (_settingsSection === 'integrations') { settingsIntegrations(wrap); }
366
399
  else if (_settingsSection === 'audio') { settingsAudio(wrap); }
367
400
  else if (_settingsSection === 'subtitles') { settingsSubtitles(wrap); }
401
+ else if (_settingsSection === 'subtitlelook') { settingsSubtitleLook(wrap); }
368
402
  else if (_settingsSection === 'skipping') { settingsSkipping(wrap); }
369
403
  else if (_settingsSection === 'seeking') { settingsSeeking(wrap); }
370
404
  else if (_settingsSection === 'menuorder') { settingsFeeds(wrap, 'menu'); }
371
405
  else if (_settingsSection === 'feeds') { settingsFeeds(wrap, 'home'); }
372
406
  else if (_settingsSection === 'searchfeeds') { settingsFeeds(wrap, 'search'); }
373
407
  else if (_settingsSection === 'ranking') { settingsFeeds(wrap, 'ranking'); }
408
+ else if (_settingsSection === 'rankfilters') { settingsRankFilters(wrap); }
409
+ else if (_settingsSection === 'rankplay') { settingsRankPlay(wrap); }
374
410
  else if (SETTINGS_GROUPS[_settingsSection]) { settingsGroup(wrap, _settingsSection); }
375
411
  else if (_settingsSection === 'logs') { settingsLogs(wrap); }
376
412
  else if (_settingsSection === 'storage') { settingsStorage(wrap); }
@@ -380,6 +416,7 @@
380
416
  else if (_settingsSection === 'appshortcuts') { settingsAppShortcuts(wrap); }
381
417
  else if (_settingsSection === 'import') { settingsImport(wrap); }
382
418
  else if (_settingsSection === 'aiomanager') { settingsAiom(wrap); }
419
+ else if (_settingsSection === 'server') { settingsServer(wrap); }
383
420
  else if (_settingsSection === 'appearance') { settingsAppearance(wrap); }
384
421
  else if (_settingsSection === 'formats') { settingsFormats(wrap); }
385
422
  else { settingsMenu(wrap); }
@@ -422,7 +459,7 @@
422
459
  wrap.appendChild(U.el('h2', null, 'Settings'));
423
460
  wrap.appendChild(sectionRow('Content', 'Addons, API keys, IPTV playlists, streaming apps, importing a configuration file', 'content'));
424
461
  wrap.appendChild(sectionRow('Playback', 'Audio and subtitles, source ranking, feed sources, formats', 'playback'));
425
- wrap.appendChild(sectionRow('Appearance', 'Background, accent color, poster orientation, Home feeds, Search results', 'appearance'));
462
+ wrap.appendChild(sectionRow('Appearance', 'Background, accent color, zoom, poster orientation, Home feeds, Search results', 'appearance'));
426
463
  wrap.appendChild(sectionRow('Diagnostics', 'Logs, storage, installed apps; how StremioK was installed', 'diagnostics'));
427
464
  if (typeof YTPlay !== 'undefined' && YTPlay.installMode && YTPlay.installMode() === 'tizenbrew') { wrap.appendChild(U.el('div', 'hint', TIZENBREW_NOTE)); }
428
465
  focusBack(wrap);
@@ -945,6 +982,60 @@
945
982
  }
946
983
  });
947
984
  }
985
+ // Streaming server: Stremio's or TorrServer, on your network (see Stremio.server). Picking one
986
+ // asks for its address; Test says whether it answers.
987
+ var SERVERS = [['', 'Off'], ['stremio', 'Stremio server'], ['torrserver', 'TorrServer']];
988
+ var SERVER_PORT = { stremio: 11470, torrserver: 8090 };
989
+ var TORRENT_WAITS = [['45', '45 seconds'], ['60', '1 minute'], ['120', '2 minutes'], ['180', '3 minutes'], ['300', '5 minutes']];
990
+ function askServerAddress(kind, url) {
991
+ var eg = 'http://192.168.0.10:' + SERVER_PORT[kind];
992
+ App.openEntry({
993
+ title: (kind === 'stremio' ? 'Stremio server' : 'TorrServer') + ': address',
994
+ hint: 'Its address on your network, like ' + eg + '. RETURN cancels.',
995
+ value: url || 'http://', placeholder: eg, phone: 'server',
996
+ validate: function (v) { return Stremio.serverAddressProblem(v); },
997
+ onConfirm: function (v) { Stremio.setServer({ kind: kind, url: v }); App.back(); renderSettings(); }
998
+ });
999
+ }
1000
+ function settingsServer(wrap) {
1001
+ wrap.appendChild(U.el('h2', null, 'Streaming server'));
1002
+ wrap.appendChild(U.el('div', 'hint', 'Torrent-only sources play through it; Stremio’s can also convert a video the TV can’t play.'));
1003
+ var sv = Stremio.server();
1004
+ wrap.appendChild(subPickRow('Server', SERVERS, 'kind', {
1005
+ get: function () { return { kind: Stremio.server().kind }; },
1006
+ set: function (cfg) {
1007
+ var cur = Stremio.server();
1008
+ if (!cfg.kind) { Stremio.setServer({ kind: '' }); renderSettings(); return; }
1009
+ askServerAddress(cfg.kind, cur.kind === cfg.kind ? cur.url : '');
1010
+ }
1011
+ }));
1012
+ if (!sv.kind) { return; }
1013
+ var addr = U.el('div', 'addon-item focusable cfg-row');
1014
+ addr.appendChild(U.el('div', 'a-name', 'Address: ' + sv.url));
1015
+ addr.appendChild(U.el('div', 'a-url', 'Press OK to change'));
1016
+ addr.__onselect = function () { askServerAddress(sv.kind, sv.url); };
1017
+ wrap.appendChild(addr);
1018
+ var test = U.el('div', 'addon-item focusable cfg-row'), said = U.el('div', 'a-url', 'Press OK to see whether it answers');
1019
+ test.appendChild(U.el('div', 'a-name', 'Test'));
1020
+ test.appendChild(said);
1021
+ test.__onselect = function () {
1022
+ said.textContent = 'Asking…';
1023
+ Stremio.testServer().then(function (r) { said.textContent = (r.ok ? '✓ ' : '✗ ') + r.text; });
1024
+ };
1025
+ wrap.appendChild(test);
1026
+ wrap.appendChild(subPickRow('Wait for a torrent to start', TORRENT_WAITS, 'wait', {
1027
+ get: function () { return { wait: '' + Stremio.server().wait }; },
1028
+ set: function (cfg) { Stremio.setServer({ kind: Stremio.server().kind, wait: +cfg.wait }); }
1029
+ }));
1030
+ if (sv.kind === 'stremio') {
1031
+ wrap.appendChild(subPickRow('Convert videos the TV can’t play', [['on', 'On'], ['off', 'Off']], 'convert', {
1032
+ get: function () { return { convert: Stremio.server().convert ? 'on' : 'off' }; },
1033
+ set: function (cfg) { Stremio.setServer({ kind: 'stremio', convert: cfg.convert === 'on' }); }
1034
+ }));
1035
+ }
1036
+ }
1037
+
1038
+ var IPTV_EVERY = [['0', 'Off'], ['1', 'Every hour'], ['3', 'Every 3 hours'], ['6', 'Every 6 hours'], ['12', 'Every 12 hours']];
948
1039
  function settingsIptv(wrap) {
949
1040
  wrap.appendChild(U.el('h2', null, 'IPTV'));
950
1041
  var playlists = Stremio.iptvGetPlaylists ? Stremio.iptvGetPlaylists() : [];
@@ -1031,6 +1122,10 @@
1031
1122
  xt.appendChild(U.el('div', 'a-url', 'Its server, username and password: its channels and TV guide added'));
1032
1123
  xt.__onselect = addXtream;
1033
1124
  wrap.appendChild(xt);
1125
+ if (playlists.length && Stremio.iptvRefreshEvery) { // (all of them, with their guides, as Reload does)
1126
+ wrap.appendChild(subPickRow('Auto-refresh', IPTV_EVERY, 'every',
1127
+ { get: function () { return { every: String(Stremio.iptvRefreshEvery()) }; }, set: function (c) { Stremio.iptvSetRefreshEvery(+c.every); } }));
1128
+ }
1034
1129
 
1035
1130
  wrap.appendChild(U.el('div', 'hint',
1036
1131
  'Channels are grouped into IPTV and Radio rows on Home. ▲ / ▼ set the playlists\' order: a channel in two playlists comes from the first.'));
@@ -1399,14 +1494,7 @@
1399
1494
  wrap.appendChild(subPickRow('Default language', SUB_LANGS, 'lang'));
1400
1495
  wrap.appendChild(subPickRow('Second choice', SUB_LANGS2, 'lang2'));
1401
1496
  wrap.appendChild(subPickRow('Second subtitle (at the top)', SUB_LANGS, 'second'));
1402
- wrap.appendChild(subPickRow('Size', SUB_SIZES, 'size'));
1403
- // [id, label] for the picker (colors with a swatch)
1404
- function plain(list) { return list.map(function (x) { return [x[0], x[1]]; }); }
1405
- wrap.appendChild(subPickRow('Color', Player.SUB_COLORS.map(function (x) { return [x[0], x[1], null, x[2]]; }), 'color'));
1406
- wrap.appendChild(subPickRow('Background', plain(Player.SUB_BGS), 'bg'));
1407
- wrap.appendChild(subPickRow('Edge', plain(Player.SUB_EDGES), 'edge'));
1408
- wrap.appendChild(subPickRow('Position', plain(Player.SUB_POSITIONS), 'pos'));
1409
- wrap.appendChild(subPickRow('Font', Player.SUB_FONTS, 'font'));
1497
+ wrap.appendChild(subPickRow('Only my languages', SUB_ONLY, 'only'));
1410
1498
  // lines to leave out: those with one of your words (a subtitle site's ads, a file's name…)
1411
1499
  var skip = Player.getSubCfg().skip || [];
1412
1500
  var sk = U.el('div', 'addon-item focusable cfg-row');
@@ -1419,6 +1507,21 @@
1419
1507
  onConfirm: function (v) { var c = Player.getSubCfg(); c.skip = ('' + v).split(','); Player.setSubCfg(c); App.back(); renderSettings(); } });
1420
1508
  };
1421
1509
  wrap.appendChild(sk);
1510
+ var look = sectionRow('Appearance ›', 'Size, color, background, edge, position, font', 'subtitlelook'); // (their own page: settingsSubtitleLook)
1511
+ wrap.appendChild(look);
1512
+ if (_focusChild === 'subtitlelook') { _focusChild = null; Focus.setScope(scr('settings'), look); } else { focusSettings(); }
1513
+ }
1514
+ // Subtitles › Appearance: how subtitles look.
1515
+ function settingsSubtitleLook(wrap) {
1516
+ wrap.appendChild(U.el('h2', null, 'Subtitle appearance'));
1517
+ wrap.appendChild(subPickRow('Size', SUB_SIZES, 'size'));
1518
+ // [id, label] for the picker (colors with a swatch)
1519
+ function plain(list) { return list.map(function (x) { return [x[0], x[1]]; }); }
1520
+ wrap.appendChild(subPickRow('Color', Player.SUB_COLORS.map(function (x) { return [x[0], x[1], null, x[2]]; }), 'color'));
1521
+ wrap.appendChild(subPickRow('Background', plain(Player.SUB_BGS), 'bg'));
1522
+ wrap.appendChild(subPickRow('Edge', plain(Player.SUB_EDGES), 'edge'));
1523
+ wrap.appendChild(subPickRow('Position', plain(Player.SUB_POSITIONS), 'pos'));
1524
+ wrap.appendChild(subPickRow('Font', Player.SUB_FONTS, 'font'));
1422
1525
  focusSettings();
1423
1526
  }
1424
1527
 
@@ -1928,7 +2031,7 @@
1928
2031
  list: function () { return Promise.resolve(Stremio.getRanking().criteria); }, keyOf: function (c) { return c.key; },
1929
2032
  move: function (k, d) { Stremio.moveRanking(k, d); return Promise.resolve(); }, toggle: function (k) { Stremio.toggleRanking(k); },
1930
2033
  hidden: function (k) { return !Stremio.getRanking().criteria.some(function (c) { return c.key === k && c.on; }); },
1931
- before: function (wrap) { // played before, above the resolution groups
2034
+ before: function (wrap) { // played before, above the resolution groups; then the two pages
1932
2035
  var played = Stremio.getRanking().played;
1933
2036
  var r = U.el('div', 'addon-item focusable cfg-row feed-extra');
1934
2037
  r.setAttribute('data-fk', 'played'); r.setAttribute('data-ctl', 'extra');
@@ -1937,48 +2040,9 @@
1937
2040
  : 'Sources you played rank like any other') + ' · OK to turn ' + (played ? 'off' : 'on')));
1938
2041
  r.__onselect = function () { Stremio.setPlayedFirst(!played); refeed('ranking', 'played', 'extra'); };
1939
2042
  wrap.appendChild(r);
1940
- // direct links only: the sources the TV can't play left out of the list
1941
- var hideUn = Stremio.getPrefs().hideUnplayable;
1942
- var un = U.el('div', 'addon-item focusable cfg-row feed-extra');
1943
- un.setAttribute('data-fk', 'unplayable'); un.setAttribute('data-ctl', 'extra');
1944
- un.appendChild(U.el('div', 'a-name', 'Sources the TV can’t play: ' + (hideUn ? 'Hidden' : 'Shown')));
1945
- un.appendChild(U.el('div', 'a-url', (hideUn ? 'Left out (torrents, links it can’t open): direct links only; where-to-watch apps stay'
1946
- : 'Listed, dimmed, at the bottom (torrents, links it can’t open)') + ' · OK to ' + (hideUn ? 'show them' : 'hide them')));
1947
- un.__onselect = function () { Stremio.setPref('hideUnplayable', !hideUn); refeed('ranking', 'unplayable', 'extra'); };
1948
- wrap.appendChild(un);
1949
- // skip the source list: Play plays the top source at once
1950
- var skipList = Stremio.getPrefs().skipSourceList;
1951
- var sl = U.el('div', 'addon-item focusable cfg-row feed-extra');
1952
- sl.setAttribute('data-fk', 'skiplist'); sl.setAttribute('data-ctl', 'extra');
1953
- sl.appendChild(U.el('div', 'a-name', 'Skip the source list: ' + (skipList ? 'On' : 'Off')));
1954
- sl.appendChild(U.el('div', 'a-url', (skipList ? '▶ Play, OK on an episode and ▶ Play SxEx play the top source at once (hold OK on an episode › Details for the list)'
1955
- : '▶ Play and OK on an episode show the sources to pick from') + ' · OK to turn ' + (skipList ? 'off' : 'on')));
1956
- sl.__onselect = function () { Stremio.setPref('skipSourceList', !skipList); refeed('ranking', 'skiplist', 'extra'); };
1957
- wrap.appendChild(sl);
1958
- // a source that doesn't start: the next one
1959
- var nextOn = Stremio.getPrefs().nextOnFail;
1960
- var nx = U.el('div', 'addon-item focusable cfg-row feed-extra');
1961
- nx.setAttribute('data-fk', 'nextonfail'); nx.setAttribute('data-ctl', 'extra');
1962
- nx.appendChild(U.el('div', 'a-name', 'The next source if one doesn’t start: ' + (nextOn ? 'On' : 'Off')));
1963
- nx.appendChild(U.el('div', 'a-url', (nextOn ? 'A source the TV refuses, or that hasn’t started after 45 s, gives way to the next best one (up to 3 more)'
1964
- : 'A source that doesn’t start takes you back to the list') + ' · OK to turn ' + (nextOn ? 'off' : 'on')));
1965
- nx.__onselect = function () { Stremio.setPref('nextOnFail', !nextOn); refeed('ranking', 'nextonfail', 'extra'); };
1966
- wrap.appendChild(nx);
1967
- // size limits: movies and series apart
1968
- [['movie', 'Movies’ sizes'], ['series', 'Series’ sizes (an episode)']].forEach(function (k) { wrap.appendChild(sizeRow(k[0], k[1])); });
1969
- // your words: sources with one of them at the bottom, for every title
1970
- var words = Stremio.getPrefs().hideWords;
1971
- var w = U.el('div', 'addon-item focusable cfg-row feed-extra');
1972
- w.setAttribute('data-fk', 'words'); w.setAttribute('data-ctl', 'extra');
1973
- w.appendChild(U.el('div', 'a-name', 'Hide sources with these words: ' + (words.length ? words.join(', ') : 'None')));
1974
- w.appendChild(U.el('div', 'a-url', 'A source with one of them in its name, description or file name (a whole word: “CAM” isn’t in “CAMERA”) goes to the bottom, for every title · OK to change'));
1975
- w.__onselect = function () {
1976
- App.openEntry({ title: 'Hide sources with these words', value: words.join(', '), placeholder: 'CAM, HDTS, TELESYNC',
1977
- hint: 'Words separated by commas, capitals or not. Sources with one go to the bottom and are never picked on their own. Leave it empty for none.',
1978
- validate: function () { return null; },
1979
- onConfirm: function (v) { App.back(); Stremio.setHideWords(v); refeed('ranking', 'words', 'extra'); } });
1980
- };
1981
- wrap.appendChild(w);
2043
+ wrap.appendChild(rankPageRow('Filters ›', filtersSummary(), 'rankfilters'));
2044
+ wrap.appendChild(rankPageRow('When you press Play ›', onPlaySummary(), 'rankplay'));
2045
+ if (_focusChild === 'rankfilters' || _focusChild === 'rankplay') { _feedFocus = { key: _focusChild, ctl: 'extra' }; _focusChild = null; } // (BACK from one: on its row)
1982
2046
  wrap.appendChild(U.el('div', 'formats-group', 'Then, in this order'));
1983
2047
  },
1984
2048
  after: function (wrap) {
@@ -1997,7 +2061,94 @@
1997
2061
  }, function () { wrap.appendChild(U.el('div', 'hint', 'Your addons couldn\'t be read just now.')); });
1998
2062
  } }
1999
2063
  };
2000
- // Size limits (Settings › Source ranking): smallest, largest and most per hour, for movies or
2064
+ // Source ranking's two pages (rankfilters, rankplay): a row on it that opens one, saying
2065
+ // what's set there.
2066
+ function rankPageRow(title, summary, section) {
2067
+ var r = U.el('div', 'addon-item focusable cfg-row feed-extra');
2068
+ r.setAttribute('data-fk', section); r.setAttribute('data-ctl', 'extra');
2069
+ r.appendChild(U.el('div', 'a-name', title));
2070
+ r.appendChild(U.el('div', 'a-url', summary));
2071
+ r.__onselect = function () { _settingsSection = section; renderSettings(); };
2072
+ return r;
2073
+ }
2074
+ function filtersSummary() {
2075
+ var P = Stremio.getPrefs(), bits = ['Sources the TV can’t play: ' + (P.hideUnplayable ? 'Hidden' : 'Shown')], sizes = [];
2076
+ [['movie', 'Movies'], ['series', 'Series']].forEach(function (k) { var L = P.sizeLimits[k[0]]; if (L.min || L.max || L.perHour) { sizes.push(k[1] + ' ' + sizeSummary(L)); } });
2077
+ bits.push(sizes.length ? sizes.join(', ') : 'No size limits');
2078
+ bits.push(P.hideWords.length ? 'Words: ' + P.hideWords.join(', ') : 'No words');
2079
+ return bits.join(' · ');
2080
+ }
2081
+ function onPlaySummary() {
2082
+ var P = Stremio.getPrefs();
2083
+ return 'Source list: ' + (P.skipSourceList ? 'skipped' : 'shown') + ' · The next source if one doesn’t start: ' + (P.nextOnFail ? 'On' : 'Off');
2084
+ }
2085
+ // A page of its own: its title, a hint, its rows; focus back on the one just changed.
2086
+ function rankPage(wrap, title, hint, rows) {
2087
+ wrap.appendChild(U.el('h2', null, 'Playback › Source ranking › ' + title));
2088
+ wrap.appendChild(U.el('div', 'hint feeds-hint', hint));
2089
+ rows.forEach(function (r) { wrap.appendChild(r); });
2090
+ var pref = null;
2091
+ if (_feedFocus) { pref = rows.filter(function (r) { return r.getAttribute('data-fk') === _feedFocus.key; })[0] || null; _feedFocus = null; }
2092
+ Focus.setScope(scr('settings'), pref || rows[0]);
2093
+ }
2094
+ // Filters: what's left out, or sent to the bottom.
2095
+ function settingsRankFilters(wrap) {
2096
+ rankPage(wrap, 'Filters', 'Sources these leave out, or send to the bottom of the list: never picked on their own.',
2097
+ [unplayableRow(), sizeRow('movie', 'Movies’ sizes'), sizeRow('series', 'Series’ sizes (an episode)'), wordsRow()]);
2098
+ }
2099
+ // When you press Play: whether the list shows, and a source that doesn't start.
2100
+ function settingsRankPlay(wrap) {
2101
+ rankPage(wrap, 'When you press Play', 'What ▶ Play does with the ranked list.', [skipListRow(), nextOnFailRow()]);
2102
+ }
2103
+ // direct links only: the sources the TV can't play left out of the list
2104
+ function unplayableRow() {
2105
+ var hideUn = Stremio.getPrefs().hideUnplayable;
2106
+ var un = U.el('div', 'addon-item focusable cfg-row feed-extra');
2107
+ un.setAttribute('data-fk', 'unplayable'); un.setAttribute('data-ctl', 'extra');
2108
+ un.appendChild(U.el('div', 'a-name', 'Sources the TV can’t play: ' + (hideUn ? 'Hidden' : 'Shown')));
2109
+ un.appendChild(U.el('div', 'a-url', (hideUn ? 'Left out (torrents, links it can’t open): direct links only; where-to-watch apps stay'
2110
+ : 'Listed, dimmed, at the bottom (torrents, links it can’t open)') + ' · OK to ' + (hideUn ? 'show them' : 'hide them')));
2111
+ un.__onselect = function () { Stremio.setPref('hideUnplayable', !hideUn); refeed('ranking', 'unplayable', 'extra'); };
2112
+ return un;
2113
+ }
2114
+ // skip the source list: Play plays the top source at once
2115
+ function skipListRow() {
2116
+ var skipList = Stremio.getPrefs().skipSourceList;
2117
+ var sl = U.el('div', 'addon-item focusable cfg-row feed-extra');
2118
+ sl.setAttribute('data-fk', 'skiplist'); sl.setAttribute('data-ctl', 'extra');
2119
+ sl.appendChild(U.el('div', 'a-name', 'Skip the source list: ' + (skipList ? 'On' : 'Off')));
2120
+ sl.appendChild(U.el('div', 'a-url', (skipList ? '▶ Play, OK on an episode and ▶ Play SxEx play the top source at once (hold OK on an episode › Details for the list)'
2121
+ : '▶ Play and OK on an episode show the sources to pick from') + ' · OK to turn ' + (skipList ? 'off' : 'on')));
2122
+ sl.__onselect = function () { Stremio.setPref('skipSourceList', !skipList); refeed('ranking', 'skiplist', 'extra'); };
2123
+ return sl;
2124
+ }
2125
+ // a source that doesn't start: the next one
2126
+ function nextOnFailRow() {
2127
+ var nextOn = Stremio.getPrefs().nextOnFail;
2128
+ var nx = U.el('div', 'addon-item focusable cfg-row feed-extra');
2129
+ nx.setAttribute('data-fk', 'nextonfail'); nx.setAttribute('data-ctl', 'extra');
2130
+ nx.appendChild(U.el('div', 'a-name', 'The next source if one doesn’t start: ' + (nextOn ? 'On' : 'Off')));
2131
+ nx.appendChild(U.el('div', 'a-url', (nextOn ? 'A source the TV refuses, or that hasn’t started after 45 s, gives way to the next best one (up to 3 more)'
2132
+ : 'A source that doesn’t start takes you back to the list') + ' · OK to turn ' + (nextOn ? 'off' : 'on')));
2133
+ nx.__onselect = function () { Stremio.setPref('nextOnFail', !nextOn); refeed('ranking', 'nextonfail', 'extra'); };
2134
+ return nx;
2135
+ }
2136
+ // your words: sources with one of them at the bottom, for every title
2137
+ function wordsRow() {
2138
+ var words = Stremio.getPrefs().hideWords;
2139
+ var w = U.el('div', 'addon-item focusable cfg-row feed-extra');
2140
+ w.setAttribute('data-fk', 'words'); w.setAttribute('data-ctl', 'extra');
2141
+ w.appendChild(U.el('div', 'a-name', 'Hide sources with these words: ' + (words.length ? words.join(', ') : 'None')));
2142
+ w.appendChild(U.el('div', 'a-url', 'A source with one of them in its name, description or file name (a whole word: “CAM” isn’t in “CAMERA”) goes to the bottom, for every title · OK to change'));
2143
+ w.__onselect = function () {
2144
+ App.openEntry({ title: 'Hide sources with these words', value: words.join(', '), placeholder: 'CAM, HDTS, TELESYNC',
2145
+ hint: 'Words separated by commas, capitals or not. Sources with one go to the bottom and are never picked on their own. Leave it empty for none.',
2146
+ validate: function () { return null; },
2147
+ onConfirm: function (v) { App.back(); Stremio.setHideWords(v); refeed('ranking', 'words', 'extra'); } });
2148
+ };
2149
+ return w;
2150
+ }
2151
+ // Size limits (Settings › Source ranking › Filters): smallest, largest and most per hour, for movies or
2001
2152
  // series; OK on the row: which one, then its value.
2002
2153
  var SIZE_CHOICES = {
2003
2154
  min: { label: 'Smallest', none: 'None', values: [0.1, 0.3, 0.5, 1, 2] },
package/js/views.js CHANGED
@@ -7,14 +7,14 @@ var Views = (function (V) {
7
7
  return {
8
8
  renderHome: V.renderHome, renderDetail: V.renderDetail, applyAppearance: V.applyAppearance,
9
9
  renderPerson: V.renderPerson, detailSnapshot: V.detailSnapshot, restoreDetail: V.restoreDetail, lightSnapshot: V.lightSnapshot, renderSettings: V.renderSettings,
10
- settingsRoot: V.settingsRoot, backInSettings: V.backInSettings,
10
+ settingsRoot: V.settingsRoot, backInSettings: V.backInSettings, guideBackOn: V.guideBackOn,
11
11
  // a screen left: its downloads still running (a title page's details, Discover's
12
12
  // pages, a search's rows) stopped
13
13
  leaveScreen: function (name) { var f = { detail: V.leaveDetail, discover: V.leaveDiscover, search: V.leaveSearch }[name]; if (f) { f(); } }, backInLocal: V.backInLocal, setHomeTab: V.setHomeTab,
14
14
  renderLive: V.renderLive,
15
15
  renderSearch: V.renderSearch, searchFor: function (q) { V.searchFor(q); }, renderDiscover: V.renderDiscover, renderEntry: V.renderEntry, backInDetail: V.backInDetail,
16
16
  homeDirty: V.homeDirty, refreshHomeIfStale: V.refreshHomeIfStale, alertNewEpisodes: V.alertNewEpisodes, previewTiles: V.previewTiles, withShowInfo: V.withShowInfo,
17
- isModalOpen: V.isModalOpen, closeModal: V.closeModal, moveKey: V.moveKey, availableTabs: V.availableTabs,
17
+ isModalOpen: V.isModalOpen, closeModal: V.closeModal, confirm: V.confirm, moveKey: V.moveKey, availableTabs: V.availableTabs,
18
18
  menuItems: V.menuItems, menuOrder: V.menuOrder, moveMenuItem: V.moveMenuItem
19
19
  };
20
20
  })(ViewParts);
package/js/ytplay.js CHANGED
@@ -65,14 +65,15 @@ var YTPlay = (function () {
65
65
  // { fix, sure, why, url, cc: [{ ch, lang }] (its closed captions), ccSure }. force: the relay
66
66
  // at once (the channel's already known to need it). h: the channel's own User-Agent /
67
67
  // Referer ({ ua, ref }). opts (with force): { fix: false (the sound as it is), cc: the caption
68
- // channels to add as subtitle tracks }.
69
- function liveOpen(url, h, force, opts) {
68
+ // channels to add as subtitle tracks }. cancel (a U.cancelToken): the look stopped, the
69
+ // service's downloads with it (another channel, the player closed).
70
+ function liveOpen(url, h, force, opts, cancel) {
70
71
  if (MODE === 'browser') { return Promise.reject(new Error('no service')); }
71
72
  var cc = opts && opts.cc ? opts.cc.map(function (c) { return c.ch + (c.lang ? '-' + c.lang : ''); }).join(',') : '';
72
73
  return ensureService().then(entryAuth).then(function (a) {
73
74
  var q = '/live/open?u=' + encodeURIComponent(url) + (h && h.ua ? '&ua=' + encodeURIComponent(h.ua) : '') + (h && h.ref ? '&ref=' + encodeURIComponent(h.ref) : '') + (force ? '&force=1' : '') +
74
75
  (force && opts && opts.fix === false ? '&fix=0' : '') + (force && cc ? '&cc=' + encodeURIComponent(cc) : '');
75
- return U.getJSON(PROXY + q + a.q, force ? 5000 : 30000, 65536, null, a.h);
76
+ return U.getJSON(PROXY + q + a.q, force ? 5000 : 30000, 65536, cancel || null, a.h);
76
77
  });
77
78
  }
78
79
  // An HLS stream's quality list, read by the service with the stream's own User-Agent /
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stremiok",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "StremioK streaming client for Tizen 3.0+ Samsung TVs (TizenBrew app module).",
5
5
  "packageType": "app",
6
6
  "appName": "StremioK",
@@ -28,6 +28,7 @@
28
28
  "MediaTrackNext",
29
29
  "ChannelUp",
30
30
  "ChannelDown",
31
+ "Guide",
31
32
  "ColorF0Red",
32
33
  "ColorF1Green",
33
34
  "ColorF2Yellow",
package/service/index.js CHANGED
@@ -1589,6 +1589,29 @@ function openPart(fs, part, append, cb) {
1589
1589
  });
1590
1590
  });
1591
1591
  }
1592
+ // A partial download's version tag (ETag / Last-Modified), for resuming it after a restart: kept
1593
+ // in StremioK's own folder by the partial file's path (DL_VALIDATORS at most, the oldest dropped),
1594
+ // never on the drive, where a prepared file could be a link elsewhere (read: its contents sent
1595
+ // as If-Range; written: overwritten). None to keep it in (TizenBrew): downloads are closed there.
1596
+ var DL_VALIDATORS = 64;
1597
+ function dlValFile() { var d = tokenDir(); return d ? d + '/dl-validators.json' : null; }
1598
+ function dlValAll() {
1599
+ var f = dlValFile(), o = null;
1600
+ if (!f) { return {}; }
1601
+ try { o = JSON.parse(require('fs').readFileSync(f, 'utf8')); } catch (e) { o = null; }
1602
+ return o && typeof o === 'object' && !Array.isArray(o) ? o : {};
1603
+ }
1604
+ function dlValGet(part) { var v = dlValAll()[part]; return typeof v === 'string' ? v.slice(0, 300) : ''; }
1605
+ function dlValSet(part, v) {
1606
+ var f = dlValFile();
1607
+ if (!f) { return; }
1608
+ var all = dlValAll();
1609
+ delete all[part];
1610
+ if (v) { all[part] = String(v).slice(0, 300); }
1611
+ var keys = Object.keys(all);
1612
+ while (keys.length > DL_VALIDATORS) { delete all[keys.shift()]; }
1613
+ try { require('fs').writeFileSync(f, JSON.stringify(all), { mode: 384 }); } catch (e) { log('download version not kept: ' + (e && (e.code || e.message))); }
1614
+ }
1592
1615
  function dlRun(e) {
1593
1616
  var fs = require('fs'), part = e.dir + '/' + e.name + '.part';
1594
1617
  e.state = 'downloading'; e.err = '';
@@ -1601,17 +1624,17 @@ function dlRun(e) {
1601
1624
  fs.mkdir(e.dir, function () {
1602
1625
  fsRealOk(e.dir, function (bad) {
1603
1626
  if (bad) { dlEnd(e, 'failed', 'the folder: ' + bad.message); return; }
1604
- fs.stat(part, function (se, st) {
1627
+ fs.lstat(part, function (se, st) { // (not through a link: a partial file is a regular one, see openPart)
1628
+ if (!se && !st.isFile()) { dlEnd(e, 'failed', 'the partial file isn\'t a regular file'); return; }
1605
1629
  var have = se ? 0 : st.size;
1606
1630
  if (!have || e.answered) { get(e.url, 0, have); return; } // (this one's own, from this run: picked up as it was)
1607
- fs.readFile(part + '.v', 'utf8', function (ve, v) { // (a partial file from before a restart: resumed only with its validator)
1608
- e.validator = ve ? '' : String(v).trim().slice(0, 300);
1609
- if (e.validator) { get(e.url, 0, have); } else { dropPart(function () { get(e.url, 0, 0); }); }
1610
- });
1631
+ e.validator = dlValGet(part); // (a partial file from before a restart: resumed only with its version kept)
1632
+ if (e.validator) { get(e.url, 0, have); } else { dropPart(function () { get(e.url, 0, 0); }); }
1611
1633
  });
1612
1634
  });
1613
1635
  });
1614
- function dropPart(then) { fs.unlink(part + '.v', function () { fs.unlink(part, function () { then(); }); }); }
1636
+ // (….part.v: where 2.1.17–2.2.1 kept the version, removed; a link removed, not what it points to)
1637
+ function dropPart(then) { dlValSet(part, ''); fs.unlink(part + '.v', function () { fs.unlink(part, function () { then(); }); }); }
1615
1638
  function get(u, hops, have) {
1616
1639
  if (e.state !== 'downloading') { return; }
1617
1640
  var pu = urllib.parse(u), mod = pu.protocol === 'https:' ? https : http;
@@ -1647,7 +1670,7 @@ function dlRun(e) {
1647
1670
  if (!resumed) { // (asked again on a resume: If-Range; kept beside the partial file, for one after a restart)
1648
1671
  var et = resp.headers.etag;
1649
1672
  e.validator = et && !/^W\//.test(et) ? et : (resp.headers['last-modified'] || '');
1650
- fs.writeFile(part + '.v', e.validator, { mode: 384 }, function () {});
1673
+ dlValSet(part, e.validator);
1651
1674
  }
1652
1675
  if (isPlaylistAnswer(resp.headers['content-type'], '')) { resp.resume(); closed = true; dlEnd(e, 'failed', 'a streamed playlist (HLS or DASH), not a file: it can\'t be downloaded'); return; }
1653
1676
  var total = 0, cr = /\/(\d+)\s*$/.exec(resp.headers['content-range'] || '');
@@ -1715,7 +1738,7 @@ function dlRun(e) {
1715
1738
  fs.rename(part, final, function (re) {
1716
1739
  if (re) { dlEnd(e, 'failed', 'couldn\'t finish the file: ' + (re.code || re.message)); return; }
1717
1740
  e.name = final.split('/').pop(); e.tries = 0;
1718
- fs.unlink(part + '.v', function () {});
1741
+ dlValSet(part, ''); fs.unlink(part + '.v', function () {});
1719
1742
  dlEnd(e, 'done');
1720
1743
  });
1721
1744
  });
@@ -3057,7 +3080,8 @@ function epgControl(req, res, path, query) {
3057
3080
  var ENTRY_PORT = PORT + 10, ENTRY_MS = 5 * 60 * 1000, ENTRY_IDLE_MS = 30000, ENTRY_MAX = 8192;
3058
3081
  var ENTRY_OPEN_MAX = 2, ENTRY_OPENS = 12, ENTRY_CONNECTIONS = 8, ENTRY_VISIT_MS = 20000, ENTRY_QUIET_MS = 10000;
3059
3082
  var ENTRY_TITLES = { link: 'Play a link on StremioK', addon: 'Add an addon to StremioK', playlist: 'Add an IPTV playlist to StremioK', config: 'Import a configuration file into StremioK',
3060
- search: 'Search on StremioK', youtube: 'Add a YouTube catalog to StremioK', aiom: 'Your AIOManager address for StremioK', aiomkey: 'Your AIOManager API key for StremioK' };
3083
+ search: 'Search on StremioK', youtube: 'Add a YouTube catalog to StremioK', aiom: 'Your AIOManager address for StremioK', aiomkey: 'Your AIOManager API key for StremioK',
3084
+ server: 'Your streaming server’s address for StremioK' };
3061
3085
  var ENTRY_TEXT = { search: 'Words to search for', youtube: 'A channel, a playlist link, or words to search YouTube for', aiomkey: 'Your account API key' }; // (plain text, not a link)
3062
3086
  var _entries = Object.create(null), _entryServer = null, _entrySockets = [], _entryOpened = [];
3063
3087
  function lanAddress() {