OverviewFilesCommitsIssuesDocsReleasesPeers

stremio-vsembed commits / 19cd657

Serve the library copy, keep the legacy cache reachable (0.9.5)
_playable_path resolves a title to its best complete local file: library first, legacy mp4-cache second. The fallback is not vestigial -- the one copy made by hand before the library existed must stay reachable, and since nothing here deletes, nothing may orphan either. It degrades correctly when TMDB is unreachable: _library_meta returns None and the legacy path still answers. _serve_mp4 and _append_mp4_row both go through it, so /mp4/ now serves an ingested file and the row appears once the library holds the episode. A library copy is labelled "in library" rather than by height, because the library holds one copy per episode and the height in the URL only keys the legacy path. test_mp4_offline now stubs _library_meta: _playable_path consults TMDB, which took the suite from 0.32s to 1.65s of real network. Those tests cover the legacy path, and no-metadata is exactly the TMDB-unreachable case they should assert. Suite is offline and back to 0.33s. 7 new tests, 100 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
19cd6576f7391a734783b557e3c53066b346f0e4
by plankatron <fleeky@gmail.com> · 2026-08-11T14:25:38.000Z

Changed files (3) +113 -7

addon.py +40 -7
……@@ -533,7 +533,7 @@ TV_GENRES = [
533533
534534MANIFEST = {
535535 "id": "org.tabertron.vsembed",
536 "version": "0.9.4",
536 "version": "0.9.5",
537537 "name": "Tabertron VsEmbed",
538538 "description": "Self-hosted HLS extractor with TMDB browse + search, played via vidcore",
539539 "types": ["movie", "series"],
……@@ -1336,13 +1336,40 @@ def _start_mp4_job(kind: str, ident: str, season: int, episode: int, height: int
13361336 return "running"
13371337
13381338
1339def _playable_path(kind: str, ident: str, season: int = 0, episode: int = 0,
1340 height: int = VIDCORE_MAX_HEIGHT) -> Optional[str]:
1341 """The best complete local file for this title, or None.
1342
1343 Library first, then the legacy mp4-cache. The fallback is not vestigial:
1344 copies made before the library existed must stay reachable. Nothing here
1345 deletes, so nothing here may orphan either.
1346
1347 Falls back cleanly when TMDB is unreachable — _library_meta returns None and
1348 the legacy path still answers.
1349 """
1350 meta = _library_meta(kind, ident, season, episode)
1351 if meta:
1352 dest = _library_path(kind, meta["title"], meta["year"], meta["tmdb_id"],
1353 season, episode, meta["episode_title"])
1354 try:
1355 if os.path.getsize(dest) > 0:
1356 return dest
1357 except OSError:
1358 pass
1359 legacy = _mp4_path(kind, ident, season, episode, height)
1360 try:
1361 return legacy if os.path.getsize(legacy) > 0 else None
1362 except OSError:
1363 return None
1364
1365
13391366def _serve_mp4(kind: str, ident: str, season: int, episode: int):
13401367 height = _requested_height()
1341 if _mp4_ready(kind, ident, season, episode, height):
1368 path = _playable_path(kind, ident, season, episode, height)
1369 if path:
13421370 # conditional=True is what answers Range requests, and Range is what
13431371 # makes the file seekable in the player.
1344 return send_file(_mp4_path(kind, ident, season, episode, height),
1345 mimetype="video/mp4", conditional=True)
1372 return send_file(path, mimetype="video/mp4", conditional=True)
13461373 state = _start_mp4_job(kind, ident, season, episode, height)
13471374 # 503 rather than 404: the copy is being made, it is not an error.
13481375 return Response(f"remux {state}\n", status=503, mimetype="text/plain")
……@@ -1385,19 +1412,25 @@ def _append_mp4_row(out: list, kind: str, ident: Optional[str],
13851412 if not ident:
13861413 return
13871414 for height in VIDCORE_HEIGHTS:
1388 if not _mp4_ready(kind, ident, season, episode, height):
1415 local = _playable_path(kind, ident, season, episode, height)
1416 if not local:
13891417 continue
1418 in_library = local.startswith(LIBRARY_ROOT + os.sep)
13901419 if kind == "series":
13911420 path = f"/mp4/series/{ident}:{season}:{episode}.mp4"
13921421 tag = f"S{season:02d}E{episode:02d} "
13931422 else:
13941423 path = f"/mp4/movie/{ident}.mp4"
13951424 tag = ""
1425 # A library copy is height-agnostic — one copy per episode — so the
1426 # height is not part of its label. It is still sent in the query string
1427 # because the legacy cache path is keyed by it.
1428 label = "in library" if in_library else f"{height}p"
13961429 out.append(_stream_dict(Stream(
13971430 url=f"http://{request.host}{path}?h={height}",
1398 quality=f"{height}p", name="local",
1431 quality=label, name="local",
13991432 referer="https://vidcore.net/",
1400 title_extra=f"{tag}{height}p seekable\nlocal copy")))
1433 title_extra=f"{tag}{label} seekable\nlocal copy")))
14011434 return
14021435
14031436
test_library_ingest.py +68 -0
……@@ -372,6 +372,74 @@ def test_ingest_fails_cleanly_when_the_source_cannot_be_resolved(ingest, monkeyp
372372 assert ingest == []
373373
374374
375# --- serving prefers the library, without orphaning the legacy cache --------
376
377@pytest.fixture
378def served(monkeypatch, tmdb, tmp_path):
379 tmdb.responses["tv/1"] = {"name": "Show", "first_air_date": "2020-01-01"}
380 tmdb.responses["tv/1/season/1/episode/1"] = {"name": "Pilot"}
381 monkeypatch.setattr(addon, "MP4_DIR", os.path.join(str(tmp_path), "cache"))
382 os.makedirs(addon.MP4_DIR, exist_ok=True)
383 monkeypatch.setattr(addon, "_prefetch_vidcore", lambda *a, **k: None)
384 monkeypatch.setattr(addon, "_start_mp4_job", lambda *a, **k: "running")
385 return tmdb
386
387
388def _write_library_copy(body=b"library"):
389 dest = addon._library_path("series", "Show", "2020", "1",
390 season=1, episode=1, episode_title="Pilot")
391 os.makedirs(os.path.dirname(dest), exist_ok=True)
392 open(dest, "wb").write(body)
393 return dest
394
395
396def test_the_library_copy_is_what_gets_served(served):
397 dest = _write_library_copy(b"library-bytes")
398 assert addon._playable_path("series", "tmdb:1", 1, 1, 720) == dest
399
400
401def test_a_legacy_cache_copy_is_still_served(served):
402 """The one file made by hand before the library existed must not become
403 unreachable -- nothing is deleted, so nothing may be orphaned either."""
404 legacy = addon._mp4_path("series", "tmdb:1", 1, 1, 720)
405 open(legacy, "wb").write(b"legacy")
406 assert addon._playable_path("series", "tmdb:1", 1, 1, 720) == legacy
407
408
409def test_the_library_wins_over_a_legacy_copy(served):
410 dest = _write_library_copy()
411 open(addon._mp4_path("series", "tmdb:1", 1, 1, 720), "wb").write(b"legacy")
412 assert addon._playable_path("series", "tmdb:1", 1, 1, 720) == dest
413
414
415def test_nothing_playable_yields_none(served):
416 assert addon._playable_path("series", "tmdb:1", 1, 1, 720) is None
417
418
419def test_the_route_serves_the_library_copy_with_range_support(served):
420 _write_library_copy(b"0123456789" * 10)
421 client = addon.app.test_client()
422 r = client.get("/mp4/series/tmdb:1:1:1.mp4?h=720",
423 headers={"Range": "bytes=0-9"})
424 assert r.status_code == 206 # seeking depends on this
425 assert r.get_data() == b"0123456789"
426
427
428def test_a_row_is_offered_once_the_library_holds_it(served):
429 _write_library_copy()
430 with addon.app.test_request_context("/stream/series/fxl:1:1:1.json"):
431 out = []
432 addon._append_mp4_row(out, "series", "tmdb:1", 1, 1)
433 assert len(out) == 1 and "/mp4/" in out[0]["url"]
434
435
436def test_no_playable_copy_means_no_row(served):
437 with addon.app.test_request_context("/stream/series/fxl:1:1:1.json"):
438 out = []
439 addon._append_mp4_row(out, "series", "tmdb:1", 1, 1)
440 assert out == []
441
442
375443def test_ingest_resolves_at_run_time_not_at_queue_time(ingest, monkeypatch):
376444 """Variant URLs are signed and expire in ~4h. A job that resolved when it
377445 was queued would fetch a dead link once the queue backs up."""
test_mp4_offline.py +5 -0
……@@ -24,6 +24,11 @@ import addon
2424def tmp_cache(tmp_path, monkeypatch):
2525 monkeypatch.setattr(addon, "MP4_DIR", str(tmp_path))
2626 monkeypatch.setattr(addon, "_mp4_jobs", {})
27 # _playable_path consults TMDB to derive the library name. These tests cover
28 # the LEGACY cache path and must never reach the network -- returning no
29 # metadata is exactly the "TMDB unreachable" case, which must still serve a
30 # cached copy. Library naming is covered in test_library_ingest.py.
31 monkeypatch.setattr(addon, "_library_meta", lambda *a, **k: None)
2732 yield
2833
2934