stremiok 2.1.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.
- package/CHANGELOG.md +82 -0
- package/README.md +9 -9
- package/config.xml +1 -1
- package/css/style.css +27 -0
- package/index.html +34 -31
- package/js/app.js +19 -7
- package/js/epg.js +320 -0
- package/js/iptv.js +198 -25
- package/js/keys.js +2 -0
- package/js/player-controls.js +50 -8
- package/js/player-core.js +112 -12
- package/js/player-store.js +21 -1
- package/js/player-subs.js +38 -8
- package/js/stremio.js +70 -6
- package/js/trakt.js +1 -0
- package/js/util.js +24 -13
- package/js/view-core.js +128 -55
- package/js/view-guide.js +332 -0
- package/js/view-home.js +148 -33
- package/js/view-settings.js +103 -35
- package/js/views.js +3 -2
- package/js/ytplay.js +53 -5
- package/package.json +2 -1
- package/service/index.js +863 -46
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
What changed in each version of StremioK, newest first. How everything works is in the
|
|
4
|
+
[user guide](docs/USER-GUIDE.md).
|
|
5
|
+
|
|
6
|
+
## 2.2.0 — 2026-10-23
|
|
7
|
+
- **A TV guide for your IPTV channels:** what's on now and next on each channel's card, in
|
|
8
|
+
Home's details strip and while a channel plays, and its schedule when you hold OK on it. From
|
|
9
|
+
the guide a playlist names, an Xtream account's own, or a *Guide link* you set (Settings ›
|
|
10
|
+
Content › IPTV).
|
|
11
|
+
- **A Guide tab:** your channels as a grid across 2½ hours that follows the clock; OK on what's
|
|
12
|
+
on now plays it, and CH+ / CH− then go through the Guide's channels. The remote's GUIDE key
|
|
13
|
+
opens it.
|
|
14
|
+
- **Guide-only channels** (`tvg-type="guide"`): a channel you watch elsewhere (a cable box, say)
|
|
15
|
+
on the Guide tab with its schedule; OK on a film finds it in your addons.
|
|
16
|
+
- **Xtream accounts:** add one with its server, username and password (checked with the server
|
|
17
|
+
first). Its channels play as MPEG-TS; *Format* switches the account to HLS, or one channel
|
|
18
|
+
while it plays (the player's menu), kept for that channel.
|
|
19
|
+
- **★ Favorite channels:** hold OK on a channel card, *Add to favorites*: a row of their own,
|
|
20
|
+
first on the IPTV and Guide tabs.
|
|
21
|
+
- **Closed captions on live channels:** captions inside an HLS channel's picture (CC1–CC4, the
|
|
22
|
+
kind VLC shows) are offered in the subtitle menu, and turn into subtitles when you pick them.
|
|
23
|
+
On an Xtream channel, switch *Format* to HLS for them.
|
|
24
|
+
- **In the player:** picture mode kept per channel; *Reload* and a *Sleep timer* in the menu;
|
|
25
|
+
the time of day at the top right.
|
|
26
|
+
- **More channels play:** more ways a playlist gives a channel's User-Agent and Referer, Xtream
|
|
27
|
+
channels that lead to YouTube, and channel logos from the TV guide.
|
|
28
|
+
- **Put the top menu in your own order:** Settings › Appearance › *Menu*.
|
|
29
|
+
- **The IPTV tab has live TV only:** a catalog of films or series sits on Movies or Series.
|
|
30
|
+
- **Fixes:** USB downloads checked part by part; a broken subtitle file or TV guide can't hold
|
|
31
|
+
the remote; a first playlist shows the IPTV tab at once.
|
|
32
|
+
|
|
33
|
+
## 2.1.0 — 2026-10-20
|
|
34
|
+
- **Live channels reconnect** when their connection drops, as often as *Reconnect live channels*
|
|
35
|
+
says (Settings › Playback › Seeking & buffering): Off, A few times or Keep trying.
|
|
36
|
+
- **Trakt:** your whole watched history is read again (Trakt now sends it a page at a time, and
|
|
37
|
+
shows without their episodes unless asked), and an episode you left part-way no longer hides
|
|
38
|
+
the show's next one once you've watched past it, on any device.
|
|
39
|
+
- **Addons:** turn one off or on, as you can a playlist, each shown with its logo (Settings ›
|
|
40
|
+
Content › Addons); "No sources found" says why, addon by addon; an addon's error page is no
|
|
41
|
+
longer shown as a title.
|
|
42
|
+
- **A YouTube tab on Home:** StremioK's YouTube began as a way to watch TV channels that stream
|
|
43
|
+
on YouTube rather than through an M3U playlist, so its catalogs sat in the IPTV tab. Now that
|
|
44
|
+
it also covers channels, playlists, searches and other addons' YouTube catalogs, it has a tab
|
|
45
|
+
of its own, between Sports and Local.
|
|
46
|
+
- **Reordering:** ⇈ / ⇊, beside ▲ / ▼, move an item straight to the top or bottom of any list you
|
|
47
|
+
put in order: addons, playlists, YouTube catalogs, Home feeds, Search results and source
|
|
48
|
+
ranking.
|
|
49
|
+
- **Playback:** a source counts as started only once it plays, so one that fails right after
|
|
50
|
+
opening gives way to the next; video quality for streams that need their own headers; skip
|
|
51
|
+
times from TheIntroDB's new API.
|
|
52
|
+
- **A faster start:** your addons don't wait for the IPTV playlists, and Home's rows appear as
|
|
53
|
+
each is ready.
|
|
54
|
+
- **Security and privacy fixes:** USB drives and downloads; the TV itself off-limits to
|
|
55
|
+
downloads and live relays; AIOManager's key kept to its own server, and addons you added
|
|
56
|
+
yourself left alone by its sync.
|
|
57
|
+
|
|
58
|
+
## 2.0.0 — 2026-10-17
|
|
59
|
+
The first public version of StremioK, a Stremio-addon client for Samsung Tizen TVs (Tizen 3.0
|
|
60
|
+
and later). What it does is in the [README](README.md#highlights); installing it, in the
|
|
61
|
+
[installation guide](docs/INSTALL.md).
|
|
62
|
+
|
|
63
|
+
- **Your addons:** catalogs, title details, sources and subtitles from the Stremio addons you add
|
|
64
|
+
by link, or import from your Stremio account, AIOManager or a configuration file.
|
|
65
|
+
- **Finding something to watch:** Home with Watching, Calendar, Movies, Series, IPTV, Radio,
|
|
66
|
+
Sports and Local tabs; Discover; Search across every addon that can search, and people by
|
|
67
|
+
name.
|
|
68
|
+
- **Title pages:** ranked sources with ▶ Play best, filters by addon and resolution, trailers,
|
|
69
|
+
ratings, Cast & more, collections, episode pages and an episode ratings grid.
|
|
70
|
+
- **Playing:** resume, skip intros, recaps and credits, the next episode on its own, the next
|
|
71
|
+
source when one doesn't start, subtitles from addons, the file, YouTube and your USB drive,
|
|
72
|
+
with timing and sync by ear, audio tracks remembered, private viewing.
|
|
73
|
+
- **Trakt:** watched marks and viewing progress sync with Trakt; Up next and watchlist rows
|
|
74
|
+
are read from your account.
|
|
75
|
+
- **IPTV and radio** from your M3U playlists; **YouTube** channels, playlists, searches and live
|
|
76
|
+
streams, with SponsorBlock, DeArrow, chapters and captions; **a USB drive's** videos matched
|
|
77
|
+
to their titles, and downloads to it.
|
|
78
|
+
- **Documentation** for every reader: the [user guide](docs/USER-GUIDE.md), a
|
|
79
|
+
[settings reference](docs/SETTINGS.md), [troubleshooting](docs/TROUBLESHOOTING.md),
|
|
80
|
+
[privacy and data](docs/PRIVACY.md), a [technical reference](docs/TECHNICAL-REFERENCE.md)
|
|
81
|
+
and the [developer guide](docs/DEVELOPER-GUIDE.md). Settings' on-screen hints say the effect
|
|
82
|
+
in a sentence or two.
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ video through Samsung's AVPlay API.
|
|
|
22
22
|
- **Your addons:** catalogs, details, sources and subtitles from the Stremio addons you add
|
|
23
23
|
(Cinemeta and OpenSubtitles to start with), in the order you choose.
|
|
24
24
|
- **Home** with Watching (Continue Watching, synced with **Trakt** if you connect it), Movies,
|
|
25
|
-
Series, IPTV, Radio, Sports and Local tabs; **Discover** to browse any catalog in full;
|
|
25
|
+
Series, IPTV, Guide, Radio, Sports, YouTube and Local tabs; **Discover** to browse any catalog in full;
|
|
26
26
|
**Search** across every addon that can search, and people by name.
|
|
27
27
|
- **Title pages:** ranked sources with **▶ Play best**, trailers, ratings (IMDb, Rotten
|
|
28
28
|
Tomatoes, Metacritic… with your own API keys), seasons and episodes with their own pages,
|
|
@@ -30,7 +30,7 @@ video through Samsung's AVPlay API.
|
|
|
30
30
|
- **The player:** resume where you stopped, skip intros, recaps and credits, next episode,
|
|
31
31
|
subtitles (addons', the file's own, and your own files on a USB drive) with your choice of look, audio
|
|
32
32
|
tracks remembered, switching source mid-playback, private viewing.
|
|
33
|
-
- **IPTV and radio** from your M3U playlists, with CH+ / CH−.
|
|
33
|
+
- **IPTV and radio** from your M3U playlists and Xtream accounts, with CH+ / CH−.
|
|
34
34
|
- **YouTube:** live channels, trailers, and your own YouTube channels, playlists and searches,
|
|
35
35
|
with SponsorBlock, DeArrow, chapters and captions.
|
|
36
36
|
- **Local:** the videos on a USB drive, matched to their titles.
|
|
@@ -55,7 +55,8 @@ Three ways, all covered step by step in the **[installation guide](docs/INSTALL.
|
|
|
55
55
|
2. **API keys**, optional: your own TMDB, OMDb and MDBList keys add ratings, *Cast & more*,
|
|
56
56
|
*where to watch* and episode trailers.
|
|
57
57
|
3. **Trakt**, optional: connect your account to share watched marks and Continue Watching.
|
|
58
|
-
4. **IPTV**, optional: your M3U playlist links
|
|
58
|
+
4. **IPTV**, optional: your M3U playlist links, or an Xtream account (its server, username and
|
|
59
|
+
password).
|
|
59
60
|
5. **A USB drive**, optional: plug it in and the Local tab appears.
|
|
60
61
|
|
|
61
62
|
[Getting started](docs/USER-GUIDE.md#getting-started) in the user guide walks through each.
|
|
@@ -73,17 +74,16 @@ a direct HTTP(S)/HLS/DASH URL, and this app embeds no torrent or debrid engine.
|
|
|
73
74
|
> shows that notice on it); its results are rent/buy/subscription *links to external
|
|
74
75
|
> services*, not video URLs. Add stream addons that return HTTP/HLS URLs for actual playback.
|
|
75
76
|
|
|
76
|
-
The **IPTV** feature likewise hosts no content: you supply your own M3U playlist URLs
|
|
77
|
-
YouTube entries, the bundled service finds the video's stream; the video itself
|
|
78
|
-
straight from YouTube. No video or audio is kept: everything plays straight from its
|
|
79
|
-
unless you download it to your own USB drive. What StremioK does keep, on the TV, is
|
|
80
|
-
progress, settings and copies that let it start faster (see
|
|
77
|
+
The **IPTV** feature likewise hosts no content: you supply your own M3U playlist URLs or Xtream
|
|
78
|
+
accounts. For YouTube entries, the bundled service finds the video's stream; the video itself
|
|
79
|
+
comes straight from YouTube. No video or audio is kept: everything plays straight from its
|
|
80
|
+
source, unless you download it to your own USB drive. What StremioK does keep, on the TV, is
|
|
81
|
+
your progress, settings and copies that let it start faster (see
|
|
81
82
|
[Privacy and data](docs/PRIVACY.md#what-stays-on-the-tv)).
|
|
82
83
|
|
|
83
84
|
## Known limitations
|
|
84
85
|
|
|
85
86
|
- AVPlay behavior varies by TV generation, codec and HLS provider.
|
|
86
|
-
- No TV guide (now / next) for IPTV channels yet.
|
|
87
87
|
- No torrent playback: sources need a direct HTTP(S)/HLS/DASH URL (e.g. through a debrid
|
|
88
88
|
addon).
|
|
89
89
|
- Home rows show a catalog's first page (up to 30 titles); Discover pages through the rest.
|
package/config.xml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
2
|
<widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets"
|
|
3
|
-
id="http://stremiok.tizen/StremioKTizen" version="2.
|
|
3
|
+
id="http://stremiok.tizen/StremioKTizen" version="2.2.0" viewmodes="maximized">
|
|
4
4
|
<tizen:application id="StremioK01.StremioK" package="StremioK01" required_version="3.0"/>
|
|
5
5
|
<content src="index.html"/>
|
|
6
6
|
<feature name="http://tizen.org/feature/screen.size.all"/>
|
package/css/style.css
CHANGED
|
@@ -513,3 +513,30 @@ html, body {
|
|
|
513
513
|
.src-s { background: #7b5bf5; }
|
|
514
514
|
.src-a { background: #0f9f8f; }
|
|
515
515
|
.src-c { background: #6b7280; }
|
|
516
|
+
/* an IPTV channel card: what's on now (its TV guide), and how far along it is, along its top (its name is at the bottom) */
|
|
517
|
+
.card-epg { position: absolute; left: 0; right: 0; top: 0; padding: 5px 8px 4px; background: rgba(10, 12, 18, 0.82); }
|
|
518
|
+
.card-epg-t { font-size: 15px; color: #e6e9ef; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
519
|
+
.card-epg-bar { height: 3px; margin-top: 3px; background: rgba(255, 255, 255, 0.18); border-radius: 2px; overflow: hidden; }
|
|
520
|
+
.card-epg-fill { display: block; height: 100%; background: #818cf8; }
|
|
521
|
+
/* the Guide tab (js/view-guide.js): the hours along the top, then a row a channel; programs
|
|
522
|
+
placed by their time (left / width in %), what's on now lighter, a line at the time now */
|
|
523
|
+
.guide { padding: 0 60px 20px 60px; } /* (the same sides as Home's rows: .row-title, .row-scroller) */
|
|
524
|
+
.guide-head { display: flex; height: 34px; }
|
|
525
|
+
.guide-corner { width: 230px; flex: none; font-size: 17px; color: #8b93a5; line-height: 34px; }
|
|
526
|
+
.guide-hours { position: relative; flex: 1; }
|
|
527
|
+
.guide-time { position: absolute; top: 0; font-size: 17px; color: #aeb6c4; line-height: 34px; padding-left: 6px; border-left: 2px solid #2a3346; }
|
|
528
|
+
.guide-row { display: flex; height: 62px; margin-bottom: 6px; }
|
|
529
|
+
.guide-ch { width: 230px; flex: none; padding: 0 12px; font-size: 19px; font-weight: bold; line-height: 62px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: #121722; border-radius: 8px 0 0 8px; }
|
|
530
|
+
.guide-track { position: relative; flex: 1; overflow: hidden; background: #0e1219; }
|
|
531
|
+
.guide-prog { position: absolute; top: 0; bottom: 0; padding: 0 10px; font-size: 18px; line-height: 56px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: #161b26; border: 3px solid #0e1117; border-radius: 6px; color: #c7cbd4; }
|
|
532
|
+
.guide-prog.now { background: #222b3d; color: #fff; }
|
|
533
|
+
.guide-prog.guide-wait { color: #8b93a5; font-style: italic; } /* (a row whose programs are still being read) */
|
|
534
|
+
.guide-prog.focusable.focused { background: #818cf8; color: #0e1117; border-color: #fff; }
|
|
535
|
+
.guide-nowline { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: #f87171; }
|
|
536
|
+
.guide-ch-logo { height: 34px; max-width: 56px; margin-right: 10px; vertical-align: middle; object-fit: contain; }
|
|
537
|
+
/* a program's picture (the TV guide), at the strip's right; its text kept clear of it */
|
|
538
|
+
.home-info-pic { position: absolute; right: 16px; top: 50%; height: 118px; width: 210px; margin-top: -59px; object-fit: cover; border-radius: 8px; }
|
|
539
|
+
.home-info.has-pic { padding-right: 240px; }
|
|
540
|
+
.home-info-pic.hidden { display: none; }
|
|
541
|
+
#osd-clock { position: fixed; top: 40px; right: 60px; font-size: 34px; font-weight: bold; color: #fff; text-shadow: 0 2px 8px rgba(0,0,0,0.9); } /* (the time of day, top right, while the bar shows) */
|
|
542
|
+
#osd-clock:empty { display: none; }
|
package/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=1920, height=1080, user-scalable=no">
|
|
6
6
|
<title>StremioK</title>
|
|
7
|
-
<link rel="stylesheet" href="css/style.css?v=2.
|
|
7
|
+
<link rel="stylesheet" href="css/style.css?v=2.2.0">
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<!-- CSS-applied sentinel (kept off-screen); app.js waits for its width before rendering -->
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
<div id="osd-chapter" class="hidden"></div>
|
|
33
33
|
<div id="osd-bar"><div id="osd-progress"></div><div id="osd-sponsor"></div><div id="osd-chapters"></div><div id="osd-buffer"></div><div id="osd-scrub-range" class="hidden"></div><div id="osd-scrub" class="hidden"><div id="osd-scrub-time"></div></div></div>
|
|
34
34
|
<div id="osd-times"><span id="osd-cur">00:00</span><span class="osd-right"><span id="osd-tags"></span><span id="osd-dur">00:00</span></span></div>
|
|
35
|
+
<div id="osd-clock"></div>
|
|
35
36
|
<div id="osd-hint">OK: Play/Pause ←/→: Seek RETURN: Back</div>
|
|
36
37
|
</div>
|
|
37
38
|
|
|
@@ -52,35 +53,37 @@
|
|
|
52
53
|
|
|
53
54
|
<!-- Load order matters: polyfills -> util -> iptv -> data -> input -> tvapps -> player parts -> player -> ytplay -> view parts -> views -> app -->
|
|
54
55
|
<!-- ?v= busts the TV webview cache on every release (keep in sync with package.json) -->
|
|
55
|
-
<script src="js/polyfills.js?v=2.
|
|
56
|
-
<script src="js/util.js?v=2.
|
|
57
|
-
<script src="js/iptv.js?v=2.
|
|
58
|
-
<script src="js/
|
|
59
|
-
<script src="js/
|
|
60
|
-
<script src="js/
|
|
61
|
-
<script src="js/
|
|
62
|
-
<script src="js/
|
|
63
|
-
<script src="js/
|
|
64
|
-
<script src="js/
|
|
65
|
-
<script src="js/player-
|
|
66
|
-
<script src="js/player-
|
|
67
|
-
<script src="js/player-
|
|
68
|
-
<script src="js/player-
|
|
69
|
-
<script src="js/player-
|
|
70
|
-
<script src="js/player-
|
|
71
|
-
<script src="js/player.js?v=2.
|
|
72
|
-
<script src="js/
|
|
73
|
-
<script src="js/
|
|
74
|
-
<script src="js/
|
|
75
|
-
<script src="js/
|
|
76
|
-
<script src="js/
|
|
77
|
-
<script src="js/view-
|
|
78
|
-
<script src="js/view-
|
|
79
|
-
<script src="js/view-
|
|
80
|
-
<script src="js/view-
|
|
81
|
-
<script src="js/view-
|
|
82
|
-
<script src="js/
|
|
83
|
-
<script src="js/
|
|
84
|
-
<script src="js/
|
|
56
|
+
<script src="js/polyfills.js?v=2.2.0"></script>
|
|
57
|
+
<script src="js/util.js?v=2.2.0"></script>
|
|
58
|
+
<script src="js/iptv.js?v=2.2.0"></script>
|
|
59
|
+
<script src="js/epg.js?v=2.2.0"></script>
|
|
60
|
+
<script src="js/youtube.js?v=2.2.0"></script>
|
|
61
|
+
<script src="js/stremio.js?v=2.2.0"></script>
|
|
62
|
+
<script src="js/trakt.js?v=2.2.0"></script>
|
|
63
|
+
<script src="js/keys.js?v=2.2.0"></script>
|
|
64
|
+
<script src="js/focus.js?v=2.2.0"></script>
|
|
65
|
+
<script src="js/tvapps.js?v=2.2.0"></script>
|
|
66
|
+
<script src="js/player-core.js?v=2.2.0"></script>
|
|
67
|
+
<script src="js/player-store.js?v=2.2.0"></script>
|
|
68
|
+
<script src="js/player-lang.js?v=2.2.0"></script>
|
|
69
|
+
<script src="js/player-quality.js?v=2.2.0"></script>
|
|
70
|
+
<script src="js/player-subs.js?v=2.2.0"></script>
|
|
71
|
+
<script src="js/player-audio.js?v=2.2.0"></script>
|
|
72
|
+
<script src="js/player-controls.js?v=2.2.0"></script>
|
|
73
|
+
<script src="js/player.js?v=2.2.0"></script>
|
|
74
|
+
<script src="js/ytplay.js?v=2.2.0"></script>
|
|
75
|
+
<script src="js/local.js?v=2.2.0"></script>
|
|
76
|
+
<script src="js/downloads.js?v=2.2.0"></script>
|
|
77
|
+
<script src="js/stremio-account.js?v=2.2.0"></script>
|
|
78
|
+
<script src="js/view-core.js?v=2.2.0"></script>
|
|
79
|
+
<script src="js/view-home.js?v=2.2.0"></script>
|
|
80
|
+
<script src="js/view-guide.js?v=2.2.0"></script>
|
|
81
|
+
<script src="js/view-detail.js?v=2.2.0"></script>
|
|
82
|
+
<script src="js/view-search.js?v=2.2.0"></script>
|
|
83
|
+
<script src="js/view-settings.js?v=2.2.0"></script>
|
|
84
|
+
<script src="js/view-discover.js?v=2.2.0"></script>
|
|
85
|
+
<script src="js/views.js?v=2.2.0"></script>
|
|
86
|
+
<script src="js/app.js?v=2.2.0"></script>
|
|
87
|
+
<script src="js/hubpreview.js?v=2.2.0"></script>
|
|
85
88
|
</body>
|
|
86
89
|
</html>
|
package/js/app.js
CHANGED
|
@@ -181,6 +181,7 @@ var App = (function () {
|
|
|
181
181
|
if (nowb - _lastBackAt < 500) { return true; }
|
|
182
182
|
_lastBackAt = nowb;
|
|
183
183
|
}
|
|
184
|
+
if (action === 'GUIDE') { guideKey(); return true; }
|
|
184
185
|
if (Player.isActive()) { return Player.handleKey(action); } // (a held OK too: the player's Cast & more has a menu behind it)
|
|
185
186
|
if (Views.moveKey && Views.moveKey(action)) { return true; } // (a Continue Watching card being moved)
|
|
186
187
|
switch (action) {
|
|
@@ -213,16 +214,27 @@ var App = (function () {
|
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
//
|
|
217
|
-
// Not over a popup, nor
|
|
217
|
+
// The remote's GUIDE key: the Guide tab (once a TV guide is read: it's in the top menu), the
|
|
218
|
+
// player closed first. Not over a popup, nor while an addon's link is typed.
|
|
219
|
+
function guideKey() {
|
|
220
|
+
var on = (Views.menuItems ? Views.menuItems() : []).some(function (m) { return m.key === 'guide'; });
|
|
221
|
+
if (!on) { U.toast('No TV guide yet (Settings › Content › IPTV › Guide link)', 3000); return; }
|
|
222
|
+
if (Player.isActive()) { Player.exit(); }
|
|
223
|
+
else if (Views.isModalOpen() || _current === 'addaddon') { return; }
|
|
224
|
+
goHome('guide');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Number keys away from the player: 0 Settings, 1–9 the top menu's items as they show (1:
|
|
228
|
+
// Watching, the rest in your order: Views.menuItems). Not over a popup, nor on a page where
|
|
229
|
+
// something's being typed.
|
|
218
230
|
function numberKey(n) {
|
|
219
231
|
if (Views.isModalOpen() || _current === 'addaddon') { return; }
|
|
220
232
|
if (n === 0) { if (_current !== 'settings') { openSettings(); } return; }
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
else if (
|
|
225
|
-
else
|
|
233
|
+
var m = (Views.menuItems ? Views.menuItems() : [])[n - 1];
|
|
234
|
+
if (!m) { return; }
|
|
235
|
+
if (m.key === 'discover') { if (_current !== 'discover') { openDiscover(); } }
|
|
236
|
+
else if (m.key === 'search') { if (_current !== 'search') { openSearch(); } }
|
|
237
|
+
else { goHome(m.key); }
|
|
226
238
|
}
|
|
227
239
|
|
|
228
240
|
function actualStart() {
|
package/js/epg.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/* TV guide (XMLTV) for IPTV channels. Global namespace: EPG
|
|
2
|
+
Which guide a playlist has (guideUrl: the Guide link you set, else the one the playlist
|
|
3
|
+
names, else an Xtream playlist's own), read by StremioK's service for your channels only
|
|
4
|
+
(their keys as a bitset: see service/index.js, epgControl), and the programs asked for as
|
|
5
|
+
the cards, Home's details strip, the player and a channel's schedule need them. Not in a
|
|
6
|
+
desktop browser (no service). */
|
|
7
|
+
var EPG = (function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// A channel's key, as the service makes it: "i:" and its tvg-id (iptv-org's feed after an @
|
|
11
|
+
// left out: "ElTrece.ar@SD" is a guide's "ElTrece.ar"), or "n:" and its name without
|
|
12
|
+
// capitals, accents, spaces, "TV" or HD / FHD / 4K tags ("El Trece HD": "n:eltrece"). A
|
|
13
|
+
// country at the end (".ar", or a name's " ARG" / " MX": COUNTRY) is kept apart, after a dot
|
|
14
|
+
// ("FOX SPORTS 2 ARG": "n:foxsports2.ar"); a guide's channel answers with it and without
|
|
15
|
+
// (keysAll), so "A24" finds a guide's "A24.ar", and "ESPN 2 MX" doesn't find "Espn2.ar".
|
|
16
|
+
var ACCENTS = { 'á': 'a', 'à': 'a', 'â': 'a', 'ä': 'a', 'ã': 'a', 'å': 'a', 'é': 'e', 'è': 'e', 'ê': 'e', 'ë': 'e', 'í': 'i', 'ì': 'i', 'î': 'i', 'ï': 'i',
|
|
17
|
+
'ó': 'o', 'ò': 'o', 'ô': 'o', 'ö': 'o', 'õ': 'o', 'ú': 'u', 'ù': 'u', 'û': 'u', 'ü': 'u', 'ñ': 'n', 'ç': 'c' };
|
|
18
|
+
var COUNTRY = { ar: 'ar', arg: 'ar', mx: 'mx', mex: 'mx', us: 'us', usa: 'us', uk: 'uk', gb: 'uk', es: 'es', br: 'br', cl: 'cl', co: 'co', pe: 'pe',
|
|
19
|
+
uy: 'uy', py: 'py', ve: 've', ec: 'ec', bo: 'bo', ca: 'ca', fr: 'fr', de: 'de', it: 'it', pt: 'pt' };
|
|
20
|
+
function parts(kind, s) {
|
|
21
|
+
var t = ('' + (s || '')).toLowerCase().replace(/[áàâäãåéèêëíìîïóòôöõúùûüñç]/g, function (c) { return ACCENTS[c]; }).trim(), cc = '', m;
|
|
22
|
+
if (kind === 'i') { t = t.replace(/@.*$/, ''); }
|
|
23
|
+
if ((m = /\.([a-z]{2})$/.exec(t))) { cc = m[1]; t = t.slice(0, -3); }
|
|
24
|
+
else if (kind === 'n' && (m = /[\s_-]+([a-z]{2,3})$/.exec(t)) && COUNTRY[m[1]] && t.length > m[0].length) { cc = COUNTRY[m[1]]; t = t.slice(0, -m[0].length); }
|
|
25
|
+
if (kind === 'n') { t = t.replace(/\[[^\]]*\]|\([^)]*\)/g, ' ').replace(/\b(hd|fhd|uhd|sd|hq|4k|8k|hevc|h265|1080p?|720p?|tv)\b/g, ' '); }
|
|
26
|
+
t = t.replace(/[^a-z0-9]+/g, '');
|
|
27
|
+
if (kind === 'n' && t.length > 4) { t = t.replace(/tv$/, ''); } // ("CronicaTV")
|
|
28
|
+
return { base: t.slice(0, 60), cc: cc };
|
|
29
|
+
}
|
|
30
|
+
function key(kind, s) { var p = parts(kind, s); return p.base ? kind + ':' + p.base + (p.cc ? '.' + p.cc : '') : ''; }
|
|
31
|
+
function keysAll(kind, s) { var p = parts(kind, s); return !p.base ? [] : p.cc ? [kind + ':' + p.base + '.' + p.cc, kind + ':' + p.base] : [kind + ':' + p.base]; }
|
|
32
|
+
function hash(s) {
|
|
33
|
+
var h = 0x811c9dc5;
|
|
34
|
+
for (var i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0; }
|
|
35
|
+
return h;
|
|
36
|
+
}
|
|
37
|
+
// The keys as a bitset, base64url: 16 bits a key (1,024 to 131,072 in all); a key's hash
|
|
38
|
+
// picks its bit.
|
|
39
|
+
var B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
40
|
+
function bitsFor(keys) {
|
|
41
|
+
var n = 1024, bytes = [], i;
|
|
42
|
+
while (n < keys.length * 16 && n < 131072) { n *= 2; }
|
|
43
|
+
for (i = 0; i < n / 8; i++) { bytes.push(0); }
|
|
44
|
+
keys.forEach(function (k) { if (k) { var b = hash(k) % n; bytes[b >> 3] |= 1 << (b & 7); } });
|
|
45
|
+
var out = '';
|
|
46
|
+
for (i = 0; i < bytes.length; i += 3) {
|
|
47
|
+
var v = (bytes[i] << 16) | ((bytes[i + 1] || 0) << 8) | (bytes[i + 2] || 0);
|
|
48
|
+
out += B64.charAt((v >> 18) & 63) + B64.charAt((v >> 12) & 63) + (i + 1 < bytes.length ? B64.charAt((v >> 6) & 63) : '') + (i + 2 < bytes.length ? B64.charAt(v & 63) : '');
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function keysOf(c) { return [c.tvgId ? key('i', c.tvgId) : '', key('n', c.name)].filter(Boolean); }
|
|
53
|
+
|
|
54
|
+
// A playlist's guide: yours (p.guide), else the first the playlist names (url-tvg), else an
|
|
55
|
+
// Xtream get.php link's xmltv.php (the same login; also in place of a guide the playlist
|
|
56
|
+
// names on its own server without the login). '' when none.
|
|
57
|
+
function param(q, name) {
|
|
58
|
+
var m = new RegExp('(?:^|&)' + name + '=([^&#]*)').exec(q || '');
|
|
59
|
+
if (!m) { return ''; }
|
|
60
|
+
try { return decodeURIComponent(m[1].replace(/\+/g, ' ')); } catch (e) { return ''; }
|
|
61
|
+
}
|
|
62
|
+
function guideUrl(p, named) {
|
|
63
|
+
if (p && p.guide) { return p.guide; }
|
|
64
|
+
var g = (named || []).filter(function (u) { return !!U.parseUrl(u) && /^https?:\/\//i.test(u); })[0];
|
|
65
|
+
var u = U.parseUrl(p && p.url), xt = '';
|
|
66
|
+
if (u && /\/get\.php$/i.test(u.path)) {
|
|
67
|
+
var user = param(u.query, 'username'), pass = param(u.query, 'password');
|
|
68
|
+
if (user && pass) { xt = u.scheme + '://' + u.host + (u.port ? ':' + u.port : '') + u.path.replace(/get\.php$/i, 'xmltv.php') + '?username=' + encodeURIComponent(user) + '&password=' + encodeURIComponent(pass); }
|
|
69
|
+
}
|
|
70
|
+
var gu = g && U.parseUrl(g);
|
|
71
|
+
if (g && xt && gu && gu.host === u.host && gu.port === u.port && !param(gu.query, 'username')) { return xt; } // (its own server's, without the login)
|
|
72
|
+
return g || xt;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function available() { return typeof YTPlay !== 'undefined' && !!YTPlay.epgLoad && YTPlay.installMode() !== 'browser'; }
|
|
76
|
+
|
|
77
|
+
// ---- the guides: asked to read, then waited for ----
|
|
78
|
+
var _guides = {}; // url -> { k, state, at, channels, programmes, partial, error, timer }
|
|
79
|
+
var _byList = {}; // playlist id -> its guide's url ('' none)
|
|
80
|
+
var POLL_MS = [300, 300, 600, 1000, 2000], POLL_MS_LATER = 3000, POLLS = 300, CHECK_MS = 60 * 60 * 1000;
|
|
81
|
+
// Every playlist's guide asked for (again: the service reads one only when it's old), for
|
|
82
|
+
// its channels' keys. After the playlists are read, and every hour. again: a playlist's id,
|
|
83
|
+
// or true for all (its Reload, IPTV.reload): their guides read again now, however new.
|
|
84
|
+
function start(again) {
|
|
85
|
+
if (!available() || typeof IPTV === 'undefined' || !IPTV.channels) { return; }
|
|
86
|
+
var byGuide = {}, force = {};
|
|
87
|
+
_byList = {};
|
|
88
|
+
IPTV.getPlaylists().forEach(function (p) {
|
|
89
|
+
if (p.off) { return; }
|
|
90
|
+
var st = IPTV.status(p.id), url = guideUrl(p, st && st.guides);
|
|
91
|
+
_byList[p.id] = url;
|
|
92
|
+
if (url && !byGuide[url]) { byGuide[url] = []; }
|
|
93
|
+
if (url && (again === true || again === p.id)) { force[url] = true; }
|
|
94
|
+
});
|
|
95
|
+
IPTV.channels().forEach(function (c) {
|
|
96
|
+
var url = _byList[c._playlistId];
|
|
97
|
+
if (url) { byGuide[url].push.apply(byGuide[url], keysOf(c)); }
|
|
98
|
+
});
|
|
99
|
+
var gone = false;
|
|
100
|
+
Object.keys(_guides).forEach(function (url) { // (a guide no playlist uses any more: dropped)
|
|
101
|
+
if (byGuide[url]) { return; }
|
|
102
|
+
if (_guides[url].timer) { clearTimeout(_guides[url].timer); }
|
|
103
|
+
delete _guides[url];
|
|
104
|
+
forget(url);
|
|
105
|
+
gone = true;
|
|
106
|
+
});
|
|
107
|
+
Object.keys(byGuide).forEach(function (url) { if (byGuide[url].length) { loadGuide(url, bitsFor(byGuide[url]), !!force[url]); } });
|
|
108
|
+
if (!_checker) { _checker = setInterval(function () { start(); }, CHECK_MS); }
|
|
109
|
+
if (gone) { changed(); }
|
|
110
|
+
}
|
|
111
|
+
var _checker = null;
|
|
112
|
+
function loadGuide(url, k, force) {
|
|
113
|
+
var g = _guides[url];
|
|
114
|
+
if (!g || g.k !== k || force) { if (g && g.timer) { clearTimeout(g.timer); } g = _guides[url] = { k: k, state: 'loading' }; }
|
|
115
|
+
else if (g.timer || g.asking) { return; }
|
|
116
|
+
(function ask(n) {
|
|
117
|
+
g.asking = true;
|
|
118
|
+
YTPlay.epgLoad(url, k, force && !n).then(function (r) {
|
|
119
|
+
g.asking = false;
|
|
120
|
+
if (_guides[url] !== g) { return; }
|
|
121
|
+
var was = g.state + '|' + g.at;
|
|
122
|
+
['state', 'at', 'channels', 'programmes', 'partial', 'error'].forEach(function (f) { g[f] = r && r[f]; });
|
|
123
|
+
if (g.state === 'loading' && n < POLLS) { g.timer = setTimeout(function () { g.timer = null; ask(n + 1); }, n < POLL_MS.length ? POLL_MS[n] : POLL_MS_LATER); }
|
|
124
|
+
if (g.state + '|' + g.at !== was) {
|
|
125
|
+
if (g.state === 'ready') { forget(url); } // (read again: what's on, asked again)
|
|
126
|
+
changed();
|
|
127
|
+
}
|
|
128
|
+
}, function () {
|
|
129
|
+
g.asking = false;
|
|
130
|
+
if (_guides[url] !== g) { return; }
|
|
131
|
+
g.state = 'failed'; g.error = 'StremioK’s service didn’t answer';
|
|
132
|
+
changed();
|
|
133
|
+
});
|
|
134
|
+
})(0);
|
|
135
|
+
}
|
|
136
|
+
// For Settings › IPTV: a playlist's guide and how it stands, or null when it has none.
|
|
137
|
+
function status(playlistId) {
|
|
138
|
+
var url = _byList[playlistId];
|
|
139
|
+
if (!url) { return null; }
|
|
140
|
+
var g = _guides[url] || { state: 'loading' };
|
|
141
|
+
return { url: url, state: g.state, at: g.at || 0, channels: g.channels || 0, programmes: g.programmes || 0, partial: !!g.partial, error: g.error || '' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---- programs: asked for in batches, kept per channel ----
|
|
145
|
+
var _progs = {}; // channel id -> { at, list: [{ start, stop, title, desc }], full (the whole window) }
|
|
146
|
+
var _icons = {}; // channel id -> its guide's logo (for one the playlist gives none: logo)
|
|
147
|
+
var _want = {}, _wantTimer = null, _asking = 0, BATCH = 60, PROG_MS = 30 * 60 * 1000;
|
|
148
|
+
function channelOf(id) { return (typeof IPTV !== 'undefined' && IPTV.channel) ? IPTV.channel(id) : null; }
|
|
149
|
+
function guideOf(c) { var url = c && _byList[c._playlistId], g = url && _guides[url]; return (g && g.state === 'ready') ? url : ''; }
|
|
150
|
+
function forget(url) { Object.keys(_progs).forEach(function (id) { if (_progs[id].url === url) { delete _progs[id]; } }); }
|
|
151
|
+
function want(ids) {
|
|
152
|
+
(ids || []).forEach(function (id) { if (/^iptv:/.test(id || '')) { _want[id] = 1; } });
|
|
153
|
+
if (!_wantTimer) { _wantTimer = setTimeout(flush, 50); }
|
|
154
|
+
}
|
|
155
|
+
function nowSec() { return Math.floor(Date.now() / 1000); }
|
|
156
|
+
function asList(rows, url) {
|
|
157
|
+
return (rows || []).filter(function (r) { return r && r.length >= 3 && r[1] > r[0]; })
|
|
158
|
+
.map(function (r) {
|
|
159
|
+
var p = { start: +r[0], stop: +r[1], title: '' + r[2], desc: '' + (r[3] || ''), picture: (typeof r[4] === 'string' && U.parseUrl(r[4]) && !(U.urlIsPrivate(r[4]) && !U.urlIsPrivate(url || ''))) ? r[4] : '' }; // (no picture into your home network from a guide on the internet)
|
|
160
|
+
var a = r[5]; // (what it is, from the guide's categories: a film's original title and year)
|
|
161
|
+
if (a && typeof a === 'object') {
|
|
162
|
+
if (a.m === 'movie' || a.m === 'series') { p.kind = a.m; }
|
|
163
|
+
if (typeof a.o === 'string' && a.o) { p.orig = a.o.slice(0, 120); }
|
|
164
|
+
if (+a.y >= 1900 && +a.y <= 2100) { p.year = +a.y; }
|
|
165
|
+
}
|
|
166
|
+
return p;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// One request a guide at a time per batch (BATCH channels): from now, the next few.
|
|
170
|
+
function flush() {
|
|
171
|
+
_wantTimer = null;
|
|
172
|
+
if (_asking) { _wantTimer = setTimeout(flush, 200); return; }
|
|
173
|
+
var byGuide = {};
|
|
174
|
+
Object.keys(_want).forEach(function (id) {
|
|
175
|
+
var c = channelOf(id), url = guideOf(c);
|
|
176
|
+
if (!url) { return; } // (no guide, or not read yet: asked again once it is)
|
|
177
|
+
delete _want[id];
|
|
178
|
+
if (fresh(id)) { return; }
|
|
179
|
+
(byGuide[url] = byGuide[url] || []).push(c);
|
|
180
|
+
});
|
|
181
|
+
Object.keys(byGuide).forEach(function (url) {
|
|
182
|
+
var list = byGuide[url];
|
|
183
|
+
for (var i = 0; i < list.length; i += BATCH) { ask(url, list.slice(i, i + BATCH), nowSec(), nowSec() + 6 * 3600, 4, false); }
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function ask(url, chans, from, to, max, full) {
|
|
187
|
+
var keys = [], gen = _gen;
|
|
188
|
+
chans.forEach(function (c) { keys.push.apply(keys, keysOf(c)); });
|
|
189
|
+
_asking++;
|
|
190
|
+
// Resolves { id: programs } for the channels answered ([] when none, or not in the guide),
|
|
191
|
+
// and _unknown: the ids not answered (failed, cut off, not read there, or the guides
|
|
192
|
+
// changed since): kept as they were and asked again, never taken as empty.
|
|
193
|
+
function unknown() { var o = {}; o._unknown = chans.map(function (c) { return c.id; }); return o; }
|
|
194
|
+
return YTPlay.epgGet(url, keys, from, to, max).then(function (r) {
|
|
195
|
+
_asking--;
|
|
196
|
+
if (gen !== _gen || !r || typeof r !== 'object' || r._unread) { return unknown(); } // (the guides changed since, or no answer to read)
|
|
197
|
+
var cut = {};
|
|
198
|
+
(Array.isArray(r._cut) ? r._cut : []).forEach(function (k) { cut[k] = 1; });
|
|
199
|
+
var out = {}, icons = (r._icons && typeof r._icons === 'object') ? r._icons : {};
|
|
200
|
+
var unk = [];
|
|
201
|
+
chans.forEach(function (c) {
|
|
202
|
+
if (_byList[c._playlistId] !== url) { unk.push(c.id); return; } // (its playlist's guide is another now)
|
|
203
|
+
var ks = keysOf(c), rows = null;
|
|
204
|
+
for (var i = 0; i < ks.length && !rows; i++) { if (Array.isArray(r[ks[i]])) { rows = r[ks[i]]; } }
|
|
205
|
+
if (!rows && ks.some(function (k) { return cut[k]; })) { unk.push(c.id); return; } // (cut off: not answered)
|
|
206
|
+
for (var j = 0; j < ks.length; j++) {
|
|
207
|
+
var icon = icons[ks[j]];
|
|
208
|
+
// (a guide on the internet can't point one into your home network)
|
|
209
|
+
if (typeof icon === 'string' && U.parseUrl(icon) && !(U.urlIsPrivate(icon) && !U.urlIsPrivate(url))) { _icons[c.id] = icon; break; }
|
|
210
|
+
}
|
|
211
|
+
var e = { at: Date.now(), url: url, list: asList(rows, url), full: full };
|
|
212
|
+
if (!full && _progs[c.id] && _progs[c.id].full && _progs[c.id].at > Date.now() - PROG_MS) { return; } // (a whole schedule kept)
|
|
213
|
+
_progs[c.id] = e;
|
|
214
|
+
out[c.id] = e.list;
|
|
215
|
+
});
|
|
216
|
+
trimProgs();
|
|
217
|
+
if (unk.length) { out._unknown = unk; }
|
|
218
|
+
changed();
|
|
219
|
+
return out;
|
|
220
|
+
}, function () { _asking--; return unknown(); });
|
|
221
|
+
}
|
|
222
|
+
// Programs kept for PROGS_KEEP channels at most (the ones read longest ago go first): a long
|
|
223
|
+
// session going through thousands of channels doesn't keep them all.
|
|
224
|
+
var PROGS_KEEP = 400;
|
|
225
|
+
function trimProgs() {
|
|
226
|
+
var ids = Object.keys(_progs);
|
|
227
|
+
if (ids.length <= PROGS_KEEP) { return; }
|
|
228
|
+
ids.sort(function (a, b) { return _progs[a].at - _progs[b].at; }).slice(0, ids.length - PROGS_KEEP).forEach(function (id) { delete _progs[id]; });
|
|
229
|
+
}
|
|
230
|
+
function fresh(id) {
|
|
231
|
+
var e = _progs[id];
|
|
232
|
+
if (!e || Date.now() - e.at > PROG_MS) { return false; }
|
|
233
|
+
var t = nowSec();
|
|
234
|
+
return !e.list.length || e.list[e.list.length - 1].stop > t; // (what's on, still in what was read)
|
|
235
|
+
}
|
|
236
|
+
// What's on now, and next: { now, next } (either may be null), or null without a guide.
|
|
237
|
+
function now(id) {
|
|
238
|
+
var c = channelOf(id);
|
|
239
|
+
if (!c || !guideOf(c)) { return null; }
|
|
240
|
+
if (!fresh(id)) { want([id]); }
|
|
241
|
+
var e = _progs[id];
|
|
242
|
+
if (!e || e.url !== guideOf(c)) { return null; }
|
|
243
|
+
var t = nowSec(), cur = null, next = null;
|
|
244
|
+
for (var i = 0; i < e.list.length; i++) {
|
|
245
|
+
var p = e.list[i];
|
|
246
|
+
if (!cur && p.start <= t && t < p.stop) { cur = p; continue; }
|
|
247
|
+
if (p.start > t && (!next || p.start < next.start)) { next = p; break; }
|
|
248
|
+
}
|
|
249
|
+
return (cur || next) ? { now: cur, next: next } : null;
|
|
250
|
+
}
|
|
251
|
+
// A channel's schedule, from 2 hours ago to tomorrow: Promise of [{ start, stop, title, desc }].
|
|
252
|
+
function schedule(id) {
|
|
253
|
+
var c = channelOf(id), url = guideOf(c);
|
|
254
|
+
if (!url) { return Promise.resolve([]); }
|
|
255
|
+
return ask(url, [c], nowSec() - 2 * 3600, nowSec() + 30 * 3600, 150, true).then(function (out) { return out[id] || []; });
|
|
256
|
+
}
|
|
257
|
+
function hasGuide(id) { return !!guideOf(channelOf(id)); }
|
|
258
|
+
// A channel's logo from its guide ('' when it gave none, or not asked yet).
|
|
259
|
+
function logo(id) { return _icons[id] || ''; }
|
|
260
|
+
// Whether any guide was read with channels in it (the Guide tab shows then), and the channels
|
|
261
|
+
// that have a guide, in the IPTV tab's order.
|
|
262
|
+
function hasAny() {
|
|
263
|
+
return Object.keys(_byList).some(function (id) { var g = _guides[_byList[id]]; return !!g && g.state === 'ready' && g.channels > 0; });
|
|
264
|
+
}
|
|
265
|
+
function guided() { // (your favorites first)
|
|
266
|
+
var all = (typeof IPTV !== 'undefined' && IPTV.channels ? IPTV.channels() : []).filter(function (c) { return !!guideOf(c); });
|
|
267
|
+
var fav = IPTV.isFavorite ? all.filter(function (c) { return IPTV.isFavorite(c.id); }) : [];
|
|
268
|
+
return fav.concat(all.filter(function (c) { return fav.indexOf(c) === -1; }));
|
|
269
|
+
}
|
|
270
|
+
// Many channels' programs from 'from' to 'to' (the Guide tab): Promise of { id: programs },
|
|
271
|
+
// RANGE_BATCH channels a request (each answer kept to 2 MB by the service: 150 programs
|
|
272
|
+
// a channel with their pictures and descriptions fit).
|
|
273
|
+
var RANGE_BATCH = 20;
|
|
274
|
+
function range(ids, from, to) {
|
|
275
|
+
var byGuide = {}, jobs = [];
|
|
276
|
+
(ids || []).forEach(function (id) { var c = channelOf(id), url = guideOf(c); if (url) { (byGuide[url] = byGuide[url] || []).push(c); } });
|
|
277
|
+
Object.keys(byGuide).forEach(function (url) {
|
|
278
|
+
var list = byGuide[url];
|
|
279
|
+
for (var i = 0; i < list.length; i += RANGE_BATCH) { jobs.push(ask(url, list.slice(i, i + RANGE_BATCH), Math.floor(from), Math.ceil(to), 150, true)); }
|
|
280
|
+
});
|
|
281
|
+
return Promise.all(jobs).then(function (outs) {
|
|
282
|
+
var all = {}, unknown = [];
|
|
283
|
+
outs.forEach(function (o) { Object.keys(o || {}).forEach(function (id) { if (id === '_unknown') { unknown = unknown.concat(o._unknown); } else { all[id] = o[id]; } }); });
|
|
284
|
+
if (unknown.length) { all._unknown = unknown; } // (not answered: see ask)
|
|
285
|
+
return all;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 20:30, or with the day when it isn't today (Tue 01:00).
|
|
290
|
+
var DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
291
|
+
function two(n) { return (n < 10 ? '0' : '') + n; }
|
|
292
|
+
function hhmm(sec) { var d = new Date(sec * 1000); return two(d.getHours()) + ':' + two(d.getMinutes()); }
|
|
293
|
+
function dayTime(sec) {
|
|
294
|
+
var d = new Date(sec * 1000), today = new Date(Date.now());
|
|
295
|
+
var same = d.getFullYear() === today.getFullYear() && d.getMonth() === today.getMonth() && d.getDate() === today.getDate();
|
|
296
|
+
return (same ? '' : DAYS[d.getDay()] + ' ') + hhmm(sec);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Told when a guide is read or programs arrive (once for a burst).
|
|
300
|
+
var _fns = [], _changeTimer = null;
|
|
301
|
+
function onChange(fn) { _fns.push(fn); }
|
|
302
|
+
function changed() {
|
|
303
|
+
if (_changeTimer) { return; }
|
|
304
|
+
_changeTimer = setTimeout(function () {
|
|
305
|
+
_changeTimer = null;
|
|
306
|
+
if (Object.keys(_want).length && !_wantTimer) { _wantTimer = setTimeout(flush, 50); } // (asked before the guide was read)
|
|
307
|
+
_fns.forEach(function (fn) { try { fn(); } catch (e) {} });
|
|
308
|
+
}, 60);
|
|
309
|
+
}
|
|
310
|
+
// The guide links changed (a playlist's Guide link, its link, its channels): asked again.
|
|
311
|
+
// An answer asked before (an older _gen) is dropped when it comes.
|
|
312
|
+
var _gen = 0;
|
|
313
|
+
function reset() { _gen++; _progs = {}; _icons = {}; _want = {}; start(); }
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
generation: function () { return _gen; }, // (changes when the guides do: what was read before isn't theirs)
|
|
317
|
+
key: key, keysAll: keysAll, bitsFor: bitsFor, guideUrl: guideUrl, available: available, start: start, reset: reset, status: status,
|
|
318
|
+
want: want, now: now, schedule: schedule, hasGuide: hasGuide, logo: logo, hasAny: hasAny, guided: guided, range: range, onChange: onChange, hhmm: hhmm, dayTime: dayTime
|
|
319
|
+
};
|
|
320
|
+
})();
|