stremiok 2.1.0 → 2.2.1

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.
@@ -17,7 +17,6 @@
17
17
  function markHomeDirty() { return V.markHomeDirty.apply(null, arguments); } // home
18
18
  function onModalClose() { return V.onModalClose.apply(null, arguments); } // core
19
19
  function pickList() { return V.pickList.apply(null, arguments); } // core
20
- function toggleList() { return V.toggleList.apply(null, arguments); } // core
21
20
  function playlistActions() { return V.playlistActions.apply(null, arguments); } // core
22
21
  function scr() { return V.scr.apply(null, arguments); } // core
23
22
  function showModal() { return V.showModal.apply(null, arguments); } // core
@@ -99,11 +98,35 @@
99
98
  }
100
99
 
101
100
  // ================= SUBTITLE CONFIG =================
102
- var SUB_LANGS = [['off', 'Off'], ['eng', 'English'], ['spa', 'Spanish'], ['fre', 'French'],
103
- ['ger', 'German'], ['ita', 'Italian'], ['por', 'Portuguese'], ['dut', 'Dutch'],
104
- ['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']];
105
126
  // A second choice, turned on when nothing is in the default language (from any source).
106
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']];
107
130
  var SUB_SIZES = [['small', 'Small'], ['medium', 'Medium'], ['large', 'Large'], ['xlarge', 'Extra Large']];
108
131
 
109
132
  function labelFor(list, v) {
@@ -207,14 +230,16 @@
207
230
  get: function () { return { spoilers: Stremio.getPrefs().spoilers }; },
208
231
  set: function (cfg) { Stremio.setPref('spoilers', cfg.spoilers); }
209
232
  }));
210
- wrap.appendChild(homeTabsRow());
233
+ var menu = sectionRow('Menu ›', 'Order the top menu, and show or hide its tabs', 'menuorder');
234
+ menu.className += ' cfg-row';
235
+ wrap.appendChild(menu);
211
236
  var feeds = sectionRow('Home feeds ›', 'Order the Home rows, and show or hide each one', 'feeds');
212
237
  feeds.className += ' cfg-row';
213
238
  wrap.appendChild(feeds);
214
239
  var sfeeds = sectionRow('Search results ›', 'Order the Search rows (one per catalog that can search), and show or hide each one', 'searchfeeds');
215
240
  sfeeds.className += ' cfg-row';
216
241
  wrap.appendChild(sfeeds);
217
- var back = _focusChild === 'feeds' ? feeds : _focusChild === 'searchfeeds' ? sfeeds : null;
242
+ var back = _focusChild === 'menuorder' ? menu : _focusChild === 'feeds' ? feeds : _focusChild === 'searchfeeds' ? sfeeds : null;
218
243
  _focusChild = null;
219
244
  if (back) { Focus.setScope(scr('settings'), back); } else { focusSettings(); }
220
245
  }
@@ -337,7 +362,7 @@
337
362
  // BACK goes up one level, onto the row it came from: from a section to its group (on
338
363
  // that section, not the first one), from Home feeds / Search results to Appearance,
339
364
  // from a group to the Settings list (on that group).
340
- var SECTION_PARENT = { feeds: 'appearance', searchfeeds: 'appearance', aiomanager: 'addons' };
365
+ var SECTION_PARENT = { menuorder: 'appearance', feeds: 'appearance', searchfeeds: 'appearance', aiomanager: 'addons', subtitlelook: 'subtitles', rankfilters: 'ranking', rankplay: 'ranking' };
341
366
  Object.keys(SETTINGS_GROUPS).forEach(function (g) {
342
367
  SETTINGS_GROUPS[g].rows.forEach(function (row) { SECTION_PARENT[row[2]] = g; });
343
368
  });
@@ -364,11 +389,15 @@
364
389
  else if (_settingsSection === 'integrations') { settingsIntegrations(wrap); }
365
390
  else if (_settingsSection === 'audio') { settingsAudio(wrap); }
366
391
  else if (_settingsSection === 'subtitles') { settingsSubtitles(wrap); }
392
+ else if (_settingsSection === 'subtitlelook') { settingsSubtitleLook(wrap); }
367
393
  else if (_settingsSection === 'skipping') { settingsSkipping(wrap); }
368
394
  else if (_settingsSection === 'seeking') { settingsSeeking(wrap); }
395
+ else if (_settingsSection === 'menuorder') { settingsFeeds(wrap, 'menu'); }
369
396
  else if (_settingsSection === 'feeds') { settingsFeeds(wrap, 'home'); }
370
397
  else if (_settingsSection === 'searchfeeds') { settingsFeeds(wrap, 'search'); }
371
398
  else if (_settingsSection === 'ranking') { settingsFeeds(wrap, 'ranking'); }
399
+ else if (_settingsSection === 'rankfilters') { settingsRankFilters(wrap); }
400
+ else if (_settingsSection === 'rankplay') { settingsRankPlay(wrap); }
372
401
  else if (SETTINGS_GROUPS[_settingsSection]) { settingsGroup(wrap, _settingsSection); }
373
402
  else if (_settingsSection === 'logs') { settingsLogs(wrap); }
374
403
  else if (_settingsSection === 'storage') { settingsStorage(wrap); }
@@ -867,6 +896,15 @@
867
896
  var _iptvFocus = null; // { id, ctl } to focus once the page is drawn again
868
897
  function two(n) { return (n < 10 ? '0' : '') + n; }
869
898
  function thousands(n) { return ('' + n).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
899
+ // The playlist rows IPTV Settings drew last: their guide lines kept current by one listener
900
+ // (one added each time it was drawn used to pile up, each holding its rows).
901
+ var _guideRows = [];
902
+ if (typeof EPG !== 'undefined' && EPG.onChange) {
903
+ EPG.onChange(function () {
904
+ _guideRows = _guideRows.filter(function (r) { return document.documentElement.contains(r.info); });
905
+ _guideRows.forEach(function (r) { r.info.textContent = playlistInfo(r.p); });
906
+ });
907
+ }
870
908
  function playlistInfo(p) {
871
909
  if (p.off) { return 'Off — its channels aren’t shown'; }
872
910
  var st = Stremio.iptvStatus(p.id), parts = [];
@@ -875,13 +913,65 @@
875
913
  if (st.fetchedAt) { var d = new Date(st.fetchedAt); parts.push('downloaded ' + U.dayLabel(st.fetchedAt) + ' ' + two(d.getHours()) + ':' + two(d.getMinutes())); }
876
914
  if (st.dropped) { parts.push(thousands(st.dropped) + ' more left out (a playlist keeps its first 30,000)'); }
877
915
  if (st.uncached) { parts.push('too large to save on the TV: downloaded each launch'); }
916
+ var g = guideInfo(p);
917
+ if (g) { parts.push(g); }
878
918
  return parts.join(' · ');
879
919
  }
920
+ // Its TV guide (js/epg.js): being read, read (for how many of its channels), or why not.
921
+ function guideInfo(p) {
922
+ var g = typeof EPG !== 'undefined' && EPG.status ? EPG.status(p.id) : null;
923
+ if (!g) { return typeof EPG !== 'undefined' && EPG.available && EPG.available() ? 'no TV guide' : ''; }
924
+ if (g.state === 'ready') {
925
+ var d = new Date(g.at);
926
+ return 'TV guide: ' + thousands(g.channels) + ' channel' + (g.channels === 1 ? '' : 's') + ', ' + thousands(g.programmes) + ' programs' + (g.partial ? ' (part of it: too large)' : '') +
927
+ (g.at ? ', read ' + U.dayLabel(g.at) + ' ' + two(d.getHours()) + ':' + two(d.getMinutes()) : '') + (g.error ? ' (the last read failed: ' + g.error + ')' : '');
928
+ }
929
+ if (g.state === 'failed') { return '⚠ TV guide: ' + (g.error || 'couldn’t be read'); }
930
+ return 'TV guide: reading…';
931
+ }
880
932
  function playlistProblem(p) {
881
933
  var st = Stremio.iptvStatus(p.id);
882
934
  if (p.off || !st.error) { return ''; }
883
935
  return '⚠ ' + (st.stale ? 'Couldn’t refresh it (showing the saved copy): ' : '') + st.error;
884
936
  }
937
+ // + Add Xtream account: its server, username and password, a page each (a whole Xtream link
938
+ // in the server's fills the rest); checked with the server, then added (Stremio.iptvAddXtream).
939
+ function addXtream() {
940
+ function done(base, user, pass) {
941
+ U.toast('Checking the account…', 15000);
942
+ IPTV.xtreamCheck(base, user, pass).then(function (r) {
943
+ if (r.unreachable) { U.toast('Couldn’t reach the server: check its address (nothing was saved)', 5000); return; }
944
+ if (r.refused) { U.toast('The server refused that username and password' + (r.status ? ' (' + r.status + ')' : '') + ': nothing was saved', 5000); return; }
945
+ Stremio.iptvAddXtream(base, user, pass);
946
+ refreshAddonRows('local:iptv');
947
+ U.toast('Xtream account added: reading its channels and TV guide…', 4000);
948
+ renderSettings();
949
+ });
950
+ }
951
+ App.openEntry({
952
+ title: 'Xtream account: server', phone: 'playlist',
953
+ hint: 'The server address your provider gave you (http://line.example.com:8080), or a whole Xtream link. RETURN cancels.',
954
+ value: 'http://', placeholder: 'http://line.example.com:8080',
955
+ validate: function (v) { return IPTV.xtreamParts(v) ? null : 'That isn’t a server address'; },
956
+ onConfirm: function (v) {
957
+ var x = IPTV.xtreamParts(v);
958
+ App.back();
959
+ if (x.user && x.pass) { done(x.base, x.user, x.pass); return; }
960
+ App.openEntry({
961
+ title: 'Xtream account: username', hint: 'Your account’s username. RETURN cancels.', value: x.user, placeholder: 'username',
962
+ validate: function (u) { return ('' + u).trim() ? null : 'Type the username'; },
963
+ onConfirm: function (u) {
964
+ App.back();
965
+ App.openEntry({
966
+ title: 'Xtream account: password', hint: 'Your account’s password (kept on the TV, in the playlist’s link). RETURN cancels.', value: '', placeholder: 'password', secret: true,
967
+ validate: function (p) { return ('' + p) ? null : 'Type the password'; },
968
+ onConfirm: function (p) { App.back(); done(x.base, ('' + u).trim(), '' + p); }
969
+ });
970
+ }
971
+ });
972
+ }
973
+ });
974
+ }
885
975
  function settingsIptv(wrap) {
886
976
  wrap.appendChild(U.el('h2', null, 'IPTV'));
887
977
  var playlists = Stremio.iptvGetPlaylists ? Stremio.iptvGetPlaylists() : [];
@@ -925,6 +1015,7 @@
925
1015
  rows.push({ p: playlist, info: info, problem: problem, idx: idx });
926
1016
  });
927
1017
 
1018
+ _guideRows = rows; // (their guide lines kept current as guides are read: _guideRows)
928
1019
  if (statusEl) {
929
1020
  Stremio.iptvLoad().then(function () {
930
1021
  var channels = 0, groups = 0, failed = 0, on = 0;
@@ -962,6 +1053,11 @@
962
1053
  });
963
1054
  };
964
1055
  wrap.appendChild(add);
1056
+ var xt = U.el('div', 'addon-item focusable add-row');
1057
+ xt.appendChild(U.el('div', 'a-name', '+ Add Xtream account'));
1058
+ xt.appendChild(U.el('div', 'a-url', 'Its server, username and password: its channels and TV guide added'));
1059
+ xt.__onselect = addXtream;
1060
+ wrap.appendChild(xt);
965
1061
 
966
1062
  wrap.appendChild(U.el('div', 'hint',
967
1063
  'Channels are grouped into IPTV and Radio rows on Home. ▲ / ▼ set the playlists\' order: a channel in two playlists comes from the first.'));
@@ -1330,14 +1426,7 @@
1330
1426
  wrap.appendChild(subPickRow('Default language', SUB_LANGS, 'lang'));
1331
1427
  wrap.appendChild(subPickRow('Second choice', SUB_LANGS2, 'lang2'));
1332
1428
  wrap.appendChild(subPickRow('Second subtitle (at the top)', SUB_LANGS, 'second'));
1333
- wrap.appendChild(subPickRow('Size', SUB_SIZES, 'size'));
1334
- // [id, label] for the picker (colors with a swatch)
1335
- function plain(list) { return list.map(function (x) { return [x[0], x[1]]; }); }
1336
- wrap.appendChild(subPickRow('Color', Player.SUB_COLORS.map(function (x) { return [x[0], x[1], null, x[2]]; }), 'color'));
1337
- wrap.appendChild(subPickRow('Background', plain(Player.SUB_BGS), 'bg'));
1338
- wrap.appendChild(subPickRow('Edge', plain(Player.SUB_EDGES), 'edge'));
1339
- wrap.appendChild(subPickRow('Position', plain(Player.SUB_POSITIONS), 'pos'));
1340
- wrap.appendChild(subPickRow('Font', Player.SUB_FONTS, 'font'));
1429
+ wrap.appendChild(subPickRow('Only my languages', SUB_ONLY, 'only'));
1341
1430
  // lines to leave out: those with one of your words (a subtitle site's ads, a file's name…)
1342
1431
  var skip = Player.getSubCfg().skip || [];
1343
1432
  var sk = U.el('div', 'addon-item focusable cfg-row');
@@ -1350,6 +1439,21 @@
1350
1439
  onConfirm: function (v) { var c = Player.getSubCfg(); c.skip = ('' + v).split(','); Player.setSubCfg(c); App.back(); renderSettings(); } });
1351
1440
  };
1352
1441
  wrap.appendChild(sk);
1442
+ var look = sectionRow('Appearance ›', 'Size, color, background, edge, position, font', 'subtitlelook'); // (their own page: settingsSubtitleLook)
1443
+ wrap.appendChild(look);
1444
+ if (_focusChild === 'subtitlelook') { _focusChild = null; Focus.setScope(scr('settings'), look); } else { focusSettings(); }
1445
+ }
1446
+ // Subtitles › Appearance: how subtitles look.
1447
+ function settingsSubtitleLook(wrap) {
1448
+ wrap.appendChild(U.el('h2', null, 'Subtitle appearance'));
1449
+ wrap.appendChild(subPickRow('Size', SUB_SIZES, 'size'));
1450
+ // [id, label] for the picker (colors with a swatch)
1451
+ function plain(list) { return list.map(function (x) { return [x[0], x[1]]; }); }
1452
+ wrap.appendChild(subPickRow('Color', Player.SUB_COLORS.map(function (x) { return [x[0], x[1], null, x[2]]; }), 'color'));
1453
+ wrap.appendChild(subPickRow('Background', plain(Player.SUB_BGS), 'bg'));
1454
+ wrap.appendChild(subPickRow('Edge', plain(Player.SUB_EDGES), 'edge'));
1455
+ wrap.appendChild(subPickRow('Position', plain(Player.SUB_POSITIONS), 'pos'));
1456
+ wrap.appendChild(subPickRow('Font', Player.SUB_FONTS, 'font'));
1353
1457
  focusSettings();
1354
1458
  }
1355
1459
 
@@ -1386,34 +1490,6 @@
1386
1490
  focusSettings();
1387
1491
  }
1388
1492
 
1389
- // Home tabs: hide the ones you never use (Watching always stays: it's where Home opens). A
1390
- // row like the ones above it; OK opens the tabs, OK on one shows or hides it (the list
1391
- // stays open). A hidden tab's rows are back when it's shown again.
1392
- function homeTabsRow() {
1393
- function hiddenNow() { return Stremio.getPrefs().hiddenTabs || []; }
1394
- var r = U.el('div', 'addon-item focusable cfg-row'), name = U.el('div', 'a-name');
1395
- var tabs = (V.HOME_TABS || []).filter(function (t) { return t.key !== 'continue'; });
1396
- function draw() {
1397
- var hidden = hiddenNow().filter(function (k) { return tabs.some(function (t) { return t.key === k; }); });
1398
- name.textContent = 'Home tabs: ' + (hidden.length ? hidden.length + ' hidden' : 'All shown');
1399
- }
1400
- draw();
1401
- r.appendChild(name);
1402
- r.appendChild(U.el('div', 'a-url', 'Press OK to choose'));
1403
- r.__onselect = function () {
1404
- var hidden = hiddenNow();
1405
- toggleList('Home tabs', tabs.map(function (t) { return [t.key, t.label + (t.key === 'local' ? ' (with a USB drive in)' : ''), hidden.indexOf(t.key) === -1]; }),
1406
- { on: 'Shown', off: 'Hidden' }, function (key, on) {
1407
- var now = hiddenNow().filter(function (k) { return k !== key; });
1408
- if (!on) { now.push(key); }
1409
- Stremio.setPref('hiddenTabs', now);
1410
- markHomeDirty();
1411
- draw();
1412
- });
1413
- };
1414
- return r;
1415
- }
1416
-
1417
1493
  var POSTER_SHAPES = [['portrait', 'Portrait'], ['landscape', 'Landscape']];
1418
1494
  // ----- Diagnostics › Logs: the recent problems U.log kept (in memory, URLs masked), and
1419
1495
  // how long launches took -----
@@ -1630,14 +1706,17 @@
1630
1706
  if (!inf.entries.length) { line(dbOn ? 'Nothing in the TV’s database.' : 'The TV’s database isn’t used (turned off above).'); }
1631
1707
  line('Settings storage: ' + fmtBytes(inf.ls) + ' (progress, watched marks, settings…)' + (inf.lsTop.length ? '; the largest:' : ''));
1632
1708
  inf.lsTop.forEach(function (e) { line(' ' + fmtBytes(e.bytes) + ' ' + e.key); });
1709
+ if (inf.guides && inf.guides.count) { line('Saved TV guides (in StremioK’s folder): ' + fmtBytes(inf.guides.bytes) + ' in ' + inf.guides.count + (inf.guides.count === 1 ? ' file' : ' files')); }
1633
1710
  if (inf.disk) { line('The TV’s own disk: ' + fmtBytes(inf.disk.free) + ' free of ' + fmtBytes(inf.disk.total) + ' (what the TV allows StremioK follows it: freeing space, unused apps for one, raises it)'); }
1634
1711
  }
1635
1712
  r.__onselect = function () {
1636
1713
  if (!U.bigStore || !U.bigStore.info || _storageInfo === 'busy') { return; }
1637
1714
  _storageInfo = 'busy'; renderSettings();
1638
- Promise.all([U.bigStore.info(), U.diskInfo ? U.diskInfo() : null]).then(function (both) {
1715
+ var guides = (typeof YTPlay !== 'undefined' && YTPlay.epgFiles) ? YTPlay.epgFiles().then(null, function () { return null; }) : null;
1716
+ Promise.all([U.bigStore.info(), U.diskInfo ? U.diskInfo() : null, guides]).then(function (both) {
1639
1717
  var inf = both[0], ls = 0, lsAll = [];
1640
1718
  inf.disk = both[1];
1719
+ inf.guides = both[2]; // ({ count, bytes }: the TV guides StremioK's service saved)
1641
1720
  inf.lsKeys = {};
1642
1721
  try {
1643
1722
  for (var i = 0; i < window.localStorage.length; i++) {
@@ -1669,12 +1748,15 @@
1669
1748
  }
1670
1749
  var clr = U.el('div', 'addon-item focusable');
1671
1750
  clr.appendChild(U.el('div', 'a-name', 'Clear saved copies'));
1672
- clr.appendChild(U.el('div', 'a-url', 'Home’s saved rows, IPTV playlist copies, the USB index: made again when needed (playlists download again)'));
1751
+ clr.appendChild(U.el('div', 'a-url', 'Home’s saved rows, IPTV playlist copies, saved TV guides, the USB index: made again when needed (playlists and guides download again)'));
1673
1752
  clr.__onselect = function () {
1674
- confirm('Clear the copies StremioK can make again (Home rows, playlist copies, USB index)?', 'Clear', function () {
1753
+ confirm('Clear the copies StremioK can make again (Home rows, playlist copies, TV guides, USB index)?', 'Clear', function () {
1675
1754
  U.bigStore.keys().then(function (ks) {
1676
1755
  return Promise.all(ks.filter(function (k) { return /^stremiok\.(homecache|local\.index|iptv\.cache\.)/.test(k); }).map(function (k) { return U.bigStore.remove(k); }));
1677
1756
  }).then(function () {
1757
+ return (typeof YTPlay !== 'undefined' && YTPlay.epgClear) ? YTPlay.epgClear().then(null, function () {}) : null; // (the saved guides: read again as they're needed)
1758
+ }).then(function () {
1759
+ if (typeof EPG !== 'undefined' && EPG.reset) { EPG.reset(); }
1678
1760
  if (typeof LocalMedia !== 'undefined') { LocalMedia.clearIndex(); }
1679
1761
  _storageInfo = null;
1680
1762
  U.toast('Saved copies cleared');
@@ -1846,7 +1928,28 @@
1846
1928
  // ----- Home feeds and Search results (Settings > Appearance): order, show / hide -----
1847
1929
  // kind 'home': the Home rows (every catalog); 'search': the Search rows (every catalog
1848
1930
  // that can search, IPTV as one per type). ▲/▼ moves one; OK on its name shows / hides it.
1931
+ // 'menu': the top menu between Watching and Settings (the tabs, Discover, Search: ViewParts'
1932
+ // menuOrder); OK on a tab shows / hides it, Discover and Search always show.
1933
+ var MENU_FIXED = { discover: 'Discover', search: 'Search' };
1934
+ function menuHidden(k) { return !MENU_FIXED[k] && (Stremio.getPrefs().hiddenTabs || []).indexOf(k) !== -1; }
1849
1935
  var FEED_KINDS = {
1936
+ menu: { section: 'menuorder', title: 'Appearance › Menu', hint: 'Watching comes first and Settings last: ⇈ ▲ ▼ ⇊ put the rest in your order · OK on a tab shows/hides it.',
1937
+ list: function () {
1938
+ return Promise.resolve(V.menuOrder().map(function (k) {
1939
+ var t = (V.HOME_TABS || []).filter(function (x) { return x.key === k; })[0];
1940
+ return { key: k, name: MENU_FIXED[k] || (t ? t.label : k) + (k === 'local' ? ' (with a USB drive in)' : k === 'guide' ? ' (once a TV guide is read)' : '') };
1941
+ }));
1942
+ },
1943
+ keyOf: function (x) { return x.key; },
1944
+ move: function (k, d) { V.moveMenuItem(k, d); markHomeDirty(); return Promise.resolve(); },
1945
+ toggle: function (k) {
1946
+ if (MENU_FIXED[k]) { U.toast(MENU_FIXED[k] + ' is always in the menu'); return; }
1947
+ var now = (Stremio.getPrefs().hiddenTabs || []).filter(function (x) { return x !== k; });
1948
+ if (!menuHidden(k)) { now.push(k); }
1949
+ Stremio.setPref('hiddenTabs', now);
1950
+ markHomeDirty();
1951
+ },
1952
+ hidden: menuHidden },
1850
1953
  home: { section: 'feeds', title: 'Appearance › Home feeds', hint: '▲/▼ reorder a feed · OK on the name shows/hides it on Home.',
1851
1954
  list: function () { return Stremio.listCatalogsOrdered(); }, keyOf: function (c) { return Stremio.catalogKey(c); },
1852
1955
  move: function (k, d) { return Stremio.moveFeed(k, d); }, toggle: function (k) { Stremio.toggleFeed(k); },
@@ -1860,7 +1963,7 @@
1860
1963
  list: function () { return Promise.resolve(Stremio.getRanking().criteria); }, keyOf: function (c) { return c.key; },
1861
1964
  move: function (k, d) { Stremio.moveRanking(k, d); return Promise.resolve(); }, toggle: function (k) { Stremio.toggleRanking(k); },
1862
1965
  hidden: function (k) { return !Stremio.getRanking().criteria.some(function (c) { return c.key === k && c.on; }); },
1863
- before: function (wrap) { // played before, above the resolution groups
1966
+ before: function (wrap) { // played before, above the resolution groups; then the two pages
1864
1967
  var played = Stremio.getRanking().played;
1865
1968
  var r = U.el('div', 'addon-item focusable cfg-row feed-extra');
1866
1969
  r.setAttribute('data-fk', 'played'); r.setAttribute('data-ctl', 'extra');
@@ -1869,48 +1972,9 @@
1869
1972
  : 'Sources you played rank like any other') + ' · OK to turn ' + (played ? 'off' : 'on')));
1870
1973
  r.__onselect = function () { Stremio.setPlayedFirst(!played); refeed('ranking', 'played', 'extra'); };
1871
1974
  wrap.appendChild(r);
1872
- // direct links only: the sources the TV can't play left out of the list
1873
- var hideUn = Stremio.getPrefs().hideUnplayable;
1874
- var un = U.el('div', 'addon-item focusable cfg-row feed-extra');
1875
- un.setAttribute('data-fk', 'unplayable'); un.setAttribute('data-ctl', 'extra');
1876
- un.appendChild(U.el('div', 'a-name', 'Sources the TV can’t play: ' + (hideUn ? 'Hidden' : 'Shown')));
1877
- un.appendChild(U.el('div', 'a-url', (hideUn ? 'Left out (torrents, links it can’t open): direct links only; where-to-watch apps stay'
1878
- : 'Listed, dimmed, at the bottom (torrents, links it can’t open)') + ' · OK to ' + (hideUn ? 'show them' : 'hide them')));
1879
- un.__onselect = function () { Stremio.setPref('hideUnplayable', !hideUn); refeed('ranking', 'unplayable', 'extra'); };
1880
- wrap.appendChild(un);
1881
- // skip the source list: Play plays the top source at once
1882
- var skipList = Stremio.getPrefs().skipSourceList;
1883
- var sl = U.el('div', 'addon-item focusable cfg-row feed-extra');
1884
- sl.setAttribute('data-fk', 'skiplist'); sl.setAttribute('data-ctl', 'extra');
1885
- sl.appendChild(U.el('div', 'a-name', 'Skip the source list: ' + (skipList ? 'On' : 'Off')));
1886
- 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)'
1887
- : '▶ Play and OK on an episode show the sources to pick from') + ' · OK to turn ' + (skipList ? 'off' : 'on')));
1888
- sl.__onselect = function () { Stremio.setPref('skipSourceList', !skipList); refeed('ranking', 'skiplist', 'extra'); };
1889
- wrap.appendChild(sl);
1890
- // a source that doesn't start: the next one
1891
- var nextOn = Stremio.getPrefs().nextOnFail;
1892
- var nx = U.el('div', 'addon-item focusable cfg-row feed-extra');
1893
- nx.setAttribute('data-fk', 'nextonfail'); nx.setAttribute('data-ctl', 'extra');
1894
- nx.appendChild(U.el('div', 'a-name', 'The next source if one doesn’t start: ' + (nextOn ? 'On' : 'Off')));
1895
- 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)'
1896
- : 'A source that doesn’t start takes you back to the list') + ' · OK to turn ' + (nextOn ? 'off' : 'on')));
1897
- nx.__onselect = function () { Stremio.setPref('nextOnFail', !nextOn); refeed('ranking', 'nextonfail', 'extra'); };
1898
- wrap.appendChild(nx);
1899
- // size limits: movies and series apart
1900
- [['movie', 'Movies’ sizes'], ['series', 'Series’ sizes (an episode)']].forEach(function (k) { wrap.appendChild(sizeRow(k[0], k[1])); });
1901
- // your words: sources with one of them at the bottom, for every title
1902
- var words = Stremio.getPrefs().hideWords;
1903
- var w = U.el('div', 'addon-item focusable cfg-row feed-extra');
1904
- w.setAttribute('data-fk', 'words'); w.setAttribute('data-ctl', 'extra');
1905
- w.appendChild(U.el('div', 'a-name', 'Hide sources with these words: ' + (words.length ? words.join(', ') : 'None')));
1906
- 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'));
1907
- w.__onselect = function () {
1908
- App.openEntry({ title: 'Hide sources with these words', value: words.join(', '), placeholder: 'CAM, HDTS, TELESYNC',
1909
- 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.',
1910
- validate: function () { return null; },
1911
- onConfirm: function (v) { App.back(); Stremio.setHideWords(v); refeed('ranking', 'words', 'extra'); } });
1912
- };
1913
- wrap.appendChild(w);
1975
+ wrap.appendChild(rankPageRow('Filters ›', filtersSummary(), 'rankfilters'));
1976
+ wrap.appendChild(rankPageRow('When you press Play ›', onPlaySummary(), 'rankplay'));
1977
+ if (_focusChild === 'rankfilters' || _focusChild === 'rankplay') { _feedFocus = { key: _focusChild, ctl: 'extra' }; _focusChild = null; } // (BACK from one: on its row)
1914
1978
  wrap.appendChild(U.el('div', 'formats-group', 'Then, in this order'));
1915
1979
  },
1916
1980
  after: function (wrap) {
@@ -1929,7 +1993,94 @@
1929
1993
  }, function () { wrap.appendChild(U.el('div', 'hint', 'Your addons couldn\'t be read just now.')); });
1930
1994
  } }
1931
1995
  };
1932
- // Size limits (Settings › Source ranking): smallest, largest and most per hour, for movies or
1996
+ // Source ranking's two pages (rankfilters, rankplay): a row on it that opens one, saying
1997
+ // what's set there.
1998
+ function rankPageRow(title, summary, section) {
1999
+ var r = U.el('div', 'addon-item focusable cfg-row feed-extra');
2000
+ r.setAttribute('data-fk', section); r.setAttribute('data-ctl', 'extra');
2001
+ r.appendChild(U.el('div', 'a-name', title));
2002
+ r.appendChild(U.el('div', 'a-url', summary));
2003
+ r.__onselect = function () { _settingsSection = section; renderSettings(); };
2004
+ return r;
2005
+ }
2006
+ function filtersSummary() {
2007
+ var P = Stremio.getPrefs(), bits = ['Sources the TV can’t play: ' + (P.hideUnplayable ? 'Hidden' : 'Shown')], sizes = [];
2008
+ [['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)); } });
2009
+ bits.push(sizes.length ? sizes.join(', ') : 'No size limits');
2010
+ bits.push(P.hideWords.length ? 'Words: ' + P.hideWords.join(', ') : 'No words');
2011
+ return bits.join(' · ');
2012
+ }
2013
+ function onPlaySummary() {
2014
+ var P = Stremio.getPrefs();
2015
+ return 'Source list: ' + (P.skipSourceList ? 'skipped' : 'shown') + ' · The next source if one doesn’t start: ' + (P.nextOnFail ? 'On' : 'Off');
2016
+ }
2017
+ // A page of its own: its title, a hint, its rows; focus back on the one just changed.
2018
+ function rankPage(wrap, title, hint, rows) {
2019
+ wrap.appendChild(U.el('h2', null, 'Playback › Source ranking › ' + title));
2020
+ wrap.appendChild(U.el('div', 'hint feeds-hint', hint));
2021
+ rows.forEach(function (r) { wrap.appendChild(r); });
2022
+ var pref = null;
2023
+ if (_feedFocus) { pref = rows.filter(function (r) { return r.getAttribute('data-fk') === _feedFocus.key; })[0] || null; _feedFocus = null; }
2024
+ Focus.setScope(scr('settings'), pref || rows[0]);
2025
+ }
2026
+ // Filters: what's left out, or sent to the bottom.
2027
+ function settingsRankFilters(wrap) {
2028
+ rankPage(wrap, 'Filters', 'Sources these leave out, or send to the bottom of the list: never picked on their own.',
2029
+ [unplayableRow(), sizeRow('movie', 'Movies’ sizes'), sizeRow('series', 'Series’ sizes (an episode)'), wordsRow()]);
2030
+ }
2031
+ // When you press Play: whether the list shows, and a source that doesn't start.
2032
+ function settingsRankPlay(wrap) {
2033
+ rankPage(wrap, 'When you press Play', 'What ▶ Play does with the ranked list.', [skipListRow(), nextOnFailRow()]);
2034
+ }
2035
+ // direct links only: the sources the TV can't play left out of the list
2036
+ function unplayableRow() {
2037
+ var hideUn = Stremio.getPrefs().hideUnplayable;
2038
+ var un = U.el('div', 'addon-item focusable cfg-row feed-extra');
2039
+ un.setAttribute('data-fk', 'unplayable'); un.setAttribute('data-ctl', 'extra');
2040
+ un.appendChild(U.el('div', 'a-name', 'Sources the TV can’t play: ' + (hideUn ? 'Hidden' : 'Shown')));
2041
+ un.appendChild(U.el('div', 'a-url', (hideUn ? 'Left out (torrents, links it can’t open): direct links only; where-to-watch apps stay'
2042
+ : 'Listed, dimmed, at the bottom (torrents, links it can’t open)') + ' · OK to ' + (hideUn ? 'show them' : 'hide them')));
2043
+ un.__onselect = function () { Stremio.setPref('hideUnplayable', !hideUn); refeed('ranking', 'unplayable', 'extra'); };
2044
+ return un;
2045
+ }
2046
+ // skip the source list: Play plays the top source at once
2047
+ function skipListRow() {
2048
+ var skipList = Stremio.getPrefs().skipSourceList;
2049
+ var sl = U.el('div', 'addon-item focusable cfg-row feed-extra');
2050
+ sl.setAttribute('data-fk', 'skiplist'); sl.setAttribute('data-ctl', 'extra');
2051
+ sl.appendChild(U.el('div', 'a-name', 'Skip the source list: ' + (skipList ? 'On' : 'Off')));
2052
+ 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)'
2053
+ : '▶ Play and OK on an episode show the sources to pick from') + ' · OK to turn ' + (skipList ? 'off' : 'on')));
2054
+ sl.__onselect = function () { Stremio.setPref('skipSourceList', !skipList); refeed('ranking', 'skiplist', 'extra'); };
2055
+ return sl;
2056
+ }
2057
+ // a source that doesn't start: the next one
2058
+ function nextOnFailRow() {
2059
+ var nextOn = Stremio.getPrefs().nextOnFail;
2060
+ var nx = U.el('div', 'addon-item focusable cfg-row feed-extra');
2061
+ nx.setAttribute('data-fk', 'nextonfail'); nx.setAttribute('data-ctl', 'extra');
2062
+ nx.appendChild(U.el('div', 'a-name', 'The next source if one doesn’t start: ' + (nextOn ? 'On' : 'Off')));
2063
+ 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)'
2064
+ : 'A source that doesn’t start takes you back to the list') + ' · OK to turn ' + (nextOn ? 'off' : 'on')));
2065
+ nx.__onselect = function () { Stremio.setPref('nextOnFail', !nextOn); refeed('ranking', 'nextonfail', 'extra'); };
2066
+ return nx;
2067
+ }
2068
+ // your words: sources with one of them at the bottom, for every title
2069
+ function wordsRow() {
2070
+ var words = Stremio.getPrefs().hideWords;
2071
+ var w = U.el('div', 'addon-item focusable cfg-row feed-extra');
2072
+ w.setAttribute('data-fk', 'words'); w.setAttribute('data-ctl', 'extra');
2073
+ w.appendChild(U.el('div', 'a-name', 'Hide sources with these words: ' + (words.length ? words.join(', ') : 'None')));
2074
+ 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'));
2075
+ w.__onselect = function () {
2076
+ App.openEntry({ title: 'Hide sources with these words', value: words.join(', '), placeholder: 'CAM, HDTS, TELESYNC',
2077
+ 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.',
2078
+ validate: function () { return null; },
2079
+ onConfirm: function (v) { App.back(); Stremio.setHideWords(v); refeed('ranking', 'words', 'extra'); } });
2080
+ };
2081
+ return w;
2082
+ }
2083
+ // Size limits (Settings › Source ranking › Filters): smallest, largest and most per hour, for movies or
1933
2084
  // series; OK on the row: which one, then its value.
1934
2085
  var SIZE_CHOICES = {
1935
2086
  min: { label: 'Smallest', none: 'None', values: [0.1, 0.3, 0.5, 1, 2] },
package/js/views.js CHANGED
@@ -7,13 +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
- renderSearch: V.renderSearch, renderDiscover: V.renderDiscover, renderEntry: V.renderEntry, backInDetail: V.backInDetail,
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, moveKey: V.moveKey, availableTabs: V.availableTabs,
18
+ menuItems: V.menuItems, menuOrder: V.menuOrder, moveMenuItem: V.moveMenuItem
18
19
  };
19
20
  })(ViewParts);
package/js/ytplay.js CHANGED
@@ -62,12 +62,16 @@ var YTPlay = (function () {
62
62
  }
63
63
  // A live HLS channel whose sound the TV's player stutters on (its server cuts the AAC sound
64
64
  // mid-frame): the service looks at it and, if so, gives a relay that repacks the sound:
65
- // { fix, sure, why, url }. force: the relay at once (the channel's already known to need it).
66
- // h: the channel's own User-Agent / Referer ({ ua, ref }).
67
- function liveOpen(url, h, force) {
65
+ // { fix, sure, why, url, cc: [{ ch, lang }] (its closed captions), ccSure }. force: the relay
66
+ // at once (the channel's already known to need it). h: the channel's own User-Agent /
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
70
  if (MODE === 'browser') { return Promise.reject(new Error('no service')); }
71
+ var cc = opts && opts.cc ? opts.cc.map(function (c) { return c.ch + (c.lang ? '-' + c.lang : ''); }).join(',') : '';
69
72
  return ensureService().then(entryAuth).then(function (a) {
70
- var q = '/live/open?u=' + encodeURIComponent(url) + (h && h.ua ? '&ua=' + encodeURIComponent(h.ua) : '') + (h && h.ref ? '&ref=' + encodeURIComponent(h.ref) : '') + (force ? '&force=1' : '');
73
+ 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
+ (force && opts && opts.fix === false ? '&fix=0' : '') + (force && cc ? '&cc=' + encodeURIComponent(cc) : '');
71
75
  return U.getJSON(PROXY + q + a.q, force ? 5000 : 30000, 65536, null, a.h);
72
76
  });
73
77
  }
@@ -91,6 +95,50 @@ var YTPlay = (function () {
91
95
  return U.getJSON(PROXY + '/aiom/addons?base=' + encodeURIComponent(base) + a.q, 40000, 1048576, null, h);
92
96
  });
93
97
  }
98
+ // A playlist's TV guide, read and kept by the service (see service/index.js, epgControl):
99
+ // epgLoad asks it to read it for these channels (k: their keys as a bitset, EPG.bitsFor) ->
100
+ // { state, at, channels, programmes, partial, error }; epgGet: their programs -> { <key>:
101
+ // [[start, stop, title, desc]] }. Both POSTs, what's asked in the body: a newer TV's service
102
+ // reads 8 KB of a request's address and headers, less than thousands of channels' keys.
103
+ // (a POST refused, a service from before it took them or one without the token: asked
104
+ // again as a GET, when it fits in the address)
105
+ // An app that can't read the service's token (its header instead): the guide ticket, asked
106
+ // for once, given with the POSTs (the TV adds an Origin to them, which the header alone doesn't pass).
107
+ var _ticket = null;
108
+ function epgTicket(a) {
109
+ if (a.q || MODE === 'tizenbrew') { return Promise.resolve(a.q); } // (the token; TizenBrew's origin passes as it is)
110
+ if (!_ticket) {
111
+ _ticket = U.getJSON(PROXY + '/epg/ticket', 8000, 4096, null, a.h).then(function (r) {
112
+ return r && /^[0-9a-f]{48}$/.test(r.ticket || '') ? '&ticket=' + r.ticket : '';
113
+ }, function () { _ticket = null; return ''; });
114
+ }
115
+ return _ticket;
116
+ }
117
+ function epgAsk(path, body, maxBytes) {
118
+ if (MODE === 'browser') { return Promise.reject(new Error('no service')); }
119
+ return ensureService().then(entryAuth).then(function (a) {
120
+ return epgTicket(a).then(function (cred) { return { a: a, cred: cred }; });
121
+ }).then(function (x) {
122
+ var a = x.a, q = x.cred ? '?' + x.cred.replace(/^&/, '') : '';
123
+ return U.postJSON(PROXY + path + q, body, 15000, maxBytes, a.h).then(null, function (err) {
124
+ if (err && /^HTTP 403\b/.test(err.message || '') && /ticket=/.test(q)) { _ticket = null; } // (the service started again since: a new ticket next time)
125
+ if (!(err && /^HTTP (403|405)\b/.test(err.message || '') && body.length < 6000)) { throw err; }
126
+ return U.getJSON(PROXY + path + (q ? q + '&' : '?') + body, 15000, maxBytes, null, a.h);
127
+ });
128
+ });
129
+ }
130
+ // Settings › Storage: the saved guides ({ count, bytes }), and deleting them all.
131
+ function epgFiles() {
132
+ if (MODE === 'browser') { return Promise.resolve(null); }
133
+ return ensureService().then(entryAuth).then(function (a) { return U.getJSON(PROXY + '/epg/files' + (a.q ? '?' + a.q.replace(/^&/, '') : ''), 8000, 4096, null, a.h); });
134
+ }
135
+ function epgClear() { return epgAsk('/epg/clear', '', 4096); }
136
+ function epgLoad(u, k, force) {
137
+ return epgAsk('/epg/load', 'u=' + encodeURIComponent(u) + '&k=' + k + (force ? '&force=1' : ''), 65536);
138
+ }
139
+ function epgGet(u, keys, from, to, max) {
140
+ return epgAsk('/epg/get', 'u=' + encodeURIComponent(u) + '&ids=' + encodeURIComponent(keys.join('|')) + '&from=' + from + '&to=' + to + (max ? '&max=' + max : ''), 4194304);
141
+ }
94
142
  // Whether YouTube gives the video out to the service (it refuses some uploads to the player
95
143
  // the service asks as): false when it says no; true when it does, or no answer came (the
96
144
  // player tries). The service keeps what it found, so playing it next asks YouTube nothing new.
@@ -181,6 +229,6 @@ var YTPlay = (function () {
181
229
  return MODE === 'tizenbrew' ? 'TizenBrew module' : MODE === 'wgt' ? 'Sideloaded app (.wgt)' : 'Browser (development)';
182
230
  }
183
231
 
184
- return { ensureService: ensureService, resolverUrl: resolverUrl, infoUrl: infoUrl, canPlay: canPlay, liveOpen: liveOpen, hlsMaster: hlsMaster, aiomAddons: aiomAddons, entryOpen: entryOpen, entryPoll: entryPoll, entryClose: entryClose, subsUrl: subsUrl, audioLang: audioLang, setAudioLang: setAudioLang, installMode: function () { return MODE; },
232
+ return { ensureService: ensureService, resolverUrl: resolverUrl, infoUrl: infoUrl, canPlay: canPlay, liveOpen: liveOpen, hlsMaster: hlsMaster, aiomAddons: aiomAddons, epgLoad: epgLoad, epgGet: epgGet, epgFiles: epgFiles, epgClear: epgClear, entryOpen: entryOpen, entryPoll: entryPoll, entryClose: entryClose, subsUrl: subsUrl, audioLang: audioLang, setAudioLang: setAudioLang, installMode: function () { return MODE; },
185
233
  installLabel: installLabel, unavailableMessage: unavailableMessage, isRunning: isRunning, port: PORT };
186
234
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stremiok",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "StremioK streaming client for Tizen 3.0+ Samsung TVs (TizenBrew app module).",
5
5
  "packageType": "app",
6
6
  "appName": "StremioK",
@@ -14,6 +14,7 @@
14
14
  "icon.png",
15
15
  "config.xml",
16
16
  "README.md",
17
+ "CHANGELOG.md",
17
18
  "LICENSE"
18
19
  ],
19
20
  "keys": [