@ -20,16 +20,12 @@ def sanitize_track_data_for_processing(track_data):
logger . info ( f " [Sanitize] Unexpected track data type: { type ( track_data ) } " )
return track_data
# Create a copy to avoid modifying original data
sanitized = track_data . copy ( )
# Handle album field - preserve dict format to retain full metadata (images, id, etc.)
# Downstream code already handles both dict and string formats defensively
raw_album = sanitized . get ( ' album ' , ' ' )
if not isinstance ( raw_album , ( dict , str ) ) :
sanitized [ ' album ' ] = str ( raw_album )
# Handle artists field - ensure it's a list of strings
raw_artists = sanitized . get ( ' artists ' , [ ] )
if isinstance ( raw_artists , list ) :
processed_artists = [ ]
@ -68,25 +64,22 @@ def get_track_artist_name(track_info):
return " Unknown Artist "
def ensure_ spo tify _track_format( track_info ) :
def ensure_ wishli st_track_format( track_info ) :
"""
Ensure track_info has proper Spotify track structure for wishlist service .
Converts webui track format to match sync . py ' s spotify_track format.
Ensure track_info has a consistent wishlist track structure .
This keeps the legacy Spotify - shaped fields because the download pipeline
still expects them , but the helper itself is provider - agnostic .
"""
if not track_info :
return { }
# If it already has the proper Spotify structure, return as-is
if isinstance ( track_info . get ( ' artists ' ) , list ) and len ( track_info . get ( ' artists ' , [ ] ) ) > 0 :
first_artist = track_info [ ' artists ' ] [ 0 ]
if isinstance ( first_artist , dict ) and ' name ' in first_artist :
# Already has proper Spotify format
return track_info
# Convert to proper Spotify format
artists_list = [ ]
# Handle different artist formats from webui
artists = track_info . get ( ' artists ' , [ ] )
if artists :
if isinstance ( artists , list ) :
@ -98,22 +91,17 @@ def ensure_spotify_track_format(track_info):
else :
artists_list . append ( { ' name ' : str ( artist ) } )
else :
# Single artist as string
artists_list . append ( { ' name ' : str ( artists ) } )
else :
# Fallback: try single artist field
artist = track_info . get ( ' artist ' )
if artist :
artists_list . append ( { ' name ' : str ( artist ) } )
else :
artists_list . append ( { ' name ' : ' Unknown Artist ' } )
# Build album object - preserve ALL fields (id, release_date, total_tracks,
# album_type, images, etc.) so wishlist tracks retain full album context
# for correct folder placement, multi-disc support, and classification
album_data = track_info . get ( ' album ' , { } )
if isinstance ( album_data , dict ) :
album = dict ( album_data ) # Copy all fields
album = dict ( album_data )
album . setdefault ( ' name ' , ' Unknown Album ' )
else :
album = {
@ -126,11 +114,10 @@ def ensure_spotify_track_format(track_info):
album . setdefault ( ' album_type ' , ' album ' )
album . setdefault ( ' total_tracks ' , 0 )
# Build proper Spotify track structure
spotify_track = {
return {
' id ' : track_info . get ( ' id ' , f " webui_ { hash ( str ( track_info ) ) } " ) ,
' name ' : track_info . get ( ' name ' , ' Unknown Track ' ) ,
' artists ' : artists_list , # Proper Spotify format
' artists ' : artists_list ,
' album ' : album ,
' duration_ms ' : track_info . get ( ' duration_ms ' , 0 ) ,
' track_number ' : track_info . get ( ' track_number ' , 1 ) ,
@ -138,18 +125,17 @@ def ensure_spotify_track_format(track_info):
' preview_url ' : track_info . get ( ' preview_url ' ) ,
' external_urls ' : track_info . get ( ' external_urls ' , { } ) ,
' popularity ' : track_info . get ( ' popularity ' , 0 ) ,
' source ' : ' webui_modal ' # Mark as coming from webui
' source ' : track_info . get ( ' source ' , ' webui_modal ' ) ,
}
return spotify_track
def ensure_spotify_track_format ( track_info ) :
""" Backward-compatible wrapper for `ensure_wishlist_track_format`. """
return ensure_wishlist_track_format ( track_info )
def build_cancelled_task_wishlist_payload ( task , profile_id : int = 1 ) :
""" Build the wishlist payload for a cancelled download task.
This preserves the current web_server . py behavior while moving the
data - shaping logic into the wishlist package .
"""
def build_cancelled_task_wishlist_payload ( task , profile_id : int = 1 ) :
""" Build the wishlist payload for a cancelled download task. """
if not task :
return { }
@ -170,31 +156,27 @@ def build_cancelled_task_wishlist_payload(task, profile_id: int = 1):
else :
formatted_artists . append ( { ' name ' : str ( artist ) } )
# Build album data - preserve all fields (including artists) for correct folder placement
album_raw = track_info . get ( ' album ' , { } )
if isinstance ( album_raw , dict ) :
album_data = dict ( album_raw ) # Copy all fields including artists
album_data = dict ( album_raw )
album_data . setdefault ( ' name ' , ' Unknown Album ' )
album_data . setdefault ( ' album_type ' , track_info . get ( ' album_type ' , ' album ' ) )
# Add images fallback if not present
if ' images ' not in album_data and track_info . get ( ' album_image_url ' ) :
album_data [ ' images ' ] = [ { ' url ' : track_info . get ( ' album_image_url ' ) } ]
else :
# album is a string (album name)
album_data = {
' name ' : str ( album_raw ) if album_raw else ' Unknown Album ' ,
' album_type ' : track_info . get ( ' album_type ' , ' album ' )
' album_type ' : track_info . get ( ' album_type ' , ' album ' ) ,
}
# Add album image if available
if track_info . get ( ' album_image_url ' ) :
album_data [ ' images ' ] = [ { ' url ' : track_info . get ( ' album_image_url ' ) } ]
spotify_ track_data = {
track_data = {
' id ' : track_info . get ( ' id ' ) ,
' name ' : track_info . get ( ' name ' ) ,
' artists ' : formatted_artists ,
' album ' : album_data ,
' duration_ms ' : track_info . get ( ' duration_ms ' )
' duration_ms ' : track_info . get ( ' duration_ms ' ) ,
}
source_context = {
@ -204,7 +186,8 @@ def build_cancelled_task_wishlist_payload(task, profile_id: int = 1):
}
return {
' spotify_track_data ' : spotify_track_data ,
' spotify_track_data ' : track_data ,
' track_data ' : track_data ,
' failure_reason ' : ' Download cancelled by user (v2) ' ,
' source_type ' : ' playlist ' ,
' source_context ' : source_context ,
@ -228,76 +211,31 @@ def build_failed_track_wishlist_context(
' track_name ' : track_info . get ( ' name ' , ' Unknown Track ' ) ,
' artist_name ' : get_track_artist_name ( track_info ) ,
' retry_count ' : retry_count ,
' spotify_track ' : ensure_spotify_track_format ( track_info ) ,
' spotify_track ' : ensure_wishlist_track_format ( track_info ) ,
' track_data ' : ensure_wishlist_track_format ( track_info ) ,
' failure_reason ' : failure_reason ,
' candidates ' : list ( candidates or [ ] ) ,
}
def extract_spotify_track_from_modal_info ( track_info : Dict [ str , Any ] ) - > Optional [ Dict [ str , Any ] ] :
"""
Extract Spotify track data from modal track_info structure .
Handles different formats from sync . py and artists . py modals .
"""
try :
# Try to find Spotify track data in various locations within track_info
# Check if we have direct Spotify track reference
if " spotify_track " in track_info and track_info [ " spotify_track " ] :
spotify_track = track_info [ " spotify_track " ]
# Convert to dictionary if it's an object
if hasattr ( spotify_track , " __dict__ " ) :
return spotify_track_object_to_dict ( spotify_track )
if isinstance ( spotify_track , dict ) :
return spotify_track
# Check if we have slskd_result with embedded metadata
if " slskd_result " in track_info and track_info [ " slskd_result " ] :
slskd_result = track_info [ " slskd_result " ]
# Look for Spotify metadata in the result
if hasattr ( slskd_result , " artist " ) and hasattr ( slskd_result , " title " ) :
album_name = getattr ( slskd_result , " album " , " " ) or getattr ( slskd_result , " title " , " Unknown Album " )
return {
" id " : f " reconstructed_ { hash ( f ' { slskd_result . artist } _ { slskd_result . title } ' ) } " ,
" name " : getattr ( slskd_result , " title " , " Unknown Track " ) ,
" artists " : [ { " name " : getattr ( slskd_result , " artist " , " Unknown Artist " ) } ] ,
" album " : { " name " : album_name , " images " : [ ] , " album_type " : " single " , " total_tracks " : 1 } ,
" duration_ms " : 0 ,
" reconstructed " : True ,
}
# If no Spotify data found, try to reconstruct from available info
logger . warning ( " Could not find Spotify track data in modal info, attempting reconstruction " )
return None
except Exception as e :
logger . error ( f " Error extracting Spotify track from modal info: { e } " )
return None
def spotify_track_object_to_dict ( spotify_track ) - > Dict [ str , Any ] :
""" Convert a Spotify track object or TrackResult object to a dictionary. """
def track_object_to_dict ( track_object ) - > Dict [ str , Any ] :
""" Convert a track object or TrackResult object to a dictionary. """
try :
logger . debug (
" Converting track object to dict: type= %s has_title= %s has_artist= %s has_id= %s " ,
type ( spotify_ track) ,
hasattr ( spotify_ track, " title " ) ,
hasattr ( spotify_ track, " artist " ) ,
hasattr ( spotify_ track, " id " ) ,
type ( track_object ) ,
hasattr ( track_object , " title " ) ,
hasattr ( track_object , " artist " ) ,
hasattr ( track_object , " id " ) ,
)
# Check if this is a TrackResult object (has title/artist but no id)
if hasattr ( spotify_track , " title " ) and hasattr ( spotify_track , " artist " ) and not hasattr ( spotify_track , " id " ) :
if hasattr ( track_object , " title " ) and hasattr ( track_object , " artist " ) and not hasattr ( track_object , " id " ) :
logger . debug ( " Detected TrackResult object, converting " )
# Handle TrackResult objects - these don't have Spotify IDs
album_name = getattr ( spotify_track , " album " , " " ) or getattr ( spotify_track , " title " , " Unknown Album " )
album_name = getattr ( track_object , " album " , " " ) or getattr ( track_object , " title " , " Unknown Album " )
result = {
" id " : f " trackresult_ { hash ( f ' { spotify_ track. artist } _ { spotify_ track. title } ' ) } " ,
" name " : getattr ( spotify_ track, " title " , " Unknown Track " ) ,
" artists " : [ { " name " : getattr ( spotify_ track, " artist " , " Unknown Artist " ) } ] ,
" id " : f " trackresult_ { hash ( f ' { track_object . artist } _ { track_object . title } ' ) } " ,
" name " : getattr ( track_object , " title " , " Unknown Track " ) ,
" artists " : [ { " name " : getattr ( track_object , " artist " , " Unknown Artist " ) } ] ,
" album " : { " name " : album_name , " images " : [ ] , " album_type " : " single " , " total_tracks " : 1 } ,
" duration_ms " : 0 ,
" preview_url " : None ,
@ -312,12 +250,10 @@ def spotify_track_object_to_dict(spotify_track) -> Dict[str, Any]:
)
return result
# Handle regular Spotify Track objects
logger . debug ( " Processing as Spotify Track object " )
logger . debug ( " Processing as track object " )
# Handle artists list carefully to avoid TrackResult serialization issues
artists_list = [ ]
raw_artists = getattr ( spotify_ track, " artists " , [ ] )
raw_artists = getattr ( track_object , " artists " , [ ] )
logger . debug ( " Raw artists: %r (type= %s ) " , raw_artists , type ( raw_artists ) )
for artist in raw_artists :
@ -327,47 +263,43 @@ def spotify_track_object_to_dict(spotify_track) -> Dict[str, Any]:
elif isinstance ( artist , str ) :
artists_list . append ( { " name " : artist } )
else :
# Convert any complex objects to string to avoid serialization issues
artists_list . append ( { " name " : str ( artist ) } )
# Handle album safely
album_name = " Unknown Album "
if hasattr ( spotify_ track, " album " ) and spotify_ track. album :
if hasattr ( spotify_ track. album , " name " ) :
album_name = spotify_ track. album . name
if hasattr ( track_object , " album " ) and track_object . album :
if hasattr ( track_object . album , " name " ) :
album_name = track_object . album . name
else :
album_name = str ( spotify_ track. album )
album_name = str ( track_object . album )
result = {
" id " : getattr ( spotify_ track, " id " , None ) ,
" name " : getattr ( spotify_ track, " name " , " Unknown Track " ) ,
" id " : getattr ( track_object , " id " , None ) ,
" name " : getattr ( track_object , " name " , " Unknown Track " ) ,
" artists " : artists_list ,
" album " : { " name " : album_name } ,
" duration_ms " : getattr ( spotify_ track, " duration_ms " , 0 ) ,
" preview_url " : getattr ( spotify_ track, " preview_url " , None ) ,
" external_urls " : getattr ( spotify_ track, " external_urls " , { } ) ,
" popularity " : getattr ( spotify_ track, " popularity " , 0 ) ,
" track_number " : getattr ( spotify_ track, " track_number " , 1 ) ,
" disc_number " : getattr ( spotify_ track, " disc_number " , 1 ) ,
" duration_ms " : getattr ( track_object , " duration_ms " , 0 ) ,
" preview_url " : getattr ( track_object , " preview_url " , None ) ,
" external_urls " : getattr ( track_object , " external_urls " , { } ) ,
" popularity " : getattr ( track_object , " popularity " , 0 ) ,
" track_number " : getattr ( track_object , " track_number " , 1 ) ,
" disc_number " : getattr ( track_object , " disc_number " , 1 ) ,
}
logger . debug (
" Spotify Track converted: name=%s artists= %s " ,
" Track converted: name=%s artists= %s " ,
result [ " name " ] ,
[ a [ " name " ] for a in result [ " artists " ] ] ,
)
# Test JSON serialization before returning to catch any remaining issues
try :
json . dumps ( result )
logger . debug ( " Conversion result is JSON serializable " )
except Exception as json_error :
logger . error ( " Conversion result is NOT JSON serializable: %s " , json_error )
logger . error ( " Conversion result content: %r " , result )
# Return a safe fallback
return {
" id " : f " fallback_ { hash ( str ( spotify_ track) ) } " ,
" name " : str ( getattr ( spotify_ track, " name " , " Unknown Track " ) ) ,
" id " : f " fallback_ { hash ( str ( track_object ) ) } " ,
" name " : str ( getattr ( track_object , " name " , " Unknown Track " ) ) ,
" artists " : [ { " name " : " Unknown Artist " } ] ,
" album " : { " name " : " Unknown Album " } ,
" duration_ms " : 0 ,
@ -380,17 +312,72 @@ def spotify_track_object_to_dict(spotify_track) -> Dict[str, Any]:
return result
except Exception as e :
logger . error ( f " Error converting track object to dict: { e } " )
logger . error ( f " Object type: { type ( spotify_ track) } " )
logger . error ( f " Object attributes: { dir ( spotify_ track) } " )
logger . error ( f " Object type: { type ( track_object ) } " )
logger . error ( f " Object attributes: { dir ( track_object ) } " )
return { }
def spotify_track_object_to_dict ( spotify_track ) - > Dict [ str , Any ] :
""" Backward-compatible wrapper for `track_object_to_dict`. """
return track_object_to_dict ( spotify_track )
def extract_wishlist_track_from_modal_info ( track_info : Dict [ str , Any ] ) - > Optional [ Dict [ str , Any ] ] :
"""
Extract a track payload from modal track_info structure .
"""
try :
if not isinstance ( track_info , dict ) :
return None
for key in ( " track_data " , " track " , " metadata_track " , " spotify_track " ) :
if key not in track_info or not track_info [ key ] :
continue
extracted = track_info [ key ]
if hasattr ( extracted , " __dict__ " ) :
return track_object_to_dict ( extracted )
if isinstance ( extracted , dict ) :
return extracted
if track_info . get ( " name " ) or track_info . get ( " title " ) :
if track_info . get ( " artists " ) or track_info . get ( " artist " ) :
return ensure_wishlist_track_format ( track_info )
if " slskd_result " in track_info and track_info [ " slskd_result " ] :
slskd_result = track_info [ " slskd_result " ]
if hasattr ( slskd_result , " artist " ) and hasattr ( slskd_result , " title " ) :
album_name = getattr ( slskd_result , " album " , " " ) or getattr ( slskd_result , " title " , " Unknown Album " )
return {
" id " : f " reconstructed_ { hash ( f ' { slskd_result . artist } _ { slskd_result . title } ' ) } " ,
" name " : getattr ( slskd_result , " title " , " Unknown Track " ) ,
" artists " : [ { " name " : getattr ( slskd_result , " artist " , " Unknown Artist " ) } ] ,
" album " : { " name " : album_name , " images " : [ ] , " album_type " : " single " , " total_tracks " : 1 } ,
" duration_ms " : 0 ,
" reconstructed " : True ,
}
logger . warning ( " Could not find track data in modal info, attempting reconstruction " )
return None
except Exception as e :
logger . error ( f " Error extracting track from modal info: { e } " )
return None
def extract_spotify_track_from_modal_info ( track_info : Dict [ str , Any ] ) - > Optional [ Dict [ str , Any ] ] :
""" Backward-compatible wrapper for `extract_wishlist_track_from_modal_info`. """
return extract_wishlist_track_from_modal_info ( track_info )
__all__ = [
" sanitize_track_data_for_processing " ,
" get_track_artist_name " ,
" ensure_wishlist_track_format " ,
" ensure_spotify_track_format " ,
" build_cancelled_task_wishlist_payload " ,
" build_failed_track_wishlist_context " ,
" extract_spotify_track_from_modal_info " ,
" track_object_to_dict " ,
" spotify_track_object_to_dict " ,
" extract_wishlist_track_from_modal_info " ,
" extract_spotify_track_from_modal_info " ,
]