You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SoulSync/core/metadata/artist_resolution.py

75 lines
2.6 KiB

"""Pure artist-list resolution for tag-write paths.
Single source of truth for "what is the canonical multi-value artists
list for this track?" Different download paths populate `context` with
different keys — Deezer-direct downloads stamp `original_search.artists`
as a proper list, but Soulseek matched downloads only carry `artist`
(singular string) in `original_search_result` while the full list lives
on `track_info` (the full Spotify track object).
Resolution order:
1. `context.original_search_result.artists` (preferred — already-
curated by the source path that constructed the context)
2. `context.track_info.artists` (Spotify/Deezer/Tidal full track
object — always carries the artists array when matched)
3. `[artist_dict.name]` as a single-element fallback when neither
carries a list (primary-artist-only)
Each list item may be a dict with a `name` key (Spotify shape), a bare
string, or any other object — the helper normalizes all three to
strings and drops empty entries.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
def _normalize_artists_iterable(items: Any) -> List[str]:
if not isinstance(items, list):
return []
result: List[str] = []
for item in items:
if isinstance(item, dict):
name = item.get("name")
if isinstance(name, str) and name.strip():
result.append(name.strip())
elif isinstance(item, str):
stripped = item.strip()
if stripped:
result.append(stripped)
elif item is not None:
text = str(item).strip()
if text:
result.append(text)
return result
def resolve_track_artists(
original_search: Optional[Dict[str, Any]],
track_info: Optional[Dict[str, Any]],
artist_dict: Optional[Dict[str, Any]],
) -> List[str]:
"""Return the canonical multi-value artists list for tag-write.
Falls through preferred → track_info → primary-artist fallback. Each
candidate is normalized to a list of stripped non-empty strings.
Empty list returned only when every candidate is empty/invalid.
"""
if isinstance(original_search, dict):
primary = _normalize_artists_iterable(original_search.get("artists"))
if primary:
return primary
if isinstance(track_info, dict):
secondary = _normalize_artists_iterable(track_info.get("artists"))
if secondary:
return secondary
if isinstance(artist_dict, dict):
name = artist_dict.get("name")
if isinstance(name, str) and name.strip():
return [name.strip()]
return []