OverviewFilesCommitsIssuesDocsReleasesPeers

stremio-vsembed commits / 06c1c5a

OpenSubtitles when a copy has no English after wyzie (0.31.0)
Hope (tmdb 1058424) landed with no subtitles: wyzie listed 9 tracks and answered 503 for every file. OpenSubtitles' stremio addon had 4 English tracks named for their releases, all measuring 0.13 s / 1.000 against our copy. Asked only when no English sidecar is left after wyzie and the sync; up to 3 tracks, each measured by the same sync. Never raises. conftest refuses the new _opensubs_for seam: the first run of the old ingest tests reached real TMDB and OpenSubtitles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
06c1c5aee2426b837d6bb60fb154f6a647722e7e
by plankatron <fleeky@gmail.com> · 2026-10-19T00:20:20.000Z

Changed files (3) +176 -10

addon.py +69 -10
……@@ -619,7 +619,7 @@ TV_GENRES = [
619619
620620MANIFEST = {
621621 "id": "org.tabertron.vsembed",
622 "version": "0.30.0",
622 "version": "0.31.0",
623623 "name": "Tabertron VsEmbed",
624624 "description": "Self-hosted extractor with TMDB browse + search, played via vidcore and HdHub",
625625 "types": ["movie", "series"],
……@@ -2544,7 +2544,61 @@ def _subtitle_summary_focused(dest: str, present: list, records: dict,
25442544 return head
25452545
25462546
2547def _fetch_subtitles_for(dest: str, meta: dict, season: int, episode: int) -> None:
2547# OpenSubtitles through its stremio addon -- the host Jacktook already uses, no
2548# key. Asked only when a copy has NO English after wyzie and the sync: on
2549# 2026-10-18 wyzie listed 9 tracks for Hope and 503'd every file, while this
2550# had 4 English tracks, each named for its release, all measuring 0.13 s /
2551# 1.000 against our copy. Capped, because each one becomes a menu entry.
2552OPENSUBS_URL = "https://opensubtitles-v3.strem.io"
2553OPENSUBS_MAX_TRACKS = 3
2554
2555
2556def _opensubs_tracks(kind: str, imdb_id: str, season: int = 0,
2557 episode: int = 0) -> list[dict]:
2558 """English tracks in _write_sidecars' shape, or [] -- never an exception."""
2559 path = (f"series/{imdb_id}:{season}:{episode}" if kind == "series"
2560 else f"movie/{imdb_id}")
2561 try:
2562 r = requests.get(f"{OPENSUBS_URL}/subtitles/{path}.json",
2563 timeout=REQUEST_TIMEOUT, headers={"User-Agent": UA})
2564 if r.status_code != 200:
2565 log.info("opensubs: none for %s (HTTP %s)", imdb_id, r.status_code)
2566 return []
2567 subs = r.json().get("subtitles") or []
2568 except Exception as exc:
2569 log.info("opensubs: none for %s (%s)", imdb_id, exc)
2570 return []
2571 english = [{"language": "en", "url": s["url"]} for s in subs
2572 if isinstance(s, dict) and s.get("lang") == "eng" and s.get("url")]
2573 log.info("opensubs: %d English track(s) for %s", len(english), imdb_id)
2574 return english[:OPENSUBS_MAX_TRACKS]
2575
2576
2577def _opensubs_for(kind: str, tmdb_id: str, season: int = 0,
2578 episode: int = 0) -> list[dict]:
2579 """OpenSubtitles is keyed by imdb id only. A seam: conftest refuses it, so
2580 no test reaches TMDB or OpenSubtitles without a stub."""
2581 imdb_id = _imdb_id_for(kind, f"tmdb:{tmdb_id}")
2582 return _opensubs_tracks(kind, imdb_id, season, episode) if imdb_id else []
2583
2584
2585def _has_english(dest: str) -> bool:
2586 return any(_sidecar_lang(dest, p) == "en" for p in _sidecars_of(dest))
2587
2588
2589def _sync_subtitles_safely(dest: str) -> None:
2590 # Measured against THIS copy's audio, because subtitle tracks are cut for
2591 # other releases (labs#13). The copy is already published; this only ever
2592 # replaces or withdraws sidecars, and never raises.
2593 _job_update(getattr(_job_ctx, "key", None) or "", phase="syncing subtitles")
2594 try:
2595 _sync_subtitles(dest)
2596 except Exception as exc:
2597 log.warning("subsync: skipped for %s (%s)", os.path.basename(dest), exc)
2598
2599
2600def _fetch_subtitles_for(dest: str, meta: dict, season: int, episode: int,
2601 kind: str = "movie") -> None:
25482602 """Put whatever subtitles exist beside a published copy. Never raises.
25492603
25502604 Called AFTER the copy is complete and published, so there is nothing a
……@@ -2557,14 +2611,19 @@ def _fetch_subtitles_for(dest: str, meta: dict, season: int, episode: int) -> No
25572611 except Exception as exc:
25582612 log.warning("subtitles: skipped for %s (%s)",
25592613 os.path.basename(dest), exc)
2560 # Measured against THIS copy's audio, because wyzie's tracks are cut for
2561 # other releases (labs#13). The copy is already published; this only ever
2562 # replaces or withdraws sidecars, and never raises.
2563 _job_update(getattr(_job_ctx, "key", None) or "", phase="syncing subtitles")
2614 _sync_subtitles_safely(dest)
2615 # Wyzie gave nothing usable in English (none, a 503, or withdrawn by the
2616 # sync as a different cut): ask the second source, and measure that too.
25642617 try:
2565 _sync_subtitles(dest)
2618 if _has_english(dest):
2619 return
2620 if not _write_sidecars(dest, _opensubs_for(kind, meta["tmdb_id"],
2621 season, episode)):
2622 return
25662623 except Exception as exc:
2567 log.warning("subsync: skipped for %s (%s)", os.path.basename(dest), exc)
2624 log.warning("opensubs: skipped for %s (%s)", os.path.basename(dest), exc)
2625 return
2626 _sync_subtitles_safely(dest)
25682627
25692628
25702629def _run_ingest(kind: str, ident: str, season: int, episode: int,
……@@ -2607,7 +2666,7 @@ def _run_ingest(kind: str, ident: str, season: int, episode: int,
26072666 # costs no browser, and one HTTP call does not change that -- while
26082667 # skipping it would mean the library only ever gains subtitles for
26092668 # things fetched after today.
2610 _fetch_subtitles_for(dest, meta, season, episode)
2669 _fetch_subtitles_for(dest, meta, season, episode, kind=kind)
26112670 return "done"
26122671 # vidcore first: it is asked by TMDB id, so what it returns IS this title.
26132672 # HdHub is the fallback because its answer is matched by filename -- see
……@@ -2667,7 +2726,7 @@ def _run_ingest(kind: str, ident: str, season: int, episode: int,
26672726 return f"{failure}; hdhub: {why}"
26682727 log.info("ingest done: %s in %.0fs (%.2f GB)", key,
26692728 time.time() - started, os.path.getsize(dest) / 1e9)
2670 _fetch_subtitles_for(dest, meta, season, episode)
2729 _fetch_subtitles_for(dest, meta, season, episode, kind=kind)
26712730 # After the sidecars, so one notification covers the copy and them.
26722731 _notify_jellyfin(dest, "Created")
26732732 return "done"
conftest.py +4 -0
……@@ -40,6 +40,10 @@ def no_hdhub_network(monkeypatch):
4040 monkeypatch.setattr(addon, "_hdhub_query", refuse)
4141 monkeypatch.setattr(addon, "_hdhub_cache", {})
4242 monkeypatch.setattr(addon, "_hdhub_pick_cache", {})
43 # The OpenSubtitles fallback (2026-10-18) runs whenever a copy ends up with
44 # no English sidecar -- which every ingest test with a fake sidecar writer
45 # does, and it reached real TMDB + OpenSubtitles on its first run.
46 monkeypatch.setattr(addon, "_opensubs_for", refuse)
4347 # On stortron the real jellyfin.json sits beside addon.py; without this any
4448 # test reaching an ingest or a delete would report temp paths to the
4549 # production Jellyfin. test_jellyfin_notify points it at its own file.
test_subtitles.py +103 -0
……@@ -275,3 +275,106 @@ def test_an_episode_passes_its_season_and_episode_through(ingest, monkeypatch):
275275 "episode_title": "Pilot"})
276276 addon._run_ingest("series", "tmdb:138502", 1, 1, 1080)
277277 assert seen == {"tmdb": "138502", "season": 1, "episode": 1}
278
279
280# ------------------------------------------- OpenSubtitles, when wyzie has none
281# 2026-10-18, Hope (tmdb 1058424): wyzie LISTED 9 tracks and answered 503 for
282# every file, so the copy landed with no subtitles at all. OpenSubtitles'
283# stremio addon (the host Jacktook already uses) had 4 English tracks, each
284# named for the release it was cut for, and all 4 measured 0.13 s / 1.000
285# against our copy. It is asked only when no English sidecar is left after
286# wyzie and the sync, and what it gives goes through the same sync.
287
288OS_BODY = {"subtitles": [
289 {"id": "1", "url": "http://os.invalid/1", "lang": "eng"},
290 {"id": "2", "url": "http://os.invalid/2", "lang": "kor"},
291 {"id": "3", "url": "http://os.invalid/3", "lang": "eng"},
292 {"id": "4", "url": "http://os.invalid/4", "lang": "eng"},
293 {"id": "5", "url": "http://os.invalid/5", "lang": "eng"},
294 {"id": "6", "lang": "eng"},
295]}
296
297
298@pytest.fixture
299def os_http(monkeypatch):
300 seen = []
301
302 class R:
303 status_code = 200
304
305 def json(self):
306 return OS_BODY
307
308 def fake_get(url, **k):
309 seen.append(url)
310 return R()
311 monkeypatch.setattr(addon.requests, "get", fake_get)
312 return seen
313
314
315def test_opensubs_asks_a_movie_by_imdb_id(os_http):
316 addon._opensubs_tracks("movie", "tt27369017")
317 assert os_http == [f"{addon.OPENSUBS_URL}/subtitles/movie/tt27369017.json"]
318
319
320def test_opensubs_asks_an_episode_with_season_and_episode(os_http):
321 addon._opensubs_tracks("series", "tt0903747", 2, 6)
322 assert os_http == [f"{addon.OPENSUBS_URL}/subtitles/series/tt0903747:2:6.json"]
323
324
325def test_opensubs_keeps_english_with_a_url_capped(os_http):
326 tracks = addon._opensubs_tracks("movie", "tt27369017")
327 assert [t["url"] for t in tracks] == [
328 "http://os.invalid/1", "http://os.invalid/3", "http://os.invalid/4"]
329 assert all(t["language"] == "en" for t in tracks)
330
331
332def test_opensubs_failure_is_an_empty_list(monkeypatch):
333 def boom(*a, **k):
334 raise RuntimeError("down")
335 monkeypatch.setattr(addon.requests, "get", boom)
336 assert addon._opensubs_tracks("movie", "tt1") == []
337
338
339REAL_OPENSUBS_FOR = addon._opensubs_for # before conftest refuses it
340
341
342@pytest.fixture
343def fallback(mp4, monkeypatch, fetched):
344 state = {"wyzie": [], "os": [{"language": "en", "url": "http://os.invalid/1"}],
345 "asked": [], "synced": 0}
346 monkeypatch.setattr(addon, "_wyzie_tracks", lambda *a, **k: state["wyzie"])
347 monkeypatch.setattr(addon, "_opensubs_for", REAL_OPENSUBS_FOR)
348 monkeypatch.setattr(addon, "_imdb_id_for", lambda kind, ident: "tt27369017")
349
350 def fake_os(kind, imdb, season=0, episode=0):
351 state["asked"].append((kind, imdb, season, episode))
352 return state["os"]
353 monkeypatch.setattr(addon, "_opensubs_tracks", fake_os)
354
355 def fake_sync(dest):
356 state["synced"] += 1
357 return {}
358 monkeypatch.setattr(addon, "_sync_subtitles", fake_sync)
359 return state
360
361
362def test_no_english_from_wyzie_asks_opensubtitles(mp4, fallback):
363 meta = {"tmdb_id": "1058424"}
364 addon._fetch_subtitles_for(mp4, meta, 0, 0, kind="movie")
365 assert fallback["asked"] == [("movie", "tt27369017", 0, 0)]
366 assert os.path.exists(os.path.splitext(mp4)[0] + ".en.srt")
367 assert fallback["synced"] == 2, "the OpenSubtitles track was not measured"
368
369
370def test_english_from_wyzie_never_asks_opensubtitles(mp4, fallback):
371 fallback["wyzie"] = _tracks(("en", "English"))
372 addon._fetch_subtitles_for(mp4, {"tmdb_id": "1058424"}, 0, 0, kind="movie")
373 assert fallback["asked"] == []
374
375
376def test_an_opensubtitles_failure_never_raises(mp4, fallback, monkeypatch):
377 def boom(*a, **k):
378 raise RuntimeError("tmdb down")
379 monkeypatch.setattr(addon, "_imdb_id_for", boom)
380 addon._fetch_subtitles_for(mp4, {"tmdb_id": "1058424"}, 0, 0, kind="movie")