'sami matar' was a wing-it FALLBACK stub — a placeholder the discovery pipeline makes when it can't resolve a track to real metadata (no album, no cover). The live sync explicitly skips wing_it_* ids for the wishlist (no metadata to act on), but my re-add didn't — so it stored a coverless, single-classified placeholder. That's why: sync didn't add it, no images, marked single. Fix (parity): reconstruct refuses ids starting 'wing_it_'. Frontend renders the '-> Wishlist' status as plain, non-clickable text for wing-it rows (with a tooltip) since they were never actually wishlisted. Real tracks keep the working button + the byte-identical-payload re-add from the prior fix.
103 lines
4.4 KiB
Python
103 lines
4.4 KiB
Python
"""Build the wishlist-add payload for a synced track — the SINGLE source of truth
|
|
shared by the live sync (core.discovery.sync) and the sync-detail "re-add to
|
|
wishlist" action, so a re-add is byte-for-byte the same payload the auto-add used.
|
|
|
|
Pure — no I/O. The web route supplies the parsed sync entry and calls the wishlist
|
|
service; the live sync calls build_original_tracks_map directly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
def normalize_wishlist_track(track: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Normalize ONE tracks_json track into the wishlist-add shape: album coerced to
|
|
a dict (preserving images + album_type/total_tracks/release_date), artists to a
|
|
list of dicts. Copy-safe — never mutates the input."""
|
|
normalized = dict(track)
|
|
raw_album = normalized.get('album', '')
|
|
if isinstance(raw_album, dict):
|
|
album = dict(raw_album)
|
|
album.setdefault('name', 'Unknown Album')
|
|
album.setdefault('images', [])
|
|
normalized['album'] = album
|
|
else:
|
|
name = raw_album if isinstance(raw_album, str) else (str(raw_album) if raw_album else '')
|
|
normalized['album'] = {
|
|
'name': name or normalized.get('name', 'Unknown Album'),
|
|
'images': [], 'album_type': 'single', 'total_tracks': 1, 'release_date': '',
|
|
}
|
|
raw_artists = normalized.get('artists', [])
|
|
if raw_artists and isinstance(raw_artists[0], str):
|
|
normalized['artists'] = [{'name': a} for a in raw_artists]
|
|
return normalized
|
|
|
|
|
|
def build_original_tracks_map(tracks_json: Optional[List[Dict[str, Any]]]) -> Dict[str, Dict[str, Any]]:
|
|
"""``{track_id: normalized_track}`` for a sync's tracks_json — the full-fidelity
|
|
map the live sync builds before auto-wishlisting unmatched tracks. One source of
|
|
truth so the re-add matches the auto-add exactly."""
|
|
out: Dict[str, Dict[str, Any]] = {}
|
|
for t in tracks_json or []:
|
|
if not isinstance(t, dict):
|
|
continue
|
|
track_id = t.get('id', '')
|
|
if track_id:
|
|
out[str(track_id)] = normalize_wishlist_track(t)
|
|
return out
|
|
|
|
|
|
def reconstruct_sync_track_data(
|
|
track_results: Optional[List[Dict[str, Any]]],
|
|
tracks: Optional[List[Dict[str, Any]]],
|
|
track_index: int,
|
|
) -> Optional[Dict[str, Any]]:
|
|
"""Return the wishlist-add ``spotify_track_data`` for re-adding a synced track.
|
|
|
|
Resolves the track the SAME way the auto-add did: the normalized tracks_json
|
|
entry (``build_original_tracks_map``), looked up by the track_result's
|
|
``source_track_id`` — so the payload (full album object + images + album_type +
|
|
total_tracks + artists-as-dicts) is identical to the original auto-add.
|
|
|
|
Only 'wishlist' rows are eligible. Falls back to a normalized rebuild from the
|
|
track_result's own fields (with the album cover from its image_url) when the
|
|
cached track is missing. None when ineligible or unidentifiable.
|
|
"""
|
|
if not track_results or track_index < 0 or track_index >= len(track_results):
|
|
return None
|
|
tr = track_results[track_index] or {}
|
|
if tr.get('download_status') != 'wishlist':
|
|
return None
|
|
|
|
sid = str(tr.get('source_track_id') or '')
|
|
# Wing-it fallback stubs have no real metadata (no album/cover) — the live sync
|
|
# SKIPS them for the wishlist (services/sync_service.py), so the re-add must too,
|
|
# rather than store a coverless, mis-classified placeholder.
|
|
if sid.startswith('wing_it_'):
|
|
return None
|
|
|
|
# Primary: the exact normalized track the auto-add used.
|
|
if sid:
|
|
payload = build_original_tracks_map(tracks).get(sid)
|
|
if payload:
|
|
return payload
|
|
|
|
# Fallback: rebuild from the track_result fields, through the SAME normalizer so
|
|
# the shape matches, and seed the album cover from the row's image_url.
|
|
if not sid:
|
|
return None
|
|
album: Dict[str, Any] = {'name': tr.get('album') or '', 'album_type': 'single',
|
|
'total_tracks': 1, 'release_date': ''}
|
|
if tr.get('image_url'):
|
|
album['images'] = [{'url': tr['image_url']}]
|
|
return normalize_wishlist_track({
|
|
'id': sid,
|
|
'name': tr.get('name') or '',
|
|
'artists': [{'name': tr.get('artist') or ''}],
|
|
'album': album,
|
|
'duration_ms': tr.get('duration_ms') or 0,
|
|
})
|
|
|
|
|
|
__all__ = ["normalize_wishlist_track", "build_original_tracks_map", "reconstruct_sync_track_data"]
|