OverviewFilesCommitsIssuesDocsReleasesPeers

stremio-vsembed commits / 6b91ad3

Triggers: playing a title keeps it (0.10.0)
Closes the chicken-and-egg. Nothing could start an ingest before: the row was offered only once a copy existed, and a copy was made only by requesting that row. The one file on disk had been made by hand. Three changes, all on the same idea -- the row must be playable the instant it is clicked, and keeping a copy is a side effect of watching: - Playing any vidcore row calls _ensure_ingest, which is idempotent and skips what the library already holds. - /mp4/ no longer 503s when the copy is unfinished. It starts the copy and redirects to the live stream, so the row plays immediately and upgrades itself to the local file once that lands. - The row is always offered for an identifiable title, labelled by state: "play & keep", "saving to library…", "in library — seekable". An unidentifiable title still offers nothing, because the row would be a lie: nothing can be written under a name we had to guess. Two existing tests asserted the contract this reverses and were rewritten, not deleted -- 503-on-missing became redirect-to-live, and no-copy-means-no-row became no-copy-still-offers-a-row. The second WAS the bug. 7 new tests, 107 pass in 0.43s with no network. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6b91ad3b7b138621f5f04c3cfe361a9714d33ac0
by plankatron <fleeky@gmail.com> · 2026-08-11T14:37:59.000Z

Changed files (3) +151 -24

addon.py +59 -19
……@@ -533,7 +533,7 @@ TV_GENRES = [
533533
534534MANIFEST = {
535535 "id": "org.tabertron.vsembed",
536 "version": "0.9.5",
536 "version": "0.10.0",
537537 "name": "Tabertron VsEmbed",
538538 "description": "Self-hosted HLS extractor with TMDB browse + search, played via vidcore",
539539 "types": ["movie", "series"],
……@@ -1026,6 +1026,9 @@ def vidcore_movie(ident: str):
10261026 url = _resolve_vidcore("movie", ident)
10271027 if not url:
10281028 abort(404)
1029 # Playing is the honest "I chose this" signal, so keep a copy of what is
1030 # being watched. Idempotent, and skipped when the library already has it.
1031 _ensure_ingest("movie", ident, height=_requested_height())
10291032 return _serve_vidcore(url)
10301033
10311034
……@@ -1038,6 +1041,8 @@ def vidcore_series(ident: str):
10381041 url = _resolve_vidcore("series", m.group(1), int(m.group(2)), int(m.group(3)))
10391042 if not url:
10401043 abort(404)
1044 _ensure_ingest("series", m.group(1), int(m.group(2)), int(m.group(3)),
1045 height=_requested_height())
10411046 return _serve_vidcore(url)
10421047
10431048
……@@ -1363,6 +1368,22 @@ def _playable_path(kind: str, ident: str, season: int = 0, episode: int = 0,
13631368 return None
13641369
13651370
1371def _ensure_ingest(kind: str, ident: str, season: int = 0, episode: int = 0,
1372 height: int = VIDCORE_MAX_HEIGHT) -> None:
1373 """Start a copy of this title unless there is a reason not to.
1374
1375 Idempotent: _start_mp4_job already refuses to run the same key twice, so
1376 repeated plays of the same episode enqueue nothing new.
1377 """
1378 if not ident:
1379 return
1380 if not _library_meta(kind, ident, season, episode):
1381 return # cannot be named, so cannot be ingested
1382 if _library_has(kind, ident, season, episode):
1383 return
1384 _start_mp4_job(kind, ident, season, episode, height)
1385
1386
13661387def _serve_mp4(kind: str, ident: str, season: int, episode: int):
13671388 height = _requested_height()
13681389 path = _playable_path(kind, ident, season, episode, height)
……@@ -1370,9 +1391,15 @@ def _serve_mp4(kind: str, ident: str, season: int, episode: int):
13701391 # conditional=True is what answers Range requests, and Range is what
13711392 # makes the file seekable in the player.
13721393 return send_file(path, mimetype="video/mp4", conditional=True)
1373 state = _start_mp4_job(kind, ident, season, episode, height)
1374 # 503 rather than 404: the copy is being made, it is not an error.
1375 return Response(f"remux {state}\n", status=503, mimetype="text/plain")
1394 # No copy yet. Start one, and hand the player the live stream meanwhile:
1395 # this row has to be playable the instant it is clicked, and a 503 would
1396 # surface as a playback failure while the copy silently proceeded. The same
1397 # URL serves the local file once it lands, so the row upgrades itself.
1398 _ensure_ingest(kind, ident, season, episode, height)
1399 url = _resolve_vidcore(kind, ident, season, episode)
1400 if not url:
1401 abort(404)
1402 return _serve_vidcore(url)
13761403
13771404
13781405@app.route("/mp4/movie/<path:ident>.mp4")
……@@ -1411,27 +1438,40 @@ def _append_mp4_row(out: list, kind: str, ident: Optional[str],
14111438 """
14121439 if not ident:
14131440 return
1441 if kind == "series":
1442 path = f"/mp4/series/{ident}:{season}:{episode}.mp4"
1443 tag = f"S{season:02d}E{episode:02d} "
1444 else:
1445 path = f"/mp4/movie/{ident}.mp4"
1446 tag = ""
1447
1448 def row(label: str, height: int) -> None:
1449 out.append(_stream_dict(Stream(
1450 url=f"http://{request.host}{path}?h={height}",
1451 quality=label, name="local",
1452 referer="https://vidcore.net/",
1453 title_extra=f"{tag}{label}\nlocal copy")))
1454
14141455 for height in VIDCORE_HEIGHTS:
14151456 local = _playable_path(kind, ident, season, episode, height)
14161457 if not local:
14171458 continue
1418 in_library = local.startswith(LIBRARY_ROOT + os.sep)
1419 if kind == "series":
1420 path = f"/mp4/series/{ident}:{season}:{episode}.mp4"
1421 tag = f"S{season:02d}E{episode:02d} "
1422 else:
1423 path = f"/mp4/movie/{ident}.mp4"
1424 tag = ""
14251459 # A library copy is height-agnostic — one copy per episode — so the
14261460 # 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"
1429 out.append(_stream_dict(Stream(
1430 url=f"http://{request.host}{path}?h={height}",
1431 quality=label, name="local",
1432 referer="https://vidcore.net/",
1433 title_extra=f"{tag}{label} seekable\nlocal copy")))
1434 return
1461 # because the legacy cache path IS keyed by it.
1462 in_library = local.startswith(LIBRARY_ROOT + os.sep)
1463 return row("in library — seekable" if in_library
1464 else f"{height}p seekable", height)
1465
1466 # No copy yet, but the row is still offered: it plays the live stream and
1467 # keeps a copy as it goes. Offering it ONLY once a copy existed is what made
1468 # the feature unreachable — the row had to exist for a copy to be made, and
1469 # a copy had to exist for the row to be offered.
1470 if not _library_meta(kind, ident, season, episode):
1471 return # cannot be named, so the row would be a lie
1472 running = _mp4_jobs.get(
1473 _mp4_key(kind, ident, season, episode, VIDCORE_MAX_HEIGHT)) == "running"
1474 row("saving to library…" if running else "play & keep", VIDCORE_MAX_HEIGHT)
14351475
14361476
14371477def _append_vidcore_row(out: list, kind: str, ident: Optional[str],
test_library_ingest.py +78 -1
……@@ -433,10 +433,87 @@ def test_a_row_is_offered_once_the_library_holds_it(served):
433433 assert len(out) == 1 and "/mp4/" in out[0]["url"]
434434
435435
436def test_no_playable_copy_means_no_row(served):
436def test_no_copy_yet_still_offers_a_row_that_will_make_one(served):
437 """Superseded the old 'no copy means no row'. That WAS the chicken-and-egg:
438 withholding the row until a copy existed meant no copy could ever be made
439 through the UI."""
437440 with addon.app.test_request_context("/stream/series/fxl:1:1:1.json"):
438441 out = []
439442 addon._append_mp4_row(out, "series", "tmdb:1", 1, 1)
443 assert len(out) == 1
444 assert "in library" not in out[0]["title"]
445
446
447# --- triggers ---------------------------------------------------------------
448# Until now nothing could START an ingest: _append_mp4_row only offered a /mp4/
449# row once the copy existed, and only a /mp4/ request created one. The two
450# guarded each other, so the single file on disk was made by hand.
451
452@pytest.fixture
453def triggered(monkeypatch, tmdb, tmp_path):
454 tmdb.responses["tv/1"] = {"name": "Show", "first_air_date": "2020-01-01"}
455 tmdb.responses["tv/1/season/1/episode/1"] = {"name": "Pilot"}
456 monkeypatch.setattr(addon, "MP4_DIR", os.path.join(str(tmp_path), "cache"))
457 os.makedirs(addon.MP4_DIR, exist_ok=True)
458 monkeypatch.setattr(addon, "_prefetch_vidcore", lambda *a, **k: None)
459 monkeypatch.setattr(addon, "_resolve_vidcore", lambda *a, **k: "http://cdn/m.m3u8")
460 monkeypatch.setattr(addon, "_cap_variant", lambda url, h: f"{url}?h={h}")
461 started = []
462 monkeypatch.setattr(addon, "_start_mp4_job",
463 lambda *a, **k: started.append(a) or "running")
464 return started
465
466
467def test_playing_a_stream_starts_the_copy(triggered):
468 """Pressing play is the honest 'I chose this' signal."""
469 addon.app.test_client().get("/vidcore/series/tmdb:1:1:1.m3u8?h=720")
470 assert triggered, "playing must enqueue the copy"
471
472
473def test_playing_does_not_re_fetch_what_the_library_already_has(triggered):
474 _write_library_copy()
475 addon.app.test_client().get("/vidcore/series/tmdb:1:1:1.m3u8?h=720")
476 assert triggered == []
477
478
479def test_an_unfinished_copy_redirects_to_the_live_stream(triggered):
480 """The row must be playable the moment it is clicked. 503 would surface as a
481 playback failure while the copy silently proceeded."""
482 r = addon.app.test_client().get("/mp4/series/tmdb:1:1:1.mp4?h=720")
483 assert r.status_code == 302
484 assert "cdn" in r.headers["Location"]
485 assert triggered, "and it must start the copy"
486
487
488def test_a_finished_copy_is_served_rather_than_redirected(triggered):
489 _write_library_copy(b"0123456789")
490 r = addon.app.test_client().get("/mp4/series/tmdb:1:1:1.mp4?h=720")
491 assert r.status_code == 200
492 assert triggered == []
493
494
495def test_the_keep_row_is_offered_before_any_copy_exists(triggered):
496 """This is the chicken-and-egg: the row had to exist for a copy to be made,
497 and a copy had to exist for the row to be offered."""
498 with addon.app.test_request_context("/stream/series/fxl:1:1:1.json"):
499 out = []
500 addon._append_mp4_row(out, "series", "tmdb:1", 1, 1)
501 assert len(out) == 1 and "/mp4/" in out[0]["url"]
502
503
504def test_the_row_says_when_the_copy_is_already_held(triggered):
505 _write_library_copy()
506 with addon.app.test_request_context("/stream/series/fxl:1:1:1.json"):
507 out = []
508 addon._append_mp4_row(out, "series", "tmdb:1", 1, 1)
509 assert "in library" in out[0]["title"]
510
511
512def test_an_unidentifiable_title_offers_no_keep_row(triggered):
513 """Nothing can be ingested without an identity, so the row would be a lie."""
514 with addon.app.test_request_context("/stream/series/fxl:9:1:1.json"):
515 out = []
516 addon._append_mp4_row(out, "series", "tmdb:999999", 1, 1)
440517 assert out == []
441518
442519
test_mp4_offline.py +14 -4
……@@ -100,12 +100,22 @@ def test_ready_file_is_served_with_range_support(client):
100100 assert r.get_data() == b"0123456789"
101101
102102
103def test_a_missing_file_reports_not_ready_and_starts_the_job(client):
103def test_a_missing_file_falls_through_to_the_live_stream(client, monkeypatch):
104 """Was 503. The row has to be playable the instant it is clicked, so a
105 missing copy now redirects to the live stream while the copy is made.
106
107 This fixture leaves _library_meta returning None (the TMDB-unreachable
108 case), so no ingest is started -- a title we cannot name must not be
109 written into the library under a guessed name.
110 """
104111 started = []
105 addon._start_mp4_job = lambda *a, **k: started.append(a) or "started"
112 monkeypatch.setattr(addon, "_start_mp4_job",
113 lambda *a, **k: started.append(a) or "started")
114 monkeypatch.setattr(addon, "_resolve_vidcore", lambda *a, **k: "http://cdn/m.m3u8")
115 monkeypatch.setattr(addon, "_cap_variant", lambda url, h: url)
106116 r = client.get("/mp4/series/tmdb:138502:2:9.mp4?h=720")
107 assert r.status_code == 503 # not an error -- "come back later"
108 assert started
117 assert r.status_code == 302
118 assert started == [], "unnameable titles are not ingested"
109119
110120
111121def test_a_bad_ident_is_rejected(client):