stremiok 2.2.1 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +7 -6
- package/config.xml +1 -1
- package/css/style.css +11 -8
- package/index.html +33 -33
- package/js/app.js +18 -19
- package/js/focus.js +5 -5
- package/js/iptv.js +15 -2
- package/js/player-core.js +71 -9
- package/js/player-store.js +2 -2
- package/js/player-subs.js +47 -7
- package/js/stremio.js +154 -23
- package/js/util.js +55 -13
- package/js/view-core.js +43 -13
- package/js/view-detail.js +35 -7
- package/js/view-discover.js +1 -1
- package/js/view-home.js +33 -8
- package/js/view-settings.js +69 -1
- package/js/views.js +1 -1
- package/js/ytplay.js +4 -3
- package/package.json +2 -1
- package/service/index.js +33 -9
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
What changed in each version of StremioK, newest first. How everything works is in the
|
|
4
4
|
[user guide](docs/USER-GUIDE.md).
|
|
5
5
|
|
|
6
|
+
## 2.2.2 — 2026-10-28
|
|
7
|
+
- **Torrents through a streaming server** (Settings › Content › Streaming server): with Stremio's
|
|
8
|
+
streaming server or a TorrServer running on your network (a computer or a NAS), torrent-only
|
|
9
|
+
sources play like any other. *Test* says whether it answers; torrents get longer to start (2
|
|
10
|
+
minutes by default).
|
|
11
|
+
- **Seeders** in Source ranking: torrent sources with more people sharing them first.
|
|
12
|
+
- **Zoom** (Settings › Appearance): 100% to 140%, everything bigger to read from further away;
|
|
13
|
+
subtitles keep their own size.
|
|
14
|
+
- **Leaving StremioK:** BACK on Home's top menu asks *Exit StremioK?* (OK exits), instead of
|
|
15
|
+
pressing BACK twice (#3).
|
|
16
|
+
- **The top menu scrolls** when its items don't all fit, the StremioK name staying at the right.
|
|
17
|
+
- **IPTV Auto-refresh** (Settings › Content › IPTV, *Off* by default): every 1, 3, 6 or 12 hours,
|
|
18
|
+
your playlists and their TV guides are read again; never while something plays.
|
|
19
|
+
- **Quicker switching:** subtitles (a track still coming isn't started over, the last 8 files
|
|
20
|
+
come back at once) and channels (what StremioK checks on each one stops when you move on).
|
|
21
|
+
- **Diagnostics says what went wrong** (*Timed out after 20 s*, *No answer…*) instead of
|
|
22
|
+
"HTTP 0"; slow subtitle servers get longer: 20 seconds for a file, 15 for an addon's list.
|
|
23
|
+
- **USB downloads:** a drive prepared with links can no longer make StremioK read or overwrite a
|
|
24
|
+
file outside it.
|
|
25
|
+
- **Fixed:** your IPTV rows (and the Radio tab) missing from Home at start-up until a Reload; a
|
|
26
|
+
next episode or a hold menu whose details came late opening after you'd left.
|
|
27
|
+
|
|
6
28
|
## 2.2.1 — 2026-10-24
|
|
7
29
|
- **Every language OpenSubtitles uses** (94, Indonesian among them, #1) for subtitles and audio:
|
|
8
30
|
to pick as your default, second choice or second subtitle, listed by name, and each track
|
package/README.md
CHANGED
|
@@ -35,8 +35,9 @@ video through Samsung's AVPlay API.
|
|
|
35
35
|
- **YouTube:** live channels, trailers, and your own YouTube channels, playlists and searches,
|
|
36
36
|
with SponsorBlock, DeArrow, chapters and captions.
|
|
37
37
|
- **Local:** the videos on a USB drive, matched to their titles.
|
|
38
|
+
- **Torrents**, through Stremio's streaming server or a TorrServer you run on your network.
|
|
38
39
|
- **Made for the remote:** D-pad navigation throughout; hold OK where an item has more to
|
|
39
|
-
offer (*Hold OK for more* says so).
|
|
40
|
+
offer (*Hold OK for more* says so); zoom up to 140% for reading from further away.
|
|
40
41
|
|
|
41
42
|
## Install
|
|
42
43
|
|
|
@@ -52,7 +53,8 @@ Three ways, all covered step by step in the **[installation guide](docs/INSTALL.
|
|
|
52
53
|
|
|
53
54
|
1. **Addons:** add the addons you use, by link, or bring them in from your Stremio account,
|
|
54
55
|
AIOManager or a configuration file. A stream addon that gives HTTP(S) links (for example
|
|
55
|
-
through a debrid service) is what makes
|
|
56
|
+
through a debrid service), or a torrent addon with a streaming server you run, is what makes
|
|
57
|
+
titles playable.
|
|
56
58
|
2. **API keys**, optional: your own TMDB, OMDb and MDBList keys add ratings, *Cast & more*,
|
|
57
59
|
*where to watch* and episode trailers.
|
|
58
60
|
3. **Trakt**, optional: connect your account to share watched marks and Continue Watching.
|
|
@@ -67,8 +69,8 @@ Three ways, all covered step by step in the **[installation guide](docs/INSTALL.
|
|
|
67
69
|
This app hosts **no** media. It is a client for the open Stremio addon protocol: catalogs and
|
|
68
70
|
metadata come from the addons you install (**Cinemeta**, the official one, by default), and
|
|
69
71
|
playable sources come from whatever **stream addons you add yourself** in Settings. Streams
|
|
70
|
-
that only expose a torrent `infoHash`
|
|
71
|
-
|
|
72
|
+
that only expose a torrent `infoHash` play only through a streaming server you run (Stremio's
|
|
73
|
+
or TorrServer): this app embeds no torrent or debrid engine.
|
|
72
74
|
> **Note on WatchHub:** StremioK doesn't install it. Its answers say it's only for clients
|
|
73
75
|
> Stremio authorizes in writing, and StremioK isn't one; "where to watch" comes from TMDB
|
|
74
76
|
> instead (with a TMDB key). You can still add it yourself as a third-party addon (Settings
|
|
@@ -85,8 +87,7 @@ your progress, settings and copies that let it start faster (see
|
|
|
85
87
|
## Known limitations
|
|
86
88
|
|
|
87
89
|
- AVPlay behavior varies by TV generation, codec and HLS provider.
|
|
88
|
-
-
|
|
89
|
-
addon).
|
|
90
|
+
- Torrents play only through a streaming server you run on your network.
|
|
90
91
|
- Home rows show a catalog's first page (up to 30 titles); Discover pages through the rest.
|
|
91
92
|
- No StremioK account and no profiles: settings, addons and progress stay on the TV. The
|
|
92
93
|
only sync is optional: Trakt (watched marks, Continue Watching, ratings).
|
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.2.
|
|
3
|
+
id="http://stremiok.tizen/StremioKTizen" version="2.2.2" 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
|
@@ -19,20 +19,20 @@ html, body {
|
|
|
19
19
|
#av-player, #html5-player {
|
|
20
20
|
position: absolute;
|
|
21
21
|
top: 0; left: 0;
|
|
22
|
-
width:
|
|
22
|
+
width: 100%; height: 100%; /* (the page: 1920×1080, or less under a zoom, scaled up) */
|
|
23
23
|
background: #000;
|
|
24
24
|
display: none;
|
|
25
25
|
z-index: 1;
|
|
26
26
|
}
|
|
27
27
|
#av-player.active, #html5-player.active { display: block; }
|
|
28
28
|
|
|
29
|
-
#app { position: absolute; top: 0; left: 0; width:
|
|
30
|
-
.screen { position: absolute; top: 0; left: 0; width:
|
|
29
|
+
#app { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; }
|
|
30
|
+
.screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
|
31
31
|
.hidden { display: none !important; }
|
|
32
32
|
|
|
33
33
|
/* --- Header --- */
|
|
34
34
|
.header { height: 96px; padding: 30px 60px 0 60px; display: flex; align-items: center; }
|
|
35
|
-
.brand { font-size: 40px; font-weight: bold; color: #818cf8; letter-spacing: 1px; }
|
|
35
|
+
.brand { font-size: 40px; font-weight: bold; color: #818cf8; letter-spacing: 1px; flex-shrink: 0; }
|
|
36
36
|
.brand .dot { color: #e6e9ef; }
|
|
37
37
|
/* private viewing on: StremioK as an outline (the fill hidden; the outline takes each letter's
|
|
38
38
|
own color, so it follows the accent color and a light background) */
|
|
@@ -94,7 +94,7 @@ html, body {
|
|
|
94
94
|
.detail-hero .shade { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, rgba(14,17,23,0.96) 30%, rgba(14,17,23,0.35) 100%), linear-gradient(0deg, #0e1117 4%, transparent 40%); }
|
|
95
95
|
/* The source and episode lists: the backdrop over the whole screen, shaded lighter, the rows
|
|
96
96
|
see-through (a focused one stays solid) */
|
|
97
|
-
.detail-hero.hero-full { height:
|
|
97
|
+
.detail-hero.hero-full { height: 100%; }
|
|
98
98
|
.detail-hero.hero-full .shade { background: linear-gradient(90deg, rgba(14,17,23,0.9) 22%, rgba(14,17,23,0.4) 100%), linear-gradient(0deg, rgba(14,17,23,0.5) 0%, transparent 45%); }
|
|
99
99
|
.streams-panel .stream, .list-panel .ep { background: rgba(22,27,38,0.7); }
|
|
100
100
|
.streams-panel .stream-group, .streams-panel .stream-for, .streams-panel .streams-hidden { text-shadow: 0 1px 3px #000, 0 0 8px rgba(0,0,0,0.6); }
|
|
@@ -319,7 +319,7 @@ html, body {
|
|
|
319
319
|
.card.landscape { width: 340px; height: 200px; }
|
|
320
320
|
|
|
321
321
|
/* --- Confirm modal --- */
|
|
322
|
-
.modal-overlay { position: absolute; top: 0; left: 0; width:
|
|
322
|
+
.modal-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 45; background: rgba(0,0,0,0.72); display: flex; align-items: center; justify-content: center; }
|
|
323
323
|
.modal-box { width: 820px; background: #161b26; border: 2px solid #2a3346; border-radius: 16px; padding: 44px 48px; text-align: center; }
|
|
324
324
|
.modal-msg { font-size: 30px; line-height: 1.4; color: #e6e9ef; margin-bottom: 36px; }
|
|
325
325
|
.modal-row { display: flex; justify-content: center; }
|
|
@@ -365,7 +365,7 @@ html, body {
|
|
|
365
365
|
/* --- Toast + spinner --- */
|
|
366
366
|
#hold-hint { position: absolute; right: 36px; bottom: 20px; z-index: 20; font-size: 17px; color: #c7cbd4; background: rgba(0,0,0,0.6); padding: 6px 14px; border-radius: 16px; pointer-events: none; }
|
|
367
367
|
#toast { position: absolute; bottom: 60px; left: 50%; margin-left: -300px; width: 600px; text-align: center; z-index: 40; background: rgba(0,0,0,0.85); padding: 18px 24px; border-radius: 10px; font-size: 22px; }
|
|
368
|
-
#spinner { position: absolute; top: 0; left: 0; width:
|
|
368
|
+
#spinner { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 50; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(0,0,0,0.3); }
|
|
369
369
|
/* status line under the spinner ("Opening stream at 1080p…"); empty when unused */
|
|
370
370
|
#spinner-msg { margin-top: 26px; font-size: 28px; color: #fff; text-shadow: 0 2px 6px #000; text-align: center; }
|
|
371
371
|
.spin { width: 70px; height: 70px; border: 7px solid rgba(255,255,255,0.2); border-top-color: #818cf8; border-radius: 50%; -webkit-animation: sp 0.9s linear infinite; animation: sp 0.9s linear infinite; }
|
|
@@ -373,7 +373,10 @@ html, body {
|
|
|
373
373
|
@keyframes sp { to { transform: rotate(360deg); } }
|
|
374
374
|
|
|
375
375
|
/* Home top row: tab pills + Search + Settings on the left, StremioK logo on the right */
|
|
376
|
-
|
|
376
|
+
/* the items scroll sideways in their strip when they don't all fit (position: the items' offsets are the strip's) */
|
|
377
|
+
.home-header .menu-scroll { flex: 1 1 auto; min-width: 0; overflow: hidden; position: relative; display: flex; align-items: center; margin-right: 24px; padding: 4px 0; }
|
|
378
|
+
.home-header .tabbar-inline { display: flex; align-items: center; flex-shrink: 0; }
|
|
379
|
+
.home-header .menu-scroll > .hchip { flex-shrink: 0; }
|
|
377
380
|
.tab-pill { display: inline-flex; align-items: center; font-size: 22px; padding: 8px 9px; margin-right: 5px; border-radius: 28px; background: #1a1f2b; color: #aeb6c4; border: 4px solid transparent; white-space: nowrap; }
|
|
378
381
|
.tab-ic { margin-left: 6px; font-size: 22px; }
|
|
379
382
|
.tab-pill.active { background: #2a3346; color: #fff; }
|
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.2.
|
|
7
|
+
<link rel="stylesheet" href="css/style.css?v=2.2.2">
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<!-- CSS-applied sentinel (kept off-screen); app.js waits for its width before rendering -->
|
|
@@ -53,37 +53,37 @@
|
|
|
53
53
|
|
|
54
54
|
<!-- Load order matters: polyfills -> util -> iptv -> data -> input -> tvapps -> player parts -> player -> ytplay -> view parts -> views -> app -->
|
|
55
55
|
<!-- ?v= busts the TV webview cache on every release (keep in sync with package.json) -->
|
|
56
|
-
<script src="js/polyfills.js?v=2.2.
|
|
57
|
-
<script src="js/util.js?v=2.2.
|
|
58
|
-
<script src="js/iptv.js?v=2.2.
|
|
59
|
-
<script src="js/epg.js?v=2.2.
|
|
60
|
-
<script src="js/youtube.js?v=2.2.
|
|
61
|
-
<script src="js/stremio.js?v=2.2.
|
|
62
|
-
<script src="js/trakt.js?v=2.2.
|
|
63
|
-
<script src="js/keys.js?v=2.2.
|
|
64
|
-
<script src="js/focus.js?v=2.2.
|
|
65
|
-
<script src="js/tvapps.js?v=2.2.
|
|
66
|
-
<script src="js/player-core.js?v=2.2.
|
|
67
|
-
<script src="js/player-store.js?v=2.2.
|
|
68
|
-
<script src="js/player-lang.js?v=2.2.
|
|
69
|
-
<script src="js/player-quality.js?v=2.2.
|
|
70
|
-
<script src="js/player-subs.js?v=2.2.
|
|
71
|
-
<script src="js/player-audio.js?v=2.2.
|
|
72
|
-
<script src="js/player-controls.js?v=2.2.
|
|
73
|
-
<script src="js/player.js?v=2.2.
|
|
74
|
-
<script src="js/ytplay.js?v=2.2.
|
|
75
|
-
<script src="js/local.js?v=2.2.
|
|
76
|
-
<script src="js/downloads.js?v=2.2.
|
|
77
|
-
<script src="js/stremio-account.js?v=2.2.
|
|
78
|
-
<script src="js/view-core.js?v=2.2.
|
|
79
|
-
<script src="js/view-home.js?v=2.2.
|
|
80
|
-
<script src="js/view-guide.js?v=2.2.
|
|
81
|
-
<script src="js/view-detail.js?v=2.2.
|
|
82
|
-
<script src="js/view-search.js?v=2.2.
|
|
83
|
-
<script src="js/view-settings.js?v=2.2.
|
|
84
|
-
<script src="js/view-discover.js?v=2.2.
|
|
85
|
-
<script src="js/views.js?v=2.2.
|
|
86
|
-
<script src="js/app.js?v=2.2.
|
|
87
|
-
<script src="js/hubpreview.js?v=2.2.
|
|
56
|
+
<script src="js/polyfills.js?v=2.2.2"></script>
|
|
57
|
+
<script src="js/util.js?v=2.2.2"></script>
|
|
58
|
+
<script src="js/iptv.js?v=2.2.2"></script>
|
|
59
|
+
<script src="js/epg.js?v=2.2.2"></script>
|
|
60
|
+
<script src="js/youtube.js?v=2.2.2"></script>
|
|
61
|
+
<script src="js/stremio.js?v=2.2.2"></script>
|
|
62
|
+
<script src="js/trakt.js?v=2.2.2"></script>
|
|
63
|
+
<script src="js/keys.js?v=2.2.2"></script>
|
|
64
|
+
<script src="js/focus.js?v=2.2.2"></script>
|
|
65
|
+
<script src="js/tvapps.js?v=2.2.2"></script>
|
|
66
|
+
<script src="js/player-core.js?v=2.2.2"></script>
|
|
67
|
+
<script src="js/player-store.js?v=2.2.2"></script>
|
|
68
|
+
<script src="js/player-lang.js?v=2.2.2"></script>
|
|
69
|
+
<script src="js/player-quality.js?v=2.2.2"></script>
|
|
70
|
+
<script src="js/player-subs.js?v=2.2.2"></script>
|
|
71
|
+
<script src="js/player-audio.js?v=2.2.2"></script>
|
|
72
|
+
<script src="js/player-controls.js?v=2.2.2"></script>
|
|
73
|
+
<script src="js/player.js?v=2.2.2"></script>
|
|
74
|
+
<script src="js/ytplay.js?v=2.2.2"></script>
|
|
75
|
+
<script src="js/local.js?v=2.2.2"></script>
|
|
76
|
+
<script src="js/downloads.js?v=2.2.2"></script>
|
|
77
|
+
<script src="js/stremio-account.js?v=2.2.2"></script>
|
|
78
|
+
<script src="js/view-core.js?v=2.2.2"></script>
|
|
79
|
+
<script src="js/view-home.js?v=2.2.2"></script>
|
|
80
|
+
<script src="js/view-guide.js?v=2.2.2"></script>
|
|
81
|
+
<script src="js/view-detail.js?v=2.2.2"></script>
|
|
82
|
+
<script src="js/view-search.js?v=2.2.2"></script>
|
|
83
|
+
<script src="js/view-settings.js?v=2.2.2"></script>
|
|
84
|
+
<script src="js/view-discover.js?v=2.2.2"></script>
|
|
85
|
+
<script src="js/views.js?v=2.2.2"></script>
|
|
86
|
+
<script src="js/app.js?v=2.2.2"></script>
|
|
87
|
+
<script src="js/hubpreview.js?v=2.2.2"></script>
|
|
88
88
|
</body>
|
|
89
89
|
</html>
|
package/js/app.js
CHANGED
|
@@ -5,7 +5,6 @@ var App = (function () {
|
|
|
5
5
|
var _current = 'home';
|
|
6
6
|
var _stack = []; // frames: { screen, focus }
|
|
7
7
|
var _lastBackAt = 0; // debounce BACK across keydown + tizenhwkey
|
|
8
|
-
var _exitArmedAt = 0; // double-Back-to-exit guard on the home root
|
|
9
8
|
|
|
10
9
|
function screenEl(name) { return U.byId('screen-' + name); }
|
|
11
10
|
|
|
@@ -121,8 +120,8 @@ var App = (function () {
|
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
// On a main page (Home's tabs, Discover, Search, Settings), BACK goes to the top menu first
|
|
124
|
-
// (its current item; DOWN comes back), and from there
|
|
125
|
-
//
|
|
123
|
+
// (its current item; DOWN comes back), and from there to the page before, or (nothing
|
|
124
|
+
// before: Home) asks "Exit StremioK?". Also a way out if focus is ever lost on a page.
|
|
126
125
|
var MAIN_SCREENS = { home: 1, discover: 1, search: 1, settings: 1 };
|
|
127
126
|
function toTopMenu() {
|
|
128
127
|
if (!MAIN_SCREENS[_current]) { return false; }
|
|
@@ -145,7 +144,7 @@ var App = (function () {
|
|
|
145
144
|
if (_current === 'home' && Views.backInLocal && Views.backInLocal()) { return; } // (the Local tab: up a folder)
|
|
146
145
|
if (toTopMenu()) { return; }
|
|
147
146
|
var f = _stack.pop();
|
|
148
|
-
if (!f) {
|
|
147
|
+
if (!f) { askExit(); return; }
|
|
149
148
|
if (f.screen !== _current) { abandon(_current); }
|
|
150
149
|
showScreen(f.screen);
|
|
151
150
|
if (f.detail) { Views.restoreDetail(f.detail); return; } // the previous title / person page
|
|
@@ -158,21 +157,21 @@ var App = (function () {
|
|
|
158
157
|
}
|
|
159
158
|
}
|
|
160
159
|
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
160
|
+
// Asked, so a stray BACK (one left over from closing the player) never closes the app; focus
|
|
161
|
+
// starts on Exit, so BACK, OK leaves.
|
|
162
|
+
function askExit() { Views.confirm('Exit StremioK?', 'Exit', exitApp, { plain: true, focusYes: true }); }
|
|
163
|
+
// The TV's own exit where StremioK has it (the sideloaded app). A TizenBrew module's page
|
|
164
|
+
// doesn't get it: the window closed instead, and if it's still open, the TV's way said.
|
|
165
|
+
function exitApp() {
|
|
166
|
+
U.sessionState('exit');
|
|
167
|
+
try {
|
|
168
|
+
if (window.tizen && tizen.application) { tizen.application.getCurrentApplication().exit(); return; }
|
|
169
|
+
} catch (e) { U.log('app', 'Exit: the TV\'s exit failed (' + e.message + ')'); }
|
|
170
|
+
try { window.close(); } catch (e2) {}
|
|
171
|
+
setTimeout(function () {
|
|
172
|
+
U.log('app', 'Exit: still open after closing the window (' + (typeof YTPlay !== 'undefined' && YTPlay.installMode ? YTPlay.installMode() : '?') + ')');
|
|
173
|
+
U.toast('Hold BACK to close StremioK', 4000);
|
|
174
|
+
}, 500);
|
|
176
175
|
}
|
|
177
176
|
|
|
178
177
|
function onKey(action) {
|
package/js/focus.js
CHANGED
|
@@ -42,7 +42,7 @@ var Focus = (function () {
|
|
|
42
42
|
// Scrolls each overflowing box around the node so it's in view, with a margin. A box
|
|
43
43
|
// marked __noScroll only clips (a page's text block): it never moves.
|
|
44
44
|
function ensureVisible(node) {
|
|
45
|
-
var p = node.parentNode;
|
|
45
|
+
var p = node.parentNode, z = (typeof U !== 'undefined' && U.zoom) ? U.zoom() : 1; // (rects are on screen, zoomed; scrolling isn't)
|
|
46
46
|
while (p && p.nodeType === 1 && p !== document.body) {
|
|
47
47
|
var canX = !p.__noScroll && p.scrollWidth > p.clientWidth + 2;
|
|
48
48
|
var canY = !p.__noScroll && p.scrollHeight > p.clientHeight + 2;
|
|
@@ -51,13 +51,13 @@ var Focus = (function () {
|
|
|
51
51
|
var nr = node.getBoundingClientRect();
|
|
52
52
|
if (canX) {
|
|
53
53
|
var mx = 90;
|
|
54
|
-
if (nr.left < pr.left + mx) { p.scrollLeft -= (pr.left + mx - nr.left); }
|
|
55
|
-
else if (nr.right > pr.right - mx) { p.scrollLeft += (nr.right - (pr.right - mx)); }
|
|
54
|
+
if (nr.left < pr.left + mx) { p.scrollLeft -= (pr.left + mx - nr.left) / z; }
|
|
55
|
+
else if (nr.right > pr.right - mx) { p.scrollLeft += (nr.right - (pr.right - mx)) / z; }
|
|
56
56
|
}
|
|
57
57
|
if (canY) {
|
|
58
58
|
var my = 70;
|
|
59
|
-
if (nr.top < pr.top + my) { p.scrollTop -= (pr.top + my - nr.top); }
|
|
60
|
-
else if (nr.bottom > pr.bottom - my) { p.scrollTop += (nr.bottom - (pr.bottom - my)); }
|
|
59
|
+
if (nr.top < pr.top + my) { p.scrollTop -= (pr.top + my - nr.top) / z; }
|
|
60
|
+
else if (nr.bottom > pr.bottom - my) { p.scrollTop += (nr.bottom - (pr.bottom - my)) / z; }
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
p = p.parentNode;
|
package/js/iptv.js
CHANGED
|
@@ -467,7 +467,7 @@ var IPTV = (function () {
|
|
|
467
467
|
var msg = (e && e.message) ? e.message : 'Could not fetch the playlist URL';
|
|
468
468
|
// status 0 / network error on a URL that opens fine in a browser almost
|
|
469
469
|
// always means the server sent no Access-Control-Allow-Origin header.
|
|
470
|
-
if (/HTTP 0\b/.test(msg) || /Network error/i.test(msg)) {
|
|
470
|
+
if (/HTTP 0\b/.test(msg) || /Network error|^No answer/i.test(msg)) {
|
|
471
471
|
msg += ' — the playlist server likely isn\'t sending an ' +
|
|
472
472
|
'Access-Control-Allow-Origin header (CORS). Serve the .m3u with CORS enabled.';
|
|
473
473
|
}
|
|
@@ -544,6 +544,19 @@ var IPTV = (function () {
|
|
|
544
544
|
return d;
|
|
545
545
|
});
|
|
546
546
|
}
|
|
547
|
+
// Auto-refresh (Settings › Content › IPTV): every this many hours (0: Off), all playlists and
|
|
548
|
+
// their guides downloaded again, as Reload does (the Home ticker: see view-home's
|
|
549
|
+
// iptvRefreshTick). EVERY: the choices.
|
|
550
|
+
var EVERY_KEY = 'stremiok.iptv.every', EVERY = [0, 1, 3, 6, 12];
|
|
551
|
+
function refreshEvery() { var h = +U.store.get(EVERY_KEY, 0); return EVERY.indexOf(h) !== -1 ? h : 0; }
|
|
552
|
+
function setRefreshEvery(h) { h = +h; U.store.set(EVERY_KEY, EVERY.indexOf(h) !== -1 ? h : 0); }
|
|
553
|
+
// When the playlists switched on were last downloaded: the oldest of them (a saved copy's own
|
|
554
|
+
// date), 0 when one never was.
|
|
555
|
+
function readAt() {
|
|
556
|
+
var on = getPlaylists().filter(function (p) { return !p.off; }), t = Infinity;
|
|
557
|
+
on.forEach(function (p) { var st = _status[p.id]; t = Math.min(t, st && st.fetchedAt ? st.fetchedAt : 0); });
|
|
558
|
+
return on.length ? t : Infinity;
|
|
559
|
+
}
|
|
547
560
|
function status(id) {
|
|
548
561
|
if (id) { return _status[id] || { id: id, url: '', count: 0, groups: 0, error: '', title: '', fetchedAt: 0 }; }
|
|
549
562
|
return _status;
|
|
@@ -649,7 +662,7 @@ var IPTV = (function () {
|
|
|
649
662
|
removePlaylist: removePlaylist, renamePlaylist: renamePlaylist, setPlaylistOff: setPlaylistOff,
|
|
650
663
|
movePlaylist: movePlaylist, setPlaylistGuide: setPlaylistGuide, label: labelOf,
|
|
651
664
|
xtreamParts: xtreamParts, xtreamUrl: xtreamUrl, xtreamCheck: xtreamCheck, addXtream: addXtream, xtreamFormat: xtreamFormat, setXtreamFormat: setXtreamFormat,
|
|
652
|
-
isFavorite: isFavorite, setFavorite: setFavorite,
|
|
665
|
+
isFavorite: isFavorite, setFavorite: setFavorite, refreshEvery: refreshEvery, setRefreshEvery: setRefreshEvery, readAt: readAt,
|
|
653
666
|
channels: function () { return _cache ? _cache.channels : []; }, channel: function (id) { return _cache ? _cache.byId[id] || null : null; }, isEnabled: isEnabled, reset: reset, status: status,
|
|
654
667
|
load: load, reload: reload, catalog: catalog, meta: meta, streams: streams, synthAddon: synthAddon, isReady: isReady,
|
|
655
668
|
onProgress: onProgress
|
package/js/player-core.js
CHANGED
|
@@ -178,12 +178,20 @@ var PlayerParts = {};
|
|
|
178
178
|
} else {
|
|
179
179
|
all[k] = { id: S.ctx.id, type: S.ctx.type, name: S.ctx.name, poster: S.ctx.poster, background: S.ctx.background, parent: S.ctx.parent,
|
|
180
180
|
position: Math.floor(S.cur), duration: Math.floor(S.dur), updated: Date.now(),
|
|
181
|
-
url: S.ctx.yt || S.ctx
|
|
181
|
+
url: S.ctx.yt || savedUrl(S.ctx), headers: S.ctx._headers, // the exact stream, to resume it directly (YouTube: its page)
|
|
182
|
+
torrent: torrentOf(S.ctx.stream), // (through a streaming server: its link made again from the one set then)
|
|
182
183
|
fileHints: (P.fileHints && P.fileHints(S.ctx.subExtra)) || undefined, // (its file, for subtitles made for it)
|
|
183
184
|
added: added || undefined };
|
|
184
185
|
}
|
|
185
186
|
writeProgress(all);
|
|
186
187
|
}
|
|
188
|
+
// What's saved to resume: a converted playback's own link (not the server's conversion), and a
|
|
189
|
+
// torrent's infoHash, file and trackers (its link depends on the server set when it resumes).
|
|
190
|
+
function savedUrl(ctx) { return (ctx._convertedFrom && /\/hlsv2\//.test(ctx.url || '')) ? ctx._convertedFrom : ctx.url; }
|
|
191
|
+
function torrentOf(st) {
|
|
192
|
+
if (!st || !st._torrent) { return undefined; }
|
|
193
|
+
return { infoHash: st.infoHash, fileIdx: st.fileIdx, sources: Array.isArray(st.sources) ? st.sources.slice(0, 20) : undefined };
|
|
194
|
+
}
|
|
187
195
|
|
|
188
196
|
// ---------- OSD ----------
|
|
189
197
|
// Subtitles move up above the progress bar while it's on screen (above the UP bar too,
|
|
@@ -400,7 +408,7 @@ var PlayerParts = {};
|
|
|
400
408
|
ctx.localSubs = s.localSubs; ctx.localFile = s.localFile; ctx.subExtra = s.subExtra; ctx.sourceSubs = s.sourceSubs;
|
|
401
409
|
ctx.formatInfo = (s.stream && typeof Stremio !== 'undefined' && Stremio.streamInfo) ? Stremio.streamInfo(s.stream) : undefined;
|
|
402
410
|
ctx.stream = s.stream; // (the addon's source: ⬇ Download, player-controls)
|
|
403
|
-
var learn = (old.learnFor && s.stream) ? old.learnFor(s.stream) : {};
|
|
411
|
+
var learn = (old.learnFor && s.stream) ? old.learnFor(s.stream, { converted: !!s.converted }) : {}; // (converted: not what the TV plays itself)
|
|
404
412
|
ctx.onStarted = learn.onStarted; ctx.onFailed = learn.onFailed;
|
|
405
413
|
ctx.resumeSec = keep ? old.resumeSec : (S.isLive ? 0 : S.cur);
|
|
406
414
|
if (keep) { ctx.fromStart = old.fromStart; }
|
|
@@ -414,6 +422,8 @@ var PlayerParts = {};
|
|
|
414
422
|
// Replacing a YouTube stream that's playing (CH+/CH− to the next channel): the TV's
|
|
415
423
|
// player can freeze on an open straight after closing it, so settle first (startAV).
|
|
416
424
|
var settle = !!(S.active && S.ctx && S.ctx.yt);
|
|
425
|
+
if (S.ctx !== ctx) { stopLiveLook(); } // (another channel: the look at the one before stopped)
|
|
426
|
+
stopFileLook();
|
|
417
427
|
S.ctx = ctx; _onExit = onExit;
|
|
418
428
|
// (private viewing, a link played as it is, or one started from a private playback —
|
|
419
429
|
// a video followed from it, another source: nothing recorded, see player-store)
|
|
@@ -445,7 +455,27 @@ var PlayerParts = {};
|
|
|
445
455
|
renderOSD(); // show the NEW title immediately (not the previous item's)
|
|
446
456
|
U.spinner(true, ctx.switching ? 'Switching source…' : ctx.initialQualityHeight ? 'Opening stream at ' + ctx.initialQualityHeight + 'p…' : 'Opening stream…');
|
|
447
457
|
|
|
448
|
-
|
|
458
|
+
// (a TorrServer torrent that doesn't say which of its files: its file list asked for first)
|
|
459
|
+
var go = function () { if (useAV) { startLive(ctx, settle); } else { startHTML5(ctx.url); } };
|
|
460
|
+
if (ctx.stream && ctx.stream._pickFile && typeof Stremio !== 'undefined' && Stremio.torrentFile) {
|
|
461
|
+
// (the source before stopped first: its callbacks are about it, not this one)
|
|
462
|
+
++_playToken; _openedAt = 0;
|
|
463
|
+
if (useAV) { try { avplay.stop(); } catch (e0) {} try { avplay.close(); } catch (e1) {} }
|
|
464
|
+
else if (S.videoEl) { try { S.videoEl.pause(); S.videoEl.removeAttribute('src'); } catch (e2) {} }
|
|
465
|
+
U.spinner(true, 'Asking your streaming server for the torrent’s files…');
|
|
466
|
+
var ep = /:(\d+):(\d+)$/.exec(ctx.id || ''), look = _fileLook = U.cancelToken();
|
|
467
|
+
Stremio.torrentFile(ctx.stream, ep ? +ep[1] : 0, ep ? +ep[2] : 0, look).then(function (url) {
|
|
468
|
+
if (look.cancelled || S.ctx !== ctx || !S.active) { return; }
|
|
469
|
+
_fileLook = null;
|
|
470
|
+
if (url) { ctx.url = url; go(); return; }
|
|
471
|
+
var why = 'The torrent has no file for S' + ep[1] + 'E' + ep[2]; // (another episode isn't played in its place)
|
|
472
|
+
U.log('player', why);
|
|
473
|
+
U.spinner(false);
|
|
474
|
+
if (nextSourceOnFail({ text: why })) { return; }
|
|
475
|
+
U.toast(why + '. Try another source.', 6000);
|
|
476
|
+
exitLater();
|
|
477
|
+
});
|
|
478
|
+
} else { go(); }
|
|
449
479
|
|
|
450
480
|
// A YouTube video (not a live channel): its audio languages and its links (end screen,
|
|
451
481
|
// cards) from the service, for the menu and the overlays (player-quality / -controls).
|
|
@@ -660,7 +690,7 @@ var PlayerParts = {};
|
|
|
660
690
|
});
|
|
661
691
|
try { avplay.setStreamingProperty('ADAPTIVE_INFO', adaptiveInfo()); } catch (e0) {}
|
|
662
692
|
_openedAt = Date.now();
|
|
663
|
-
setTimeout(startCheck,
|
|
693
|
+
setTimeout(startCheck, startMs() + 500);
|
|
664
694
|
avplay.prepareAsync(function () {
|
|
665
695
|
if (!S.active || token !== _playToken) { return; }
|
|
666
696
|
U.spinner(false);
|
|
@@ -1022,6 +1052,21 @@ var PlayerParts = {};
|
|
|
1022
1052
|
U.spinner(true, msg); U.toast(msg, notice ? 7000 : 4000);
|
|
1023
1053
|
return true;
|
|
1024
1054
|
}
|
|
1055
|
+
// With Stremio's streaming server (Settings › Content › Streaming server, Convert on): a source
|
|
1056
|
+
// the TV refused is tried once more, converted by the server, before the next source. Once a
|
|
1057
|
+
// playback (the next sources aren't). Not a source that needs its own request headers.
|
|
1058
|
+
function convertOnFail() {
|
|
1059
|
+
var c = S.ctx;
|
|
1060
|
+
if (!c || !S.active || S.playing || S.isLive || c.trailer || c.yt || c.localFile || !c.stream || c._converted || c._headers) { return false; }
|
|
1061
|
+
var u = (typeof Stremio !== 'undefined' && Stremio.convertUrl) ? Stremio.convertUrl(c.url) : null;
|
|
1062
|
+
if (!u) { return false; }
|
|
1063
|
+
c._converted = true; c._convertedFrom = c.url; // (kept by what replaces it: Continue Watching saves its own link)
|
|
1064
|
+
var msg = 'Didn’t start: converting it on your streaming server…';
|
|
1065
|
+
U.log('player', 'Source didn’t start: converting it on the streaming server');
|
|
1066
|
+
replaceSource({ url: u, stream: c.stream, subExtra: c.subExtra, sourceSubs: c.sourceSubs, localSubs: c.localSubs, converted: true }, true);
|
|
1067
|
+
U.spinner(true, msg); U.toast(msg, 5000);
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1025
1070
|
// A start that failed: is the link a debrid addon's notice video (Torrentio's failed_access,
|
|
1026
1071
|
// limits_exceeded…: noticeOf)? Then that's said, and for the account's ones the next source
|
|
1027
1072
|
// skips the addon's links through that service; then the next source, or what's to say.
|
|
@@ -1033,6 +1078,7 @@ var PlayerParts = {};
|
|
|
1033
1078
|
U.log('player', 'The source sent a notice: ' + notice.kind + (notice.service ? ' (' + notice.service + ')' : ''));
|
|
1034
1079
|
if (notice.account && notice.tag) { c._avoid = (c._avoid || []).concat([{ addon: st._addon, tag: notice.tag }]); }
|
|
1035
1080
|
}
|
|
1081
|
+
if (!notice && convertOnFail()) { return; }
|
|
1036
1082
|
if (nextSourceOnFail(notice)) { return; }
|
|
1037
1083
|
U.toast(notice ? notice.text : why, notice ? 8000 : undefined);
|
|
1038
1084
|
exitLater();
|
|
@@ -1072,15 +1118,21 @@ var PlayerParts = {};
|
|
|
1072
1118
|
return n;
|
|
1073
1119
|
}
|
|
1074
1120
|
// Not playing after AV_START_MS (no answer from the server, or opened and nothing shown): the
|
|
1075
|
-
// next source when there's one; else it stops and says so (not while you've paused it).
|
|
1121
|
+
// next source when there's one; else it stops and says so (not while you've paused it). A
|
|
1122
|
+
// torrent through a streaming server has longer (Settings › Content › Streaming server: finding
|
|
1123
|
+
// people sharing it takes a while).
|
|
1124
|
+
function startMs() {
|
|
1125
|
+
var st = S.ctx && S.ctx.stream;
|
|
1126
|
+
return (st && st._torrent && typeof Stremio !== 'undefined' && Stremio.server) ? Stremio.server().wait * 1000 : AV_START_MS;
|
|
1127
|
+
}
|
|
1076
1128
|
function startCheck() {
|
|
1077
|
-
if (!S.active || S.playing || !useAV || !_openedAt || Date.now() - _openedAt <
|
|
1129
|
+
if (!S.active || S.playing || !useAV || !_openedAt || Date.now() - _openedAt < startMs()) { return; }
|
|
1078
1130
|
if (_reco.tries) { liveDropped('Couldn’t reconnect'); return; } // (a reconnect's try: the next one)
|
|
1079
1131
|
if (nextSourceOnFail()) { return; }
|
|
1080
1132
|
if (S.playStarted && isPaused()) { return; }
|
|
1081
|
-
U.log('player', 'Didn’t start: nothing played in ' + (
|
|
1133
|
+
U.log('player', 'Didn’t start: nothing played in ' + (startMs() / 1000) + ' s (' + where(S.ctx && S.ctx.url) + ')');
|
|
1082
1134
|
U.spinner(false);
|
|
1083
|
-
U.toast('Didn’t start: nothing played in ' + (
|
|
1135
|
+
U.toast('Didn’t start: nothing played in ' + (startMs() / 1000) + ' seconds. Try another source.', 6000);
|
|
1084
1136
|
exitLater();
|
|
1085
1137
|
}
|
|
1086
1138
|
P.startCheck = startCheck;
|
|
@@ -1147,6 +1199,12 @@ var PlayerParts = {};
|
|
|
1147
1199
|
startAV(ctx.url, settle);
|
|
1148
1200
|
}, function () { if (S.ctx === ctx && S.active) { startAV(ctx.url, settle); } });
|
|
1149
1201
|
}
|
|
1202
|
+
// The look under way (_liveLook, a cancel token): stopped when another channel starts or the
|
|
1203
|
+
// player closes (stopLiveLook), the service's downloads for it with it.
|
|
1204
|
+
var _fileLook = null; // (TorrServer's file list asked for: see play)
|
|
1205
|
+
function stopFileLook() { if (_fileLook) { _fileLook.cancel(); _fileLook = null; } }
|
|
1206
|
+
var _liveLook = null;
|
|
1207
|
+
function stopLiveLook() { if (_liveLook) { _liveLook.cancel(); _liveLook = null; } }
|
|
1150
1208
|
function liveCheck() {
|
|
1151
1209
|
var ctx = S.ctx;
|
|
1152
1210
|
if (!ctx || ctx._liveChecked || !liveFixable(ctx)) { return; }
|
|
@@ -1154,7 +1212,10 @@ var PlayerParts = {};
|
|
|
1154
1212
|
var known = liveKnown(ctx.url);
|
|
1155
1213
|
if (known && ccOf(known)) { return; }
|
|
1156
1214
|
var url = ctx.url;
|
|
1157
|
-
|
|
1215
|
+
stopLiveLook();
|
|
1216
|
+
var look = _liveLook = U.cancelToken();
|
|
1217
|
+
YTPlay.liveOpen(url, ctx._headers, false, null, look).then(function (r) {
|
|
1218
|
+
if (_liveLook === look) { _liveLook = null; }
|
|
1158
1219
|
if (!r || !r.sure) { return; } // (couldn't tell: asked again next time)
|
|
1159
1220
|
var cc = (r.ccSure || r.cc === undefined) ? (ccOf(r) || []) : null, fix = known ? !!known.fix : !!r.fix; // (captions with no text yet: asked again next time; the sound's answer: as first found)
|
|
1160
1221
|
liveKeep(url, fix, cc);
|
|
@@ -1278,6 +1339,7 @@ var PlayerParts = {};
|
|
|
1278
1339
|
|
|
1279
1340
|
function exit() {
|
|
1280
1341
|
if (!S.active) { return; }
|
|
1342
|
+
stopLiveLook(); stopFileLook();
|
|
1281
1343
|
if (U.crumb) { U.crumb('player closed'); }
|
|
1282
1344
|
if (P.sleepClear) { P.sleepClear(); } // (the sleep timer: this playback's)
|
|
1283
1345
|
saveProgress();
|
package/js/player-store.js
CHANGED
|
@@ -130,14 +130,14 @@
|
|
|
130
130
|
var SIZE_PX = { small: 28, medium: 36, large: 46, xlarge: 58 };
|
|
131
131
|
function styleSpan(span) {
|
|
132
132
|
var cfg = getSubCfg();
|
|
133
|
-
span.style.fontSize = (SIZE_PX[cfg.size] || 36) + 'px';
|
|
133
|
+
span.style.fontSize = (SIZE_PX[cfg.size] || 36) / U.zoom() + 'px'; // (their own size, whatever the zoom)
|
|
134
134
|
span.style.fontFamily = subFontFamily(cfg.font);
|
|
135
135
|
span.style.color = pick3(SUB_COLORS, cfg.color)[2];
|
|
136
136
|
span.style.background = pick3(SUB_BGS, cfg.bg)[2];
|
|
137
137
|
span.style.textShadow = pick3(SUB_EDGES, cfg.edge)[2];
|
|
138
138
|
if (span.parentNode) { span.parentNode.style.bottom = Math.max(subsPosPx(), (P.S && P.S.subsLift) || 0) + 'px'; } // (raised over the progress bar: core's liftSubs)
|
|
139
139
|
}
|
|
140
|
-
function subsPosPx() { return pick3(SUB_POSITIONS, getSubCfg().pos)[2]; }
|
|
140
|
+
function subsPosPx() { return pick3(SUB_POSITIONS, getSubCfg().pos)[2] / U.zoom(); }
|
|
141
141
|
|
|
142
142
|
// ---------- progress (Continue Watching) ----------
|
|
143
143
|
function progressAll() { return U.store.get(PROGRESS_KEY, {}); }
|