stremiok 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,332 @@
1
+ /* ViewParts: the Guide tab: the IPTV channels' TV guide as a grid (js/epg.js).
2
+ A row a channel that has a guide (in the IPTV tab's order, favorites first), across SPAN from
3
+ the current half hour, a line at the time now. Only ROWS rows are drawn at a time: ↑ / ↓ past
4
+ them move the window along, through every channel; programs are read for the rows drawn and
5
+ the AHEAD after them (a row not read yet says so until they come). Each program knows its
6
+ neighbors (Focus's __go hooks): ← / → the program before / after it (the hours move along past
7
+ an edge, never back before the current half hour for one still on), ↑ / ↓ the channel above /
8
+ below, at the same time. Home's details strip describes the focused program; OK on what's on
9
+ now plays the channel, on another says what it is. A guide-only channel's (tvg-type="guide":
10
+ watched elsewhere) any program: the film looked up in your addons (findInAddons). */
11
+ (function (V) {
12
+ 'use strict';
13
+ function scr() { return V.scr.apply(null, arguments); } // core
14
+
15
+ var SPAN = 150 * 60, STEP = 30 * 60, ROWS = 10, AHEAD = 10;
16
+ var _g = null; // the grid on screen: { el, start, chans, lists: { id: programs }, asked: { id: 1 }, top, nodes: [row: cells], box }
17
+ function floorStep(t) { return Math.floor(t / STEP) * STEP; }
18
+ function pct(x) { return Math.round(x * 10000) / 100; }
19
+
20
+ // Drawn again soon after (Home redrawn, the tab picked again): from what the grid before had
21
+ // read (REUSE_MS), the same channels in the same window and hours, focus where it was;
22
+ // no "Reading…" between, nothing asked again.
23
+ var REUSE_MS = 5 * 60 * 1000;
24
+ function guideGrid(rows) {
25
+ stopFinding(); // (a lookup from the grid drawn before: stopped)
26
+ var now = Date.now() / 1000, chans = EPG.guided(), was = _g;
27
+ var g = _g = { el: rows, start: floorStep(now), live: true, chans: chans, lists: {}, asked: {}, failed: {}, got: {}, top: 0, nodes: [], at: Date.now(), gen: EPG.generation ? EPG.generation() : 0 }; // (live: the hours show the present, and follow the clock: tick)
28
+ if (!chans.length) { rows.appendChild(U.el('div', 'row-empty', 'No TV guide yet (Settings › Content › IPTV › Guide link).')); return; }
29
+ if (was && was.at && Date.now() - was.at < REUSE_MS && was.gen === g.gen && sameChans(was, chans)) {
30
+ var f = Focus.current(), want = (f && was.box && was.box.contains(f) && f.__chan) ? (f.__loading ? { id: f.__chan, t: f.__t } : { id: f.__chan, start: f.__p.start }) : null;
31
+ g.lists = was.lists; g.asked = was.asked; g.failed = was.failed || {}; g.got = was.got || {}; g.at = was.at; g.top = was.top;
32
+ g.chans = chans.filter(function (c) { return !(c.id in g.lists) || g.lists[c.id].length; });
33
+ if (!was.live) { g.start = was.start; g.live = false; }
34
+ var n = draw(g, want), cur = Focus.current(); // (focus: back on the cell it was on; else as when first drawn)
35
+ if (n && (want || !cur || !document.documentElement.contains(cur) || /\b(tab-pill|hchip)\b/.test(cur.className || ''))) { Focus.setScope(scr('home'), n); }
36
+ return;
37
+ }
38
+ rows.appendChild(U.el('div', 'row-empty guide-loading', 'Reading the TV guide…'));
39
+ ensure(g).then(function () {
40
+ if (_g !== g || !document.documentElement.contains(rows)) { return; }
41
+ var first = draw(g);
42
+ var cur = Focus.current(); // (focus still where the tab was picked: onto what's on now)
43
+ if (first && (!cur || !document.documentElement.contains(cur) || /\b(tab-pill|hchip)\b/.test(cur.className || '') || rows.contains(cur))) { Focus.setScope(scr('home'), first); }
44
+ });
45
+ }
46
+
47
+ function sameChans(was, chans) { // (the same channels as the grid before: none added, none gone)
48
+ var ids = {}, n = 0;
49
+ was.chans.forEach(function (c) { ids[c.id] = 1; });
50
+ Object.keys(was.lists).forEach(function (id) { if (!ids[id] && !was.lists[id].length) { ids[id] = 1; } }); // (dropped for having nothing)
51
+ for (var i = 0; i < chans.length; i++) { if (!ids[chans[i].id]) { return false; } n++; }
52
+ return n >= was.chans.length;
53
+ }
54
+ // The programs of the rows drawn and the AHEAD after them, those not asked for yet (from 2
55
+ // hours ago to 30 ahead: the hours move without asking again). A channel with nothing at all
56
+ // in that time is dropped once read. Resolves once they're in.
57
+ // What's kept: the programs of the channels within KEEP rows of the window, read in the last
58
+ // KEEP_MS (older, or further off: let go, read again if they're come back to); all of it,
59
+ // once the guides changed (EPG's generation).
60
+ var KEEP = 40, KEEP_MS = 2 * 3600 * 1000;
61
+ function prune(g) {
62
+ if (EPG.generation && g.gen !== EPG.generation()) { g.gen = EPG.generation(); g.lists = {}; g.asked = {}; g.failed = {}; g.got = {}; return; }
63
+ var near = {}, now = Date.now();
64
+ g.chans.slice(Math.max(0, g.top - KEEP), g.top + ROWS + AHEAD + KEEP).forEach(function (c) { near[c.id] = 1; });
65
+ Object.keys(g.lists).forEach(function (id) {
66
+ if (!g.lists[id].length) { return; } // (known to have nothing: kept, it's what keeps it off the grid)
67
+ if (!near[id] || now - (g.got[id] || 0) > KEEP_MS) { delete g.lists[id]; delete g.asked[id]; delete g.got[id]; }
68
+ });
69
+ }
70
+ function ensure(g) {
71
+ prune(g);
72
+ var ids = g.chans.slice(g.top, g.top + ROWS + AHEAD).filter(function (c) { return !g.asked[c.id]; }).map(function (c) { g.asked[c.id] = 1; return c.id; });
73
+ if (!ids.length) { return Promise.resolve(false); }
74
+ var now = Date.now() / 1000;
75
+ return EPG.range(ids, now - 2 * 3600, now + 30 * 3600).then(function (lists) {
76
+ var unknown = {};
77
+ ((lists && lists._unknown) || []).forEach(function (id) { unknown[id] = 1; });
78
+ ids.forEach(function (id) {
79
+ if (unknown[id]) { delete g.asked[id]; g.failed[id] = 1; return; } // (not answered: kept, asked again)
80
+ delete g.failed[id];
81
+ g.lists[id] = (lists && lists[id]) || [];
82
+ g.got[id] = Date.now();
83
+ });
84
+ g.chans = g.chans.filter(function (c) { return !(c.id in g.lists) || g.lists[c.id].length; });
85
+ g.top = Math.max(0, Math.min(g.top, g.chans.length - ROWS));
86
+ if (Object.keys(unknown).length) { retryLater(g); }
87
+ return true;
88
+ }, function () { ids.forEach(function (id) { delete g.asked[id]; g.failed[id] = 1; }); retryLater(g); return true; });
89
+ }
90
+ // Channels whose programs didn't come (the service didn't answer, or not all of it): asked
91
+ // again RETRY_MS later, while the grid is still on screen; their rows say so meanwhile.
92
+ var RETRY_MS = 30000;
93
+ function retryLater(g) {
94
+ if (g.retry) { return; }
95
+ g.retry = setTimeout(function () {
96
+ g.retry = null;
97
+ if (_g !== g || !g.box || !document.documentElement.contains(g.box)) { return; }
98
+ ensure(g).then(function (got) { if (got) { refresh(g); } });
99
+ }, RETRY_MS);
100
+ }
101
+ // Rows read after the grid was drawn: drawn again, focus kept where it was (a "Loading…" row's:
102
+ // that row, at the same time).
103
+ function refresh(g) {
104
+ if (_g !== g || !g.box || !document.documentElement.contains(g.box)) { return; }
105
+ var cur = Focus.current(), mine = cur && g.box.contains(cur), want = null;
106
+ if (mine && cur.__p) { want = cur.__loading ? { id: cur.__chan, t: cur.__t } : { id: cur.__chan, start: cur.__p.start }; }
107
+ var n = draw(g, want);
108
+ if (mine && n) { Focus.setScope(scr('home'), n); }
109
+ }
110
+ // The guides changed (EPG's generation) while the grid is on screen: what it had let go, the
111
+ // rows on screen read again.
112
+ if (typeof EPG !== 'undefined' && EPG.onChange) {
113
+ EPG.onChange(function () {
114
+ var g = _g;
115
+ if (!g || !g.box || !document.documentElement.contains(g.box) || !EPG.generation || g.gen === EPG.generation()) { return; }
116
+ ensure(g).then(function (got) { if (got) { refresh(g); } });
117
+ });
118
+ }
119
+ // The window moved to show row r (↑ / ↓): the rows to read next asked for, and drawn again once in.
120
+ function showRow(g, r) {
121
+ if (r < g.top) { g.top = r; } else if (r >= g.top + ROWS) { g.top = r - ROWS + 1; }
122
+ ensure(g).then(function (got) { if (got) { refresh(g); } });
123
+ }
124
+
125
+ // The grid for g.start, rows g.top to g.top + ROWS: the hours, then a row a channel. Returns
126
+ // the cell wanted: { id, start } that program, { id, t } that channel's at time t; else
127
+ // what's on now on the first channel drawn.
128
+ function draw(g, want) {
129
+ var old = g.el.querySelector('.guide'), loading = g.el.querySelector('.guide-loading');
130
+ if (loading) { loading.parentNode.removeChild(loading); }
131
+ var box = U.el('div', 'guide'), now = Date.now() / 1000, end = g.start + SPAN, found = null, first = null;
132
+ var head = U.el('div', 'guide-head'), hours = U.el('div', 'guide-hours');
133
+ head.appendChild(U.el('div', 'guide-corner', EPG.dayTime(g.start).length > 5 ? EPG.dayTime(g.start).split(' ')[0] : 'Today'));
134
+ for (var t = g.start; t < end; t += STEP) {
135
+ var mark = U.el('div', 'guide-time', EPG.hhmm(t));
136
+ mark.setAttribute('style', 'left: ' + pct((t - g.start) / SPAN) + '%');
137
+ hours.appendChild(mark);
138
+ }
139
+ head.appendChild(hours);
140
+ box.appendChild(head);
141
+ g.nodes = [];
142
+ var any = false;
143
+ g.chans.slice(g.top, g.top + ROWS).forEach(function (c, k) {
144
+ var r = g.top + k, row = U.el('div', 'guide-row'), track = U.el('div', 'guide-track'), nodes = [], onNode = null;
145
+ var name = U.el('div', 'guide-ch'), logo = c.logo || EPG.logo(c.id);
146
+ if (logo && U.cardImage(logo, 'logo')) {
147
+ var img = U.el('img', 'guide-ch-logo');
148
+ img.onerror = function () { if (img.parentNode) { img.parentNode.removeChild(img); } };
149
+ name.appendChild(img);
150
+ U.deferImage(img, U.cardImage(logo, 'logo'));
151
+ }
152
+ name.appendChild(document.createTextNode(c.name));
153
+ row.appendChild(name);
154
+ function hooks(n, i) {
155
+ n.__row = r; n.__chan = c.id;
156
+ n.__goLEFT = function () { return i < 0 ? false : step(g, r, i, -1); };
157
+ n.__goRIGHT = function () { return i < 0 ? false : step(g, r, i, 1); };
158
+ n.__goUP = function () { return vertical(g, r, -1, n); };
159
+ n.__goDOWN = function () { return vertical(g, r, 1, n); };
160
+ }
161
+ if (!(c.id in g.lists)) { // (not read yet: says so, and can be passed through)
162
+ var wait = U.el('div', 'guide-prog guide-wait focusable', g.failed[c.id] ? 'Its TV guide didn’t come: asked again shortly' : 'Loading…');
163
+ wait.setAttribute('style', 'left: 0%; width: 100%');
164
+ wait.__p = { start: g.start, stop: end, title: '' }; wait.__loading = true;
165
+ wait.__t = want && want.id === c.id && typeof want.t === 'number' ? want.t : Math.max(now, g.start);
166
+ wait.__meta = { name: c.name, releaseInfo: 'Reading its TV guide…' };
167
+ hooks(wait, -1);
168
+ track.appendChild(wait);
169
+ nodes.push(wait);
170
+ if (want && want.id === c.id) { found = wait; }
171
+ } else {
172
+ g.lists[c.id].forEach(function (p, i) {
173
+ if (p.stop <= g.start || p.start >= end) { return; }
174
+ var on = p.start <= now && now < p.stop, a = Math.max(p.start, g.start), b = Math.min(p.stop, end);
175
+ var n = U.el('div', 'guide-prog focusable' + (on ? ' now' : ''), p.title);
176
+ n.setAttribute('style', 'left: ' + pct((a - g.start) / SPAN) + '%; width: ' + pct((b - a) / SPAN) + '%');
177
+ n.__meta = { name: p.title, releaseInfo: c.name + ' · ' + EPG.dayTime(p.start) + '–' + EPG.hhmm(p.stop) + (c.guideOnly ? ' · OK: find it in your addons' : ''), description: p.desc, picture: p.picture };
178
+ n.__p = p; n.__i = i;
179
+ n.__onselect = function () { pick(g, r, p); };
180
+ hooks(n, i);
181
+ track.appendChild(n);
182
+ nodes.push(n);
183
+ any = true;
184
+ if (want && want.id === c.id && want.start === p.start) { found = n; }
185
+ if (on) { onNode = n; }
186
+ });
187
+ if (want && want.id === c.id && typeof want.t === 'number' && !found) { found = at(nodes, want.t, g.start) || null; } // (a time, 0 too)
188
+ }
189
+ if (k === 0) { first = onNode || nodes[0] || null; }
190
+ if (now >= g.start && now < end) {
191
+ var line = U.el('div', 'guide-nowline');
192
+ line.setAttribute('style', 'left: ' + pct((now - g.start) / SPAN) + '%');
193
+ track.appendChild(line);
194
+ }
195
+ row.appendChild(track);
196
+ box.appendChild(row);
197
+ g.nodes[r] = nodes;
198
+ });
199
+ if (!any && g.chans.slice(g.top, g.top + ROWS).every(function (c) { return c.id in g.lists; })) {
200
+ box.appendChild(U.el('div', 'row-empty guide-empty', 'Nothing on any channel in these hours.'));
201
+ }
202
+ if (old) { g.el.removeChild(old); } // (the grid is all the tab has)
203
+ g.el.appendChild(box);
204
+ g.box = box;
205
+ if (!_ticker) { _ticker = setInterval(tick, 60000); }
206
+ return found || first || (g.nodes[g.top] && g.nodes[g.top][0]) || null;
207
+ }
208
+
209
+ // Every minute, while the grid is on screen: the line at the time now moved along, and what's
210
+ // on now marked again (nothing drawn again: focus and the cells stay). At a new half hour, a
211
+ // grid showing the present moves its hours along (follow). Stops once it's gone.
212
+ var _ticker = null;
213
+ function tick() {
214
+ var g = _g;
215
+ if (!g || !g.box || !document.documentElement.contains(g.box)) { clearInterval(_ticker); _ticker = null; return; }
216
+ if (/\bhidden\b/.test(scr('home').className) || document.hidden) { return; }
217
+ var now = Date.now() / 1000, end = g.start + SPAN;
218
+ if (g.live && g.start < floorStep(now)) { follow(g, now); return; }
219
+ var lines = g.box.querySelectorAll('.guide-nowline'); // (a loop: a NodeList's forEach is newer than the 2017 TV's browser)
220
+ for (var i = 0; i < lines.length; i++) {
221
+ if (now >= g.start && now < end) { lines[i].setAttribute('style', 'left: ' + pct((now - g.start) / SPAN) + '%'); } else { lines[i].setAttribute('style', 'display: none'); }
222
+ }
223
+ (g.nodes || []).forEach(function (nodes) {
224
+ nodes.forEach(function (n) { var on = n.__p.start <= now && now < n.__p.stop; if (on !== /\bnow\b/.test(n.className)) { n.className = on ? n.className + ' now' : n.className.replace(/\s*\bnow\b/, ''); } });
225
+ });
226
+ }
227
+ // The current half hour first again: focus on the program it was on, or (it has ended) what's
228
+ // on now on that channel.
229
+ function follow(g, now) {
230
+ var start = floorStep(now), f = Focus.current(), mine = !!(f && g.box.contains(f)), want = null;
231
+ if (mine && f.__chan) { want = (f.__p && !f.__loading && f.__p.stop > start) ? { id: f.__chan, start: f.__p.start } : { id: f.__chan, t: now }; }
232
+ g.start = start;
233
+ var n = draw(g, want);
234
+ if (mine && n) { Focus.setScope(scr('home'), n); }
235
+ }
236
+ // ← / →: the program before / after this one on its channel; past an edge, the hours move
237
+ // to it (for one still on, never before the current half hour).
238
+ function step(g, r, i, dir) {
239
+ var c = g.chans[r], list = g.lists[c.id] || [], p = list[i + dir];
240
+ if (!p) { return false; }
241
+ if (p.stop <= g.start || p.start >= g.start + SPAN) {
242
+ var now = Date.now() / 1000;
243
+ g.start = dir > 0 ? Math.max(floorStep(p.start) - 2 * STEP, floorStep(now))
244
+ : (p.stop <= now ? floorStep(p.start) : Math.max(floorStep(p.start), floorStep(now)));
245
+ g.live = g.start === floorStep(now); // (moved away from the present: stays put)
246
+ return draw(g, { id: c.id, start: p.start });
247
+ }
248
+ return find(g, r, p);
249
+ }
250
+ // ↑ / ↓: the next channel up / down with something in these hours (one with nothing in them
251
+ // is passed; one not read yet is stopped at: it says so); from the top row, on up to the menu.
252
+ // Past the rows drawn, the window moves along to it.
253
+ function vertical(g, r, dir, from) {
254
+ var end = g.start + SPAN, t = from.__loading ? from.__t : Math.max(from.__p.start, g.start);
255
+ for (var k = r + dir; k >= 0 && k < g.chans.length; k += dir) {
256
+ var c = g.chans[k], list = g.lists[c.id];
257
+ var has = !list || list.some(function (p) { return p.stop > g.start && p.start < end; });
258
+ if (!has) { continue; }
259
+ if (k >= g.top && k < g.top + ROWS) { return at(g.nodes[k] || [], t, g.start) || false; }
260
+ showRow(g, k);
261
+ return draw(g, { id: c.id, t: t }) || false;
262
+ }
263
+ return dir < 0 ? undefined : false;
264
+ }
265
+ // In a row's cells: the program on at time t (or the window's start, for one that began
266
+ // before it); else the nearest.
267
+ function at(nodes, t, start) {
268
+ var best = null, gap = Infinity;
269
+ for (var i = 0; i < nodes.length; i++) {
270
+ var p = nodes[i].__p;
271
+ if (p.start <= t && t < p.stop) { return nodes[i]; }
272
+ var d = Math.abs(Math.max(p.start, start) - t);
273
+ if (d < gap) { gap = d; best = nodes[i]; }
274
+ }
275
+ return best;
276
+ }
277
+ function find(g, r, p) {
278
+ var nodes = g.nodes[r] || [];
279
+ for (var i = 0; i < nodes.length; i++) { if (nodes[i].__p === p) { return nodes[i]; } }
280
+ return false;
281
+ }
282
+ // OK: what's on now plays the channel; another program says what it is.
283
+ // Played, CH+ / CH− go through the Guide's channels; each one's card (where BACK lands, on
284
+ // the channel you end on) is what's on now on it.
285
+ function pick(g, r, p) {
286
+ var c = g.chans[r], now = Date.now() / 1000;
287
+ if (c.guideOnly) { findInAddons(p); return; }
288
+ if (p.start <= now && now < p.stop) {
289
+ var list = [], idx = 0;
290
+ g.chans.forEach(function (ch, i) { // (every channel: a cell only for the rows drawn, where BACK lands)
291
+ if (ch.guideOnly) { return; } // (nothing to play)
292
+ var nodes = g.nodes[i] || [], card = nodes[0] || null;
293
+ for (var j = 0; j < nodes.length; j++) { if (nodes[j].__p.start <= now && now < nodes[j].__p.stop) { card = nodes[j]; break; } }
294
+ if (i === r) { idx = list.length; }
295
+ list.push({ meta: { id: ch.id, type: ch.type, name: ch.name, poster: ch.logo || '', logo: ch.logo || '', posterShape: 'landscape' }, card: card });
296
+ });
297
+ App.openLive(c.type, c.id, list[idx].meta, { list: list, idx: idx });
298
+ return;
299
+ }
300
+ U.toast(EPG.dayTime(p.start) + '–' + EPG.hhmm(p.stop) + ' ' + c.name + ' · ' + p.title + (p.desc ? ': ' + p.desc : '') +
301
+ (p.stop <= now ? ' (already shown)' : ''), 8000);
302
+ }
303
+
304
+ // A guide-only channel's program: a film found by the guide's original title and year (else
305
+ // its title) opens its page with your addons' sources, as Discover does; a show, or a film
306
+ // without one sure match, opens Search with its name.
307
+ // Each pick stops the lookup before it (its answer ignored, its spinner gone); an answer that
308
+ // comes once the Guide was left (another tab, or drawn again) opens nothing.
309
+ var _finding = 0, _findCancel = null;
310
+ function stopFinding() {
311
+ _finding++;
312
+ if (_findCancel) { _findCancel.cancel(); _findCancel = null; U.spinner(false); }
313
+ }
314
+ function onGuide(grid) { return _g === grid && !!grid.box && document.documentElement.contains(grid.box) && !/\bhidden\b/.test(scr('home').className); }
315
+ function findInAddons(p) {
316
+ stopFinding();
317
+ function search() { App.openSearch(); Views.searchFor(p.title); }
318
+ if (p.kind === 'series') { search(); return; }
319
+ var gen = _finding, grid = _g, cancel = _findCancel = U.cancelToken();
320
+ U.spinner(true, 'Looking for “' + p.title + '”…');
321
+ function done(hit) {
322
+ if (gen !== _finding) { return; }
323
+ _findCancel = null;
324
+ U.spinner(false);
325
+ if (!onGuide(grid)) { return; }
326
+ if (hit) { App.openDetail(hit.type, hit.id, { name: hit.name, poster: hit.poster, type: hit.type }); } else { search(); }
327
+ }
328
+ Stremio.findFilm({ title: p.title, orig: p.orig, year: p.year }, cancel).then(done, function () { done(null); });
329
+ }
330
+
331
+ V.guideGrid = guideGrid;
332
+ })(ViewParts);