Skip wishlist adds for manual library matches

This commit is contained in:
Broque Thomas 2026-05-17 20:50:55 -07:00
parent 3e7eeb7c9c
commit 0345478361
4 changed files with 96 additions and 1 deletions

View file

@ -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:

View file

@ -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:

56
pr_description.md Normal file
View file

@ -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
```

View file

@ -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
# ---------------------------------------------------------------------------