diff --git a/core/imports/context.py b/core/imports/context.py index c19aa180..e24b8663 100644 --- a/core/imports/context.py +++ b/core/imports/context.py @@ -49,11 +49,12 @@ def normalize_import_context(context: Optional[Dict[str, Any]]) -> Dict[str, Any album = _as_dict(context.get("album") or context.get("spotify_album")) track_info = _as_dict(context.get("track_info")) original_search = _as_dict(context.get("original_search_result")) + search_result = _as_dict(context.get("search_result")) context["artist"] = artist context["album"] = album context["track_info"] = track_info - context["original_search_result"] = original_search + context["original_search_result"] = original_search or search_result context.pop("spotify_artist", None) context.pop("spotify_album", None) @@ -102,6 +103,12 @@ def get_import_original_search(context: Optional[Dict[str, Any]]) -> Dict[str, A return _as_dict(context.get("original_search_result")) +def get_import_search_result(context: Optional[Dict[str, Any]]) -> Dict[str, Any]: + if not isinstance(context, dict): + return {} + return _as_dict(context.get("search_result")) + + def get_import_source(context: Optional[Dict[str, Any]]) -> str: if not isinstance(context, dict): return "" @@ -199,6 +206,7 @@ def get_import_has_full_metadata(context: Optional[Dict[str, Any]]) -> bool: def get_import_source_ids(context: Optional[Dict[str, Any]]) -> Dict[str, str]: track_info = get_import_track_info(context) original_search = get_import_original_search(context) + search_result = get_import_search_result(context) artist = get_import_context_artist(context) album = get_import_context_album(context) @@ -208,12 +216,16 @@ def get_import_source_ids(context: Optional[Dict[str, Any]]) -> Dict[str, str]: _first_value(track_info, "spotify_track_id", "itunes_track_id", "deezer_id", "deezer_track_id", "discogs_id", "soul_id", default=""), _first_value(original_search, "id", "track_id", "source_track_id", default=""), _first_value(original_search, "spotify_track_id", "itunes_track_id", "deezer_id", "deezer_track_id", "discogs_id", "soul_id", default=""), + _first_value(search_result, "id", "track_id", "source_track_id", default=""), + _first_value(search_result, "spotify_track_id", "itunes_track_id", "deezer_id", "deezer_track_id", "discogs_id", "soul_id", default=""), ), "artist_id": _first_id_value( _first_value(artist, "id", "artist_id", "source_artist_id", default=""), _first_value(artist, "spotify_artist_id", "itunes_artist_id", "deezer_id", "deezer_artist_id", "discogs_id", "soul_id", default=""), _first_value(original_search, "artist_id", "source_artist_id", default=""), _first_value(original_search, "spotify_artist_id", "itunes_artist_id", "deezer_id", "deezer_artist_id", "discogs_id", "soul_id", default=""), + _first_value(search_result, "artist_id", "source_artist_id", default=""), + _first_value(search_result, "spotify_artist_id", "itunes_artist_id", "deezer_id", "deezer_artist_id", "discogs_id", "soul_id", default=""), ), "album_id": _first_id_value( _first_value(album, "id", "album_id", "collectionId", "source_album_id", default=""), @@ -222,6 +234,8 @@ def get_import_source_ids(context: Optional[Dict[str, Any]]) -> Dict[str, str]: _first_value(original_search, "spotify_album_id", "itunes_album_id", "deezer_id", "deezer_album_id", "discogs_id", "soul_id", "album_soul_id", "hydrabase_album_id", default=""), _first_value(track_info, "album_id", "source_album_id", default=""), _first_value(track_info, "spotify_album_id", "itunes_album_id", "deezer_id", "deezer_album_id", "discogs_id", "soul_id", "album_soul_id", "hydrabase_album_id", default=""), + _first_value(search_result, "album_id", "source_album_id", default=""), + _first_value(search_result, "spotify_album_id", "itunes_album_id", "deezer_id", "deezer_album_id", "discogs_id", "soul_id", "album_soul_id", "hydrabase_album_id", default=""), ), } diff --git a/core/imports/pipeline.py b/core/imports/pipeline.py index 5d202a2a..7eaacc5d 100644 --- a/core/imports/pipeline.py +++ b/core/imports/pipeline.py @@ -257,6 +257,10 @@ def post_process_matched_download(context_key, context, file_path, runtime, meta emit_track_downloaded(context, automation_engine) record_library_history_download(context) record_download_provenance(context) + try: + check_and_remove_from_wishlist(context) + except Exception as wishlist_error: + logger.error(f"[Simple Download] Error checking wishlist removal: {wishlist_error}") return logger.info(f"Starting robust post-processing for: {context_key}") diff --git a/core/imports/side_effects.py b/core/imports/side_effects.py index b44bfe61..9c7c6636 100644 --- a/core/imports/side_effects.py +++ b/core/imports/side_effects.py @@ -15,6 +15,7 @@ from core.imports.context import ( get_import_context_album, get_import_context_artist, get_import_original_search, + get_import_search_result, get_import_source, get_import_source_ids, get_import_track_info, @@ -522,7 +523,8 @@ def check_and_remove_from_wishlist(context: Dict[str, Any]) -> None: "discogs": "Discogs", "hydrabase": "Hydrabase", }.get(source, "Source") - track_info = context.get("track_info", {}) + track_info = get_import_track_info(context) or get_import_search_result(context) + search_result = get_import_original_search(context) or get_import_search_result(context) track_id = None if source == "spotify": @@ -540,8 +542,8 @@ def check_and_remove_from_wishlist(context: Dict[str, Any]) -> None: break if not track_id: - track_name = track_info.get("name") or context.get("original_search_result", {}).get("title", "") - artist_name = _primary_track_artist_name(track_info) or _primary_track_artist_name(context.get("original_search_result", {})) + track_name = track_info.get("name") or search_result.get("title", "") + artist_name = _primary_track_artist_name(track_info) or _primary_track_artist_name(search_result) if track_name and artist_name: logger.warning("[Wishlist] No track ID found, checking for fuzzy match: '%s' by '%s'", track_name, artist_name) diff --git a/core/metadata_service.py b/core/metadata_service.py index 66487d60..44ded0d2 100644 --- a/core/metadata_service.py +++ b/core/metadata_service.py @@ -841,25 +841,6 @@ def _normalize_context_artists(artists: Any) -> List[Dict[str, Any]]: return normalized -def get_single_track_import_context( - title: str, - artist: str = '', - override_id: Optional[str] = None, - override_source: str = 'spotify', - source_override: Optional[str] = None, -) -> Dict[str, Any]: - """Compatibility wrapper for the single-track import resolver.""" - from core.imports.resolution import get_single_track_import_context as _get_single_track_import_context - - return _get_single_track_import_context( - title, - artist=artist, - override_id=override_id, - override_source=override_source, - source_override=source_override, - ) - - def resolve_album_reference( album_id: str, preferred_source: Optional[str] = None, diff --git a/tests/imports/test_import_pipeline.py b/tests/imports/test_import_pipeline.py index 231a0437..51b34361 100644 --- a/tests/imports/test_import_pipeline.py +++ b/tests/imports/test_import_pipeline.py @@ -59,6 +59,7 @@ def test_verification_wrapper_handles_simple_download(tmp_path, monkeypatch): completion_calls = [] scan_calls = [] activity_calls = [] + wishlist_calls = [] original_matched_context = dict(runtime_state.matched_downloads_context) original_download_tasks = dict(runtime_state.download_tasks) @@ -89,6 +90,7 @@ def test_verification_wrapper_handles_simple_download(tmp_path, monkeypatch): monkeypatch.setattr(import_pipeline, "emit_track_downloaded", lambda *args, **kwargs: None) monkeypatch.setattr(import_pipeline, "record_library_history_download", lambda *args, **kwargs: None) monkeypatch.setattr(import_pipeline, "record_download_provenance", lambda *args, **kwargs: None) + monkeypatch.setattr(import_pipeline, "check_and_remove_from_wishlist", lambda context: wishlist_calls.append(dict(context))) monkeypatch.setattr(import_pipeline, "_mark_task_completed", lambda task, track_info: mark_calls.append((task, track_info))) monkeypatch.setattr(import_pipeline.threading, "Thread", _ImmediateThread) @@ -114,6 +116,7 @@ def test_verification_wrapper_handles_simple_download(tmp_path, monkeypatch): assert completion_calls == [(batch_id, task_id, True)] assert context_key not in runtime_state.matched_downloads_context assert scan_calls == ["Simple download completed"] + assert wishlist_calls and wishlist_calls[0]["search_result"]["is_simple_download"] is True assert activity_calls finally: runtime_state.matched_downloads_context.clear() diff --git a/tests/imports/test_import_side_effects.py b/tests/imports/test_import_side_effects.py index 697d845b..d1dc744a 100644 --- a/tests/imports/test_import_side_effects.py +++ b/tests/imports/test_import_side_effects.py @@ -12,6 +12,19 @@ class _FakeDB: return self._conn +class _FakeWishlistService: + def __init__(self, tracks): + self.tracks = tracks + self.removed = [] + + def get_wishlist_tracks_for_download(self, profile_id=1): + return list(self.tracks) + + def mark_track_download_result(self, spotify_track_id, success, error_message=None, profile_id=1): + self.removed.append((spotify_track_id, success, error_message, profile_id)) + return True + + def _make_soulsync_db(): conn = sqlite3.connect(":memory:") conn.row_factory = sqlite3.Row @@ -142,3 +155,33 @@ def test_record_soulsync_library_entry_writes_artist_album_and_track(tmp_path, m assert track_row["track_artist"] == "Guest Artist" assert track_row["album_id"] == album_row["id"] assert track_row["file_path"] == str(final_path) + + +def test_check_and_remove_from_wishlist_uses_search_result_fallback(monkeypatch): + fake_db = SimpleNamespace(get_all_profiles=lambda: [{"id": 1}]) + wishlist_service = _FakeWishlistService([ + { + "wishlist_id": 11, + "spotify_track_id": "sp-track-1", + "id": "sp-track-1", + "name": "Song One", + "artists": [{"name": "Artist One"}], + } + ]) + + monkeypatch.setattr(side_effects, "get_database", lambda: fake_db) + monkeypatch.setattr(side_effects, "get_wishlist_service", lambda: wishlist_service) + + context = { + "search_result": { + "title": "Song One", + "artist": "Artist One", + "album": "Album One", + }, + "track_info": {}, + "original_search_result": {}, + } + + side_effects.check_and_remove_from_wishlist(context) + + assert wishlist_service.removed == [("sp-track-1", True, None, 1)] diff --git a/tests/metadata/test_metadata_service_single_import_context.py b/tests/metadata/test_metadata_service_single_import_context.py index ed7ae567..0d09d668 100644 --- a/tests/metadata/test_metadata_service_single_import_context.py +++ b/tests/metadata/test_metadata_service_single_import_context.py @@ -1,6 +1,7 @@ from types import SimpleNamespace from core import metadata_service +from core.imports import resolution class FakeClient: @@ -74,7 +75,7 @@ def test_get_single_track_import_context_uses_primary_source_priority(monkeypatc lambda source: {"deezer": deezer_client, "spotify": spotify_client, "itunes": None}.get(source), ) - result = metadata_service.get_single_track_import_context("Song One", "Artist One") + result = resolution.get_single_track_import_context("Song One", "Artist One") assert result["success"] is True assert result["source"] == "deezer" @@ -108,7 +109,7 @@ def test_get_single_track_import_context_falls_back_to_next_source(monkeypatch): lambda source: {"deezer": deezer_client, "spotify": spotify_client, "itunes": None}.get(source), ) - result = metadata_service.get_single_track_import_context("Song Two", "Artist Two") + result = resolution.get_single_track_import_context("Song Two", "Artist Two") assert result["success"] is True assert result["source"] == "spotify" @@ -141,7 +142,7 @@ def test_get_single_track_import_context_uses_explicit_override_first(monkeypatc lambda source: {"deezer": deezer_client, "spotify": spotify_client, "itunes": None}.get(source), ) - result = metadata_service.get_single_track_import_context( + result = resolution.get_single_track_import_context( "Ignored Title", "Ignored Artist", override_id="override-track-1", @@ -174,7 +175,7 @@ def test_get_single_track_import_context_uses_explicit_override_source(monkeypat lambda source: {"deezer": deezer_client, "spotify": spotify_client, "itunes": itunes_client}.get(source), ) - result = metadata_service.get_single_track_import_context( + result = resolution.get_single_track_import_context( "Ignored Title", "Ignored Artist", override_id="override-track-2", diff --git a/web_server.py b/web_server.py index 751266bd..b548b08f 100644 --- a/web_server.py +++ b/web_server.py @@ -48267,7 +48267,7 @@ def import_singles_process(): if not artist: artist = parsed.get('artist', '') - from core.metadata_service import get_single_track_import_context + from core.imports.resolution import get_single_track_import_context resolved = get_single_track_import_context( title,