From 1ad80d77a63a98022aee4d06fb6e8a0a313fcd60 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Mon, 22 Jun 2026 09:06:34 -0700 Subject: [PATCH] =?UTF-8?q?#901:=20one-time=20backfill=20=E2=80=94=20stabl?= =?UTF-8?q?e=20ids=20for=20EXISTING=20file-import=20mirrored=20tracks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mirror_playlist fix only assigns stable ids to newly-imported playlists, so a user with an existing file-import playlist would still have empty-id rows (and dead Find & Add matches) until a manual re-import. Add an idempotent startup backfill that assigns the SAME stable id a fresh import would to any mirrored track missing one — so existing matches start sticking with no re-import. Runs once per db/process (the init is guarded), only touches empty-id rows (no-op afterward), native ids untouched. Tests: backfill fills empty ids with the exact fresh-import id, is idempotent (2nd run = 0), and leaves native ids alone. --- database/music_database.py | 33 +++++++++++++++++++++++ tests/database/test_mirrored_playlists.py | 33 +++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/database/music_database.py b/database/music_database.py index f047e1d9..20e6b217 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -992,6 +992,39 @@ class MusicDatabase: raise self._init_manual_library_match_table() + self._backfill_mirrored_track_source_ids() + + def _backfill_mirrored_track_source_ids(self) -> int: + """One-time, idempotent: assign a stable source_track_id to mirrored tracks + that have none (file-import / iTunes-only playlists imported before #901), so + their existing Find & Add matches start sticking without a manual re-import. + Only touches empty-id rows, so it's a no-op once they're filled.""" + from core.playlists.source_refs import stable_source_track_id + updated = 0 + try: + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT id, track_name, artist_name, album_name + FROM mirrored_playlist_tracks + WHERE source_track_id IS NULL OR source_track_id = '' + """) + rows = cursor.fetchall() + for r in rows: + sid = stable_source_track_id({ + 'track_name': r['track_name'], 'artist_name': r['artist_name'], + 'album_name': r['album_name']}) + if sid: + cursor.execute( + "UPDATE mirrored_playlist_tracks SET source_track_id = ? WHERE id = ?", + (sid, r['id'])) + updated += 1 + conn.commit() + if updated: + logger.info("Backfilled stable source_track_id on %d mirrored tracks (#901)", updated) + except Exception as e: + logger.error("mirrored track source_id backfill failed: %s", e) + return updated # Bump when the schema's generation meaningfully changes. Stamped into # PRAGMA user_version as a backstop indicator; nothing GATES on it yet. diff --git a/tests/database/test_mirrored_playlists.py b/tests/database/test_mirrored_playlists.py index ff563e68..c1d0b6b6 100644 --- a/tests/database/test_mirrored_playlists.py +++ b/tests/database/test_mirrored_playlists.py @@ -92,3 +92,36 @@ def test_native_ids_still_used_verbatim(tmp_path): profile_id=1) rows = db.get_mirrored_playlist_tracks(pid) assert rows[0]["source_track_id"] == "spotify123" # native id untouched + + +def test_backfill_fills_existing_empty_ids_idempotently(tmp_path): + # #901 backfill: a file-import playlist mirrored BEFORE the fix has empty-id rows. + # The backfill assigns the SAME stable ids a fresh import would, so existing + # Find & Add matches start working without a re-import. + db = MusicDatabase(str(tmp_path / "music.db")) + pid = db.mirror_playlist(source="file", source_playlist_id="old", name="Old", + tracks=[{"track_name": "Slow Ride", "artist_name": "Foghat"}], profile_id=1) + # simulate a pre-fix row: blank out the id + with db._get_connection() as conn: + conn.execute("UPDATE mirrored_playlist_tracks SET source_track_id = '' WHERE playlist_id = ?", (pid,)) + conn.commit() + + n = db._backfill_mirrored_track_source_ids() + assert n == 1 + rows = db.get_mirrored_playlist_tracks(pid) + from core.playlists.source_refs import stable_source_track_id + assert rows[0]["source_track_id"] == stable_source_track_id( + {"track_name": "Slow Ride", "artist_name": "Foghat"}) # same id a fresh import gives + + # idempotent — second run touches nothing + assert db._backfill_mirrored_track_source_ids() == 0 + + +def test_backfill_leaves_native_ids_untouched(tmp_path): + db = MusicDatabase(str(tmp_path / "music.db")) + pid = db.mirror_playlist(source="spotify", source_playlist_id="sp", name="Sp", + tracks=[{"track_name": "S", "artist_name": "A", "source_track_id": "spotify123"}], + profile_id=1) + db._backfill_mirrored_track_source_ids() + rows = db.get_mirrored_playlist_tracks(pid) + assert rows[0]["source_track_id"] == "spotify123"