Move single-track resolver; fix wishlist cleanup

- keep single-track import lookup in imports/resolution.py
- normalize simple-download search_result data before wishlist matching
- run wishlist cleanup for simple-download post-processing
- keep source-only artist detail on resolved names and MB short-circuit
This commit is contained in:
Antti Kettunen 2026-04-25 21:09:11 +03:00
parent 9321fc4ad2
commit 4c819681a1
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7
8 changed files with 76 additions and 28 deletions

View file

@ -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=""),
),
}

View file

@ -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}")

View file

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

View file

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

View file

@ -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()

View file

@ -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)]

View file

@ -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",

View file

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