OverviewFilesCommitsIssuesDocsReleasesPeers

stremio-vsembed commits / a855398

Two rows that do something, where the person actually is (0.16.0)
The completeness gate catches a copy that is short. It cannot catch the wrong episode, the wrong season, or audio nobody can listen to. Those need eyes, and the eyes are in front of the TV holding Kore -- which cannot drive Kodi's own UI, so the source list Jacktook already puts up before playing is the only surface they can reach. /library is the right place to audit; it is the wrong place to be standing when you notice. So: a re-download row, offered only when a copy actually exists (with none, _append_mp4_row's "play & keep" already fetches one and this would be offering to redo nothing), and a season row on every series. Both last in the list -- nobody is usually reaching for them and a Kodi source list is navigated by arrow key. These routes are GET, which is the opposite of the POST-only rule on /library/reingest, and the difference is not an oversight. A Stremio row is a URL the player opens, and Kodi opens it with GET; there is no other shape available. What makes it safe is that both job starters already refuse a duplicate, because Kodi issues GET + HEAD + GET for a single stream URL -- the same behaviour that in August turned one fixed Chromium profile into three resolvers fighting over one browser session. That de-duplication is now load-bearing rather than incidental: without it one click is three remuxes. Each answers with the live upstream rather than a status code. That is _serve_mp4's established idiom -- a row has to be playable the instant it is clicked, and a 503 would surface as a playback failure while the work went on invisibly. Verified against real TMDB: Strange New Worlds season 4 lists ten episodes and has aired six, so the season row would fetch only the one missing aired episode and none of the four airing through September. Counting episodes instead of reading air dates would have queued all four, at a browser resolve each, to learn nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a855398953100bf321a13dc3c29c44c77b09ba84
by plankatron <fleeky@gmail.com> · 2026-08-31T14:52:50.000Z

Changed files (2) +261 -1

addon.py +96 -1
……@@ -596,7 +596,7 @@ TV_GENRES = [
596596
597597MANIFEST = {
598598 "id": "org.tabertron.vsembed",
599 "version": "0.15.0",
599 "version": "0.16.0",
600600 "name": "Tabertron VsEmbed",
601601 "description": "Self-hosted HLS extractor with TMDB browse + search, played via vidcore",
602602 "types": ["movie", "series"],
……@@ -2064,6 +2064,44 @@ def _append_mp4_row(out: list, kind: str, ident: Optional[str],
20642064 else "saving to library…", VIDCORE_MAX_HEIGHT)
20652065
20662066
2067def _append_action_rows(out: list, kind: str, ident: Optional[str],
2068 season: int = 0, episode: int = 0) -> None:
2069 """Rows that DO something, for the judgments no measurement can make.
2070
2071 The completeness gate catches a copy that is SHORT. It cannot catch the
2072 wrong episode, the wrong season, or audio nobody can listen to -- those need
2073 eyes, and the eyes are in front of the TV, not the audit page. So the escape
2074 hatch has to be here, in the list Jacktook already puts up before playing,
2075 which is the one surface Kore can drive.
2076
2077 Listed LAST, after every row anyone actually came for: a Kodi source list is
2078 navigated by arrow key.
2079 """
2080 if not ident or not _library_meta(kind, ident, season, episode):
2081 return # cannot be named, so the row would be a lie
2082 if kind == "series":
2083 refetch = f"/refetch/series/{ident}:{season}:{episode}.m3u8"
2084 tag = f"S{season:02d}E{episode:02d} "
2085 else:
2086 refetch = f"/refetch/movie/{ident}.m3u8"
2087 tag = ""
2088
2089 def row(path: str, quality: str, title: str) -> None:
2090 out.append(_stream_dict(Stream(
2091 url=f"http://{request.host}{path}?h={VIDCORE_MAX_HEIGHT}",
2092 quality=quality, name="local", referer="https://vidcore.net/",
2093 title_extra=title)))
2094
2095 # Only when a copy actually exists. With none, _append_mp4_row's "play &
2096 # keep" row already fetches one, and this would be offering to redo nothing.
2097 if _playable_path(kind, ident, season, episode, VIDCORE_MAX_HEIGHT):
2098 row(refetch, "re-download",
2099 f"{tag}wrong copy? re-download\nplays live meanwhile")
2100 if kind == "series":
2101 row(f"/season/series/{ident}:{season}:{episode}.m3u8", "season",
2102 f"get season {season} — aired episodes\nplays this one meanwhile")
2103
2104
20672105def _append_vidcore_row(out: list, kind: str, ident: Optional[str],
20682106 season: int = 0, episode: int = 0) -> None:
20692107 """Additive lazy vidcore rows — one per quality, hidden for known misses.
……@@ -2110,6 +2148,61 @@ def _append_vidcore_row(out: list, kind: str, ident: Optional[str],
21102148 _vidcore_row(request.host, kind, ident, season, episode, height=height)))
21112149
21122150
2151# These MUST be GET. A Stremio row is a URL the player opens, and Kodi opens it
2152# with GET -- the exact opposite of the POST-only rule on /library/reingest.
2153#
2154# It is safe here for a specific reason rather than by luck. Kodi issues
2155# GET + HEAD + GET for a single stream URL (measured 2026-08-10, when a fixed
2156# Chromium profile turned that into three resolvers fighting over one browser
2157# session), so anything these start fires up to three times. Both job starters
2158# already refuse a duplicate: _start_mp4_job returns early while its key is
2159# "running", _start_season_job while that season is. That idempotency is
2160# LOAD-BEARING here, not incidental -- remove it and one click becomes three
2161# remuxes of the same episode.
2162#
2163# Each answers with the live upstream, which is _serve_mp4's established idiom:
2164# a row has to be playable the instant it is clicked, and a 503 would surface as
2165# a playback failure while the work went on invisibly.
2166
2167def _serve_action(kind: str, ident: str, season: int, episode: int,
2168 whole_season: bool = False):
2169 height = _requested_height()
2170 if whole_season:
2171 _start_season_job(ident, season, height)
2172 else:
2173 _start_mp4_job(kind, ident, season, episode, height, force=True)
2174 url = _resolve_vidcore(kind, ident, season, episode,
2175 wait=VIDCORE_PLAY_DEADLINE)
2176 if not url:
2177 abort(404)
2178 return _serve_vidcore(url)
2179
2180
2181@app.route("/refetch/movie/<path:ident>.m3u8")
2182def refetch_movie(ident: str):
2183 if not re.fullmatch(VIDCORE_IDENT, ident):
2184 abort(404)
2185 return _serve_action("movie", ident, 0, 0)
2186
2187
2188@app.route("/refetch/series/<path:ident>.m3u8")
2189def refetch_series(ident: str):
2190 m = re.fullmatch(rf"({VIDCORE_IDENT}):(\d+):(\d+)", ident)
2191 if not m:
2192 abort(404)
2193 return _serve_action("series", m.group(1), int(m.group(2)), int(m.group(3)))
2194
2195
2196@app.route("/season/series/<path:ident>.m3u8")
2197def season_series_row(ident: str):
2198 """Queue the season, and play the episode it was asked from meanwhile."""
2199 m = re.fullmatch(rf"({VIDCORE_IDENT}):(\d+):(\d+)", ident)
2200 if not m:
2201 abort(404)
2202 return _serve_action("series", m.group(1), int(m.group(2)),
2203 int(m.group(3)), whole_season=True)
2204
2205
21132206@app.route("/stream/movie/<path:ident>.json")
21142207def stream_movie(ident: str):
21152208 ident = ident.strip()
……@@ -2118,6 +2211,7 @@ def stream_movie(ident: str):
21182211 out: list = []
21192212 _append_mp4_row(out, "movie", f"tmdb:{fxl.group(1)}")
21202213 _append_vidcore_row(out, "movie", f"tmdb:{fxl.group(1)}")
2214 _append_action_rows(out, "movie", f"tmdb:{fxl.group(1)}")
21212215 return jsonify({"streams": out})
21222216 fmv = re.fullmatch(r"fmv:(\d+):([a-z0-9\-]+)", ident)
21232217 if fmv:
……@@ -2150,6 +2244,7 @@ def stream_series(ident: str):
21502244 out: list = []
21512245 _append_mp4_row(out, "series", f"tmdb:{tmdb}", season, episode)
21522246 _append_vidcore_row(out, "series", f"tmdb:{tmdb}", season, episode)
2247 _append_action_rows(out, "series", f"tmdb:{tmdb}", season, episode)
21532248 return jsonify({"streams": out})
21542249 fmv = re.fullmatch(r"fmv:(\d+):([a-z0-9\-]+):(\d+):(\d+)", ident)
21552250 if fmv:
test_kodi_action_rows.py +165 -0
……@@ -0,0 +1,165 @@
1"""The two rows that DO something, in the list Jacktook shows before playing.
2
3The completeness gate catches a copy that is short. It cannot catch the wrong
4episode, the wrong season, or unwatchable audio -- and the person who can is in
5front of the TV holding Kore, which cannot drive Kodi's own UI. The source list
6is the one surface they can reach, so the escape hatch lives there.
7
8These routes are GET, unlike /library/reingest, because a Stremio row is a URL
9the player opens. Kodi opens a stream URL with GET + HEAD + GET, so each action
10fires up to three times and the job starters' de-duplication is what stops one
11click becoming three remuxes.
12
13Run: .venv/bin/python3 -m pytest test_kodi_action_rows.py -q
14"""
15
16import os
17
18import pytest
19
20import addon
21
22
23@pytest.fixture(autouse=True)
24def isolated(tmp_path, monkeypatch):
25 monkeypatch.setattr(addon, "LIBRARY_ROOT", str(tmp_path))
26 monkeypatch.setattr(addon, "INGEST_QUEUE_FILE",
27 os.path.join(str(tmp_path), "queue.json"))
28 monkeypatch.setattr(addon, "LIBRARY_VERIFIED_FILE",
29 os.path.join(str(tmp_path), "verified.json"))
30 monkeypatch.setattr(addon, "_library_meta_cache", {})
31 monkeypatch.setattr(addon, "_duration_cache", {})
32 monkeypatch.setattr(addon, "_mp4_jobs", {})
33 monkeypatch.setattr(addon, "_season_jobs", {})
34 monkeypatch.setattr(addon, "_tmdb_get", lambda path, params=None: {
35 "tv/1": {"name": "Show", "first_air_date": "2020-01-01"},
36 "tv/1/season/4/episode/4": {"name": "Four", "runtime": 50},
37 "movie/9": {"title": "Film", "release_date": "2020-01-01", "runtime": 100},
38 }[path])
39 yield
40
41
42@pytest.fixture
43def has_copy(monkeypatch):
44 monkeypatch.setattr(addon, "_playable_path", lambda *a, **k: "/some/file.mp4")
45
46
47@pytest.fixture
48def no_copy(monkeypatch):
49 monkeypatch.setattr(addon, "_playable_path", lambda *a, **k: None)
50
51
52def _rows(kind="series", ident="tmdb:1", season=4, episode=4):
53 out = []
54 with addon.app.test_request_context("/"):
55 addon._append_action_rows(out, kind, ident, season, episode)
56 return out
57
58
59def _titles(rows):
60 return " | ".join(r["title"] for r in rows)
61
62
63# --- which rows are offered -------------------------------------------------
64
65def test_a_held_copy_gets_a_re_download_row(has_copy):
66 assert "re-download" in _titles(_rows())
67
68
69def test_no_copy_means_no_re_download_row(no_copy):
70 """_append_mp4_row's "play & keep" already fetches one. Offering to redo
71 nothing would be a dead row."""
72 assert "re-download" not in _titles(_rows())
73
74
75def test_a_series_always_gets_a_season_row(no_copy):
76 assert "get season 4" in _titles(_rows())
77
78
79def test_a_movie_gets_no_season_row(has_copy):
80 titles = _titles(_rows("movie", "tmdb:9", 0, 0))
81 assert "re-download" in titles and "season" not in titles
82
83
84def test_an_unidentifiable_title_gets_no_rows(has_copy):
85 assert _rows("series", "tmdb:404", 1, 1) == []
86
87
88def test_the_rows_point_at_this_host(has_copy):
89 urls = [r["url"] for r in _rows()]
90 assert any("/refetch/series/tmdb:1:4:4.m3u8" in u for u in urls)
91 assert any("/season/series/tmdb:1:4:4.m3u8" in u for u in urls)
92
93
94# --- the routes -------------------------------------------------------------
95
96@pytest.fixture
97def route(monkeypatch):
98 """Routes with the work stubbed, recording what each would have started."""
99 jobs, seasons = [], []
100 monkeypatch.setattr(addon, "_start_mp4_job",
101 lambda *a, **k: (jobs.append((a, k)), "running")[1])
102 monkeypatch.setattr(addon, "_start_season_job",
103 lambda *a, **k: (seasons.append(a), "running")[1])
104 monkeypatch.setattr(addon, "_resolve_vidcore",
105 lambda *a, **k: "http://cdn/master.m3u8")
106 monkeypatch.setattr(addon, "_cap_variant", lambda url, h: f"{url}?h={h}")
107 return type("R", (), {"jobs": jobs, "seasons": seasons,
108 "client": addon.app.test_client()})()
109
110
111def test_refetch_forces_a_re_ingest_and_plays_live(route):
112 r = route.client.get("/refetch/series/tmdb:1:4:4.m3u8?h=1080")
113 assert r.status_code == 302 and "cdn/master.m3u8" in r.headers["Location"]
114 assert route.jobs[0][0] == ("series", "tmdb:1", 4, 4, 1080)
115 assert route.jobs[0][1]["force"] is True
116
117
118def test_the_season_row_queues_the_season_and_plays_the_episode(route):
119 r = route.client.get("/season/series/tmdb:1:4:4.m3u8?h=1080")
120 assert r.status_code == 302
121 assert route.seasons[0] == ("tmdb:1", 4, 1080)
122 assert route.jobs == [] # the episode is not forced too
123
124
125def test_a_movie_refetch_works(route):
126 r = route.client.get("/refetch/movie/tmdb:9.m3u8?h=1080")
127 assert r.status_code == 302
128 assert route.jobs[0][0] == ("movie", "tmdb:9", 0, 0, 1080)
129
130
131def test_a_malformed_ident_is_refused(route):
132 for url in ("/refetch/series/nope.m3u8", "/season/series/tmdb:1.m3u8",
133 "/refetch/movie/../etc.m3u8"):
134 assert route.client.get(url).status_code == 404
135
136
137def test_an_unresolvable_title_does_not_pretend_to_play(route, monkeypatch):
138 monkeypatch.setattr(addon, "_resolve_vidcore", lambda *a, **k: None)
139 assert route.client.get("/refetch/series/tmdb:1:4:4.m3u8").status_code == 404
140
141
142# --- the de-duplication these routes lean on --------------------------------
143
144def test_kodis_three_requests_start_one_job(monkeypatch):
145 """Kodi opens a stream URL with GET + HEAD + GET. Without this guard one
146 click would be three remuxes of the same episode."""
147 started = []
148 monkeypatch.setattr(addon.threading, "Thread",
149 lambda *a, **k: type("T", (), {
150 "start": lambda self: started.append(k.get("args")),
151 "daemon": True})())
152 for _ in range(3):
153 addon._start_mp4_job("series", "tmdb:1", 4, 4, 1080, force=True)
154 assert len(started) == 1
155
156
157def test_a_second_season_request_is_refused_while_one_runs(monkeypatch):
158 monkeypatch.setattr(addon, "_season_jobs", {"season:tmdb:1:4:1080": "running"})
159 started = []
160 monkeypatch.setattr(addon.threading, "Thread",
161 lambda *a, **k: type("T", (), {
162 "start": lambda self: started.append(1),
163 "daemon": True})())
164 assert addon._start_season_job("tmdb:1", 4, 1080) == "running"
165 assert started == []