OverviewFilesCommitsIssuesDocsReleasesPeers

stremio-vsembed commits / 64fa4e3

Library ingest: identity lookup, and refusing to guess (0.9.3)
_library_meta resolves ident -> {tmdb_id, title, year, episode_title}, taking tt-ids through TMDB's find endpoint since the cinemeta path supplies those but the library names by TMDB id. A title that cannot be identified is NOT ingested. Naming is only as safe as the identity behind it, and the 2026-07-28 cleanup exists because wrongly-named media accumulated -- a bad file in the library costs more than a missing one. A "not found" is cached because it is stable; a TMDB *failure* is not. Hardening one bad request into "unidentifiable" is the same mistake that hid playable titles for a week, so inconclusive stays retryable. _library_has reports presence at ANY height: height is absent from the library path, so the library holds one copy per episode. A later request at another quality is a no-op, and nothing is deleted to make room for an upgrade. 10 new tests, 85 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
64fa4e3e3b0addf2ac73407eb5ec3e3419712dd4
by plankatron <fleeky@gmail.com> · 2026-08-11T13:56:39.000Z

Changed files (2) +186 -1

addon.py +79 -1
……@@ -533,7 +533,7 @@ TV_GENRES = [
533533
534534MANIFEST = {
535535 "id": "org.tabertron.vsembed",
536 "version": "0.9.2",
536 "version": "0.9.3",
537537 "name": "Tabertron VsEmbed",
538538 "description": "Self-hosted HLS extractor with TMDB browse + search, played via vidcore",
539539 "types": ["movie", "series"],
……@@ -1162,6 +1162,84 @@ def _part_path(dest: str) -> str:
11621162 return dest + ".part"
11631163
11641164
1165# Naming is only as safe as the identity behind it, so a title we cannot
1166# identify is NOT ingested. The 2026-07-28 cleanup exists because wrongly-named
1167# media accumulated; a bad file in the library costs more than a missing one.
1168_library_meta_cache: dict[str, Optional[dict]] = {}
1169
1170
1171def _tmdb_id_for(kind: str, ident: str) -> Optional[str]:
1172 """`tmdb:<n>` verbatim, or a `tt<n>` translated through TMDB's find."""
1173 if ident.startswith("tmdb:"):
1174 return ident.split(":", 1)[1]
1175 data = _tmdb_get(f"find/{ident}", {"external_source": "imdb_id"})
1176 key = "tv_results" if kind == "series" else "movie_results"
1177 results = data.get(key) or []
1178 return str(results[0]["id"]) if results else None
1179
1180
1181def _library_meta(kind: str, ident: str, season: int = 0,
1182 episode: int = 0) -> Optional[dict]:
1183 """Everything needed to name one file, or None if it cannot be identified.
1184
1185 A "not found" answer is cached (it is stable), but a TMDB *failure* is not:
1186 marking a title permanently unidentifiable because of one bad request is the
1187 same mistake that hid playable titles for a week -- inconclusive must not
1188 harden into absent.
1189 """
1190 key = f"{kind}:{ident}:{season}:{episode}"
1191 if key in _library_meta_cache:
1192 return _library_meta_cache[key]
1193 try:
1194 tmdb_id = _tmdb_id_for(kind, ident)
1195 if not tmdb_id:
1196 log.info("library: %s has no TMDB id — not ingestable", ident)
1197 _library_meta_cache[key] = None
1198 return None
1199 if kind == "series":
1200 show = _tmdb_get(f"tv/{tmdb_id}")
1201 title = show.get("name") or ""
1202 year = (show.get("first_air_date") or "")[:4]
1203 ep = _tmdb_get(f"tv/{tmdb_id}/season/{season}/episode/{episode}")
1204 episode_title = ep.get("name") or ""
1205 else:
1206 film = _tmdb_get(f"movie/{tmdb_id}")
1207 title = film.get("title") or ""
1208 year = (film.get("release_date") or "")[:4]
1209 episode_title = ""
1210 except Exception as exc:
1211 # Deliberately NOT cached — see docstring.
1212 log.warning("library meta failed for %s: %s", key, exc)
1213 return None
1214 if not title:
1215 log.info("library: %s resolved to no title — not ingestable", ident)
1216 _library_meta_cache[key] = None
1217 return None
1218 meta = {"tmdb_id": tmdb_id, "title": title,
1219 "year": year, "episode_title": episode_title}
1220 _library_meta_cache[key] = meta
1221 return meta
1222
1223
1224def _library_has(kind: str, ident: str, season: int = 0,
1225 episode: int = 0) -> bool:
1226 """True if the library already holds this episode, at ANY height.
1227
1228 Height is deliberately absent from the library path: the library holds one
1229 copy per episode. A later request at a different quality is a no-op rather
1230 than a second file, and nothing is ever deleted to make room for an upgrade.
1231 """
1232 meta = _library_meta(kind, ident, season, episode)
1233 if not meta:
1234 return False
1235 dest = _library_path(kind, meta["title"], meta["year"], meta["tmdb_id"],
1236 season, episode, meta["episode_title"])
1237 try:
1238 return os.path.getsize(dest) > 0
1239 except OSError:
1240 return False
1241
1242
11651243def _remux_to_mp4(variant_url: str, out_path: str) -> None:
11661244 """Stream-copy an HLS variant into a progressive MP4.
11671245
test_library_ingest.py +107 -0
……@@ -158,3 +158,110 @@ def test_the_part_file_is_a_sibling_of_its_destination():
158158 dest = addon._library_path("series", "Show", "2020", "1",
159159 season=1, episode=1, episode_title="T")
160160 assert os.path.dirname(addon._part_path(dest)) == os.path.dirname(dest)
161
162
163# --- metadata lookup --------------------------------------------------------
164# Naming is only safe if the identity behind it is. A title we cannot identify
165# must NOT be ingested under a guessed name: the 2026-07-28 cleanup existed
166# precisely because wrongly-named media had accumulated, and a bad file in the
167# library costs more than a missing one.
168
169@pytest.fixture
170def tmdb(monkeypatch):
171 """Stand in for TMDB. Tests must never reach the network."""
172 calls = []
173 responses = {}
174
175 def fake(path, params=None):
176 calls.append(path)
177 if path not in responses:
178 raise RuntimeError(f"unexpected TMDB call: {path}")
179 return responses[path]
180
181 monkeypatch.setattr(addon, "_tmdb_get", fake)
182 monkeypatch.setattr(addon, "_library_meta_cache", {})
183 return type("T", (), {"calls": calls, "responses": responses})()
184
185
186def test_series_metadata_comes_from_the_tmdb_id(tmdb):
187 tmdb.responses["tv/138502"] = {"name": "X-Men '97", "first_air_date": "2024-03-20"}
188 tmdb.responses["tv/138502/season/2/episode/7"] = {"name": "Bright Eyes"}
189 m = addon._library_meta("series", "tmdb:138502", 2, 7)
190 assert m == {"tmdb_id": "138502", "title": "X-Men '97",
191 "year": "2024", "episode_title": "Bright Eyes"}
192
193
194def test_movie_metadata_comes_from_the_tmdb_id(tmdb):
195 tmdb.responses["movie/78"] = {"title": "Blade Runner", "release_date": "1982-06-25"}
196 m = addon._library_meta("movie", "tmdb:78")
197 assert m["title"] == "Blade Runner" and m["year"] == "1982"
198 assert m["episode_title"] == ""
199
200
201def test_an_imdb_ident_is_translated_to_a_tmdb_id(tmdb):
202 """The cinemeta path supplies tt-ids, but the library names by TMDB id."""
203 tmdb.responses["find/tt10310354"] = {
204 "tv_results": [{"id": 138502}], "movie_results": []}
205 tmdb.responses["tv/138502"] = {"name": "X-Men '97", "first_air_date": "2024-03-20"}
206 tmdb.responses["tv/138502/season/2/episode/7"] = {"name": "Bright Eyes"}
207 m = addon._library_meta("series", "tt10310354", 2, 7)
208 assert m["tmdb_id"] == "138502"
209
210
211def test_an_unresolvable_ident_yields_no_metadata(tmdb):
212 """No id, no ingest -- rather than inventing a folder name."""
213 tmdb.responses["find/tt00000000"] = {"tv_results": [], "movie_results": []}
214 assert addon._library_meta("series", "tt00000000", 1, 1) is None
215
216
217def test_a_tmdb_failure_yields_no_metadata_rather_than_a_guess(tmdb):
218 """_tmdb_get raises for anything unstubbed, so this exercises the real
219 failure path: the lookup must swallow it and decline, not propagate."""
220 assert addon._library_meta("series", "tmdb:999999", 1, 1) is None
221
222
223def test_a_missing_episode_title_is_not_fatal(tmdb):
224 """TMDB carries the show but not always the episode name. The episode is
225 still ingestable -- Jellyfin keys on SxxExx."""
226 tmdb.responses["tv/1"] = {"name": "Show", "first_air_date": "2020-01-01"}
227 tmdb.responses["tv/1/season/1/episode/1"] = {}
228 m = addon._library_meta("series", "tmdb:1", 1, 1)
229 assert m["episode_title"] == "" and m["title"] == "Show"
230
231
232def test_a_show_with_no_air_date_still_resolves(tmdb):
233 tmdb.responses["tv/1"] = {"name": "Show"}
234 tmdb.responses["tv/1/season/1/episode/1"] = {"name": "Pilot"}
235 m = addon._library_meta("series", "tmdb:1", 1, 1)
236 assert m["year"] == "" and m["title"] == "Show"
237
238
239def test_metadata_is_cached_across_calls(tmdb):
240 """Ingest asks per episode; the show lookup must not be repeated for every
241 episode of a season."""
242 tmdb.responses["tv/1"] = {"name": "Show", "first_air_date": "2020-01-01"}
243 tmdb.responses["tv/1/season/1/episode/1"] = {"name": "Pilot"}
244 addon._library_meta("series", "tmdb:1", 1, 1)
245 before = len(tmdb.calls)
246 addon._library_meta("series", "tmdb:1", 1, 1)
247 assert len(tmdb.calls) == before
248
249
250# --- one copy per episode, regardless of height -----------------------------
251
252def test_an_existing_copy_at_any_height_counts_as_present(tmdb):
253 """The library holds one copy per episode. A 1080p re-request must not
254 produce a second file beside an existing 720p one -- and nothing is ever
255 deleted to make room for an upgrade."""
256 tmdb.responses["tv/1"] = {"name": "Show", "first_air_date": "2020-01-01"}
257 tmdb.responses["tv/1/season/1/episode/1"] = {"name": "Pilot"}
258 dest = addon._library_path("series", "Show", "2020", "1",
259 season=1, episode=1, episode_title="Pilot")
260 assert addon._library_has("series", "tmdb:1", 1, 1) is False
261 os.makedirs(os.path.dirname(dest), exist_ok=True)
262 open(dest, "wb").write(b"x")
263 assert addon._library_has("series", "tmdb:1", 1, 1) is True
264
265
266def test_an_unidentifiable_title_is_reported_absent_not_crashing(tmdb):
267 assert addon._library_has("series", "tmdb:999999", 1, 1) is False