added market logic

This commit is contained in:
Xoconoch
2025-06-04 14:44:14 -06:00
parent 0ab091f492
commit 1f6ce13f27
11 changed files with 800 additions and 190 deletions

View File

@@ -983,6 +983,7 @@ class DW_ALBUM:
# Report album initializing status
album_name_for_report = self.__song_metadata.get('album', 'Unknown Album')
total_tracks_for_report = self.__song_metadata.get('nb_tracks', 0)
album_link_for_report = self.__preferences.link # Get album link from preferences
Download_JOB.report_progress({
"type": "album",
@@ -990,7 +991,7 @@ class DW_ALBUM:
"status": "initializing",
"total_tracks": total_tracks_for_report,
"title": album_name_for_report,
"url": f"https://deezer.com/album/{self.__ids}"
"url": album_link_for_report # Use the actual album link
})
infos_dw = API_GW.get_album_data(self.__ids)['data']
@@ -1024,67 +1025,99 @@ class DW_ALBUM:
total_tracks = len(infos_dw)
for a in range(total_tracks):
track_number = a + 1
c_infos_dw = infos_dw[a]
# c_infos_dw is from API_GW.get_album_data, used for SNG_ID, SNG_TITLE etc.
c_infos_dw_item = infos_dw[a]
# Retrieve the contributors info from the API response.
# It might be an empty list.
contributors = c_infos_dw.get('SNG_CONTRIBUTORS', {})
# self.__song_metadata is the dict-of-lists from API.tracking_album
# We need to construct c_song_metadata_for_easydw for the current track 'a'
# by picking the ath element from each list in self.__song_metadata.
# Check if contributors is an empty list.
if isinstance(contributors, list) and not contributors:
# Flag indicating we do NOT have contributors data to process.
has_contributors = False
else:
has_contributors = True
current_track_constructed_metadata = {}
potential_error_marker = None
is_current_track_error = False
# If we have contributor data, build the artist and composer strings.
if has_contributors:
main_artist = "; ".join(contributors.get('main_artist', []))
featuring = "; ".join(contributors.get('featuring', []))
# Check the 'music' field first for an error dict from API.tracking_album
if 'music' in self.__song_metadata and isinstance(self.__song_metadata['music'], list) and len(self.__song_metadata['music']) > a:
music_field_value = self.__song_metadata['music'][a]
if isinstance(music_field_value, dict) and 'error_type' in music_field_value:
is_current_track_error = True
potential_error_marker = music_field_value
# The error marker dict itself will serve as the metadata for the failed track object
current_track_constructed_metadata = potential_error_marker
if not is_current_track_error:
# Populate current_track_constructed_metadata from self.__song_metadata lists
for key, value_list_template in self.__song_metadata_items: # self.__song_metadata_items is items() of dict-of-lists
if isinstance(value_list_template, list): # e.g. self.__song_metadata['artist']
if len(self.__song_metadata[key]) > a:
current_track_constructed_metadata[key] = self.__song_metadata[key][a]
else:
current_track_constructed_metadata[key] = "Unknown" # Fallback if list is too short
else: # Album-wide metadata (e.g. 'album', 'label')
current_track_constructed_metadata[key] = self.__song_metadata[key]
artist_parts = [main_artist]
if featuring:
artist_parts.append(f"(feat. {featuring})")
artist_str = " ".join(artist_parts)
composer_str = "; ".join(contributors.get('composer', []))
# Build the core track metadata.
# When there is no contributor info, we intentionally leave out the 'artist'
# and 'composer' keys so that the album-level metadata merge will supply them.
c_song_metadata = {
'music': c_infos_dw.get('SNG_TITLE', 'Unknown'),
'album': self.__song_metadata['album'],
'date': c_infos_dw.get('DIGITAL_RELEASE_DATE', ''),
'genre': self.__song_metadata.get('genre', 'Latin Music'),
'tracknum': f"{track_number}",
'discnum': f"{c_infos_dw.get('DISK_NUMBER', 1)}",
'isrc': c_infos_dw.get('ISRC', ''),
'album_artist': derived_album_artist_from_contributors,
'publisher': 'CanZion R',
'duration': int(c_infos_dw.get('DURATION', 0)),
'explicit': '1' if c_infos_dw.get('EXPLICIT_LYRICS', '0') == '1' else '0'
}
# Only add contributor-based metadata if available.
if has_contributors:
c_song_metadata['artist'] = artist_str
c_song_metadata['composer'] = composer_str
# Ensure essential fields from c_infos_dw_item are preferred or added if missing from API.tracking_album results
current_track_constructed_metadata['music'] = current_track_constructed_metadata.get('music') or c_infos_dw_item.get('SNG_TITLE', 'Unknown')
# artist might be complex due to contributors, rely on what API.tracking_album prepared
# current_track_constructed_metadata['artist'] = current_track_constructed_metadata.get('artist') # Already populated or None
current_track_constructed_metadata['tracknum'] = current_track_constructed_metadata.get('tracknum') or f"{track_number}"
current_track_constructed_metadata['discnum'] = current_track_constructed_metadata.get('discnum') or f"{c_infos_dw_item.get('DISK_NUMBER', 1)}"
current_track_constructed_metadata['isrc'] = current_track_constructed_metadata.get('isrc') or c_infos_dw_item.get('ISRC', '')
current_track_constructed_metadata['duration'] = current_track_constructed_metadata.get('duration') or int(c_infos_dw_item.get('DURATION', 0))
current_track_constructed_metadata['explicit'] = current_track_constructed_metadata.get('explicit') or ('1' if c_infos_dw_item.get('EXPLICIT_LYRICS', '0') == '1' else '0')
current_track_constructed_metadata['album_artist'] = current_track_constructed_metadata.get('album_artist') or derived_album_artist_from_contributors
if is_current_track_error:
error_type = potential_error_marker.get('error_type', 'UnknownError')
error_message = potential_error_marker.get('message', 'An unknown error occurred.')
track_name_for_log = potential_error_marker.get('name', c_infos_dw_item.get('SNG_TITLE', f'Track {track_number}'))
track_id_for_log = potential_error_marker.get('ids', c_infos_dw_item.get('SNG_ID'))
# Construct market_info string based on actual checked_markets from error dict or preferences fallback
checked_markets_str = ""
if error_type == 'MarketAvailabilityError':
# Prefer checked_markets from the error dict if available
if 'checked_markets' in c_infos_dw_item and c_infos_dw_item['checked_markets']:
checked_markets_str = c_infos_dw_item['checked_markets']
# Fallback to preferences.market if not in error dict (though it should be)
elif self.__preferences.market:
if isinstance(self.__preferences.market, list):
checked_markets_str = ", ".join([m.upper() for m in self.__preferences.market])
elif isinstance(self.__preferences.market, str):
checked_markets_str = self.__preferences.market.upper()
market_log_info = f" (Market(s): {checked_markets_str})" if checked_markets_str else ""
logger.warning(f"Skipping download of track '{track_name_for_log}' (ID: {track_id_for_log}) in album '{album.album_name}' due to {error_type}{market_log_info}: {error_message}")
failed_track_link = f"https://deezer.com/track/{track_id_for_log}" if track_id_for_log else self.__preferences.link # Fallback to album link
# current_track_constructed_metadata is already the error_marker dict
track = Track(
tags=current_track_constructed_metadata,
song_path=None, file_format=None, quality=None,
link=failed_track_link,
ids=track_id_for_log
)
track.success = False
track.error_message = error_message
tracks.append(track)
# Optionally, report progress for this failed track within the album context here
continue # to the next track in the album
# No progress reporting here - done at the track level
# Merge album-level metadata (only add fields not already set in c_song_metadata)
for key, item in self.__song_metadata_items:
if key not in c_song_metadata:
if isinstance(item, list):
c_song_metadata[key] = self.__song_metadata[key][a] if len(self.__song_metadata[key]) > a else 'Unknown'
else:
c_song_metadata[key] = self.__song_metadata[key]
# This was the old logic, current_track_constructed_metadata should be fairly complete now or an error dict.
# for key, item in self.__song_metadata_items:
# if key not in current_track_constructed_metadata:
# if isinstance(item, list):
# current_track_constructed_metadata[key] = self.__song_metadata[key][a] if len(self.__song_metadata[key]) > a else 'Unknown'
# else:
# current_track_constructed_metadata[key] = self.__song_metadata[key]
# Continue with the rest of your processing (media handling, download, etc.)
c_infos_dw['media_url'] = medias[a]
c_infos_dw_item['media_url'] = medias[a] # medias is from Download_JOB.check_sources(infos_dw, ...)
c_preferences = deepcopy(self.__preferences)
c_preferences.song_metadata = c_song_metadata.copy()
c_preferences.ids = c_infos_dw['SNG_ID']
c_preferences.song_metadata = current_track_constructed_metadata.copy()
c_preferences.ids = c_infos_dw_item['SNG_ID']
c_preferences.track_number = track_number
# Add additional information for consistent parent info
@@ -1093,22 +1126,41 @@ class DW_ALBUM:
c_preferences.total_tracks = total_tracks
c_preferences.link = f"https://deezer.com/track/{c_preferences.ids}"
current_track_object = None
try:
track = EASY_DW(c_infos_dw, c_preferences, parent='album').download_try()
except TrackNotFound:
try:
song = f"{c_song_metadata['music']} - {c_song_metadata.get('artist', self.__song_metadata['artist'])}"
ids = API.not_found(song, c_song_metadata['music'])
c_infos_dw = API_GW.get_song_data(ids)
c_media = Download_JOB.check_sources([c_infos_dw], self.__quality_download)
c_infos_dw['media_url'] = c_media[0]
track = EASY_DW(c_infos_dw, c_preferences, parent='album').download_try()
except TrackNotFound:
track = Track(c_song_metadata, None, None, None, None, None)
track.success = False
track.error_message = f"Track not found after fallback attempt for: {song}"
logger.warning(f"Track not found: {song} :( Details: {track.error_message}. URL: {c_preferences.link if c_preferences else 'N/A'}")
tracks.append(track)
# This is where EASY_DW().easy_dw() or EASY_DW().download_try() is effectively called
current_track_object = EASY_DW(c_infos_dw_item, c_preferences, parent='album').easy_dw()
except TrackNotFound as e_tnf:
logger.error(f"Track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' by '{c_preferences.song_metadata.get('artist', 'Unknown Artist')}' (Album: {album.album_name}) failed: {str(e_tnf)}")
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
current_track_object.success = False
current_track_object.error_message = str(e_tnf)
except QualityNotFound as e_qnf:
logger.error(f"Quality issue for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Album: {album.album_name}): {str(e_qnf)}")
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
current_track_object.success = False
current_track_object.error_message = str(e_qnf)
except requests.exceptions.ConnectionError as e_conn:
logger.error(f"Connection error for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Album: {album.album_name}): {str(e_conn)}")
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
current_track_object.success = False
current_track_object.error_message = str(e_conn) # Store specific connection error
except Exception as e_general: # Catch any other unexpected error during this track's processing
logger.error(f"Unexpected error for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Album: {album.album_name}): {str(e_general)}")
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
current_track_object.success = False
current_track_object.error_message = str(e_general)
if current_track_object:
tracks.append(current_track_object)
else: # Should not happen if exceptions are caught, but as a fallback
logger.error(f"Track object was not created for SNG_ID {c_infos_dw_item['SNG_ID']} in album {album.album_name}. Skipping.")
# Create a generic failed track to ensure list length matches expectation if needed elsewhere
failed_placeholder = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
failed_placeholder.success = False
failed_placeholder.error_message = "Track processing failed to produce a result object."
tracks.append(failed_placeholder)
# Save album cover image
if self.__preferences.save_cover and album.image and album_base_directory:
@@ -1137,7 +1189,7 @@ class DW_ALBUM:
"status": "done",
"total_tracks": total_tracks,
"title": album_name,
"url": f"https://deezer.com/album/{self.__ids}"
"url": album_link_for_report # Use the actual album link
})
return album
@@ -1198,37 +1250,117 @@ class DW_PLAYLIST:
)
# Process each track
for idx, (c_infos_dw, c_media, c_song_metadata) in enumerate(zip(infos_dw, medias, self.__song_metadata), 1):
for idx, (c_infos_dw_item, c_media, c_song_metadata_item) in enumerate(zip(infos_dw, medias, self.__song_metadata), 1):
# Skip if song metadata is not valid
if type(c_song_metadata) is str:
# Skip if song metadata indicates an error (e.g., from market availability or NoDataApi)
if isinstance(c_song_metadata_item, dict) and 'error_type' in c_song_metadata_item:
track_name = c_song_metadata_item.get('name', 'Unknown Track')
track_ids = c_song_metadata_item.get('ids')
error_message = c_song_metadata_item.get('message', 'Unknown error.')
error_type = c_song_metadata_item.get('error_type', 'UnknownError')
# market_info = f" (Market: {self.__preferences.market})" if self.__preferences.market and error_type == 'MarketAvailabilityError' else ""
# Construct market_info string based on actual checked_markets from error dict or preferences fallback
checked_markets_str = ""
if error_type == 'MarketAvailabilityError':
# Prefer checked_markets from the error dict if available
if 'checked_markets' in c_song_metadata_item and c_song_metadata_item['checked_markets']:
checked_markets_str = c_song_metadata_item['checked_markets']
# Fallback to preferences.market if not in error dict (though it should be)
elif self.__preferences.market:
if isinstance(self.__preferences.market, list):
checked_markets_str = ", ".join([m.upper() for m in self.__preferences.market])
elif isinstance(self.__preferences.market, str):
checked_markets_str = self.__preferences.market.upper()
market_log_info = f" (Market(s): {checked_markets_str})" if checked_markets_str else ""
logger.warning(f"Skipping download for track '{track_name}' (ID: {track_ids}) from playlist '{playlist_name_sanitized}' due to {error_type}{market_log_info}: {error_message}")
failed_track_link = f"https://deezer.com/track/{track_ids}" if track_ids else self.__preferences.link # Fallback to playlist link
# c_song_metadata_item is the error dict, use it as tags for the Track object
track = Track(
tags=c_song_metadata_item,
song_path=None, file_format=None, quality=None,
link=failed_track_link,
ids=track_ids
)
track.success = False
track.error_message = error_message
tracks.append(track)
# Optionally, report progress for this failed track within the playlist context here
continue # Move to the next track in the playlist
# Original check for string type, should be less common if API returns dicts for errors
if type(c_song_metadata_item) is str:
logger.warning(f"Track metadata is a string for a track in playlist '{playlist_name_sanitized}': '{c_song_metadata_item}'. Skipping.")
# Create a basic failed track object if metadata is just a string error
# This is a fallback, ideally c_song_metadata_item would be an error dict
error_placeholder_tags = {'name': 'Unknown Track (metadata error)', 'artist': 'Unknown Artist', 'error_type': 'StringError', 'message': c_song_metadata_item}
track = Track(
tags=error_placeholder_tags,
song_path=None, file_format=None, quality=None,
link=self.__preferences.link, # Playlist link
ids=None # No specific ID available from string error
)
track.success = False
track.error_message = c_song_metadata_item
tracks.append(track)
continue
c_infos_dw['media_url'] = c_media
c_infos_dw_item['media_url'] = c_media
c_preferences = deepcopy(self.__preferences)
c_preferences.ids = c_infos_dw['SNG_ID']
c_preferences.song_metadata = c_song_metadata
c_preferences.ids = c_infos_dw_item['SNG_ID']
c_preferences.song_metadata = c_song_metadata_item # This is the full metadata dict for a successful track
c_preferences.track_number = idx
c_preferences.total_tracks = total_tracks
# Download the track using the EASY_DW downloader
track = EASY_DW(c_infos_dw, c_preferences, parent='playlist').easy_dw()
# Wrap this in a try-except block to handle individual track failures
current_track_object = None
try:
current_track_object = EASY_DW(c_infos_dw_item, c_preferences, parent='playlist').easy_dw()
except TrackNotFound as e_tnf:
logger.error(f"Track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' by '{c_preferences.song_metadata.get('artist', 'Unknown Artist')}' (Playlist: {playlist_name_sanitized}) failed: {str(e_tnf)}")
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
current_track_object.success = False
current_track_object.error_message = str(e_tnf)
except QualityNotFound as e_qnf:
logger.error(f"Quality issue for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Playlist: {playlist_name_sanitized}): {str(e_qnf)}")
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
current_track_object.success = False
current_track_object.error_message = str(e_qnf)
except requests.exceptions.ConnectionError as e_conn: # Catch connection errors specifically
logger.error(f"Connection error for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Playlist: {playlist_name_sanitized}): {str(e_conn)}")
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
current_track_object.success = False
current_track_object.error_message = str(e_conn) # Store specific connection error
except Exception as e_general: # Catch any other unexpected error during this track's processing
logger.error(f"Unexpected error for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Playlist: {playlist_name_sanitized}): {str(e_general)}")
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
current_track_object.success = False
current_track_object.error_message = str(e_general)
# Track-level progress reporting is handled in EASY_DW
# Only log a warning if the track failed and was NOT intentionally skipped
if not track.success and not getattr(track, 'was_skipped', False):
song = f"{c_song_metadata['music']} - {c_song_metadata['artist']}"
error_detail = getattr(track, 'error_message', 'Download failed for unspecified reason.')
logger.warning(f"Cannot download '{song}'. Reason: {error_detail} (Link: {track.link or c_preferences.link})")
tracks.append(track)
if current_track_object: # Ensure a track object was created
tracks.append(current_track_object)
# Only log a warning if the track failed and was NOT intentionally skipped
if not current_track_object.success and not getattr(current_track_object, 'was_skipped', False):
# The error logging is now done within the except blocks above, more specifically.
pass # logger.warning(f"Cannot download '{song}'. Reason: {error_detail} (Link: {track.link or c_preferences.link})")
else:
# This case should ideally not be reached if exceptions are handled correctly.
logger.error(f"Track object was not created for SNG_ID {c_infos_dw_item['SNG_ID']} in playlist {playlist_name_sanitized}. Skipping.")
# Create a generic failed track to ensure list length matches expectation if needed elsewhere
failed_placeholder = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
failed_placeholder.success = False
failed_placeholder.error_message = "Track processing failed to produce a result object."
tracks.append(failed_placeholder)
# --- Append the final track path to the m3u file ---
# Build a relative path from the playlists directory
if track.success and hasattr(track, 'song_path') and track.song_path:
if current_track_object and current_track_object.success and hasattr(current_track_object, 'song_path') and current_track_object.song_path:
relative_song_path = os.path.relpath(
track.song_path,
current_track_object.song_path,
start=os.path.join(self.__output_dir, "playlists")
)
with open(m3u_path, "a", encoding="utf-8") as m3u_file: