From 03454783610d503166bd9f7249665103a40f4b26 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 17 May 2026 20:50:55 -0700 Subject: [PATCH] Skip wishlist adds for manual library matches --- core/library/manual_library_match.py | 5 ++- database/music_database.py | 10 +++++ pr_description.md | 56 ++++++++++++++++++++++++++++ tests/test_manual_library_match.py | 26 +++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 pr_description.md diff --git a/core/library/manual_library_match.py b/core/library/manual_library_match.py index 6d604bfc..1387c203 100644 --- a/core/library/manual_library_match.py +++ b/core/library/manual_library_match.py @@ -36,7 +36,10 @@ def get_match( server_source: str = "", ) -> Optional[dict]: """Return match row dict or None if not found.""" - return db.get_manual_library_match(profile_id, source, source_track_id, server_source) + getter = getattr(db, "get_manual_library_match", None) + if getter is None: + return None + return getter(profile_id, source, source_track_id, server_source) def delete_match(db, match_id: int, profile_id: int) -> bool: diff --git a/database/music_database.py b/database/music_database.py index a117465c..0e654d06 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -7217,6 +7217,16 @@ class MusicDatabase: logger.error("Cannot add track to wishlist: missing track ID") return False + track_source = spotify_track_data.get('provider') or spotify_track_data.get('source') or 'spotify' + if self.get_manual_library_match(profile_id, track_source, track_id): + logger.info( + "Skipping wishlist add for manually matched track: '%s' (%s:%s)", + spotify_track_data.get('name', 'Unknown Track'), + track_source, + track_id, + ) + return True + track_name = spotify_track_data.get('name', 'Unknown Track') artists = spotify_track_data.get('artists', []) if artists: diff --git a/pr_description.md b/pr_description.md new file mode 100644 index 00000000..da9aeb6c --- /dev/null +++ b/pr_description.md @@ -0,0 +1,56 @@ +## Summary + +Adds a new **Manual Library Match** tool that lets users map a wishlist/sync source track to an existing library track. Once saved, SoulSync can treat that source track as already owned/found instead of repeatedly trying to download it. + +## What Changed + +- Added a centralized Manual Library Match tool on the Tools page and a shortcut from the Sync page. +- Added side-by-side source-track and library-track search UI, plus an existing matches table with removal support. +- Added `manual_library_track_matches` persistence with profile/source/source-track scoping. +- Added backend search/list/save/delete endpoints for manual matches. +- Integrated manual matches into download analysis so matched source tracks are marked found and skipped. +- Preserved the distinction between wishlist internal force mode and explicit user Force Download All: + - Wishlist/internal `force_download_all` still honors manual matches. + - User-facing Force Download All ignores manual matches and downloads anyway. +- Updated wishlist cleanup so manual matches are removed from the wishlist through: + - manual wishlist cleanup, + - automatic cleanup after DB update, + - wishlist download analysis. +- Prevented manually matched source tracks from being re-added to the wishlist in the common database add path. +- Polished the Tools page card so Manual Library Match visually matches the Discovery Pool tool card. + +## Behavior + +- Manual match saved: + - `source + source_track_id + profile_id -> library_track_id` +- Wishlist cleanup: + - removes the item if a manual match exists. +- Wishlist add: + - skips inserting the item if a manual match already exists, returning a harmless success/no-op. +- Wishlist download: + - checks manual match first, marks found, skips download, and attempts wishlist removal. +- Normal download with Force Download All off: + - checks manual match before normal library matching. +- Normal download with Force Download All on: + - skips manual matches and downloads selected tracks intentionally. + +## Tests + +Verified by user: + +```bash +./.venv/bin/python -m pytest tests/test_manual_library_match.py tests/downloads/test_downloads_master.py +``` + +Result: + +```text +43 passed in 10.29s +``` + +Additional local verification: + +```text +py_compile passed for touched Python files +node --check passed for touched JS +``` diff --git a/tests/test_manual_library_match.py b/tests/test_manual_library_match.py index 7a80b2c1..4a87c236 100644 --- a/tests/test_manual_library_match.py +++ b/tests/test_manual_library_match.py @@ -79,6 +79,32 @@ def test_get_returns_none_when_absent(db): assert db.get_manual_library_match(1, "spotify", "nonexistent") is None +def test_add_to_wishlist_skips_manual_matched_track(db): + db.save_manual_library_match(1, "spotify", "track-abc", 42) + + ok = db.add_to_wishlist( + track_data={ + "id": "track-abc", + "name": "HUMBLE.", + "artists": [{"name": "Kendrick Lamar"}], + "album": {"name": "DAMN."}, + "provider": "spotify", + }, + failure_reason="Download failed", + profile_id=1, + ) + + assert ok is True + assert db.get_wishlist_tracks(profile_id=1) == [] + + +def test_get_match_returns_none_when_db_lacks_manual_match_method(): + class _MinimalDB: + pass + + assert mlm.get_match(_MinimalDB(), 1, "spotify", "track-abc") is None + + # --------------------------------------------------------------------------- # Service-layer tests # ---------------------------------------------------------------------------