First steps to spotify-tagging when using spo-dee flow

This commit is contained in:
Xoconoch
2025-08-11 20:44:00 -06:00
parent afe88172ec
commit 2f9c33c269
3 changed files with 196 additions and 180 deletions

View File

@@ -44,7 +44,7 @@ from deezspot.libutils.progress_reporter import (
report_album_initializing, report_album_done, report_playlist_initializing, report_playlist_done report_album_initializing, report_album_done, report_playlist_initializing, report_playlist_done
) )
from deezspot.libutils.taggers import ( from deezspot.libutils.taggers import (
enhance_metadata_with_image, add_deezer_enhanced_metadata, process_and_tag_track, enhance_metadata_with_image, add_deezer_enhanced_metadata, add_spotify_enhanced_metadata, process_and_tag_track,
save_cover_image_for_track save_cover_image_for_track
) )
from mutagen.flac import FLAC from mutagen.flac import FLAC
@@ -264,10 +264,16 @@ class EASY_DW:
else: else:
# Get the track object from preferences # Get the track object from preferences
self.__track_obj: trackCbObject = preferences.song_metadata self.__track_obj: trackCbObject = preferences.song_metadata
# If spotify metadata flag is set and a spotify track object is provided, prefer it for tagging
if getattr(preferences, 'spotify_metadata', False) and getattr(preferences, 'spotify_track_obj', None):
self.__track_obj = preferences.spotify_track_obj
# Convert it to the dictionary format needed for legacy functions # Convert it to the dictionary format needed for legacy functions
artist_separator = getattr(preferences, 'artist_separator', '; ') artist_separator = getattr(preferences, 'artist_separator', '; ')
self.__song_metadata_dict = track_object_to_dict(self.__track_obj, source_type='deezer', artist_separator=artist_separator) # Auto-select source type based on preference
use_spotify = getattr(preferences, 'spotify_metadata', False)
source_type = 'spotify' if use_spotify else 'deezer'
self.__song_metadata_dict = track_object_to_dict(self.__track_obj, source_type=source_type, artist_separator=artist_separator)
# Maintain legacy attribute expected elsewhere # Maintain legacy attribute expected elsewhere
self.__song_metadata = self.__song_metadata_dict self.__song_metadata = self.__song_metadata_dict
self.__download_type = "track" self.__download_type = "track"
@@ -283,7 +289,7 @@ class EASY_DW:
current_track_val = None current_track_val = None
total_tracks_val = None total_tracks_val = None
if self.__parent == "playlist" and hasattr(self.__preferences, "json_data"): if self.__parent == "playlist" and hasattr(self.__preferences, "json_data") and self.__preferences.json_data:
playlist_data = self.__preferences.json_data playlist_data = self.__preferences.json_data
if isinstance(playlist_data, dict): if isinstance(playlist_data, dict):
@@ -307,7 +313,7 @@ class EASY_DW:
total_tracks_val = getattr(self.__preferences, 'total_tracks', 0) total_tracks_val = getattr(self.__preferences, 'total_tracks', 0)
current_track_val = getattr(self.__preferences, 'track_number', 0) current_track_val = getattr(self.__preferences, 'track_number', 0)
elif self.__parent == "album" and hasattr(self.__preferences, "json_data"): elif self.__parent == "album" and hasattr(self.__preferences, "json_data") and self.__preferences.json_data:
album_data = self.__preferences.json_data album_data = self.__preferences.json_data
album_id = album_data.ids.deezer album_id = album_data.ids.deezer
parent_obj = albumCbObject( parent_obj = albumCbObject(
@@ -400,11 +406,18 @@ class EASY_DW:
def easy_dw(self) -> Track: def easy_dw(self) -> Track:
# Get image URL and enhance metadata # Get image URL and enhance metadata
pic = None
if self.__infos_dw.get('__TYPE__') == 'episode': if self.__infos_dw.get('__TYPE__') == 'episode':
pic = self.__infos_dw.get('EPISODE_IMAGE_MD5', '') pic = self.__infos_dw.get('EPISODE_IMAGE_MD5', '')
image = API.choose_img(pic)
else: else:
pic = self.__infos_dw['ALB_PICTURE'] # If using Spotify metadata, prefer the best Spotify image URL from the track object
image = API.choose_img(pic) if getattr(self.__preferences, 'spotify_metadata', False) and hasattr(self.__track_obj, 'album') and getattr(self.__track_obj.album, 'images', None):
from deezspot.libutils.metadata_converter import _get_best_image_url
image = _get_best_image_url(self.__track_obj.album.images, 'spotify')
else:
pic = self.__infos_dw['ALB_PICTURE']
image = API.choose_img(pic)
self.__song_metadata['image'] = image self.__song_metadata['image'] = image
# Process image data using unified utility # Process image data using unified utility
@@ -567,21 +580,30 @@ class EASY_DW:
# If we reach here, the item should be successful and not skipped. # If we reach here, the item should be successful and not skipped.
if current_item.success: if current_item.success:
if self.__infos_dw.get('__TYPE__') != 'episode': # Assuming pic is for tracks if self.__infos_dw.get('__TYPE__') != 'episode' and pic: # Assuming pic is for tracks
current_item.md5_image = pic # Set md5_image for tracks current_item.md5_image = pic # Set md5_image for tracks
# Apply tags using unified utility with Deezer enhancements # Apply tags using unified utility with Deezer or Spotify enhancements
from deezspot.deezloader.deegw_api import API_GW from deezspot.deezloader.deegw_api import API_GW
enhanced_metadata = add_deezer_enhanced_metadata( use_spotify = getattr(self.__preferences, 'spotify_metadata', False)
self.__song_metadata, if use_spotify:
self.__infos_dw, enhanced_metadata = add_spotify_enhanced_metadata(self.__song_metadata, self.__track_obj)
self.__ids, process_and_tag_track(
API_GW track=current_item,
) metadata_dict=enhanced_metadata,
process_and_tag_track( source_type='spotify'
track=current_item, )
metadata_dict=enhanced_metadata, else:
source_type='deezer' enhanced_metadata = add_deezer_enhanced_metadata(
) self.__song_metadata,
self.__infos_dw,
self.__ids,
API_GW
)
process_and_tag_track(
track=current_item,
metadata_dict=enhanced_metadata,
source_type='deezer'
)
return current_item return current_item
@@ -763,20 +785,31 @@ class EASY_DW:
# Add Deezer-specific enhanced metadata and apply tags # Add Deezer-specific enhanced metadata and apply tags
from deezspot.deezloader.deegw_api import API_GW from deezspot.deezloader.deegw_api import API_GW
enhanced_metadata = add_deezer_enhanced_metadata( # Build metadata: if using Spotify metadata, enhance for Spotify; else Deezer
self.__song_metadata, use_spotify = getattr(self.__preferences, 'spotify_metadata', False)
self.__infos_dw, if use_spotify:
self.__ids, enhanced_metadata = add_spotify_enhanced_metadata(self.__song_metadata, self.__track_obj)
API_GW process_and_tag_track(
) track=self.__c_track,
metadata_dict=enhanced_metadata,
# Apply tags using unified utility source_type='spotify',
process_and_tag_track( save_cover=getattr(self.__preferences, 'save_cover', False)
track=self.__c_track, )
metadata_dict=enhanced_metadata, else:
source_type='deezer', enhanced_metadata = add_deezer_enhanced_metadata(
save_cover=getattr(self.__preferences, 'save_cover', False) self.__song_metadata,
) self.__infos_dw,
self.__ids,
API_GW
)
# Apply tags using unified utility
process_and_tag_track(
track=self.__c_track,
metadata_dict=enhanced_metadata,
source_type='deezer',
save_cover=getattr(self.__preferences, 'save_cover', False)
)
if self.__convert_to: if self.__convert_to:
format_name, bitrate = self._parse_format_string(self.__convert_to) format_name, bitrate = self._parse_format_string(self.__convert_to)
@@ -800,19 +833,28 @@ class EASY_DW:
logger.error(f"Audio conversion error: {str(conv_error)}. Proceeding with original format.") logger.error(f"Audio conversion error: {str(conv_error)}. Proceeding with original format.")
register_active_download(path_before_conversion) register_active_download(path_before_conversion)
# Apply tags using unified utility with Deezer enhancements # Apply tags using unified utility with Deezer or Spotify enhancements
from deezspot.deezloader.deegw_api import API_GW from deezspot.deezloader.deegw_api import API_GW
enhanced_metadata = add_deezer_enhanced_metadata( use_spotify = getattr(self.__preferences, 'spotify_metadata', False)
self.__song_metadata, if use_spotify:
self.__infos_dw, enhanced_metadata = add_spotify_enhanced_metadata(self.__song_metadata, self.__track_obj)
self.__ids, process_and_tag_track(
API_GW track=self.__c_track,
) metadata_dict=enhanced_metadata,
process_and_tag_track( source_type='spotify'
track=self.__c_track, )
metadata_dict=enhanced_metadata, else:
source_type='deezer' enhanced_metadata = add_deezer_enhanced_metadata(
) self.__song_metadata,
self.__infos_dw,
self.__ids,
API_GW
)
process_and_tag_track(
track=self.__c_track,
metadata_dict=enhanced_metadata,
source_type='deezer'
)
self.__c_track.success = True self.__c_track.success = True
unregister_active_download(self.__song_path) unregister_active_download(self.__song_path)

View File

@@ -65,6 +65,13 @@ from deezspot.models.callback.common import IDs
from deezspot.models.callback.user import userObject from deezspot.models.callback.user import userObject
def _sim(a: str, b: str) -> float:
a = (a or '').strip().lower()
b = (b or '').strip().lower()
if not a or not b:
return 0.0
return SequenceMatcher(None, a, b).ratio()
API() API()
# Create a logger for the deezspot library # Create a logger for the deezspot library
@@ -126,7 +133,8 @@ class DeeLogin:
save_cover=stock_save_cover, save_cover=stock_save_cover,
market=stock_market, market=stock_market,
playlist_context=None, playlist_context=None,
artist_separator: str = "; " artist_separator: str = "; ",
spotify_metadata: bool = False
) -> Track: ) -> Track:
link_is_valid(link_track) link_is_valid(link_track)
@@ -147,7 +155,7 @@ class DeeLogin:
report_progress(reporter=self.progress_reporter, callback_obj=callback_obj) report_progress(reporter=self.progress_reporter, callback_obj=callback_obj)
try: try:
# Get standardized track object using our enhanced API module # Default: Get standardized Deezer track object for tagging
track_obj = API.get_track(ids) track_obj = API.get_track(ids)
except (NoDataApi, MarketAvailabilityError) as e: except (NoDataApi, MarketAvailabilityError) as e:
# Try to get fallback track information # Try to get fallback track information
@@ -173,10 +181,16 @@ class DeeLogin:
report_error(e, ids, link_track) report_error(e, ids, link_track)
raise e raise e
# If requested and provided via context, override with Spotify metadata for tagging
if spotify_metadata and playlist_context and playlist_context.get('spotify_track_obj'):
track_obj_for_tagging = playlist_context.get('spotify_track_obj')
else:
track_obj_for_tagging = track_obj
# Set up download preferences # Set up download preferences
preferences = Preferences() preferences = Preferences()
preferences.link = link_track preferences.link = link_track
preferences.song_metadata = track_obj # Use our standardized track object preferences.song_metadata = track_obj_for_tagging # Use selected track object (Spotify or Deezer) for tagging
preferences.quality_download = quality_download preferences.quality_download = quality_download
preferences.output_dir = output_dir preferences.output_dir = output_dir
preferences.ids = ids preferences.ids = ids
@@ -194,15 +208,17 @@ class DeeLogin:
preferences.save_cover = save_cover preferences.save_cover = save_cover
preferences.market = market preferences.market = market
preferences.artist_separator = artist_separator preferences.artist_separator = artist_separator
preferences.spotify_metadata = bool(spotify_metadata)
preferences.spotify_track_obj = playlist_context.get('spotify_track_obj') if (playlist_context and playlist_context.get('spotify_track_obj')) else None
if playlist_context: if playlist_context:
preferences.json_data = playlist_context['json_data'] preferences.json_data = playlist_context.get('json_data')
preferences.track_number = playlist_context['track_number'] preferences.track_number = playlist_context.get('track_number')
preferences.total_tracks = playlist_context['total_tracks'] preferences.total_tracks = playlist_context.get('total_tracks')
preferences.spotify_url = playlist_context['spotify_url'] preferences.spotify_url = playlist_context.get('spotify_url')
try: try:
parent = 'playlist' if playlist_context else None parent = 'playlist' if (playlist_context and playlist_context.get('json_data')) else None
track = DW_TRACK(preferences, parent=parent).dw() track = DW_TRACK(preferences, parent=parent).dw()
return track return track
except Exception as e: except Exception as e:
@@ -393,111 +409,58 @@ class DeeLogin:
def convert_spoty_to_dee_link_track(self, link_track): def convert_spoty_to_dee_link_track(self, link_track):
link_is_valid(link_track) link_is_valid(link_track)
ids = get_ids(link_track) ids = get_ids(link_track)
# Attempt via ISRC first
track_json = Spo.get_track(ids) track_json = Spo.get_track(ids)
external_ids = track_json.get('external_ids') if not track_json:
raise TrackNotFound(url=link_track, message="Spotify track metadata fetch failed.")
if not external_ids or 'isrc' not in external_ids: external_ids = track_json.get('external_ids') or {}
msg = f"⚠ The track '{track_json.get('name', 'Unknown Track')}' has no ISRC and can't be converted to Deezer link :( ⚠" spo_isrc = (external_ids.get('isrc') or '').upper()
logger.warning(msg)
raise TrackNotFound(url=link_track, message=msg)
def _sim(a: str, b: str) -> float:
a = (a or '').strip().lower()
b = (b or '').strip().lower()
if not a or not b:
return 0.0
return SequenceMatcher(None, a, b).ratio()
spo_title = track_json.get('name', '') spo_title = track_json.get('name', '')
spo_album_title = (track_json.get('album') or {}).get('name', '') spo_album_title = (track_json.get('album') or {}).get('name', '')
spo_tracknum = int(track_json.get('track_number') or 0) spo_tracknum = int(track_json.get('track_number') or 0)
spo_isrc = (external_ids.get('isrc') or '').upper()
# 1) Primary attempt: /track/isrc:CODE then validate with strict checks
try: try:
dz = API.get_track_json(f"isrc:{spo_isrc}") dz = API.get_track_json(f"isrc:{spo_isrc}")
if dz and dz.get('id'):
dz_json = dz
tn = (dz_json.get('track_position') or dz_json.get('track_number') or 0)
title_match = max(
_sim(spo_title, dz_json.get('title', '')),
_sim(spo_title, dz_json.get('title_short', '')),
)
album_match = _sim(spo_album_title, (dz_json.get('album') or {}).get('title', ''))
if title_match >= 0.90 and album_match >= 0.90 and tn == spo_tracknum:
return f"https://www.deezer.com/track/{dz_json.get('id')}"
except Exception: except Exception:
dz = {} pass
def _track_ok(dz_json: dict) -> bool: # Fallback: search by title + album
if not dz_json or not dz_json.get('id'):
return False
title_match = max(
_sim(spo_title, dz_json.get('title', '')),
_sim(spo_title, dz_json.get('title_short', '')),
)
album_match = _sim(spo_album_title, (dz_json.get('album') or {}).get('title', ''))
tn = int(dz_json.get('track_position') or dz_json.get('track_number') or 0)
return title_match >= 0.90 and album_match >= 0.90 and tn == spo_tracknum
if _track_ok(dz):
deezer_id = dz['id']
return f"https://www.deezer.com/track/{deezer_id}"
# 2) Fallback: search tracks by "title album" and validate, minimizing extra calls
query = f'"{spo_title} {spo_album_title}"' query = f'"{spo_title} {spo_album_title}"'
try: try:
candidates = API.search_tracks_raw(query, limit=5) candidates = API.search_tracks_raw(query, limit=5)
except Exception: except Exception:
candidates = [] candidates = []
for cand in candidates: for cand in candidates:
title_match = max( if max(_sim(spo_title, cand.get('title', '')), _sim(spo_title, cand.get('title_short', ''))) < 0.90:
_sim(spo_title, cand.get('title', '')),
_sim(spo_title, cand.get('title_short', '')),
)
album_match = _sim(spo_album_title, (cand.get('album') or {}).get('title', ''))
if title_match < 0.90 or album_match < 0.90:
continue continue
c_id = cand.get('id') c_id = cand.get('id')
if not c_id: if not c_id:
continue continue
# Fetch details only for promising candidates to check track number and ISRC
try: try:
dzc = API.get_track_json(str(c_id)) dzc = API.get_track_json(str(c_id))
except Exception: except Exception:
continue continue
tn = int(dzc.get('track_position') or dzc.get('track_number') or 0) # Validate using track number and ISRC to be safe
tn = (dzc.get('track_position') or dzc.get('track_number') or 0)
if tn != spo_tracknum: if tn != spo_tracknum:
continue continue
if (dzc.get('isrc') or '').upper() != spo_isrc: t_isrc = (dzc.get('isrc') or '').upper()
if spo_isrc and t_isrc and t_isrc != spo_isrc:
continue continue
return f"https://www.deezer.com/track/{dzc['id']}" return f"https://www.deezer.com/track/{c_id}"
# 3) Last resort: search albums by album title; inspect tracks to find exact track number
try:
album_candidates = API.search_albums_raw(f'"{spo_album_title}"', limit=5)
except Exception:
album_candidates = []
for alb in album_candidates:
if _sim(spo_album_title, alb.get('title', '')) < 0.90:
continue
alb_id = alb.get('id')
if not alb_id:
continue
try:
# Use standardized album object to get detailed tracks (includes ISRC in IDs)
full_album = API.get_album(alb_id)
except Exception:
continue
# full_album.tracks is a list of trackAlbumObject with ids.isrc and track_number
for t in getattr(full_album, 'tracks', []) or []:
try:
t_title = getattr(t, 'title', '')
t_num = int(getattr(t, 'track_number', 0))
t_isrc = (getattr(getattr(t, 'ids', None), 'isrc', '') or '').upper()
except Exception:
continue
if t_num != spo_tracknum:
continue
if _sim(spo_title, t_title) < 0.90:
continue
if t_isrc != spo_isrc:
continue
return f"https://www.deezer.com/track/{getattr(getattr(t, 'ids', None), 'deezer', '')}"
raise TrackNotFound(url=link_track, message=f"Failed to find Deezer equivalent for ISRC {spo_isrc} from Spotify track {link_track}") raise TrackNotFound(url=link_track, message=f"Failed to find Deezer equivalent for ISRC {spo_isrc} from Spotify track {link_track}")
def convert_isrc_to_dee_link_track(self, isrc_code: str) -> str: def convert_isrc_to_dee_link_track(self, isrc_code: str) -> str:
@@ -526,36 +489,27 @@ class DeeLogin:
def convert_spoty_to_dee_link_album(self, link_album): def convert_spoty_to_dee_link_album(self, link_album):
link_is_valid(link_album) link_is_valid(link_album)
ids = get_ids(link_album) ids = get_ids(link_album)
link_dee = None
spotify_album_data = Spo.get_album(ids) spotify_album_data = Spo.get_album(ids)
if not spotify_album_data:
def _sim(a: str, b: str) -> float: raise AlbumNotFound(f"Failed to fetch Spotify album metadata for {link_album}")
a = (a or '').strip().lower()
b = (b or '').strip().lower()
if not a or not b:
return 0.0
return SequenceMatcher(None, a, b).ratio()
spo_album_title = spotify_album_data.get('name', '') spo_album_title = spotify_album_data.get('name', '')
# Prefer main artist name for better search, if available
spo_artists = spotify_album_data.get('artists') or [] spo_artists = spotify_album_data.get('artists') or []
spo_main_artist = (spo_artists[0].get('name') if spo_artists else '') or '' spo_main_artist = (spo_artists[0].get('name') if spo_artists else '') or ''
external_ids = spotify_album_data.get('external_ids') or {} external_ids = spotify_album_data.get('external_ids') or {}
spo_upc = str(external_ids.get('upc') or '').strip() spo_upc = str(external_ids.get('upc') or '').strip()
# 1) Primary attempt: /album/upc:CODE then validate title similarity # Try UPC first
dz_album = {}
if spo_upc: if spo_upc:
try: try:
dz_album = API.get_album_json(f"upc:{spo_upc}") dz_album = API.get_album_json(f"upc:{spo_upc}")
if dz_album.get('id') and _sim(spo_album_title, dz_album.get('title', '')) >= 0.90:
return f"https://www.deezer.com/album/{dz_album.get('id')}"
except Exception: except Exception:
dz_album = {} pass
if dz_album.get('id') and _sim(spo_album_title, dz_album.get('title', '')) >= 0.90:
link_dee = f"https://www.deezer.com/album/{dz_album['id']}" # Fallback: title search
return link_dee
# 2) Fallback: search albums by album title (+ main artist) and confirm UPC
q = f'"{spo_album_title}" {spo_main_artist}'.strip() q = f'"{spo_album_title}" {spo_main_artist}'.strip()
try: try:
candidates = API.search_albums_raw(q, limit=5) candidates = API.search_albums_raw(q, limit=5)
@@ -598,11 +552,26 @@ class DeeLogin:
save_cover=stock_save_cover, save_cover=stock_save_cover,
market=stock_market, market=stock_market,
playlist_context=None, playlist_context=None,
artist_separator: str = "; " artist_separator: str = "; ",
spotify_metadata: bool = False
) -> Track: ) -> Track:
link_dee = self.convert_spoty_to_dee_link_track(link_track) link_dee = self.convert_spoty_to_dee_link_track(link_track)
# If requested, prepare Spotify track object for tagging in preferences via playlist_context
if spotify_metadata:
try:
from deezspot.spotloader.__spo_api__ import tracking as spo_tracking
spo_ids = get_ids(link_track)
spo_track_obj = spo_tracking(spo_ids)
if spo_track_obj:
if playlist_context is None:
playlist_context = {}
playlist_context = dict(playlist_context)
playlist_context['spotify_track_obj'] = spo_track_obj
except Exception:
pass
track = self.download_trackdee( track = self.download_trackdee(
link_dee, link_dee,
output_dir=output_dir, output_dir=output_dir,
@@ -621,7 +590,8 @@ class DeeLogin:
save_cover=save_cover, save_cover=save_cover,
market=market, market=market,
playlist_context=playlist_context, playlist_context=playlist_context,
artist_separator=artist_separator artist_separator=artist_separator,
spotify_metadata=spotify_metadata
) )
return track return track
@@ -689,7 +659,8 @@ class DeeLogin:
bitrate=None, bitrate=None,
save_cover=stock_save_cover, save_cover=stock_save_cover,
market=stock_market, market=stock_market,
artist_separator: str = "; " artist_separator: str = "; ",
spotify_metadata: bool = False
) -> Playlist: ) -> Playlist:
link_is_valid(link_playlist) link_is_valid(link_playlist)
@@ -818,12 +789,19 @@ class DeeLogin:
continue continue
try: try:
playlist_context = { playlist_ctx = {
'json_data': playlist_json, 'json_data': playlist_json,
'track_number': index, 'track_number': index,
'total_tracks': total_tracks, 'total_tracks': total_tracks,
'spotify_url': link_track 'spotify_url': link_track
} }
# Attach Spotify track object for tagging if requested
if spotify_metadata:
try:
from deezspot.spotloader.__spo_api__ import json_to_track_playlist_object
playlist_ctx['spotify_track_obj'] = json_to_track_playlist_object(track_info)
except Exception:
pass
downloaded_track = self.download_trackspo( downloaded_track = self.download_trackspo(
link_track, link_track,
output_dir=output_dir, quality_download=quality_download, output_dir=output_dir, quality_download=quality_download,
@@ -832,35 +810,27 @@ class DeeLogin:
custom_track_format=custom_track_format, pad_tracks=pad_tracks, custom_track_format=custom_track_format, pad_tracks=pad_tracks,
initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase,
max_retries=max_retries, convert_to=convert_to, bitrate=bitrate, max_retries=max_retries, convert_to=convert_to, bitrate=bitrate,
save_cover=save_cover, market=market, playlist_context=playlist_context, save_cover=save_cover, market=market, playlist_context=playlist_ctx,
artist_separator=artist_separator artist_separator=artist_separator, spotify_metadata=spotify_metadata
) )
tracks.append(downloaded_track) tracks.append(downloaded_track)
# After download, check status for summary # After download, check status for summary
track_obj_for_cb = trackCbObject(title=track_name, artists=[artistTrackObject(name=artist_name)])
if getattr(downloaded_track, 'was_skipped', False): if getattr(downloaded_track, 'was_skipped', False):
skipped_tracks_cb.append(track_obj_for_cb) skipped_tracks_cb.append(playlist_obj.tracks[index-1])
elif downloaded_track.success: elif downloaded_track.success:
successful_tracks_cb.append(track_obj_for_cb) successful_tracks_cb.append(playlist_obj.tracks[index-1])
else: else:
failed_tracks_cb.append(failedTrackObject( failed_tracks_cb.append(failedTrackObject(track=playlist_obj.tracks[index-1], reason=getattr(downloaded_track, 'error_message', 'Unknown reason')))
track=track_obj_for_cb, except Exception as e:
reason=getattr(downloaded_track, 'error_message', 'Unknown reason') logger.error(f"Track '{track_name}' in playlist '{playlist_obj.title}' failed: {e}")
)) failed_tracks_cb.append(failedTrackObject(track=playlist_obj.tracks[index-1], reason=str(e)))
current_track_object = Track({'music': track_name, 'artist': artist_name}, None, None, None, link_track, None)
current_track_object.success = False
current_track_object.error_message = str(e)
tracks.append(current_track_object)
except (TrackNotFound, NoDataApi) as e: # Finalize summary and callbacks (existing logic continues below in file)...
logger.error(f"Failed to download track: {track_name} - {artist_name}: {e}")
failed_track_obj = trackCbObject(title=track_name, artists=[artistTrackObject(name=artist_name)])
failed_tracks_cb.append(failedTrackObject(track=failed_track_obj, reason=str(e)))
# Create a placeholder for the failed item
failed_track = Track(
tags={'name': track_name, 'artist': artist_name},
song_path=None, file_format=None, quality=None, link=link_track, ids=None
)
failed_track.success = False
failed_track.error_message = str(e)
tracks.append(failed_track)
total_from_spotify = playlist_json['tracks']['total'] total_from_spotify = playlist_json['tracks']['total']
processed_count = len(successful_tracks_cb) + len(skipped_tracks_cb) + len(failed_tracks_cb) processed_count = len(successful_tracks_cb) + len(skipped_tracks_cb) + len(failed_tracks_cb)

View File

@@ -22,4 +22,8 @@ class Preferences:
self.max_retries = 5 # Default maximum number of retries per track self.max_retries = 5 # Default maximum number of retries per track
self.save_cover: bool = False # Option to save a cover.jpg image self.save_cover: bool = False # Option to save a cover.jpg image
# New: artist separator for joining multiple artists or album artists # New: artist separator for joining multiple artists or album artists
self.artist_separator: str = "; " self.artist_separator: str = "; "
# New: when True, use Spotify metadata for tagging in spo flows
self.spotify_metadata: bool = False
# New: optional Spotify trackObject to use when spotify_metadata is True
self.spotify_track_obj = None