diff --git a/database/video_database.py b/database/video_database.py index abe47caf..6b308f28 100644 --- a/database/video_database.py +++ b/database/video_database.py @@ -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() diff --git a/tests/test_video_database.py b/tests/test_video_database.py index 5fef251d..5fd4f5b9 100644 --- a/tests/test_video_database.py +++ b/tests/test_video_database.py @@ -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"})