OverviewFilesCommitsIssuesDocsReleasesPeers

stremio-vsembed commits / 8d3f17c

Preview relay: allow the URLs the playlist lists; delete answers at once (0.26.1)
Live on stortron, 7 of 8 relay requests got 403 from the relay's own guard: the playlist came from one CDN host and its segments from another (paperlake.top), so "same host as the playlist" refused every segment. The relay now records the exact absolute URLs its relayed playlist handed out and relays only those -- still not an open proxy, and a segment requested before any playlist is refused. Red first. Also found: the tests shared one module-level preview dict, so one test's allow-list let another pass by ordering. _live() now copies it. Delete: the Jellyfin notice runs in the background. Measured live: Jellyfin took ~5 s to answer and the click waited on it. Suite 468. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8d3f17c3fc32a4260224345e11ba17cd9bee3040
by plankatron <fleeky@gmail.com> · 2026-10-05T01:16:18.000Z

Changed files (3) +68 -7

addon.py +15 -6
……@@ -619,7 +619,7 @@ TV_GENRES = [
619619
620620MANIFEST = {
621621 "id": "org.tabertron.vsembed",
622 "version": "0.26.0",
622 "version": "0.26.1",
623623 "name": "Tabertron VsEmbed",
624624 "description": "Self-hosted extractor with TMDB browse + search, played via vidcore and HdHub",
625625 "types": ["movie", "series"],
……@@ -4696,7 +4696,10 @@ def _delete_copy(kind: str, tmdb: str, season: int, episode: int) -> tuple[int,
46964696 "took": 0, "dest": dest})
46974697 finally:
46984698 lock.release()
4699 _notify_jellyfin(dest, "Deleted")
4699 # In the background: measured live, Jellyfin took ~5 s to answer, and the
4700 # click waited on it. The delete is complete either way.
4701 threading.Thread(target=_notify_jellyfin, args=(dest, "Deleted"),
4702 name="jellyfin-notify", daemon=True).start()
47004703 return 200, f"deleted {os.path.basename(dest)}"
47014704
47024705
……@@ -4911,9 +4914,15 @@ def web_relay_playlist():
49114914 log.warning("preview: playlist fetch failed for %s (%s)", key, exc)
49124915 abort(502)
49134916 k = quote(key, safe="")
4917 # Exactly the URLs this playlist lists may be relayed. Not "same host as
4918 # the playlist": measured live, segments came from a different CDN host
4919 # (paperlake.top) than the playlist itself.
4920 allowed = pv.setdefault("allowed", set())
49144921
49154922 def relayed(uri: str) -> str:
4916 return f"/web/relay/seg?key={k}&u={quote(urljoin(pv['url'], uri), safe='')}"
4923 absolute = urljoin(pv["url"], uri)
4924 allowed.add(absolute)
4925 return f"/web/relay/seg?key={k}&u={quote(absolute, safe='')}"
49174926
49184927 out = []
49194928 for line in r.text.splitlines():
……@@ -4930,12 +4939,12 @@ def web_relay_playlist():
49304939
49314940@app.route("/web/relay/seg")
49324941def web_relay_seg():
4933 """One segment, fetched with the referer the CDN demands. Only from the
4934 host of this job's own playlist -- never an open proxy."""
4942 """One segment, fetched with the referer the CDN demands. Only a URL the
4943 job's own relayed playlist listed -- never an open proxy."""
49354944 key = request.args.get("key") or ""
49364945 pv = _relay_target(key)
49374946 u = request.args.get("u") or ""
4938 if urlparse(u).scheme != "https" or urlparse(u).netloc != urlparse(pv["url"]).netloc:
4947 if u not in pv.get("allowed", ()):
49394948 abort(403)
49404949 headers = {"User-Agent": UA, "Referer": VIDCORE_REFERER}
49414950 if request.headers.get("Range"):
test_web_delete.py +17 -0
……@@ -209,11 +209,28 @@ def test_a_movie_delete_returns_to_the_movie(client):
209209
210210
211211def test_delete_notifies_jellyfin(client, isolated):
212 import threading
212213 path = _hold("series", 2, 6)
213214 _post(client)
215 for t in threading.enumerate():
216 if t.name == "jellyfin-notify":
217 t.join(5)
214218 assert isolated == [(path, "Deleted")]
215219
216220
221def test_delete_does_not_wait_for_jellyfin(client, monkeypatch):
222 """Measured live: Jellyfin took ~5 s to answer and the click waited."""
223 import threading
224 import time
225 gate = threading.Event()
226 monkeypatch.setattr(addon, "_notify_jellyfin", lambda p, t: gate.wait(5))
227 _hold("series", 2, 6)
228 t0 = time.time()
229 assert _post(client).status_code == 303
230 assert time.time() - t0 < 1
231 gate.set()
232
233
217234@pytest.mark.parametrize("data", [
218235 {"kind": "series", "tmdb": "../1", "season": "2", "episode": "6"},
219236 {"kind": "anime", "tmdb": "1", "season": "2", "episode": "6"},
test_web_preview.py +36 -1
……@@ -63,7 +63,9 @@ def _live(preview=None, phase="downloading"):
6363 "kind": "series", "ident": "tmdb:1", "season": 2, "episode": 6,
6464 "height": 1080, "label": "S02E06", "title": "Show"}
6565 if preview:
66 addon._job_info[KEY]["preview"] = preview
66 # A copy: the relay records its allow-list on the preview dict, and a
67 # shared module-level dict let one test's allow-list pass another.
68 addon._job_info[KEY]["preview"] = dict(preview)
6769 addon._mp4_jobs[KEY] = "running"
6870
6971
……@@ -237,10 +239,14 @@ def test_a_segment_is_relayed_with_the_referer_and_range(client, monkeypatch):
237239
238240 def get(url, headers=None, timeout=None, stream=False):
239241 calls.append((url, headers))
242 if url == VARIANT:
243 return FakeResp(PLAYLIST.encode())
240244 return FakeResp(b"MOOF", status=206,
241245 headers={"Content-Type": "text/html",
242246 "Content-Range": "bytes 0-3/10"})
243247 monkeypatch.setattr(addon.requests, "get", get)
248 client.get(f"/web/relay/playlist?key={quote(KEY)}") # as hls.js does first
249 calls.clear()
244250 u = "https://cdn.example/vd/abc/sd/seg-1.html?token=a"
245251 r = client.get(f"/web/relay/seg?key={quote(KEY)}&u={quote(u, safe='')}",
246252 headers={"Range": "bytes=0-3"})
……@@ -257,6 +263,7 @@ def test_the_relay_is_not_an_open_proxy(client, monkeypatch):
257263 monkeypatch.setattr(addon.requests, "get",
258264 lambda *a, **k: pytest.fail("fetched a foreign host"))
259265 u = "https://evil.example/x"
266 # Never listed by any playlist, so never fetched.
260267 assert client.get(f"/web/relay/seg?key={quote(KEY)}&u={quote(u, safe='')}").status_code == 403
261268
262269
……@@ -273,3 +280,31 @@ def test_an_upstream_error_is_passed_on_not_crashed(client, monkeypatch):
273280 monkeypatch.setattr(addon.requests, "get",
274281 lambda *a, **k: FakeResp(b"no", status=403))
275282 assert client.get(f"/web/relay/playlist?key={quote(KEY)}").status_code == 502
283
284
285def test_segments_on_a_different_cdn_host_are_relayed(client, monkeypatch):
286 """Measured live 2026-10-04: the playlist came from one host and its
287 segments from another (paperlake.top). The relay allows exactly the URLs
288 the job's own relayed playlist listed -- still not an open proxy."""
289 _live(HLS)
290 other = "https://paperlake.top/vd/x/seg-9.html?token=z"
291 body = PLAYLIST.replace("seg-1.html?token=a", other)
292 calls = []
293
294 def get(url, headers=None, timeout=None, stream=False):
295 calls.append(url)
296 return FakeResp(body.encode() if url == VARIANT else b"SEG", status=200)
297 monkeypatch.setattr(addon.requests, "get", get)
298 assert client.get(f"/web/relay/playlist?key={quote(KEY)}").status_code == 200
299 r = client.get(f"/web/relay/seg?key={quote(KEY)}&u={quote(other, safe='')}")
300 assert r.status_code == 200 and r.get_data() == b"SEG"
301 stranger = "https://paperlake.top/vd/x/not-listed.html"
302 assert client.get(f"/web/relay/seg?key={quote(KEY)}&u={quote(stranger, safe='')}").status_code == 403
303
304
305def test_a_segment_before_any_playlist_is_refused(client, monkeypatch):
306 _live(HLS)
307 monkeypatch.setattr(addon.requests, "get",
308 lambda *a, **k: pytest.fail("fetched an unlisted URL"))
309 u = "https://cdn.example/vd/abc/sd/seg-1.html?token=a"
310 assert client.get(f"/web/relay/seg?key={quote(KEY)}&u={quote(u, safe='')}").status_code == 403