OverviewFilesCommitsIssuesDocsReleasesPeers

stremio-vsembed commits / 708c7e4

Retry subtitles daily for fetched copies that never had English (0.34.0)
An ingest asks for subtitles once. On 10-18 wyzie 503'd all evening and 43 fetched copies had none, with nothing to retry them. A daemon thread now runs the ingest's own subtitle step (wyzie, sync, OpenSubtitles, sync) for every fetched copy that has never had an English track: 10 min after boot, then every 24 h. English measured and withdrawn as another cut is not fetched again; a copy being written is left to its ingest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
708c7e4121e2c7f3bfc89ee3f42ede0ebe8d0dce
by plankatron <fleeky@gmail.com> · 2026-10-19T01:37:58.000Z

Changed files (2) +207 -1

addon.py +68 -1
……@@ -620,7 +620,7 @@ TV_GENRES = [
620620
621621MANIFEST = {
622622 "id": "org.tabertron.vsembed",
623 "version": "0.33.0",
623 "version": "0.34.0",
624624 "name": "Tabertron VsEmbed",
625625 "description": "Self-hosted extractor with TMDB browse + search, played via vidcore and HdHub",
626626 "types": ["movie", "series"],
……@@ -2680,6 +2680,72 @@ def _fetch_subtitles_for(dest: str, meta: dict, season: int, episode: int,
26802680 _sync_subtitles_safely(dest)
26812681
26822682
2683# Once a day, the ingest's subtitle step again for every fetched copy that has
2684# never had English (2026-10-18: wyzie 503'd all evening and 43 copies had
2685# none, with nothing to retry them). "Never had": English measured and
2686# withdrawn as another cut already had both sources tried.
2687SUBS_RETRY_FIRST_DELAY = 10 * 60
2688SUBS_RETRY_INTERVAL = 24 * 3600
2689
2690
2691def _never_had_english(dest: str, records: dict) -> bool:
2692 if _has_english(dest):
2693 return False
2694 folder = os.path.dirname(os.path.abspath(dest))
2695 stem = os.path.splitext(os.path.basename(dest))[0]
2696 pattern = re.compile(re.escape(stem) + r"\.en(\.\d+)?\.srt$")
2697 return not any(os.path.dirname(k) == folder and pattern.fullmatch(os.path.basename(k))
2698 for k in records)
2699
2700
2701def _subtitle_retry_pass() -> dict:
2702 """One pass over the fetched copies. Never raises."""
2703 result = {"checked": 0, "retried": 0, "gained": 0}
2704 try:
2705 records = _load_sync()
2706 except Exception:
2707 return result # unreadable record: touch nothing
2708 for dest, keys in sorted(_fetched_dests().items()):
2709 if not os.path.exists(dest):
2710 continue
2711 result["checked"] += 1
2712 if not _never_had_english(dest, records):
2713 continue
2714 if any(_mp4_jobs.get(k) == "running" for k in keys):
2715 continue # its own ingest asks for subtitles
2716 parts = keys[0].split(":") # kind:ident:season:episode:height
2717 kind, ident = parts[0], ":".join(parts[1:-3])
2718 season, episode = int(parts[-3]), int(parts[-2])
2719 try:
2720 meta = _library_meta(kind, ident, season, episode)
2721 if not meta:
2722 continue
2723 result["retried"] += 1
2724 _fetch_subtitles_for(dest, meta, season, episode, kind=kind)
2725 if _has_english(dest):
2726 result["gained"] += 1
2727 except Exception as exc:
2728 log.warning("subs-retry: %s skipped (%s)", os.path.basename(dest), exc)
2729 log.info("subs-retry: %d fetched copies, %d without English retried, %d gained it",
2730 result["checked"], result["retried"], result["gained"])
2731 return result
2732
2733
2734def _subtitle_retry_loop() -> None:
2735 time.sleep(SUBS_RETRY_FIRST_DELAY)
2736 while True:
2737 try:
2738 _subtitle_retry_pass()
2739 except Exception as exc:
2740 log.warning("subs-retry: pass failed (%s)", exc)
2741 time.sleep(SUBS_RETRY_INTERVAL)
2742
2743
2744def _start_subtitle_retry() -> None:
2745 threading.Thread(target=_subtitle_retry_loop, name="subs-retry",
2746 daemon=True).start()
2747
2748
26832749def _run_ingest(kind: str, ident: str, season: int, episode: int,
26842750 height: int, force: bool = False) -> str:
26852751 """Fetch one title into the library. Synchronous; returns the final state.
……@@ -6105,4 +6171,5 @@ if __name__ == "__main__":
61056171 args = parser.parse_args()
61066172 # Pick up anything a previous process left mid-flight before serving.
61076173 _resume_ingests()
6174 _start_subtitle_retry()
61086175 app.run(host=args.host, port=args.port, debug=False, threaded=True)
test_subtitle_retry.py +139 -0
……@@ -0,0 +1,139 @@
1"""Daily retry for fetched copies that never got English subtitles (2026-10-18).
2
3An ingest asks for subtitles once, when the copy lands. On 10-18 wyzie
4answered 503 for every file all evening, and 43 fetched copies had none --
5fixed by a one-off sweep, but nothing would have retried them. So once a day
6the service runs the ingest's own subtitle step (_fetch_subtitles_for: wyzie,
7sync, OpenSubtitles, sync) for every fetched copy that has never had an
8English track.
9
10"Never had" is deliberate: a copy whose English was measured and withdrawn
11(a different cut) already had both sources tried, and fetching the same bad
12track again every day would only refill the originals store. A copy being
13written is left to its own ingest.
14
15Run: .venv/bin/python3 -m pytest test_subtitle_retry.py -q
16"""
17
18import json
19import os
20
21import pytest
22
23import addon
24
25
26@pytest.fixture
27def lib(tmp_path, monkeypatch):
28 root = tmp_path / "lib"
29 monkeypatch.setattr(addon, "LIBRARY_ROOT", str(root))
30 monkeypatch.setattr(addon, "_mp4_jobs", {})
31 copies = {}
32
33 def hold(name, key, subs=()):
34 path = root / name
35 path.parent.mkdir(parents=True, exist_ok=True)
36 path.write_bytes(b"video")
37 for code in subs:
38 (root / (os.path.splitext(name)[0] + f".{code}.srt")).write_text("x")
39 copies[os.path.abspath(path)] = [key]
40 return str(path)
41
42 monkeypatch.setattr(addon, "_fetched_dests", lambda: dict(copies))
43 monkeypatch.setattr(addon, "_library_meta",
44 lambda kind, ident, s=0, e=0: {"tmdb_id": ident.split(":")[1],
45 "title": "T", "year": "2020",
46 "episode_title": ""})
47 calls = []
48
49 def fetch(dest, meta, season, episode, kind="movie"):
50 calls.append((os.path.basename(dest), kind, meta["tmdb_id"], season, episode))
51 monkeypatch.setattr(addon, "_fetch_subtitles_for", fetch)
52 return hold, calls
53
54
55def test_a_copy_with_no_subtitles_is_retried_with_its_own_ids(lib):
56 hold, calls = lib
57 hold("Movies/Film (2025).mp4", "movie:tmdb:2:0:0:1080")
58 hold("Shows/Show - S02E06.mp4", "series:tmdb:1:2:6:1080")
59 addon._subtitle_retry_pass()
60 assert sorted(calls) == [("Film (2025).mp4", "movie", "2", 0, 0),
61 ("Show - S02E06.mp4", "series", "1", 2, 6)]
62
63
64def test_a_copy_with_english_is_left_alone(lib):
65 hold, calls = lib
66 hold("Movies/Film (2025).mp4", "movie:tmdb:2:0:0:1080", subs=("en",))
67 addon._subtitle_retry_pass()
68 assert calls == []
69
70
71def test_other_languages_alone_still_count_as_no_english(lib):
72 hold, calls = lib
73 hold("Movies/Film (2025).mp4", "movie:tmdb:2:0:0:1080", subs=("fr", "de"))
74 addon._subtitle_retry_pass()
75 assert len(calls) == 1
76
77
78def test_english_that_was_withdrawn_is_not_fetched_again(lib):
79 hold, calls = lib
80 dest = hold("Movies/Film (2025).mp4", "movie:tmdb:2:0:0:1080")
81 en = os.path.splitext(dest)[0] + ".en.srt"
82 with open(addon.SUBTITLE_SYNC_FILE, "w") as f:
83 json.dump({os.path.abspath(en): {"status": "unfixable", "ref": 2,
84 "offset": 30.0, "scale": 1.0}}, f)
85 addon._subtitle_retry_pass()
86 assert calls == []
87
88
89def test_a_copy_being_written_is_skipped(lib, monkeypatch):
90 hold, calls = lib
91 hold("Movies/Film (2025).mp4", "movie:tmdb:2:0:0:1080")
92 monkeypatch.setattr(addon, "_mp4_jobs", {"movie:tmdb:2:0:0:1080": "running"})
93 addon._subtitle_retry_pass()
94 assert calls == []
95
96
97def test_a_copy_gone_from_disk_is_skipped(lib):
98 hold, calls = lib
99 dest = hold("Movies/Film (2025).mp4", "movie:tmdb:2:0:0:1080")
100 os.remove(dest)
101 addon._subtitle_retry_pass()
102 assert calls == []
103
104
105def test_the_pass_reports_what_it_did(lib, monkeypatch):
106 hold, calls = lib
107 hold("Movies/A (2025).mp4", "movie:tmdb:2:0:0:1080")
108 hold("Movies/B (2025).mp4", "movie:tmdb:3:0:0:1080", subs=("en",))
109 hold("Movies/C (2025).mp4", "movie:tmdb:4:0:0:1080")
110
111 def fetch(dest, meta, season, episode, kind="movie"):
112 if "A (2025)" in dest:
113 open(os.path.splitext(dest)[0] + ".en.srt", "w").close()
114 monkeypatch.setattr(addon, "_fetch_subtitles_for", fetch)
115 assert addon._subtitle_retry_pass() == {"checked": 3, "retried": 2, "gained": 1}
116
117
118def test_one_copy_failing_does_not_stop_the_pass(lib, monkeypatch):
119 hold, calls = lib
120 hold("Movies/A (2025).mp4", "movie:tmdb:2:0:0:1080")
121 hold("Movies/B (2025).mp4", "movie:tmdb:3:0:0:1080")
122 seen = []
123
124 def fetch(dest, meta, season, episode, kind="movie"):
125 seen.append(os.path.basename(dest))
126 if "A (2025)" in dest:
127 raise RuntimeError("boom")
128 monkeypatch.setattr(addon, "_fetch_subtitles_for", fetch)
129 addon._subtitle_retry_pass()
130 assert sorted(seen) == ["A (2025).mp4", "B (2025).mp4"]
131
132
133def test_an_unreadable_sync_record_touches_nothing(lib):
134 hold, calls = lib
135 hold("Movies/Film (2025).mp4", "movie:tmdb:2:0:0:1080")
136 with open(addon.SUBTITLE_SYNC_FILE, "w") as f:
137 f.write("{not json")
138 addon._subtitle_retry_pass()
139 assert calls == []