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 # Report album initializing status
album_name_for_report = self.__song_metadata.get('album', 'Unknown Album') album_name_for_report = self.__song_metadata.get('album', 'Unknown Album')
total_tracks_for_report = self.__song_metadata.get('nb_tracks', 0) 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({ Download_JOB.report_progress({
"type": "album", "type": "album",
@@ -990,7 +991,7 @@ class DW_ALBUM:
"status": "initializing", "status": "initializing",
"total_tracks": total_tracks_for_report, "total_tracks": total_tracks_for_report,
"title": album_name_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'] infos_dw = API_GW.get_album_data(self.__ids)['data']
@@ -1024,67 +1025,99 @@ class DW_ALBUM:
total_tracks = len(infos_dw) total_tracks = len(infos_dw)
for a in range(total_tracks): for a in range(total_tracks):
track_number = a + 1 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. # self.__song_metadata is the dict-of-lists from API.tracking_album
# It might be an empty list. # We need to construct c_song_metadata_for_easydw for the current track 'a'
contributors = c_infos_dw.get('SNG_CONTRIBUTORS', {}) # by picking the ath element from each list in self.__song_metadata.
# Check if contributors is an empty list. current_track_constructed_metadata = {}
if isinstance(contributors, list) and not contributors: potential_error_marker = None
# Flag indicating we do NOT have contributors data to process. is_current_track_error = False
has_contributors = False
else:
has_contributors = True
# If we have contributor data, build the artist and composer strings. # Check the 'music' field first for an error dict from API.tracking_album
if has_contributors: if 'music' in self.__song_metadata and isinstance(self.__song_metadata['music'], list) and len(self.__song_metadata['music']) > a:
main_artist = "; ".join(contributors.get('main_artist', [])) music_field_value = self.__song_metadata['music'][a]
featuring = "; ".join(contributors.get('featuring', [])) 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] # Ensure essential fields from c_infos_dw_item are preferred or added if missing from API.tracking_album results
if featuring: current_track_constructed_metadata['music'] = current_track_constructed_metadata.get('music') or c_infos_dw_item.get('SNG_TITLE', 'Unknown')
artist_parts.append(f"(feat. {featuring})") # artist might be complex due to contributors, rely on what API.tracking_album prepared
artist_str = " ".join(artist_parts) # current_track_constructed_metadata['artist'] = current_track_constructed_metadata.get('artist') # Already populated or None
composer_str = "; ".join(contributors.get('composer', [])) 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)}"
# Build the core track metadata. current_track_constructed_metadata['isrc'] = current_track_constructed_metadata.get('isrc') or c_infos_dw_item.get('ISRC', '')
# When there is no contributor info, we intentionally leave out the 'artist' current_track_constructed_metadata['duration'] = current_track_constructed_metadata.get('duration') or int(c_infos_dw_item.get('DURATION', 0))
# and 'composer' keys so that the album-level metadata merge will supply them. 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')
c_song_metadata = { current_track_constructed_metadata['album_artist'] = current_track_constructed_metadata.get('album_artist') or derived_album_artist_from_contributors
'music': c_infos_dw.get('SNG_TITLE', 'Unknown'),
'album': self.__song_metadata['album'], if is_current_track_error:
'date': c_infos_dw.get('DIGITAL_RELEASE_DATE', ''), error_type = potential_error_marker.get('error_type', 'UnknownError')
'genre': self.__song_metadata.get('genre', 'Latin Music'), error_message = potential_error_marker.get('message', 'An unknown error occurred.')
'tracknum': f"{track_number}", track_name_for_log = potential_error_marker.get('name', c_infos_dw_item.get('SNG_TITLE', f'Track {track_number}'))
'discnum': f"{c_infos_dw.get('DISK_NUMBER', 1)}", track_id_for_log = potential_error_marker.get('ids', c_infos_dw_item.get('SNG_ID'))
'isrc': c_infos_dw.get('ISRC', ''),
'album_artist': derived_album_artist_from_contributors, # Construct market_info string based on actual checked_markets from error dict or preferences fallback
'publisher': 'CanZion R', checked_markets_str = ""
'duration': int(c_infos_dw.get('DURATION', 0)), if error_type == 'MarketAvailabilityError':
'explicit': '1' if c_infos_dw.get('EXPLICIT_LYRICS', '0') == '1' else '0' # 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']
# Only add contributor-based metadata if available. # Fallback to preferences.market if not in error dict (though it should be)
if has_contributors: elif self.__preferences.market:
c_song_metadata['artist'] = artist_str if isinstance(self.__preferences.market, list):
c_song_metadata['composer'] = composer_str 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) # Merge album-level metadata (only add fields not already set in c_song_metadata)
for key, item in self.__song_metadata_items: # This was the old logic, current_track_constructed_metadata should be fairly complete now or an error dict.
if key not in c_song_metadata: # for key, item in self.__song_metadata_items:
if isinstance(item, list): # if key not in current_track_constructed_metadata:
c_song_metadata[key] = self.__song_metadata[key][a] if len(self.__song_metadata[key]) > a else 'Unknown' # if isinstance(item, list):
else: # current_track_constructed_metadata[key] = self.__song_metadata[key][a] if len(self.__song_metadata[key]) > a else 'Unknown'
c_song_metadata[key] = self.__song_metadata[key] # else:
# current_track_constructed_metadata[key] = self.__song_metadata[key]
# Continue with the rest of your processing (media handling, download, etc.) # 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 = deepcopy(self.__preferences)
c_preferences.song_metadata = c_song_metadata.copy() c_preferences.song_metadata = current_track_constructed_metadata.copy()
c_preferences.ids = c_infos_dw['SNG_ID'] c_preferences.ids = c_infos_dw_item['SNG_ID']
c_preferences.track_number = track_number c_preferences.track_number = track_number
# Add additional information for consistent parent info # Add additional information for consistent parent info
@@ -1093,22 +1126,41 @@ class DW_ALBUM:
c_preferences.total_tracks = total_tracks c_preferences.total_tracks = total_tracks
c_preferences.link = f"https://deezer.com/track/{c_preferences.ids}" c_preferences.link = f"https://deezer.com/track/{c_preferences.ids}"
current_track_object = None
try: try:
track = EASY_DW(c_infos_dw, c_preferences, parent='album').download_try() # This is where EASY_DW().easy_dw() or EASY_DW().download_try() is effectively called
except TrackNotFound: current_track_object = EASY_DW(c_infos_dw_item, c_preferences, parent='album').easy_dw()
try:
song = f"{c_song_metadata['music']} - {c_song_metadata.get('artist', self.__song_metadata['artist'])}" except TrackNotFound as e_tnf:
ids = API.not_found(song, c_song_metadata['music']) 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)}")
c_infos_dw = API_GW.get_song_data(ids) current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
c_media = Download_JOB.check_sources([c_infos_dw], self.__quality_download) current_track_object.success = False
c_infos_dw['media_url'] = c_media[0] current_track_object.error_message = str(e_tnf)
track = EASY_DW(c_infos_dw, c_preferences, parent='album').download_try() except QualityNotFound as e_qnf:
except TrackNotFound: logger.error(f"Quality issue for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Album: {album.album_name}): {str(e_qnf)}")
track = Track(c_song_metadata, None, None, None, None, None) current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
track.success = False current_track_object.success = False
track.error_message = f"Track not found after fallback attempt for: {song}" current_track_object.error_message = str(e_qnf)
logger.warning(f"Track not found: {song} :( Details: {track.error_message}. URL: {c_preferences.link if c_preferences else 'N/A'}") except requests.exceptions.ConnectionError as e_conn:
tracks.append(track) 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 # Save album cover image
if self.__preferences.save_cover and album.image and album_base_directory: if self.__preferences.save_cover and album.image and album_base_directory:
@@ -1137,7 +1189,7 @@ class DW_ALBUM:
"status": "done", "status": "done",
"total_tracks": total_tracks, "total_tracks": total_tracks,
"title": album_name, "title": album_name,
"url": f"https://deezer.com/album/{self.__ids}" "url": album_link_for_report # Use the actual album link
}) })
return album return album
@@ -1198,37 +1250,117 @@ class DW_PLAYLIST:
) )
# Process each track # 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 # Skip if song metadata indicates an error (e.g., from market availability or NoDataApi)
if type(c_song_metadata) is str: 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 continue
c_infos_dw['media_url'] = c_media c_infos_dw_item['media_url'] = c_media
c_preferences = deepcopy(self.__preferences) c_preferences = deepcopy(self.__preferences)
c_preferences.ids = c_infos_dw['SNG_ID'] c_preferences.ids = c_infos_dw_item['SNG_ID']
c_preferences.song_metadata = c_song_metadata 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.track_number = idx
c_preferences.total_tracks = total_tracks c_preferences.total_tracks = total_tracks
# Download the track using the EASY_DW downloader # 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 # Track-level progress reporting is handled in EASY_DW
if current_track_object: # Ensure a track object was created
# Only log a warning if the track failed and was NOT intentionally skipped tracks.append(current_track_object)
if not track.success and not getattr(track, 'was_skipped', False): # Only log a warning if the track failed and was NOT intentionally skipped
song = f"{c_song_metadata['music']} - {c_song_metadata['artist']}" if not current_track_object.success and not getattr(current_track_object, 'was_skipped', False):
error_detail = getattr(track, 'error_message', 'Download failed for unspecified reason.') # The error logging is now done within the except blocks above, more specifically.
logger.warning(f"Cannot download '{song}'. Reason: {error_detail} (Link: {track.link or c_preferences.link})") pass # logger.warning(f"Cannot download '{song}'. Reason: {error_detail} (Link: {track.link or c_preferences.link})")
else:
tracks.append(track) # 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 --- # --- Append the final track path to the m3u file ---
# Build a relative path from the playlists directory # 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( relative_song_path = os.path.relpath(
track.song_path, current_track_object.song_path,
start=os.path.join(self.__output_dir, "playlists") start=os.path.join(self.__output_dir, "playlists")
) )
with open(m3u_path, "a", encoding="utf-8") as m3u_file: with open(m3u_path, "a", encoding="utf-8") as m3u_file:

View File

@@ -26,12 +26,13 @@ from deezspot.exceptions import (
TrackNotFound, TrackNotFound,
NoDataApi, NoDataApi,
AlbumNotFound, AlbumNotFound,
MarketAvailabilityError,
) )
from deezspot.libutils.utils import ( from deezspot.libutils.utils import (
create_zip, create_zip,
get_ids, get_ids,
link_is_valid, link_is_valid,
what_kind, what_kind
) )
from deezspot.libutils.others_settings import ( from deezspot.libutils.others_settings import (
stock_output, stock_output,
@@ -40,8 +41,10 @@ from deezspot.libutils.others_settings import (
stock_not_interface, stock_not_interface,
stock_zip, stock_zip,
stock_save_cover, stock_save_cover,
stock_market
) )
from deezspot.libutils.logging_utils import ProgressReporter, logger from deezspot.libutils.logging_utils import ProgressReporter, logger
import requests
API() API()
@@ -105,14 +108,24 @@ class DeeLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market=stock_market
) -> Track: ) -> Track:
link_is_valid(link_track) link_is_valid(link_track)
ids = get_ids(link_track) ids = get_ids(link_track)
song_metadata = None
market_str = market
if isinstance(market, list):
market_str = ", ".join([m.upper() for m in market])
elif isinstance(market, str):
market_str = market.upper()
try: try:
song_metadata = API.tracking(ids) song_metadata = API.tracking(ids, market=market)
except MarketAvailabilityError as e:
logger.error(f"Track {ids} is not available in market(s) '{market_str}'. Error: {e.message}")
raise TrackNotFound(url=link_track, message=e.message) from e
except NoDataApi: except NoDataApi:
infos = self.__gw_api.get_song_data(ids) infos = self.__gw_api.get_song_data(ids)
@@ -120,7 +133,13 @@ class DeeLogin:
raise TrackNotFound(link_track) raise TrackNotFound(link_track)
ids = infos['FALLBACK']['SNG_ID'] ids = infos['FALLBACK']['SNG_ID']
song_metadata = API.tracking(ids) try:
song_metadata = API.tracking(ids, market=market)
except MarketAvailabilityError as e:
logger.error(f"Fallback track {ids} is not available in market(s) '{market_str}'. Error: {e.message}")
raise TrackNotFound(url=link_track, message=e.message) from e
except NoDataApi:
raise TrackNotFound(link_track)
preferences = Preferences() preferences = Preferences()
preferences.link = link_track preferences.link = link_track
@@ -131,19 +150,16 @@ class DeeLogin:
preferences.recursive_quality = recursive_quality preferences.recursive_quality = recursive_quality
preferences.recursive_download = recursive_download preferences.recursive_download = recursive_download
preferences.not_interface = not_interface preferences.not_interface = not_interface
# New custom formatting preferences:
preferences.custom_dir_format = custom_dir_format preferences.custom_dir_format = custom_dir_format
preferences.custom_track_format = custom_track_format preferences.custom_track_format = custom_track_format
# Track number padding option
preferences.pad_tracks = pad_tracks preferences.pad_tracks = pad_tracks
# Retry parameters
preferences.initial_retry_delay = initial_retry_delay preferences.initial_retry_delay = initial_retry_delay
preferences.retry_delay_increase = retry_delay_increase preferences.retry_delay_increase = retry_delay_increase
preferences.max_retries = max_retries preferences.max_retries = max_retries
# Audio conversion parameter
preferences.convert_to = convert_to preferences.convert_to = convert_to
preferences.bitrate = bitrate preferences.bitrate = bitrate
preferences.save_cover = save_cover preferences.save_cover = save_cover
preferences.market = market
track = DW_TRACK(preferences).dw() track = DW_TRACK(preferences).dw()
@@ -165,18 +181,25 @@ class DeeLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market=stock_market
) -> Album: ) -> Album:
link_is_valid(link_album) link_is_valid(link_album)
ids = get_ids(link_album) ids = get_ids(link_album)
album_json = None
market_str = market
if isinstance(market, list):
market_str = ", ".join([m.upper() for m in market])
elif isinstance(market, str):
market_str = market.upper()
try: try:
album_json = API.get_album(ids) album_json = API.get_album(ids)
except NoDataApi: except NoDataApi:
raise AlbumNotFound(link_album) raise AlbumNotFound(link_album)
song_metadata = API.tracking_album(album_json) song_metadata = API.tracking_album(album_json, market=market)
preferences = Preferences() preferences = Preferences()
preferences.link = link_album preferences.link = link_album
@@ -189,19 +212,16 @@ class DeeLogin:
preferences.recursive_download = recursive_download preferences.recursive_download = recursive_download
preferences.not_interface = not_interface preferences.not_interface = not_interface
preferences.make_zip = make_zip preferences.make_zip = make_zip
# New custom formatting preferences:
preferences.custom_dir_format = custom_dir_format preferences.custom_dir_format = custom_dir_format
preferences.custom_track_format = custom_track_format preferences.custom_track_format = custom_track_format
# Track number padding option
preferences.pad_tracks = pad_tracks preferences.pad_tracks = pad_tracks
# Retry parameters
preferences.initial_retry_delay = initial_retry_delay preferences.initial_retry_delay = initial_retry_delay
preferences.retry_delay_increase = retry_delay_increase preferences.retry_delay_increase = retry_delay_increase
preferences.max_retries = max_retries preferences.max_retries = max_retries
# Audio conversion parameter
preferences.convert_to = convert_to preferences.convert_to = convert_to
preferences.bitrate = bitrate preferences.bitrate = bitrate
preferences.save_cover = save_cover preferences.save_cover = save_cover
preferences.market = market
album = DW_ALBUM(preferences).dw() album = DW_ALBUM(preferences).dw()
@@ -223,7 +243,8 @@ class DeeLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market=stock_market
) -> Playlist: ) -> Playlist:
link_is_valid(link_playlist) link_is_valid(link_playlist)
@@ -231,20 +252,93 @@ class DeeLogin:
song_metadata = [] song_metadata = []
playlist_json = API.get_playlist(ids) playlist_json = API.get_playlist(ids)
market_str_playlist = market
if isinstance(market, list):
market_str_playlist = ", ".join([m.upper() for m in market])
elif isinstance(market, str):
market_str_playlist = market.upper()
for track in playlist_json['tracks']['data']: for track in playlist_json['tracks']['data']:
c_ids = track['id'] c_ids = track['id']
c_song_metadata_item = None
track_title_for_error = track.get('title', 'Unknown Track')
track_artist_for_error = track.get('artist', {}).get('name', 'Unknown Artist')
try: try:
c_song_metadata = API.tracking(c_ids) c_song_metadata_item = API.tracking(c_ids, market=market)
except MarketAvailabilityError as e:
logger.warning(f"Track '{track_title_for_error}' (ID: {c_ids}) in playlist not available in market(s) '{market_str_playlist}': {e.message}")
c_song_metadata_item = {
'error_type': 'MarketAvailabilityError',
'message': e.message,
'name': track_title_for_error,
'artist': track_artist_for_error,
'ids': c_ids,
'checked_markets': market_str_playlist
}
except NoDataApi: except NoDataApi:
infos = self.__gw_api.get_song_data(c_ids) infos = self.__gw_api.get_song_data(c_ids)
if not "FALLBACK" in infos: if not "FALLBACK" in infos:
c_song_metadata = f"{track['title']} - {track['artist']['name']}" logger.warning(f"Track '{track_title_for_error}' (ID: {c_ids}) in playlist not found on Deezer and no fallback.")
c_song_metadata_item = {
'error_type': 'NoDataApi',
'message': f"Track {track_title_for_error} - {track_artist_for_error} (ID: {c_ids}) not found.",
'name': track_title_for_error,
'artist': track_artist_for_error,
'ids': c_ids
}
else: else:
c_song_metadata = API.tracking(c_ids) fallback_ids = infos['FALLBACK']['SNG_ID']
try:
song_metadata.append(c_song_metadata) c_song_metadata_item = API.tracking(fallback_ids, market=market)
except MarketAvailabilityError as e_fallback:
logger.warning(f"Fallback track (Original ID: {c_ids}, Fallback ID: {fallback_ids}) for '{track_title_for_error}' in playlist not available in market(s) '{market_str_playlist}': {e_fallback.message}")
c_song_metadata_item = {
'error_type': 'MarketAvailabilityError',
'message': e_fallback.message,
'name': track_title_for_error,
'artist': track_artist_for_error,
'ids': fallback_ids,
'checked_markets': market_str_playlist
}
except NoDataApi:
logger.warning(f"Fallback track (Original ID: {c_ids}, Fallback ID: {fallback_ids}) for '{track_title_for_error}' in playlist also not found on Deezer.")
c_song_metadata_item = {
'error_type': 'NoDataApi',
'message': f"Fallback for track {track_title_for_error} (ID: {fallback_ids}) also not found.",
'name': track_title_for_error,
'artist': track_artist_for_error,
'ids': fallback_ids
}
except requests.exceptions.ConnectionError as e_conn_fallback:
logger.warning(f"Connection error fetching metadata for fallback track (Original ID: {c_ids}, Fallback ID: {fallback_ids}) for '{track_title_for_error}' in playlist: {str(e_conn_fallback)}")
c_song_metadata_item = {
'error_type': 'ConnectionError',
'message': f"Connection error on fallback: {str(e_conn_fallback)}",
'name': track_title_for_error,
'artist': track_artist_for_error,
'ids': fallback_ids
}
except requests.exceptions.ConnectionError as e_conn:
logger.warning(f"Connection error fetching metadata for track '{track_title_for_error}' (ID: {c_ids}) in playlist: {str(e_conn)}")
c_song_metadata_item = {
'error_type': 'ConnectionError',
'message': f"Connection error: {str(e_conn)}",
'name': track_title_for_error,
'artist': track_artist_for_error,
'ids': c_ids
}
except Exception as e_other_metadata:
logger.warning(f"Unexpected error fetching metadata for track '{track_title_for_error}' (ID: {c_ids}) in playlist: {str(e_other_metadata)}")
c_song_metadata_item = {
'error_type': 'MetadataError',
'message': str(e_other_metadata),
'name': track_title_for_error,
'artist': track_artist_for_error,
'ids': c_ids
}
song_metadata.append(c_song_metadata_item)
preferences = Preferences() preferences = Preferences()
preferences.link = link_playlist preferences.link = link_playlist
@@ -257,19 +351,16 @@ class DeeLogin:
preferences.recursive_download = recursive_download preferences.recursive_download = recursive_download
preferences.not_interface = not_interface preferences.not_interface = not_interface
preferences.make_zip = make_zip preferences.make_zip = make_zip
# New custom formatting preferences:
preferences.custom_dir_format = custom_dir_format preferences.custom_dir_format = custom_dir_format
preferences.custom_track_format = custom_track_format preferences.custom_track_format = custom_track_format
# Track number padding option
preferences.pad_tracks = pad_tracks preferences.pad_tracks = pad_tracks
# Retry parameters
preferences.initial_retry_delay = initial_retry_delay preferences.initial_retry_delay = initial_retry_delay
preferences.retry_delay_increase = retry_delay_increase preferences.retry_delay_increase = retry_delay_increase
preferences.max_retries = max_retries preferences.max_retries = max_retries
# Audio conversion parameter
preferences.convert_to = convert_to preferences.convert_to = convert_to
preferences.bitrate = bitrate preferences.bitrate = bitrate
preferences.save_cover = save_cover preferences.save_cover = save_cover
preferences.market = market
playlist = DW_PLAYLIST(preferences).dw() playlist = DW_PLAYLIST(preferences).dw()
@@ -287,7 +378,8 @@ class DeeLogin:
pad_tracks=True, pad_tracks=True,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market=stock_market
) -> list[Track]: ) -> list[Track]:
link_is_valid(link_artist) link_is_valid(link_artist)
@@ -305,7 +397,8 @@ class DeeLogin:
pad_tracks=pad_tracks, pad_tracks=pad_tracks,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate, bitrate=bitrate,
save_cover=save_cover save_cover=save_cover,
market=market
) )
for track in playlist_json for track in playlist_json
] ]
@@ -348,7 +441,8 @@ class DeeLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market=stock_market
) -> Track: ) -> Track:
track_link_dee = self.convert_spoty_to_dee_link_track(link_track) track_link_dee = self.convert_spoty_to_dee_link_track(link_track)
@@ -368,7 +462,8 @@ class DeeLogin:
max_retries=max_retries, max_retries=max_retries,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate, bitrate=bitrate,
save_cover=save_cover save_cover=save_cover,
market=market
) )
return track return track
@@ -468,7 +563,8 @@ class DeeLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market=stock_market
) -> Album: ) -> Album:
link_dee = self.convert_spoty_to_dee_link_album(link_album) link_dee = self.convert_spoty_to_dee_link_album(link_album)
@@ -486,7 +582,8 @@ class DeeLogin:
max_retries=max_retries, max_retries=max_retries,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate, bitrate=bitrate,
save_cover=save_cover save_cover=save_cover,
market=market
) )
return album return album
@@ -507,7 +604,8 @@ class DeeLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market=stock_market
) -> Playlist: ) -> Playlist:
link_is_valid(link_playlist) link_is_valid(link_playlist)
@@ -585,7 +683,8 @@ class DeeLogin:
max_retries=max_retries, max_retries=max_retries,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate, bitrate=bitrate,
save_cover=save_cover save_cover=save_cover,
market=market
) )
tracks.append(downloaded_track) tracks.append(downloaded_track)
except (TrackNotFound, NoDataApi) as e: except (TrackNotFound, NoDataApi) as e:
@@ -641,7 +740,8 @@ class DeeLogin:
pad_tracks=True, pad_tracks=True,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market=stock_market
) -> Track: ) -> Track:
query = f"track:{song} artist:{artist}" query = f"track:{song} artist:{artist}"
@@ -675,7 +775,8 @@ class DeeLogin:
max_retries=max_retries, max_retries=max_retries,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate, bitrate=bitrate,
save_cover=save_cover save_cover=save_cover,
market=market
) )
return track return track
@@ -696,18 +797,34 @@ class DeeLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market=stock_market
) -> Episode: ) -> Episode:
link_is_valid(link_episode) link_is_valid(link_episode)
ids = get_ids(link_episode) ids = get_ids(link_episode)
episode_metadata = None
market_str_episode = market
if isinstance(market, list):
market_str_episode = ", ".join([m.upper() for m in market])
elif isinstance(market, str):
market_str_episode = market.upper()
try: try:
episode_metadata = API.tracking(ids) episode_metadata = API.tracking(ids, market=market)
except MarketAvailabilityError as e:
logger.error(f"Episode {ids} is not available in market(s) '{market_str_episode}'. Error: {e.message}")
# For episodes, structure of error might be different than TrackNotFound expects if it uses track-specific fields
# Creating a message that TrackNotFound can use
raise TrackNotFound(url=link_episode, message=f"Episode not available in market(s) '{market_str_episode}': {e.message}") from e
except NoDataApi: except NoDataApi:
infos = self.__gw_api.get_episode_data(ids) infos = self.__gw_api.get_episode_data(ids)
if not infos: if not infos:
raise TrackNotFound("Episode not found") raise TrackNotFound(f"Episode {ids} not found")
# For episodes, API.tracking is usually not called again with GW API data in this flow.
# We construct metadata directly.
# No direct market check here as available_countries might not be in GW response for episodes.
# The initial API.tracking call is the main point for market check for episodes.
episode_metadata = { episode_metadata = {
'music': infos.get('EPISODE_TITLE', ''), 'music': infos.get('EPISODE_TITLE', ''),
'artist': infos.get('SHOW_NAME', ''), 'artist': infos.get('SHOW_NAME', ''),
@@ -738,6 +855,7 @@ class DeeLogin:
preferences.bitrate = bitrate preferences.bitrate = bitrate
preferences.save_cover = save_cover preferences.save_cover = save_cover
preferences.is_episode = True preferences.is_episode = True
preferences.market = market
episode = DW_EPISODE(preferences).dw() episode = DW_EPISODE(preferences).dw()
@@ -759,7 +877,8 @@ class DeeLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market=stock_market
) -> Smart: ) -> Smart:
link_is_valid(link) link_is_valid(link)
@@ -804,7 +923,8 @@ class DeeLogin:
max_retries=max_retries, max_retries=max_retries,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate, bitrate=bitrate,
save_cover=save_cover save_cover=save_cover,
market=market
) )
smart.type = "track" smart.type = "track"
smart.track = track smart.track = track
@@ -833,7 +953,8 @@ class DeeLogin:
max_retries=max_retries, max_retries=max_retries,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate, bitrate=bitrate,
save_cover=save_cover save_cover=save_cover,
market=market
) )
smart.type = "album" smart.type = "album"
smart.album = album smart.album = album
@@ -862,7 +983,8 @@ class DeeLogin:
max_retries=max_retries, max_retries=max_retries,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate, bitrate=bitrate,
save_cover=save_cover save_cover=save_cover,
market=market
) )
smart.type = "playlist" smart.type = "playlist"
smart.playlist = playlist smart.playlist = playlist

View File

@@ -11,6 +11,7 @@ from deezspot.exceptions import (
NoDataApi, NoDataApi,
QuotaExceeded, QuotaExceeded,
TrackNotFound, TrackNotFound,
MarketAvailabilityError,
) )
from deezspot.libutils.logging_utils import logger from deezspot.libutils.logging_utils import logger
import requests import requests
@@ -222,17 +223,51 @@ class API:
image = req_get(image_url).content image = req_get(image_url).content
if len(image) == 13: if len(image) == 13:
logger.debug(f"Received 13-byte image for md5_image: {md5_image}. Attempting fallback image.")
image_url = cls.get_img_url("", size) image_url = cls.get_img_url("", size)
image = req_get(image_url).content image = req_get(image_url).content
if len(image) == 13:
logger.warning(f"Fallback image for md5_image {md5_image} (using empty md5) also resulted in a 13-byte response.")
return image return image
@classmethod @classmethod
def tracking(cls, ids, album = False) -> dict: def tracking(cls, ids, album = False, market = None) -> dict:
song_metadata = {} song_metadata = {}
json_track = cls.get_track(ids) json_track = cls.get_track(ids)
# Ensure ISRC is always fetched # Market availability check
if market:
available_countries = json_track.get("available_countries")
track_available_in_specified_markets = False
markets_checked_str = ""
if isinstance(market, list):
markets_checked_str = ", ".join([m.upper() for m in market])
if available_countries:
for m_code in market:
if m_code.upper() in available_countries:
track_available_in_specified_markets = True
break # Found in one market, no need to check further
else: # available_countries is None or empty
track_available_in_specified_markets = False # Cannot be available if API lists no countries
elif isinstance(market, str):
markets_checked_str = market.upper()
if available_countries and market.upper() in available_countries:
track_available_in_specified_markets = True
else: # available_countries is None or empty, or market not in list
track_available_in_specified_markets = False
else:
logger.warning(f"Market parameter has an unexpected type: {type(market)}. Skipping market check.")
track_available_in_specified_markets = True # Default to available if market param is malformed
if not track_available_in_specified_markets:
track_title = json_track.get('title', 'Unknown Title')
artist_name = json_track.get('artist', {}).get('name', 'Unknown Artist')
error_msg = f"Track '{track_title}' by '{artist_name}' (ID: {ids}) is not available in market(s): '{markets_checked_str}'."
logger.warning(error_msg)
raise MarketAvailabilityError(message=error_msg)
song_metadata['isrc'] = json_track.get('isrc', '') song_metadata['isrc'] = json_track.get('isrc', '')
if not album: if not album:
@@ -254,7 +289,6 @@ class API:
song_metadata['ar_album'] = "; ".join(ar_album) song_metadata['ar_album'] = "; ".join(ar_album)
song_metadata['album'] = album_json['title'] song_metadata['album'] = album_json['title']
song_metadata['label'] = album_json['label'] song_metadata['label'] = album_json['label']
# Ensure UPC is fetched from album data
song_metadata['upc'] = album_json.get('upc', '') song_metadata['upc'] = album_json.get('upc', '')
song_metadata['nb_tracks'] = album_json['nb_tracks'] song_metadata['nb_tracks'] = album_json['nb_tracks']
@@ -275,13 +309,12 @@ class API:
song_metadata['year'] = convert_to_date(json_track['release_date']) song_metadata['year'] = convert_to_date(json_track['release_date'])
song_metadata['bpm'] = json_track['bpm'] song_metadata['bpm'] = json_track['bpm']
song_metadata['duration'] = json_track['duration'] song_metadata['duration'] = json_track['duration']
# song_metadata['isrc'] = json_track['isrc'] # Already handled above
song_metadata['gain'] = json_track['gain'] song_metadata['gain'] = json_track['gain']
return song_metadata return song_metadata
@classmethod @classmethod
def tracking_album(cls, album_json): def tracking_album(cls, album_json, market = None):
song_metadata: dict[ song_metadata: dict[
str, str,
Union[list, str, int, datetime] Union[list, str, int, datetime]
@@ -292,12 +325,11 @@ class API:
"discnum": [], "discnum": [],
"bpm": [], "bpm": [],
"duration": [], "duration": [],
"isrc": [], # Ensure isrc list is present for tracks "isrc": [],
"gain": [], "gain": [],
"album": album_json['title'], "album": album_json['title'],
"label": album_json['label'], "label": album_json['label'],
"year": convert_to_date(album_json['release_date']), "year": convert_to_date(album_json['release_date']),
# Ensure UPC is fetched at album level
"upc": album_json.get('upc', ''), "upc": album_json.get('upc', ''),
"nb_tracks": album_json['nb_tracks'] "nb_tracks": album_json['nb_tracks']
} }
@@ -318,16 +350,76 @@ class API:
song_metadata['ar_album'] = "; ".join(ar_album) song_metadata['ar_album'] = "; ".join(ar_album)
sm_items = song_metadata.items() sm_items = song_metadata.items()
for track in album_json['tracks']['data']: for track_info_from_album_json in album_json['tracks']['data']:
c_ids = track['id'] c_ids = track_info_from_album_json['id']
detas = cls.tracking(c_ids, album = True) track_title_for_error = track_info_from_album_json.get('title', 'Unknown Track')
track_artist_for_error = track_info_from_album_json.get('artist', {}).get('name', 'Unknown Artist')
current_track_metadata_or_error = None
track_failed = False
for key, item in sm_items: try:
if type(item) is list: # Get detailed metadata for the current track
# Ensure ISRC is appended for each track current_track_metadata_or_error = cls.tracking(c_ids, album=True, market=market)
if key == 'isrc': except MarketAvailabilityError as e:
song_metadata[key].append(detas.get('isrc', '')) market_str = market
if isinstance(market, list):
market_str = ", ".join([m.upper() for m in market])
elif isinstance(market, str):
market_str = market.upper()
logger.warning(f"Track '{track_title_for_error}' (ID: {c_ids}) in album '{album_json.get('title','Unknown Album')}' not available in market(s) '{market_str}': {e.message}")
current_track_metadata_or_error = {
'error_type': 'MarketAvailabilityError',
'message': e.message,
'name': track_title_for_error,
'artist': track_artist_for_error,
'ids': c_ids,
'checked_markets': market_str # Store the markets that were checked
}
track_failed = True
except NoDataApi as e_nd:
logger.warning(f"Track '{track_title_for_error}' (ID: {c_ids}) in album '{album_json.get('title','Unknown Album')}' data not found: {str(e_nd)}")
current_track_metadata_or_error = {
'error_type': 'NoDataApi',
'message': str(e_nd),
'name': track_title_for_error,
'artist': track_artist_for_error,
'ids': c_ids
}
track_failed = True
except requests.exceptions.ConnectionError as e_conn: # Added to catch connection errors here
logger.warning(f"Connection error fetching metadata for track '{track_title_for_error}' (ID: {c_ids}) in album '{album_json.get('title','Unknown Album')}': {str(e_conn)}")
current_track_metadata_or_error = {
'error_type': 'ConnectionError',
'message': f"Connection error: {str(e_conn)}",
'name': track_title_for_error,
'artist': track_artist_for_error,
'ids': c_ids
}
track_failed = True
except Exception as e_other_track_meta: # Catch any other unexpected error for this specific track
logger.warning(f"Unexpected error fetching metadata for track '{track_title_for_error}' (ID: {c_ids}) in album '{album_json.get('title','Unknown Album')}': {str(e_other_track_meta)}")
current_track_metadata_or_error = {
'error_type': 'TrackMetadataError',
'message': str(e_other_track_meta),
'name': track_title_for_error,
'artist': track_artist_for_error,
'ids': c_ids
}
track_failed = True
for key, list_template in sm_items:
if isinstance(list_template, list):
if track_failed:
if key == 'music':
song_metadata[key].append(current_track_metadata_or_error)
elif key == 'artist' and isinstance(current_track_metadata_or_error, dict):
song_metadata[key].append(current_track_metadata_or_error.get('artist'))
elif key == 'ids' and isinstance(current_track_metadata_or_error, dict):
pass
else:
song_metadata[key].append(None)
else: else:
song_metadata[key].append(detas[key]) song_metadata[key].append(current_track_metadata_or_error.get(key))
return song_metadata return song_metadata

View File

@@ -74,4 +74,9 @@ class BadCredentials(Exception):
else: else:
self.msg = f"Wrong credentials email: {self.email}, password: {self.password}" self.msg = f"Wrong credentials email: {self.email}, password: {self.password}"
super().__init__(self.msg) super().__init__(self.msg)
class MarketAvailabilityError(Exception):
def __init__(self, message):
self.message = message
super().__init__(self.message)

View File

@@ -23,3 +23,4 @@ stock_not_interface = False
stock_zip = False stock_zip = False
stock_real_time_dl = False stock_real_time_dl = False
stock_save_cover = False # Default for saving cover image stock_save_cover = False # Default for saving cover image
stock_market = None

View File

@@ -5,6 +5,7 @@ from mutagen import File
from mutagen.easyid3 import EasyID3 from mutagen.easyid3 import EasyID3
from mutagen.oggvorbis import OggVorbis from mutagen.oggvorbis import OggVorbis
from mutagen.flac import FLAC from mutagen.flac import FLAC
from mutagen.mp3 import MP3 # Added for explicit MP3 type checking
# from mutagen.mp4 import MP4 # MP4 is usually handled by File for .m4a # from mutagen.mp4 import MP4 # MP4 is usually handled by File for .m4a
# AUDIO_FORMATS and get_output_path will be imported from audio_converter # AUDIO_FORMATS and get_output_path will be imported from audio_converter
@@ -30,9 +31,24 @@ def read_metadata_from_file(file_path, logger):
title = None title = None
album = None album = None
if isinstance(audio, EasyID3): # MP3 if isinstance(audio, EasyID3): # This might occur if easy=True was used, but we use easy=False
# This branch is less likely to be hit with current File(..., easy=False) usage for MP3s
title = audio.get('title', [None])[0] title = audio.get('title', [None])[0]
album = audio.get('album', [None])[0] album = audio.get('album', [None])[0]
elif isinstance(audio, MP3): # Correctly handle MP3 objects when easy=False
# For mutagen.mp3.MP3, tags are typically accessed via audio.tags (an ID3 object)
# Common ID3 frames for title and album are TIT2 and TALB respectively.
# The .text attribute of a frame object usually holds a list of strings.
if audio.tags:
title_frame = audio.tags.get('TIT2')
if title_frame:
title = title_frame.text[0] if title_frame.text else None
album_frame = audio.tags.get('TALB')
if album_frame:
album = album_frame.text[0] if album_frame.text else None
else:
logger.debug(f"No tags found in MP3 file: {file_path}")
elif isinstance(audio, OggVorbis): # OGG elif isinstance(audio, OggVorbis): # OGG
title = audio.get('TITLE', [None])[0] # Vorbis tags are case-insensitive but typically uppercase title = audio.get('TITLE', [None])[0] # Vorbis tags are case-insensitive but typically uppercase
album = audio.get('ALBUM', [None])[0] album = audio.get('ALBUM', [None])[0]

View File

@@ -13,7 +13,11 @@ class Track:
self.tags = tags self.tags = tags
self.__set_tags() self.__set_tags()
self.song_name = f"{self.music} - {self.artist}"
music_display = getattr(self, 'music', getattr(self, 'name', "Unknown Track"))
artist_display = getattr(self, 'artist', "Unknown Artist")
self.song_name = f"{music_display} - {artist_display}"
self.song_path = song_path self.song_path = song_path
self.file_format = file_format self.file_format = file_format
self.quality = quality self.quality = quality

View File

@@ -1204,31 +1204,123 @@ class DW_PLAYLIST:
m3u_path = os.path.join(playlist_m3u_dir, f"{playlist_name_sanitized}.m3u") m3u_path = os.path.join(playlist_m3u_dir, f"{playlist_name_sanitized}.m3u")
if not os.path.exists(m3u_path): if not os.path.exists(m3u_path):
with open(m3u_path, "w", encoding="utf-8") as m3u_file: with open(m3u_path, "w", encoding="utf-8") as m3u_file:
m3u_file.write("#EXTM3U\\n") m3u_file.write("#EXTM3U\n")
# ------------------------------------- # -------------------------------------
playlist = Playlist() playlist = Playlist()
tracks = playlist.tracks tracks = playlist.tracks
for idx, c_song_metadata in enumerate(self.__song_metadata): for idx, c_song_metadata in enumerate(self.__song_metadata):
# Check if c_song_metadata indicates a pre-identified error from metadata fetching stage
if isinstance(c_song_metadata, dict) and 'error_type' in c_song_metadata:
track_name = c_song_metadata.get('name', 'Unknown Track')
track_ids = c_song_metadata.get('ids', None)
error_message = c_song_metadata.get('error_message', 'Unknown error during metadata retrieval.')
error_type = c_song_metadata.get('error_type', 'UnknownError')
logger.warning(f"Skipping download for track '{track_name}' (ID: {track_ids}) from playlist '{playlist_name}' due to {error_type}: {error_message}")
# Create a placeholder Track object to represent this failure
# The link might not be available or relevant if IDs itself was the issue
failed_track_link = f"https://open.spotify.com/track/{track_ids}" if track_ids else None
# Basic metadata for the Track object constructor
# We use c_song_metadata itself as it contains name, ids, etc.
# Ensure it's a dict for Track constructor
track_obj_metadata = c_song_metadata if isinstance(c_song_metadata, dict) else {'name': track_name, 'ids': track_ids}
track = Track(
tags=track_obj_metadata,
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)
continue # Move to the next track in the playlist
# Original handling for string type (though this should be less common with new error dicts)
if type(c_song_metadata) is str: if type(c_song_metadata) is str:
print(f"Track not found {c_song_metadata} :(") logger.warning(f"Encountered string as song metadata for a track in playlist '{playlist_name}': {c_song_metadata}. Treating as error.")
# Attempt to create a basic Track object with this string as an error message.
# This is a fallback for older error reporting styles.
error_track_name = "Unknown Track (error)"
error_track_ids = None
# Try to parse some info if the string is very specific, otherwise use generic.
if "Track not found" in c_song_metadata:
# This was an old message format, may not contain structured info.
pass # Keep generic error_track_name for now.
track = Track(
tags={'name': error_track_name, 'ids': error_track_ids, 'artist': 'Unknown Artist'}, # Minimal metadata
song_path=None,
file_format=None,
quality=None,
link=None, # No reliable link from just an error string
ids=error_track_ids
)
track.success = False
track.error_message = c_song_metadata # The string itself is the error
tracks.append(track)
continue continue
# If c_song_metadata is a valid metadata dictionary (no 'error_type')
c_preferences = deepcopy(self.__preferences) c_preferences = deepcopy(self.__preferences)
c_preferences.ids = c_song_metadata['ids'] c_preferences.ids = c_song_metadata.get('ids') # Use .get for safety, though it should exist
c_preferences.song_metadata = c_song_metadata c_preferences.song_metadata = c_song_metadata
c_preferences.json_data = self.__json_data # Pass playlist data for reporting c_preferences.json_data = self.__json_data # Pass playlist data for reporting
c_preferences.track_number = idx + 1 # Track number in the playlist c_preferences.track_number = idx + 1 # Track number in the playlist
c_preferences.link = f"https://open.spotify.com/track/{c_preferences.ids}" if c_preferences.ids else None
# Use track-level reporting through EASY_DW easy_dw_instance = EASY_DW(c_preferences, parent='playlist')
track = EASY_DW(c_preferences, parent='playlist').easy_dw() track = None # Initialize track for this iteration
# Only log a warning if the track failed and was NOT intentionally skipped try:
if not track.success and not getattr(track, 'was_skipped', False): track = easy_dw_instance.easy_dw()
song = f"{c_song_metadata['music']} - {c_song_metadata['artist']}" except TrackNotFound as e_track_nf:
error_detail = getattr(track, 'error_message', 'Download failed for unspecified reason.') track = easy_dw_instance.get_no_dw_track() # Retrieve the track instance from EASY_DW
logger.warning(f"Cannot download '{song}' from playlist '{playlist_name}'. Reason: {error_detail} (URL: {track.link or c_preferences.link})") # Ensure track object is a valid Track instance and has error info
if not isinstance(track, Track): # Fallback if get_no_dw_track didn't return a Track
track = Track(c_song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
track.success = False # Explicitly set success to False
# Ensure error message is set, preferring the one from the exception if track doesn't have one
if not getattr(track, 'error_message', None) or str(e_track_nf): # Prioritize exception message if available
track.error_message = str(e_track_nf)
song_name_log = c_song_metadata.get('music', 'Unknown Song')
artist_name_log = c_song_metadata.get('artist', 'Unknown Artist')
playlist_name_log = self.__json_data.get('name', 'Unknown Playlist')
logger.warning(
f"Failed to download track '{song_name_log}' by '{artist_name_log}' from playlist '{playlist_name_log}'. "
f"Reason: {track.error_message} (URL: {track.link or c_preferences.link})"
)
except Exception as e_generic:
# Catch any other unexpected exceptions during the track download process
track = Track(c_song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
track.success = False
track.error_message = f"An unexpected error occurred while processing track: {str(e_generic)}"
song_name_log = c_song_metadata.get('music', 'Unknown Song')
artist_name_log = c_song_metadata.get('artist', 'Unknown Artist')
playlist_name_log = self.__json_data.get('name', 'Unknown Playlist')
logger.error(
f"Unexpected error downloading track '{song_name_log}' by '{artist_name_log}' from playlist '{playlist_name_log}'. "
f"Reason: {track.error_message} (URL: {track.link or c_preferences.link})"
)
# Ensure track is not None before appending (should be assigned in try/except)
if track is None:
# This is a fallback, should ideally not be reached.
track = Track(c_song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
track.success = False
track.error_message = "Track processing resulted in an unhandled null track object."
logger.error(f"Track '{c_song_metadata.get('music', 'Unknown Track')}' from playlist '{self.__json_data.get('name', 'Unknown Playlist')}' "
f"was not properly processed.")
tracks.append(track) tracks.append(track)
# --- Append the final track path to the m3u file using a relative path --- # --- Append the final track path to the m3u file using a relative path ---
if track.success and hasattr(track, 'song_path') and track.song_path: if track.success and hasattr(track, 'song_path') and track.song_path:
# Build the relative path from the playlists directory # Build the relative path from the playlists directory

View File

@@ -3,9 +3,9 @@ import traceback
from os.path import isfile from os.path import isfile
from deezspot.easy_spoty import Spo from deezspot.easy_spoty import Spo
from librespot.core import Session from librespot.core import Session
from deezspot.exceptions import InvalidLink from deezspot.exceptions import InvalidLink, MarketAvailabilityError
from deezspot.spotloader.__spo_api__ import tracking, tracking_album, tracking_episode from deezspot.spotloader.__spo_api__ import tracking, tracking_album, tracking_episode
from deezspot.spotloader.spotify_settings import stock_quality from deezspot.spotloader.spotify_settings import stock_quality, stock_market
from deezspot.libutils.utils import ( from deezspot.libutils.utils import (
get_ids, get_ids,
link_is_valid, link_is_valid,
@@ -33,7 +33,8 @@ from deezspot.libutils.others_settings import (
stock_not_interface, stock_not_interface,
stock_zip, stock_zip,
stock_save_cover, stock_save_cover,
stock_real_time_dl stock_real_time_dl,
stock_market
) )
from deezspot.libutils.logging_utils import logger, ProgressReporter from deezspot.libutils.logging_utils import logger, ProgressReporter
@@ -98,13 +99,17 @@ class SpoLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market: list[str] | None = stock_market
) -> Track: ) -> Track:
try: try:
link_is_valid(link_track) link_is_valid(link_track)
ids = get_ids(link_track) ids = get_ids(link_track)
song_metadata = tracking(ids) song_metadata = tracking(ids, market=market)
if song_metadata is None:
raise Exception(f"Could not retrieve metadata for track {link_track}. It might not be available or an API error occurred.")
logger.info(f"Starting download for track: {song_metadata.get('music', 'Unknown')} - {song_metadata.get('artist', 'Unknown')}") logger.info(f"Starting download for track: {song_metadata.get('music', 'Unknown')} - {song_metadata.get('artist', 'Unknown')}")
preferences = Preferences() preferences = Preferences()
@@ -131,10 +136,14 @@ class SpoLogin:
preferences.convert_to = convert_to preferences.convert_to = convert_to
preferences.bitrate = bitrate preferences.bitrate = bitrate
preferences.save_cover = save_cover preferences.save_cover = save_cover
preferences.market = market
track = DW_TRACK(preferences).dw() track = DW_TRACK(preferences).dw()
return track return track
except MarketAvailabilityError as e:
logger.error(f"Track download failed due to market availability: {str(e)}")
raise
except Exception as e: except Exception as e:
logger.error(f"Failed to download track: {str(e)}") logger.error(f"Failed to download track: {str(e)}")
traceback.print_exc() traceback.print_exc()
@@ -157,15 +166,20 @@ class SpoLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market: list[str] | None = stock_market
) -> Album: ) -> Album:
try: try:
link_is_valid(link_album) link_is_valid(link_album)
ids = get_ids(link_album) ids = get_ids(link_album)
# Use stored credentials for API calls
album_json = Spo.get_album(ids) album_json = Spo.get_album(ids)
song_metadata = tracking_album(album_json) if not album_json:
raise Exception(f"Could not retrieve album data for {link_album}.")
song_metadata = tracking_album(album_json, market=market)
if song_metadata is None:
raise Exception(f"Could not process album metadata for {link_album}. It might not be available in the specified market(s) or an API error occurred.")
logger.info(f"Starting download for album: {song_metadata.get('album', 'Unknown')} - {song_metadata.get('ar_album', 'Unknown')}") logger.info(f"Starting download for album: {song_metadata.get('album', 'Unknown')} - {song_metadata.get('ar_album', 'Unknown')}")
preferences = Preferences() preferences = Preferences()
@@ -194,10 +208,14 @@ class SpoLogin:
preferences.convert_to = convert_to preferences.convert_to = convert_to
preferences.bitrate = bitrate preferences.bitrate = bitrate
preferences.save_cover = save_cover preferences.save_cover = save_cover
preferences.market = market
album = DW_ALBUM(preferences).dw() album = DW_ALBUM(preferences).dw()
return album return album
except MarketAvailabilityError as e:
logger.error(f"Album download failed due to market availability: {str(e)}")
raise
except Exception as e: except Exception as e:
logger.error(f"Failed to download album: {str(e)}") logger.error(f"Failed to download album: {str(e)}")
traceback.print_exc() traceback.print_exc()
@@ -220,30 +238,95 @@ class SpoLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market: list[str] | None = stock_market
) -> Playlist: ) -> Playlist:
try: try:
link_is_valid(link_playlist) link_is_valid(link_playlist)
ids = get_ids(link_playlist) ids = get_ids(link_playlist)
song_metadata = [] song_metadata = []
# Use stored credentials for API calls
playlist_json = Spo.get_playlist(ids) playlist_json = Spo.get_playlist(ids)
if not playlist_json:
raise Exception(f"Could not retrieve playlist data for {link_playlist}.")
logger.info(f"Starting download for playlist: {playlist_json.get('name', 'Unknown')}") logger.info(f"Starting download for playlist: {playlist_json.get('name', 'Unknown')}")
for track in playlist_json['tracks']['items']: for track_item_wrapper in playlist_json['tracks']['items']:
is_track = track['track'] track_info = track_item_wrapper.get('track')
if not is_track: c_song_metadata = None # Initialize for each item
if not track_info:
logger.warning(f"Skipping an item in playlist {playlist_json.get('name', 'Unknown Playlist')} as it does not appear to be a valid track object.")
# Create a placeholder for this unidentifiable item
c_song_metadata = {
'name': 'Unknown Skipped Item',
'ids': None,
'error_type': 'InvalidItemStructure',
'error_message': 'Playlist item was not a valid track object.'
}
song_metadata.append(c_song_metadata)
continue continue
external_urls = is_track['external_urls']
if not external_urls: track_name_for_logs = track_info.get('name', 'Unknown Track')
c_song_metadata = f"The track \"{is_track['name']}\" is not available on Spotify :(" track_id_for_logs = track_info.get('id', 'Unknown ID') # Track's own ID if available
logger.warning(f"Track not available: {is_track['name']}") external_urls = track_info.get('external_urls')
if not external_urls or not external_urls.get('spotify'):
logger.warning(f"Track \"{track_name_for_logs}\" (ID: {track_id_for_logs}) in playlist {playlist_json.get('name', 'Unknown Playlist')} is not available on Spotify or has no URL.")
c_song_metadata = {
'name': track_name_for_logs,
'ids': track_id_for_logs, # Use track's own ID if available, otherwise will be None
'error_type': 'MissingTrackURL',
'error_message': f"Track \"{track_name_for_logs}\" is not available on Spotify or has no URL."
}
else: else:
ids = get_ids(external_urls['spotify']) track_spotify_url = external_urls['spotify']
c_song_metadata = tracking(ids) track_ids_from_url = get_ids(track_spotify_url) # This is the ID used for fetching with 'tracking'
song_metadata.append(c_song_metadata) try:
# Market check for each track is done within tracking()
# Pass market. tracking() will raise MarketAvailabilityError if unavailable.
fetched_metadata = tracking(track_ids_from_url, market=market)
if fetched_metadata:
c_song_metadata = fetched_metadata
else:
# tracking() returned None, but didn't raise MarketAvailabilityError. General fetch error.
logger.warning(f"Could not retrieve full metadata for track {track_name_for_logs} (ID: {track_ids_from_url}, URL: {track_spotify_url}) in playlist {playlist_json.get('name', 'Unknown Playlist')}. API error or other issue.")
c_song_metadata = {
'name': track_name_for_logs,
'ids': track_ids_from_url,
'error_type': 'MetadataFetchError',
'error_message': f"Failed to fetch full metadata for track {track_name_for_logs}."
}
except MarketAvailabilityError as e:
logger.warning(f"Track {track_name_for_logs} (ID: {track_ids_from_url}, URL: {track_spotify_url}) in playlist {playlist_json.get('name', 'Unknown Playlist')} is not available in the specified market(s). Skipping. Error: {str(e)}")
c_song_metadata = {
'name': track_name_for_logs,
'ids': track_ids_from_url,
'error_type': 'MarketAvailabilityError',
'error_message': str(e)
}
except Exception as e_tracking: # Catch any other unexpected error from tracking()
logger.error(f"Unexpected error fetching metadata for track {track_name_for_logs} (ID: {track_ids_from_url}, URL: {track_spotify_url}): {str(e_tracking)}")
c_song_metadata = {
'name': track_name_for_logs,
'ids': track_ids_from_url,
'error_type': 'UnexpectedTrackingError',
'error_message': f"Unexpected error fetching metadata: {str(e_tracking)}"
}
if c_song_metadata: # Ensure something is appended
song_metadata.append(c_song_metadata)
else:
# This case should ideally not be reached if logic above is complete
logger.error(f"Logic error: c_song_metadata remained None for track {track_name_for_logs} in playlist {playlist_json.get('name', 'Unknown Playlist')}")
song_metadata.append({
'name': track_name_for_logs,
'ids': track_id_for_logs or track_ids_from_url,
'error_type': 'InternalLogicError',
'error_message': 'Internal error processing playlist track metadata.'
})
preferences = Preferences() preferences = Preferences()
preferences.real_time_dl = real_time_dl preferences.real_time_dl = real_time_dl
@@ -271,10 +354,14 @@ class SpoLogin:
preferences.convert_to = convert_to preferences.convert_to = convert_to
preferences.bitrate = bitrate preferences.bitrate = bitrate
preferences.save_cover = save_cover preferences.save_cover = save_cover
preferences.market = market
playlist = DW_PLAYLIST(preferences).dw() playlist = DW_PLAYLIST(preferences).dw()
return playlist return playlist
except MarketAvailabilityError as e:
logger.error(f"Playlist download failed due to market availability issues with one or more tracks: {str(e)}")
raise
except Exception as e: except Exception as e:
logger.error(f"Failed to download playlist: {str(e)}") logger.error(f"Failed to download playlist: {str(e)}")
traceback.print_exc() traceback.print_exc()
@@ -296,14 +383,19 @@ class SpoLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market: list[str] | None = stock_market
) -> Episode: ) -> Episode:
try: try:
link_is_valid(link_episode) link_is_valid(link_episode)
ids = get_ids(link_episode) ids = get_ids(link_episode)
# Use stored credentials for API calls
episode_json = Spo.get_episode(ids) episode_json = Spo.get_episode(ids)
episode_metadata = tracking_episode(ids) if not episode_json:
raise Exception(f"Could not retrieve episode data for {link_episode} from API.")
episode_metadata = tracking_episode(ids, market=market)
if episode_metadata is None:
raise Exception(f"Could not process episode metadata for {link_episode}. It might not be available in the specified market(s) or an API error occurred.")
logger.info(f"Starting download for episode: {episode_metadata.get('name', 'Unknown')} - {episode_metadata.get('show', 'Unknown')}") logger.info(f"Starting download for episode: {episode_metadata.get('name', 'Unknown')} - {episode_metadata.get('show', 'Unknown')}")
@@ -331,10 +423,14 @@ class SpoLogin:
preferences.convert_to = convert_to preferences.convert_to = convert_to
preferences.bitrate = bitrate preferences.bitrate = bitrate
preferences.save_cover = save_cover preferences.save_cover = save_cover
preferences.market = market
episode = DW_EPISODE(preferences).dw() episode = DW_EPISODE(preferences).dw()
return episode return episode
except MarketAvailabilityError as e:
logger.error(f"Episode download failed due to market availability: {str(e)}")
raise
except Exception as e: except Exception as e:
logger.error(f"Failed to download episode: {str(e)}") logger.error(f"Failed to download episode: {str(e)}")
traceback.print_exc() traceback.print_exc()
@@ -358,7 +454,9 @@ class SpoLogin:
retry_delay_increase=30, retry_delay_increase=30,
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None bitrate=None,
market: list[str] | None = stock_market,
save_cover=stock_save_cover
): ):
""" """
Download all albums (or a subset based on album_type and limit) from an artist. Download all albums (or a subset based on album_type and limit) from an artist.
@@ -396,7 +494,9 @@ class SpoLogin:
retry_delay_increase=retry_delay_increase, retry_delay_increase=retry_delay_increase,
max_retries=max_retries, max_retries=max_retries,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate bitrate=bitrate,
market=market,
save_cover=save_cover
) )
downloaded_albums.append(downloaded_album) downloaded_albums.append(downloaded_album)
return downloaded_albums return downloaded_albums
@@ -422,7 +522,8 @@ class SpoLogin:
max_retries=5, max_retries=5,
convert_to=None, convert_to=None,
bitrate=None, bitrate=None,
save_cover=stock_save_cover save_cover=stock_save_cover,
market: list[str] | None = stock_market
) -> Smart: ) -> Smart:
try: try:
link_is_valid(link) link_is_valid(link)
@@ -454,7 +555,8 @@ class SpoLogin:
max_retries=max_retries, max_retries=max_retries,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate, bitrate=bitrate,
save_cover=save_cover save_cover=save_cover,
market=market
) )
smart.type = "track" smart.type = "track"
smart.track = track smart.track = track
@@ -479,7 +581,8 @@ class SpoLogin:
max_retries=max_retries, max_retries=max_retries,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate, bitrate=bitrate,
save_cover=save_cover save_cover=save_cover,
market=market
) )
smart.type = "album" smart.type = "album"
smart.album = album smart.album = album
@@ -504,7 +607,8 @@ class SpoLogin:
max_retries=max_retries, max_retries=max_retries,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate, bitrate=bitrate,
save_cover=save_cover save_cover=save_cover,
market=market
) )
smart.type = "playlist" smart.type = "playlist"
smart.playlist = playlist smart.playlist = playlist
@@ -528,7 +632,8 @@ class SpoLogin:
max_retries=max_retries, max_retries=max_retries,
convert_to=convert_to, convert_to=convert_to,
bitrate=bitrate, bitrate=bitrate,
save_cover=save_cover save_cover=save_cover,
market=market
) )
smart.type = "episode" smart.type = "episode"
smart.episode = episode smart.episode = episode

View File

@@ -5,6 +5,23 @@ from datetime import datetime
from deezspot.libutils.utils import convert_to_date from deezspot.libutils.utils import convert_to_date
import traceback import traceback
from deezspot.libutils.logging_utils import logger from deezspot.libutils.logging_utils import logger
from deezspot.exceptions import MarketAvailabilityError
def _check_market_availability(item_name: str, item_type: str, api_available_markets: list[str] | None, user_markets: list[str] | None):
"""Checks if an item is available in any of the user-specified markets."""
if user_markets and api_available_markets is not None:
is_available_in_any_user_market = any(m in api_available_markets for m in user_markets)
if not is_available_in_any_user_market:
markets_str = ", ".join(user_markets)
raise MarketAvailabilityError(f"{item_type} '{item_name}' not available in provided market(s): {markets_str}")
elif user_markets and api_available_markets is None:
# Log a warning if user specified markets, but API response doesn't include 'available_markets'
# This might indicate the item is available in all markets or API doesn't provide this info for this item type.
# For now, we proceed without raising an error, as we cannot confirm it's "not available".
logger.warning(
f"Market availability check for {item_type} '{item_name}' skipped: "
"API response did not include 'available_markets' field. Assuming availability."
)
def _get_best_image_urls(images_list): def _get_best_image_urls(images_list):
urls = {'image': '', 'image2': '', 'image3': ''} urls = {'image': '', 'image2': '', 'image3': ''}
@@ -28,7 +45,7 @@ def _get_best_image_urls(images_list):
return urls return urls
def tracking(ids, album_data_for_track=None): def tracking(ids, album_data_for_track=None, market: list[str] | None = None):
datas = {} datas = {}
try: try:
json_track = Spo.get_track(ids) json_track = Spo.get_track(ids)
@@ -36,6 +53,11 @@ def tracking(ids, album_data_for_track=None):
logger.error(f"Failed to get track details for ID: {ids} from Spotify API.") logger.error(f"Failed to get track details for ID: {ids} from Spotify API.")
return None return None
# Perform market availability check for the track
track_name_for_check = json_track.get('name', f'Track ID {ids}')
api_track_markets = json_track.get('available_markets')
_check_market_availability(track_name_for_check, "Track", api_track_markets, market)
# Album details section # Album details section
# Use provided album_data_for_track if available (from tracking_album context) # Use provided album_data_for_track if available (from tracking_album context)
# Otherwise, fetch from track's album info or make a new API call for more details # Otherwise, fetch from track's album info or make a new API call for more details
@@ -129,6 +151,8 @@ def tracking(ids, album_data_for_track=None):
datas['ids'] = ids datas['ids'] = ids
logger.debug(f"Successfully tracked metadata for track {ids}") logger.debug(f"Successfully tracked metadata for track {ids}")
except MarketAvailabilityError: # Re-raise to be caught by the calling download method
raise
except Exception as e: except Exception as e:
logger.error(f"Failed to track metadata for track {ids}: {str(e)}") logger.error(f"Failed to track metadata for track {ids}: {str(e)}")
logger.debug(traceback.format_exc()) logger.debug(traceback.format_exc())
@@ -136,13 +160,18 @@ def tracking(ids, album_data_for_track=None):
return datas return datas
def tracking_album(album_json): def tracking_album(album_json, market: list[str] | None = None):
if not album_json: if not album_json:
logger.error("tracking_album received None or empty album_json.") logger.error("tracking_album received None or empty album_json.")
return None return None
song_metadata = {} song_metadata = {}
try: try:
# Perform market availability check for the album itself
album_name_for_check = album_json.get('name', f"Album ID {album_json.get('id', 'Unknown')}")
api_album_markets = album_json.get('available_markets')
_check_market_availability(album_name_for_check, "Album", api_album_markets, market)
initial_list_fields = { initial_list_fields = {
"music": [], "artist": [], "tracknum": [], "discnum": [], "music": [], "artist": [], "tracknum": [], "discnum": [],
"duration": [], "isrc": [], "ids": [], "explicit_list": [], "popularity_list": [] "duration": [], "isrc": [], "ids": [], "explicit_list": [], "popularity_list": []
@@ -201,7 +230,8 @@ def tracking_album(album_json):
continue continue
# Pass the main album_json as album_data_for_track to avoid refetching it in tracking() # Pass the main album_json as album_data_for_track to avoid refetching it in tracking()
track_details = tracking(c_ids, album_data_for_track=album_json) # Also pass the market parameter
track_details = tracking(c_ids, album_data_for_track=album_json, market=market)
if track_details: if track_details:
song_metadata['music'].append(track_details.get('music', 'Unknown Track')) song_metadata['music'].append(track_details.get('music', 'Unknown Track'))
@@ -234,6 +264,8 @@ def tracking_album(album_json):
logger.debug(f"Successfully tracked metadata for album {album_json.get('id', 'N/A')}") logger.debug(f"Successfully tracked metadata for album {album_json.get('id', 'N/A')}")
except MarketAvailabilityError: # Re-raise
raise
except Exception as e: except Exception as e:
logger.error(f"Failed to track album metadata for album ID {album_json.get('id', 'N/A') if album_json else 'N/A'}: {str(e)}") logger.error(f"Failed to track album metadata for album ID {album_json.get('id', 'N/A') if album_json else 'N/A'}: {str(e)}")
logger.debug(traceback.format_exc()) logger.debug(traceback.format_exc())
@@ -241,7 +273,7 @@ def tracking_album(album_json):
return song_metadata return song_metadata
def tracking_episode(ids): def tracking_episode(ids, market: list[str] | None = None):
datas = {} datas = {}
try: try:
json_episode = Spo.get_episode(ids) json_episode = Spo.get_episode(ids)
@@ -249,6 +281,11 @@ def tracking_episode(ids):
logger.error(f"Failed to get episode details for ID: {ids} from Spotify API.") logger.error(f"Failed to get episode details for ID: {ids} from Spotify API.")
return None return None
# Perform market availability check for the episode
episode_name_for_check = json_episode.get('name', f'Episode ID {ids}')
api_episode_markets = json_episode.get('available_markets')
_check_market_availability(episode_name_for_check, "Episode", api_episode_markets, market)
image_urls = _get_best_image_urls(json_episode.get('images', [])) image_urls = _get_best_image_urls(json_episode.get('images', []))
datas.update(image_urls) datas.update(image_urls)
@@ -305,6 +342,8 @@ def tracking_episode(ids):
logger.debug(f"Successfully tracked metadata for episode {ids}") logger.debug(f"Successfully tracked metadata for episode {ids}")
except MarketAvailabilityError: # Re-raise
raise
except Exception as e: except Exception as e:
logger.error(f"Failed to track episode metadata for ID {ids}: {str(e)}") logger.error(f"Failed to track episode metadata for ID {ids}: {str(e)}")
logger.debug(traceback.format_exc()) logger.debug(traceback.format_exc())

View File

@@ -24,3 +24,5 @@ qualities = {
"s_quality": "NORMAL" "s_quality": "NORMAL"
} }
} }
stock_market = None