video enrichment: survive legacy UNIQUE on tmdb_id/tvdb_id (no scan crash)

Existing DBs created before the schema dropped UNIQUE still have shows.tvdb_id /
movies.tmdb_id UNIQUE, so enrichment matching two items to the same id threw
'UNIQUE constraint failed' repeatedly. enrichment_apply now catches the
IntegrityError and retries without the id columns — keeps the existing
(authoritative) id and still records match_status + metadata. Non-destructive
(no table rebuild). Test simulates the legacy unique index.
This commit is contained in:
BoulderBadgeDad 2026-06-14 12:34:52 -07:00
parent 2901e4ec4d
commit 776afdd1fd
2 changed files with 45 additions and 10 deletions

View file

@ -179,21 +179,39 @@ class VideoDatabase:
if not spec:
return
tbl, idc, sc, ac = spec
sets = [f"{sc}=?", f"{ac}=CURRENT_TIMESTAMP"]
params = ["matched" if matched else "not_found"]
if matched and external_id is not None:
sets.append(f"{idc}=?")
params.append(external_id)
allowed = _ENRICH_META_COLS.get(tbl, set())
for col, val in (metadata or {}).items():
if val is not None and col in allowed:
# On legacy DBs tmdb_id/tvdb_id may still carry a UNIQUE index; if a match
# would collide with another row's id we drop the id columns and keep the
# existing (authoritative) id, still recording status + metadata.
id_cols = {"tmdb_id", "tvdb_id", "imdb_id"}
def build(include_ids):
sets = [f"{sc}=?", f"{ac}=CURRENT_TIMESTAMP"]
params = ["matched" if matched else "not_found"]
if matched and external_id is not None and include_ids:
sets.append(f"{idc}=?")
params.append(external_id)
for col, val in (metadata or {}).items():
if val is None or col not in allowed:
continue
if not include_ids and col in id_cols:
continue
sets.append(f"{col}=?")
params.append(val)
params.append(item_id)
params.append(item_id)
return f"UPDATE {tbl} SET {', '.join(sets)} WHERE id=?", params
conn = self._get_connection()
try:
conn.execute(f"UPDATE {tbl} SET {', '.join(sets)} WHERE id=?", params)
conn.commit()
sql, params = build(True)
try:
conn.execute(sql, params)
conn.commit()
except sqlite3.IntegrityError:
conn.rollback()
sql, params = build(False) # keep existing id, just record status/metadata
conn.execute(sql, params)
conn.commit()
finally:
conn.close()

View file

@ -365,6 +365,23 @@ def test_enrichment_apply_matched_sets_id_status_and_metadata(db):
assert (row["overview"], row["backdrop_url"], row["imdb_id"]) == ("O", "/b.jpg", "tt1")
def test_enrichment_apply_survives_legacy_unique(db):
# Simulate a pre-existing DB where tvdb_id still carries a UNIQUE index.
with db.connect() as c:
c.execute("CREATE UNIQUE INDEX ux_legacy_shows_tvdb ON shows(tvdb_id)")
c.commit()
a = db.upsert_show_tree("plex", {"server_id": "s1", "title": "A", "seasons": []})
b = db.upsert_show_tree("plex", {"server_id": "s2", "title": "B", "seasons": []})
db.enrichment_apply("tvdb", "show", a, matched=True, external_id=555, metadata={"overview": "OA"})
# b would collide on tvdb_id=555 — must NOT crash; keeps existing id, records the rest.
db.enrichment_apply("tvdb", "show", b, matched=True, external_id=555, metadata={"overview": "OB"})
with db.connect() as c:
ra = c.execute("SELECT tvdb_id, tvdb_match_status FROM shows WHERE id=?", (a,)).fetchone()
rb = c.execute("SELECT tvdb_id, tvdb_match_status, overview FROM shows WHERE id=?", (b,)).fetchone()
assert (ra["tvdb_id"], ra["tvdb_match_status"]) == (555, "matched")
assert rb["tvdb_id"] is None and rb["tvdb_match_status"] == "matched" and rb["overview"] == "OB"
def test_enrichment_breakdown_unmatched_retry(db):
a = db.upsert_movie("plex", {"server_id": "m1", "title": "A"})
b = db.upsert_movie("plex", {"server_id": "m2", "title": "B"})