diff --git a/core/wishlist/routes.py b/core/wishlist/routes.py index a467676c..c9536828 100644 --- a/core/wishlist/routes.py +++ b/core/wishlist/routes.py @@ -501,6 +501,10 @@ def add_album_track_to_wishlist( source_type=source_type, source_context=enhanced_source_context, profile_id=runtime.profile_id, + # Explicit user click in the album modal — must bypass + clear the + # ignore-list, even if the user previously cancelled this track + # (otherwise the add is silently dropped — carlosjfcasero, #897). + user_initiated=True, ) if success: diff --git a/core/wishlist/service.py b/core/wishlist/service.py index df3be19a..1f76f2d9 100644 --- a/core/wishlist/service.py +++ b/core/wishlist/service.py @@ -100,6 +100,7 @@ class WishlistService: source_type: str = "manual", source_context: Dict[str, Any] = None, profile_id: int = 1, + user_initiated: bool = False, ) -> bool: """ Directly add a track to the wishlist. @@ -110,6 +111,8 @@ class WishlistService: source_type: Source type ('playlist', 'album', 'manual') source_context: Additional context information profile_id: Profile to add to + user_initiated: True for an explicit user add — bypasses + clears the + ignore-list while keeping the real source_type (#874/#897). """ if track_data is None: track_data = spotify_track_data @@ -124,6 +127,7 @@ class WishlistService: source_type=source_type, source_info=source_context or {}, profile_id=profile_id, + user_initiated=user_initiated, ) def add_spotify_track_to_wishlist( diff --git a/database/music_database.py b/database/music_database.py index 263d993e..7fb473bd 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -8925,8 +8925,15 @@ class MusicDatabase: source_info: Dict[str, Any] = None, profile_id: int = 1, track_data: Dict[str, Any] = None, + user_initiated: bool = False, ) -> bool: - """Add a failed track to the wishlist for retry""" + """Add a failed track to the wishlist for retry. + + ``user_initiated`` marks an explicit user add (e.g. the library album + "add to wishlist" modal). Like ``source_type == 'manual'`` it bypasses + the ignore-list gate AND clears any stale ignore — but unlike changing + ``source_type`` it preserves the real provenance ('album'), which the + wishlist categorisation (Albums vs Singles) relies on (#874/#897).""" try: if track_data is not None and spotify_track_data is None: spotify_track_data = track_data @@ -8956,7 +8963,7 @@ class MusicDatabase: # clear any stale ignore so it sticks. Fail-open: any error here # must never block a legitimate wishlist add. try: - if source_type == 'manual': + if source_type == 'manual' or user_initiated: self.remove_from_wishlist_ignore(track_id, profile_id=profile_id) elif self.is_track_ignored(track_id, profile_id=profile_id): logger.info("Skipping wishlist add — track is on the ignore-list (#874): %s", track_id) diff --git a/tests/wishlist/test_wishlist_ignore.py b/tests/wishlist/test_wishlist_ignore.py index 783511f2..023b6113 100644 --- a/tests/wishlist/test_wishlist_ignore.py +++ b/tests/wishlist/test_wishlist_ignore.py @@ -146,6 +146,26 @@ def test_gate_blocks_auto_readd_but_manual_bypasses_and_clears(db): assert db.is_track_ignored("t1") is False +def test_user_initiated_add_bypasses_and_clears_keeping_source_type(db): + # #897 / carlosjfcasero: a user manually adds an album track they had + # previously cancelled. It must bypass the gate AND clear the ignore — but + # WITHOUT pretending to be source_type='manual' (the album modal sends + # source_type='album', which the Albums/Singles categorisation relies on, + # and which an automatic path like repair_worker also legitimately uses). + track = _track("t7") + db.add_to_wishlist_ignore("t7", "Owned Song", "Owned Artist", REASON_CANCELLED) + # An automatic 'album' add (e.g. repair_worker) is still correctly blocked. + assert db.add_to_wishlist(track, source_type="album") is False + assert db.is_track_ignored("t7") is True + # The explicit user click (user_initiated) goes through and clears the ignore, + # while the stored source_type stays 'album'. + assert db.add_to_wishlist(track, source_type="album", user_initiated=True) is True + assert db.is_track_ignored("t7") is False + # Provenance preserved: the stored row is still source_type='album', NOT 'manual'. + row = next(r for r in db.get_wishlist_tracks() if str(r.get("spotify_track_id")) == "t7") + assert row.get("source_type") == "album" + + def test_gate_failopen_when_ignore_table_errors(db, monkeypatch): # If the ignore check raises, the add must still succeed (never block). monkeypatch.setattr(db, "is_track_ignored", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("boom"))) diff --git a/webui/index.html b/webui/index.html index 69d96e55..cc7e4de1 100644 --- a/webui/index.html +++ b/webui/index.html @@ -7320,6 +7320,10 @@
+