added market logic
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
artist_parts = [main_artist]
|
if not is_current_track_error:
|
||||||
if featuring:
|
# Populate current_track_constructed_metadata from self.__song_metadata lists
|
||||||
artist_parts.append(f"(feat. {featuring})")
|
for key, value_list_template in self.__song_metadata_items: # self.__song_metadata_items is items() of dict-of-lists
|
||||||
artist_str = " ".join(artist_parts)
|
if isinstance(value_list_template, list): # e.g. self.__song_metadata['artist']
|
||||||
composer_str = "; ".join(contributors.get('composer', []))
|
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]
|
||||||
|
|
||||||
# Build the core track metadata.
|
# Ensure essential fields from c_infos_dw_item are preferred or added if missing from API.tracking_album results
|
||||||
# When there is no contributor info, we intentionally leave out the 'artist'
|
current_track_constructed_metadata['music'] = current_track_constructed_metadata.get('music') or c_infos_dw_item.get('SNG_TITLE', 'Unknown')
|
||||||
# and 'composer' keys so that the album-level metadata merge will supply them.
|
# artist might be complex due to contributors, rely on what API.tracking_album prepared
|
||||||
c_song_metadata = {
|
# current_track_constructed_metadata['artist'] = current_track_constructed_metadata.get('artist') # Already populated or None
|
||||||
'music': c_infos_dw.get('SNG_TITLE', 'Unknown'),
|
current_track_constructed_metadata['tracknum'] = current_track_constructed_metadata.get('tracknum') or f"{track_number}"
|
||||||
'album': self.__song_metadata['album'],
|
current_track_constructed_metadata['discnum'] = current_track_constructed_metadata.get('discnum') or f"{c_infos_dw_item.get('DISK_NUMBER', 1)}"
|
||||||
'date': c_infos_dw.get('DIGITAL_RELEASE_DATE', ''),
|
current_track_constructed_metadata['isrc'] = current_track_constructed_metadata.get('isrc') or c_infos_dw_item.get('ISRC', '')
|
||||||
'genre': self.__song_metadata.get('genre', 'Latin Music'),
|
current_track_constructed_metadata['duration'] = current_track_constructed_metadata.get('duration') or int(c_infos_dw_item.get('DURATION', 0))
|
||||||
'tracknum': f"{track_number}",
|
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')
|
||||||
'discnum': f"{c_infos_dw.get('DISK_NUMBER', 1)}",
|
current_track_constructed_metadata['album_artist'] = current_track_constructed_metadata.get('album_artist') or derived_album_artist_from_contributors
|
||||||
'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 is_current_track_error:
|
||||||
if has_contributors:
|
error_type = potential_error_marker.get('error_type', 'UnknownError')
|
||||||
c_song_metadata['artist'] = artist_str
|
error_message = potential_error_marker.get('message', 'An unknown error occurred.')
|
||||||
c_song_metadata['composer'] = composer_str
|
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'))
|
||||||
|
|
||||||
# No progress reporting here - done at the track level
|
# 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
|
||||||
|
|
||||||
# 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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
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)
|
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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
for key, item in sm_items:
|
current_track_metadata_or_error = None
|
||||||
if type(item) is list:
|
track_failed = False
|
||||||
# Ensure ISRC is appended for each track
|
|
||||||
if key == 'isrc':
|
try:
|
||||||
song_metadata[key].append(detas.get('isrc', ''))
|
# Get detailed metadata for the current track
|
||||||
|
current_track_metadata_or_error = cls.tracking(c_ids, album=True, market=market)
|
||||||
|
except MarketAvailabilityError as e:
|
||||||
|
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
|
||||||
|
|||||||
@@ -75,3 +75,8 @@ class BadCredentials(Exception):
|
|||||||
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)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,12 +99,16 @@ 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')}")
|
||||||
|
|
||||||
@@ -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,14 +166,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
|
||||||
) -> 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')}")
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -24,3 +24,5 @@ qualities = {
|
|||||||
"s_quality": "NORMAL"
|
"s_quality": "NORMAL"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stock_market = None
|
||||||
|
|||||||
Reference in New Issue
Block a user