added market logic
This commit is contained in:
@@ -983,6 +983,7 @@ class DW_ALBUM:
|
||||
# Report album initializing status
|
||||
album_name_for_report = self.__song_metadata.get('album', 'Unknown Album')
|
||||
total_tracks_for_report = self.__song_metadata.get('nb_tracks', 0)
|
||||
album_link_for_report = self.__preferences.link # Get album link from preferences
|
||||
|
||||
Download_JOB.report_progress({
|
||||
"type": "album",
|
||||
@@ -990,7 +991,7 @@ class DW_ALBUM:
|
||||
"status": "initializing",
|
||||
"total_tracks": total_tracks_for_report,
|
||||
"title": album_name_for_report,
|
||||
"url": f"https://deezer.com/album/{self.__ids}"
|
||||
"url": album_link_for_report # Use the actual album link
|
||||
})
|
||||
|
||||
infos_dw = API_GW.get_album_data(self.__ids)['data']
|
||||
@@ -1024,67 +1025,99 @@ class DW_ALBUM:
|
||||
total_tracks = len(infos_dw)
|
||||
for a in range(total_tracks):
|
||||
track_number = a + 1
|
||||
c_infos_dw = infos_dw[a]
|
||||
# c_infos_dw is from API_GW.get_album_data, used for SNG_ID, SNG_TITLE etc.
|
||||
c_infos_dw_item = infos_dw[a]
|
||||
|
||||
# Retrieve the contributors info from the API response.
|
||||
# It might be an empty list.
|
||||
contributors = c_infos_dw.get('SNG_CONTRIBUTORS', {})
|
||||
# self.__song_metadata is the dict-of-lists from API.tracking_album
|
||||
# We need to construct c_song_metadata_for_easydw for the current track 'a'
|
||||
# by picking the ath element from each list in self.__song_metadata.
|
||||
|
||||
# Check if contributors is an empty list.
|
||||
if isinstance(contributors, list) and not contributors:
|
||||
# Flag indicating we do NOT have contributors data to process.
|
||||
has_contributors = False
|
||||
current_track_constructed_metadata = {}
|
||||
potential_error_marker = None
|
||||
is_current_track_error = False
|
||||
|
||||
# Check the 'music' field first for an error dict from API.tracking_album
|
||||
if 'music' in self.__song_metadata and isinstance(self.__song_metadata['music'], list) and len(self.__song_metadata['music']) > a:
|
||||
music_field_value = self.__song_metadata['music'][a]
|
||||
if isinstance(music_field_value, dict) and 'error_type' in music_field_value:
|
||||
is_current_track_error = True
|
||||
potential_error_marker = music_field_value
|
||||
# The error marker dict itself will serve as the metadata for the failed track object
|
||||
current_track_constructed_metadata = potential_error_marker
|
||||
|
||||
if not is_current_track_error:
|
||||
# Populate current_track_constructed_metadata from self.__song_metadata lists
|
||||
for key, value_list_template in self.__song_metadata_items: # self.__song_metadata_items is items() of dict-of-lists
|
||||
if isinstance(value_list_template, list): # e.g. self.__song_metadata['artist']
|
||||
if len(self.__song_metadata[key]) > a:
|
||||
current_track_constructed_metadata[key] = self.__song_metadata[key][a]
|
||||
else:
|
||||
has_contributors = True
|
||||
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]
|
||||
|
||||
# If we have contributor data, build the artist and composer strings.
|
||||
if has_contributors:
|
||||
main_artist = "; ".join(contributors.get('main_artist', []))
|
||||
featuring = "; ".join(contributors.get('featuring', []))
|
||||
# Ensure essential fields from c_infos_dw_item are preferred or added if missing from API.tracking_album results
|
||||
current_track_constructed_metadata['music'] = current_track_constructed_metadata.get('music') or c_infos_dw_item.get('SNG_TITLE', 'Unknown')
|
||||
# artist might be complex due to contributors, rely on what API.tracking_album prepared
|
||||
# current_track_constructed_metadata['artist'] = current_track_constructed_metadata.get('artist') # Already populated or None
|
||||
current_track_constructed_metadata['tracknum'] = current_track_constructed_metadata.get('tracknum') or f"{track_number}"
|
||||
current_track_constructed_metadata['discnum'] = current_track_constructed_metadata.get('discnum') or f"{c_infos_dw_item.get('DISK_NUMBER', 1)}"
|
||||
current_track_constructed_metadata['isrc'] = current_track_constructed_metadata.get('isrc') or c_infos_dw_item.get('ISRC', '')
|
||||
current_track_constructed_metadata['duration'] = current_track_constructed_metadata.get('duration') or int(c_infos_dw_item.get('DURATION', 0))
|
||||
current_track_constructed_metadata['explicit'] = current_track_constructed_metadata.get('explicit') or ('1' if c_infos_dw_item.get('EXPLICIT_LYRICS', '0') == '1' else '0')
|
||||
current_track_constructed_metadata['album_artist'] = current_track_constructed_metadata.get('album_artist') or derived_album_artist_from_contributors
|
||||
|
||||
artist_parts = [main_artist]
|
||||
if featuring:
|
||||
artist_parts.append(f"(feat. {featuring})")
|
||||
artist_str = " ".join(artist_parts)
|
||||
composer_str = "; ".join(contributors.get('composer', []))
|
||||
if is_current_track_error:
|
||||
error_type = potential_error_marker.get('error_type', 'UnknownError')
|
||||
error_message = potential_error_marker.get('message', 'An unknown error occurred.')
|
||||
track_name_for_log = potential_error_marker.get('name', c_infos_dw_item.get('SNG_TITLE', f'Track {track_number}'))
|
||||
track_id_for_log = potential_error_marker.get('ids', c_infos_dw_item.get('SNG_ID'))
|
||||
|
||||
# Build the core track metadata.
|
||||
# When there is no contributor info, we intentionally leave out the 'artist'
|
||||
# and 'composer' keys so that the album-level metadata merge will supply them.
|
||||
c_song_metadata = {
|
||||
'music': c_infos_dw.get('SNG_TITLE', 'Unknown'),
|
||||
'album': self.__song_metadata['album'],
|
||||
'date': c_infos_dw.get('DIGITAL_RELEASE_DATE', ''),
|
||||
'genre': self.__song_metadata.get('genre', 'Latin Music'),
|
||||
'tracknum': f"{track_number}",
|
||||
'discnum': f"{c_infos_dw.get('DISK_NUMBER', 1)}",
|
||||
'isrc': c_infos_dw.get('ISRC', ''),
|
||||
'album_artist': derived_album_artist_from_contributors,
|
||||
'publisher': 'CanZion R',
|
||||
'duration': int(c_infos_dw.get('DURATION', 0)),
|
||||
'explicit': '1' if c_infos_dw.get('EXPLICIT_LYRICS', '0') == '1' else '0'
|
||||
}
|
||||
# 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 ""
|
||||
|
||||
# Only add contributor-based metadata if available.
|
||||
if has_contributors:
|
||||
c_song_metadata['artist'] = artist_str
|
||||
c_song_metadata['composer'] = composer_str
|
||||
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}")
|
||||
|
||||
# No progress reporting here - done at the track level
|
||||
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)
|
||||
for key, item in self.__song_metadata_items:
|
||||
if key not in c_song_metadata:
|
||||
if isinstance(item, list):
|
||||
c_song_metadata[key] = self.__song_metadata[key][a] if len(self.__song_metadata[key]) > a else 'Unknown'
|
||||
else:
|
||||
c_song_metadata[key] = self.__song_metadata[key]
|
||||
# This was the old logic, current_track_constructed_metadata should be fairly complete now or an error dict.
|
||||
# for key, item in self.__song_metadata_items:
|
||||
# if key not in current_track_constructed_metadata:
|
||||
# if isinstance(item, list):
|
||||
# current_track_constructed_metadata[key] = self.__song_metadata[key][a] if len(self.__song_metadata[key]) > a else 'Unknown'
|
||||
# else:
|
||||
# current_track_constructed_metadata[key] = self.__song_metadata[key]
|
||||
|
||||
# Continue with the rest of your processing (media handling, download, etc.)
|
||||
c_infos_dw['media_url'] = medias[a]
|
||||
c_infos_dw_item['media_url'] = medias[a] # medias is from Download_JOB.check_sources(infos_dw, ...)
|
||||
c_preferences = deepcopy(self.__preferences)
|
||||
c_preferences.song_metadata = c_song_metadata.copy()
|
||||
c_preferences.ids = c_infos_dw['SNG_ID']
|
||||
c_preferences.song_metadata = current_track_constructed_metadata.copy()
|
||||
c_preferences.ids = c_infos_dw_item['SNG_ID']
|
||||
c_preferences.track_number = track_number
|
||||
|
||||
# Add additional information for consistent parent info
|
||||
@@ -1093,22 +1126,41 @@ class DW_ALBUM:
|
||||
c_preferences.total_tracks = total_tracks
|
||||
c_preferences.link = f"https://deezer.com/track/{c_preferences.ids}"
|
||||
|
||||
current_track_object = None
|
||||
try:
|
||||
track = EASY_DW(c_infos_dw, c_preferences, parent='album').download_try()
|
||||
except TrackNotFound:
|
||||
try:
|
||||
song = f"{c_song_metadata['music']} - {c_song_metadata.get('artist', self.__song_metadata['artist'])}"
|
||||
ids = API.not_found(song, c_song_metadata['music'])
|
||||
c_infos_dw = API_GW.get_song_data(ids)
|
||||
c_media = Download_JOB.check_sources([c_infos_dw], self.__quality_download)
|
||||
c_infos_dw['media_url'] = c_media[0]
|
||||
track = EASY_DW(c_infos_dw, c_preferences, parent='album').download_try()
|
||||
except TrackNotFound:
|
||||
track = Track(c_song_metadata, None, None, None, None, None)
|
||||
track.success = False
|
||||
track.error_message = f"Track not found after fallback attempt for: {song}"
|
||||
logger.warning(f"Track not found: {song} :( Details: {track.error_message}. URL: {c_preferences.link if c_preferences else 'N/A'}")
|
||||
tracks.append(track)
|
||||
# This is where EASY_DW().easy_dw() or EASY_DW().download_try() is effectively called
|
||||
current_track_object = EASY_DW(c_infos_dw_item, c_preferences, parent='album').easy_dw()
|
||||
|
||||
except TrackNotFound as e_tnf:
|
||||
logger.error(f"Track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' by '{c_preferences.song_metadata.get('artist', 'Unknown Artist')}' (Album: {album.album_name}) failed: {str(e_tnf)}")
|
||||
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
|
||||
current_track_object.success = False
|
||||
current_track_object.error_message = str(e_tnf)
|
||||
except QualityNotFound as e_qnf:
|
||||
logger.error(f"Quality issue for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Album: {album.album_name}): {str(e_qnf)}")
|
||||
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
|
||||
current_track_object.success = False
|
||||
current_track_object.error_message = str(e_qnf)
|
||||
except requests.exceptions.ConnectionError as e_conn:
|
||||
logger.error(f"Connection error for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Album: {album.album_name}): {str(e_conn)}")
|
||||
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
|
||||
current_track_object.success = False
|
||||
current_track_object.error_message = str(e_conn) # Store specific connection error
|
||||
except Exception as e_general: # Catch any other unexpected error during this track's processing
|
||||
logger.error(f"Unexpected error for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Album: {album.album_name}): {str(e_general)}")
|
||||
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
|
||||
current_track_object.success = False
|
||||
current_track_object.error_message = str(e_general)
|
||||
|
||||
if current_track_object:
|
||||
tracks.append(current_track_object)
|
||||
else: # Should not happen if exceptions are caught, but as a fallback
|
||||
logger.error(f"Track object was not created for SNG_ID {c_infos_dw_item['SNG_ID']} in album {album.album_name}. Skipping.")
|
||||
# Create a generic failed track to ensure list length matches expectation if needed elsewhere
|
||||
failed_placeholder = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
|
||||
failed_placeholder.success = False
|
||||
failed_placeholder.error_message = "Track processing failed to produce a result object."
|
||||
tracks.append(failed_placeholder)
|
||||
|
||||
# Save album cover image
|
||||
if self.__preferences.save_cover and album.image and album_base_directory:
|
||||
@@ -1137,7 +1189,7 @@ class DW_ALBUM:
|
||||
"status": "done",
|
||||
"total_tracks": total_tracks,
|
||||
"title": album_name,
|
||||
"url": f"https://deezer.com/album/{self.__ids}"
|
||||
"url": album_link_for_report # Use the actual album link
|
||||
})
|
||||
|
||||
return album
|
||||
@@ -1198,37 +1250,117 @@ class DW_PLAYLIST:
|
||||
)
|
||||
|
||||
# Process each track
|
||||
for idx, (c_infos_dw, c_media, c_song_metadata) in enumerate(zip(infos_dw, medias, self.__song_metadata), 1):
|
||||
for idx, (c_infos_dw_item, c_media, c_song_metadata_item) in enumerate(zip(infos_dw, medias, self.__song_metadata), 1):
|
||||
|
||||
# Skip if song metadata is not valid
|
||||
if type(c_song_metadata) is str:
|
||||
# Skip if song metadata indicates an error (e.g., from market availability or NoDataApi)
|
||||
if isinstance(c_song_metadata_item, dict) and 'error_type' in c_song_metadata_item:
|
||||
track_name = c_song_metadata_item.get('name', 'Unknown Track')
|
||||
track_ids = c_song_metadata_item.get('ids')
|
||||
error_message = c_song_metadata_item.get('message', 'Unknown error.')
|
||||
error_type = c_song_metadata_item.get('error_type', 'UnknownError')
|
||||
# market_info = f" (Market: {self.__preferences.market})" if self.__preferences.market and error_type == 'MarketAvailabilityError' else ""
|
||||
# Construct market_info string based on actual checked_markets from error dict or preferences fallback
|
||||
checked_markets_str = ""
|
||||
if error_type == 'MarketAvailabilityError':
|
||||
# Prefer checked_markets from the error dict if available
|
||||
if 'checked_markets' in c_song_metadata_item and c_song_metadata_item['checked_markets']:
|
||||
checked_markets_str = c_song_metadata_item['checked_markets']
|
||||
# Fallback to preferences.market if not in error dict (though it should be)
|
||||
elif self.__preferences.market:
|
||||
if isinstance(self.__preferences.market, list):
|
||||
checked_markets_str = ", ".join([m.upper() for m in self.__preferences.market])
|
||||
elif isinstance(self.__preferences.market, str):
|
||||
checked_markets_str = self.__preferences.market.upper()
|
||||
market_log_info = f" (Market(s): {checked_markets_str})" if checked_markets_str else ""
|
||||
|
||||
logger.warning(f"Skipping download for track '{track_name}' (ID: {track_ids}) from playlist '{playlist_name_sanitized}' due to {error_type}{market_log_info}: {error_message}")
|
||||
|
||||
failed_track_link = f"https://deezer.com/track/{track_ids}" if track_ids else self.__preferences.link # Fallback to playlist link
|
||||
|
||||
# c_song_metadata_item is the error dict, use it as tags for the Track object
|
||||
track = Track(
|
||||
tags=c_song_metadata_item,
|
||||
song_path=None, file_format=None, quality=None,
|
||||
link=failed_track_link,
|
||||
ids=track_ids
|
||||
)
|
||||
track.success = False
|
||||
track.error_message = error_message
|
||||
tracks.append(track)
|
||||
# Optionally, report progress for this failed track within the playlist context here
|
||||
continue # Move to the next track in the playlist
|
||||
|
||||
# Original check for string type, should be less common if API returns dicts for errors
|
||||
if type(c_song_metadata_item) is str:
|
||||
logger.warning(f"Track metadata is a string for a track in playlist '{playlist_name_sanitized}': '{c_song_metadata_item}'. Skipping.")
|
||||
# Create a basic failed track object if metadata is just a string error
|
||||
# This is a fallback, ideally c_song_metadata_item would be an error dict
|
||||
error_placeholder_tags = {'name': 'Unknown Track (metadata error)', 'artist': 'Unknown Artist', 'error_type': 'StringError', 'message': c_song_metadata_item}
|
||||
track = Track(
|
||||
tags=error_placeholder_tags,
|
||||
song_path=None, file_format=None, quality=None,
|
||||
link=self.__preferences.link, # Playlist link
|
||||
ids=None # No specific ID available from string error
|
||||
)
|
||||
track.success = False
|
||||
track.error_message = c_song_metadata_item
|
||||
tracks.append(track)
|
||||
continue
|
||||
|
||||
c_infos_dw['media_url'] = c_media
|
||||
c_infos_dw_item['media_url'] = c_media
|
||||
c_preferences = deepcopy(self.__preferences)
|
||||
c_preferences.ids = c_infos_dw['SNG_ID']
|
||||
c_preferences.song_metadata = c_song_metadata
|
||||
c_preferences.ids = c_infos_dw_item['SNG_ID']
|
||||
c_preferences.song_metadata = c_song_metadata_item # This is the full metadata dict for a successful track
|
||||
c_preferences.track_number = idx
|
||||
c_preferences.total_tracks = total_tracks
|
||||
|
||||
# Download the track using the EASY_DW downloader
|
||||
track = EASY_DW(c_infos_dw, c_preferences, parent='playlist').easy_dw()
|
||||
# Wrap this in a try-except block to handle individual track failures
|
||||
current_track_object = None
|
||||
try:
|
||||
current_track_object = EASY_DW(c_infos_dw_item, c_preferences, parent='playlist').easy_dw()
|
||||
except TrackNotFound as e_tnf:
|
||||
logger.error(f"Track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' by '{c_preferences.song_metadata.get('artist', 'Unknown Artist')}' (Playlist: {playlist_name_sanitized}) failed: {str(e_tnf)}")
|
||||
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
|
||||
current_track_object.success = False
|
||||
current_track_object.error_message = str(e_tnf)
|
||||
except QualityNotFound as e_qnf:
|
||||
logger.error(f"Quality issue for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Playlist: {playlist_name_sanitized}): {str(e_qnf)}")
|
||||
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
|
||||
current_track_object.success = False
|
||||
current_track_object.error_message = str(e_qnf)
|
||||
except requests.exceptions.ConnectionError as e_conn: # Catch connection errors specifically
|
||||
logger.error(f"Connection error for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Playlist: {playlist_name_sanitized}): {str(e_conn)}")
|
||||
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
|
||||
current_track_object.success = False
|
||||
current_track_object.error_message = str(e_conn) # Store specific connection error
|
||||
except Exception as e_general: # Catch any other unexpected error during this track's processing
|
||||
logger.error(f"Unexpected error for track '{c_preferences.song_metadata.get('music', 'Unknown Track')}' (Playlist: {playlist_name_sanitized}): {str(e_general)}")
|
||||
current_track_object = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
|
||||
current_track_object.success = False
|
||||
current_track_object.error_message = str(e_general)
|
||||
|
||||
# Track-level progress reporting is handled in EASY_DW
|
||||
|
||||
if current_track_object: # Ensure a track object was created
|
||||
tracks.append(current_track_object)
|
||||
# Only log a warning if the track failed and was NOT intentionally skipped
|
||||
if not track.success and not getattr(track, 'was_skipped', False):
|
||||
song = f"{c_song_metadata['music']} - {c_song_metadata['artist']}"
|
||||
error_detail = getattr(track, 'error_message', 'Download failed for unspecified reason.')
|
||||
logger.warning(f"Cannot download '{song}'. Reason: {error_detail} (Link: {track.link or c_preferences.link})")
|
||||
|
||||
tracks.append(track)
|
||||
if not current_track_object.success and not getattr(current_track_object, 'was_skipped', False):
|
||||
# The error logging is now done within the except blocks above, more specifically.
|
||||
pass # logger.warning(f"Cannot download '{song}'. Reason: {error_detail} (Link: {track.link or c_preferences.link})")
|
||||
else:
|
||||
# This case should ideally not be reached if exceptions are handled correctly.
|
||||
logger.error(f"Track object was not created for SNG_ID {c_infos_dw_item['SNG_ID']} in playlist {playlist_name_sanitized}. Skipping.")
|
||||
# Create a generic failed track to ensure list length matches expectation if needed elsewhere
|
||||
failed_placeholder = Track(c_preferences.song_metadata, None, None, None, c_preferences.link, c_preferences.ids)
|
||||
failed_placeholder.success = False
|
||||
failed_placeholder.error_message = "Track processing failed to produce a result object."
|
||||
tracks.append(failed_placeholder)
|
||||
|
||||
# --- Append the final track path to the m3u file ---
|
||||
# Build a relative path from the playlists directory
|
||||
if track.success and hasattr(track, 'song_path') and track.song_path:
|
||||
if current_track_object and current_track_object.success and hasattr(current_track_object, 'song_path') and current_track_object.song_path:
|
||||
relative_song_path = os.path.relpath(
|
||||
track.song_path,
|
||||
current_track_object.song_path,
|
||||
start=os.path.join(self.__output_dir, "playlists")
|
||||
)
|
||||
with open(m3u_path, "a", encoding="utf-8") as m3u_file:
|
||||
|
||||
@@ -26,12 +26,13 @@ from deezspot.exceptions import (
|
||||
TrackNotFound,
|
||||
NoDataApi,
|
||||
AlbumNotFound,
|
||||
MarketAvailabilityError,
|
||||
)
|
||||
from deezspot.libutils.utils import (
|
||||
create_zip,
|
||||
get_ids,
|
||||
link_is_valid,
|
||||
what_kind,
|
||||
what_kind
|
||||
)
|
||||
from deezspot.libutils.others_settings import (
|
||||
stock_output,
|
||||
@@ -40,8 +41,10 @@ from deezspot.libutils.others_settings import (
|
||||
stock_not_interface,
|
||||
stock_zip,
|
||||
stock_save_cover,
|
||||
stock_market
|
||||
)
|
||||
from deezspot.libutils.logging_utils import ProgressReporter, logger
|
||||
import requests
|
||||
|
||||
API()
|
||||
|
||||
@@ -105,14 +108,24 @@ class DeeLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market=stock_market
|
||||
) -> Track:
|
||||
|
||||
link_is_valid(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:
|
||||
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:
|
||||
infos = self.__gw_api.get_song_data(ids)
|
||||
|
||||
@@ -120,7 +133,13 @@ class DeeLogin:
|
||||
raise TrackNotFound(link_track)
|
||||
|
||||
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.link = link_track
|
||||
@@ -131,19 +150,16 @@ class DeeLogin:
|
||||
preferences.recursive_quality = recursive_quality
|
||||
preferences.recursive_download = recursive_download
|
||||
preferences.not_interface = not_interface
|
||||
# New custom formatting preferences:
|
||||
preferences.custom_dir_format = custom_dir_format
|
||||
preferences.custom_track_format = custom_track_format
|
||||
# Track number padding option
|
||||
preferences.pad_tracks = pad_tracks
|
||||
# Retry parameters
|
||||
preferences.initial_retry_delay = initial_retry_delay
|
||||
preferences.retry_delay_increase = retry_delay_increase
|
||||
preferences.max_retries = max_retries
|
||||
# Audio conversion parameter
|
||||
preferences.convert_to = convert_to
|
||||
preferences.bitrate = bitrate
|
||||
preferences.save_cover = save_cover
|
||||
preferences.market = market
|
||||
|
||||
track = DW_TRACK(preferences).dw()
|
||||
|
||||
@@ -165,18 +181,25 @@ class DeeLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market=stock_market
|
||||
) -> Album:
|
||||
|
||||
link_is_valid(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:
|
||||
album_json = API.get_album(ids)
|
||||
except NoDataApi:
|
||||
raise AlbumNotFound(link_album)
|
||||
|
||||
song_metadata = API.tracking_album(album_json)
|
||||
song_metadata = API.tracking_album(album_json, market=market)
|
||||
|
||||
preferences = Preferences()
|
||||
preferences.link = link_album
|
||||
@@ -189,19 +212,16 @@ class DeeLogin:
|
||||
preferences.recursive_download = recursive_download
|
||||
preferences.not_interface = not_interface
|
||||
preferences.make_zip = make_zip
|
||||
# New custom formatting preferences:
|
||||
preferences.custom_dir_format = custom_dir_format
|
||||
preferences.custom_track_format = custom_track_format
|
||||
# Track number padding option
|
||||
preferences.pad_tracks = pad_tracks
|
||||
# Retry parameters
|
||||
preferences.initial_retry_delay = initial_retry_delay
|
||||
preferences.retry_delay_increase = retry_delay_increase
|
||||
preferences.max_retries = max_retries
|
||||
# Audio conversion parameter
|
||||
preferences.convert_to = convert_to
|
||||
preferences.bitrate = bitrate
|
||||
preferences.save_cover = save_cover
|
||||
preferences.market = market
|
||||
|
||||
album = DW_ALBUM(preferences).dw()
|
||||
|
||||
@@ -223,7 +243,8 @@ class DeeLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market=stock_market
|
||||
) -> Playlist:
|
||||
|
||||
link_is_valid(link_playlist)
|
||||
@@ -231,20 +252,93 @@ class DeeLogin:
|
||||
|
||||
song_metadata = []
|
||||
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']:
|
||||
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:
|
||||
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:
|
||||
infos = self.__gw_api.get_song_data(c_ids)
|
||||
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:
|
||||
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.link = link_playlist
|
||||
@@ -257,19 +351,16 @@ class DeeLogin:
|
||||
preferences.recursive_download = recursive_download
|
||||
preferences.not_interface = not_interface
|
||||
preferences.make_zip = make_zip
|
||||
# New custom formatting preferences:
|
||||
preferences.custom_dir_format = custom_dir_format
|
||||
preferences.custom_track_format = custom_track_format
|
||||
# Track number padding option
|
||||
preferences.pad_tracks = pad_tracks
|
||||
# Retry parameters
|
||||
preferences.initial_retry_delay = initial_retry_delay
|
||||
preferences.retry_delay_increase = retry_delay_increase
|
||||
preferences.max_retries = max_retries
|
||||
# Audio conversion parameter
|
||||
preferences.convert_to = convert_to
|
||||
preferences.bitrate = bitrate
|
||||
preferences.save_cover = save_cover
|
||||
preferences.market = market
|
||||
|
||||
playlist = DW_PLAYLIST(preferences).dw()
|
||||
|
||||
@@ -287,7 +378,8 @@ class DeeLogin:
|
||||
pad_tracks=True,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market=stock_market
|
||||
) -> list[Track]:
|
||||
|
||||
link_is_valid(link_artist)
|
||||
@@ -305,7 +397,8 @@ class DeeLogin:
|
||||
pad_tracks=pad_tracks,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate,
|
||||
save_cover=save_cover
|
||||
save_cover=save_cover,
|
||||
market=market
|
||||
)
|
||||
for track in playlist_json
|
||||
]
|
||||
@@ -348,7 +441,8 @@ class DeeLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market=stock_market
|
||||
) -> Track:
|
||||
|
||||
track_link_dee = self.convert_spoty_to_dee_link_track(link_track)
|
||||
@@ -368,7 +462,8 @@ class DeeLogin:
|
||||
max_retries=max_retries,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate,
|
||||
save_cover=save_cover
|
||||
save_cover=save_cover,
|
||||
market=market
|
||||
)
|
||||
|
||||
return track
|
||||
@@ -468,7 +563,8 @@ class DeeLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market=stock_market
|
||||
) -> Album:
|
||||
|
||||
link_dee = self.convert_spoty_to_dee_link_album(link_album)
|
||||
@@ -486,7 +582,8 @@ class DeeLogin:
|
||||
max_retries=max_retries,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate,
|
||||
save_cover=save_cover
|
||||
save_cover=save_cover,
|
||||
market=market
|
||||
)
|
||||
|
||||
return album
|
||||
@@ -507,7 +604,8 @@ class DeeLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market=stock_market
|
||||
) -> Playlist:
|
||||
|
||||
link_is_valid(link_playlist)
|
||||
@@ -585,7 +683,8 @@ class DeeLogin:
|
||||
max_retries=max_retries,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate,
|
||||
save_cover=save_cover
|
||||
save_cover=save_cover,
|
||||
market=market
|
||||
)
|
||||
tracks.append(downloaded_track)
|
||||
except (TrackNotFound, NoDataApi) as e:
|
||||
@@ -641,7 +740,8 @@ class DeeLogin:
|
||||
pad_tracks=True,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market=stock_market
|
||||
) -> Track:
|
||||
|
||||
query = f"track:{song} artist:{artist}"
|
||||
@@ -675,7 +775,8 @@ class DeeLogin:
|
||||
max_retries=max_retries,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate,
|
||||
save_cover=save_cover
|
||||
save_cover=save_cover,
|
||||
market=market
|
||||
)
|
||||
|
||||
return track
|
||||
@@ -696,18 +797,34 @@ class DeeLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market=stock_market
|
||||
) -> Episode:
|
||||
|
||||
link_is_valid(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:
|
||||
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:
|
||||
infos = self.__gw_api.get_episode_data(ids)
|
||||
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 = {
|
||||
'music': infos.get('EPISODE_TITLE', ''),
|
||||
'artist': infos.get('SHOW_NAME', ''),
|
||||
@@ -738,6 +855,7 @@ class DeeLogin:
|
||||
preferences.bitrate = bitrate
|
||||
preferences.save_cover = save_cover
|
||||
preferences.is_episode = True
|
||||
preferences.market = market
|
||||
|
||||
episode = DW_EPISODE(preferences).dw()
|
||||
|
||||
@@ -759,7 +877,8 @@ class DeeLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market=stock_market
|
||||
) -> Smart:
|
||||
|
||||
link_is_valid(link)
|
||||
@@ -804,7 +923,8 @@ class DeeLogin:
|
||||
max_retries=max_retries,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate,
|
||||
save_cover=save_cover
|
||||
save_cover=save_cover,
|
||||
market=market
|
||||
)
|
||||
smart.type = "track"
|
||||
smart.track = track
|
||||
@@ -833,7 +953,8 @@ class DeeLogin:
|
||||
max_retries=max_retries,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate,
|
||||
save_cover=save_cover
|
||||
save_cover=save_cover,
|
||||
market=market
|
||||
)
|
||||
smart.type = "album"
|
||||
smart.album = album
|
||||
@@ -862,7 +983,8 @@ class DeeLogin:
|
||||
max_retries=max_retries,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate,
|
||||
save_cover=save_cover
|
||||
save_cover=save_cover,
|
||||
market=market
|
||||
)
|
||||
smart.type = "playlist"
|
||||
smart.playlist = playlist
|
||||
|
||||
@@ -11,6 +11,7 @@ from deezspot.exceptions import (
|
||||
NoDataApi,
|
||||
QuotaExceeded,
|
||||
TrackNotFound,
|
||||
MarketAvailabilityError,
|
||||
)
|
||||
from deezspot.libutils.logging_utils import logger
|
||||
import requests
|
||||
@@ -222,17 +223,51 @@ class API:
|
||||
image = req_get(image_url).content
|
||||
|
||||
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 = 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
|
||||
|
||||
@classmethod
|
||||
def tracking(cls, ids, album = False) -> dict:
|
||||
def tracking(cls, ids, album = False, market = None) -> dict:
|
||||
song_metadata = {}
|
||||
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', '')
|
||||
|
||||
if not album:
|
||||
@@ -254,7 +289,6 @@ class API:
|
||||
song_metadata['ar_album'] = "; ".join(ar_album)
|
||||
song_metadata['album'] = album_json['title']
|
||||
song_metadata['label'] = album_json['label']
|
||||
# Ensure UPC is fetched from album data
|
||||
song_metadata['upc'] = album_json.get('upc', '')
|
||||
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['bpm'] = json_track['bpm']
|
||||
song_metadata['duration'] = json_track['duration']
|
||||
# song_metadata['isrc'] = json_track['isrc'] # Already handled above
|
||||
song_metadata['gain'] = json_track['gain']
|
||||
|
||||
return song_metadata
|
||||
|
||||
@classmethod
|
||||
def tracking_album(cls, album_json):
|
||||
def tracking_album(cls, album_json, market = None):
|
||||
song_metadata: dict[
|
||||
str,
|
||||
Union[list, str, int, datetime]
|
||||
@@ -292,12 +325,11 @@ class API:
|
||||
"discnum": [],
|
||||
"bpm": [],
|
||||
"duration": [],
|
||||
"isrc": [], # Ensure isrc list is present for tracks
|
||||
"isrc": [],
|
||||
"gain": [],
|
||||
"album": album_json['title'],
|
||||
"label": album_json['label'],
|
||||
"year": convert_to_date(album_json['release_date']),
|
||||
# Ensure UPC is fetched at album level
|
||||
"upc": album_json.get('upc', ''),
|
||||
"nb_tracks": album_json['nb_tracks']
|
||||
}
|
||||
@@ -318,16 +350,76 @@ class API:
|
||||
song_metadata['ar_album'] = "; ".join(ar_album)
|
||||
sm_items = song_metadata.items()
|
||||
|
||||
for track in album_json['tracks']['data']:
|
||||
c_ids = track['id']
|
||||
detas = cls.tracking(c_ids, album = True)
|
||||
for track_info_from_album_json in album_json['tracks']['data']:
|
||||
c_ids = track_info_from_album_json['id']
|
||||
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:
|
||||
if type(item) is list:
|
||||
# Ensure ISRC is appended for each track
|
||||
if key == 'isrc':
|
||||
song_metadata[key].append(detas.get('isrc', ''))
|
||||
current_track_metadata_or_error = None
|
||||
track_failed = False
|
||||
|
||||
try:
|
||||
# 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(detas[key])
|
||||
song_metadata[key].append(None)
|
||||
else:
|
||||
song_metadata[key].append(current_track_metadata_or_error.get(key))
|
||||
|
||||
return song_metadata
|
||||
|
||||
@@ -75,3 +75,8 @@ class BadCredentials(Exception):
|
||||
self.msg = f"Wrong credentials email: {self.email}, password: {self.password}"
|
||||
|
||||
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_real_time_dl = False
|
||||
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.oggvorbis import OggVorbis
|
||||
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
|
||||
|
||||
# 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
|
||||
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]
|
||||
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
|
||||
title = audio.get('TITLE', [None])[0] # Vorbis tags are case-insensitive but typically uppercase
|
||||
album = audio.get('ALBUM', [None])[0]
|
||||
|
||||
@@ -13,7 +13,11 @@ class Track:
|
||||
|
||||
self.tags = 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.file_format = file_format
|
||||
self.quality = quality
|
||||
|
||||
@@ -1204,31 +1204,123 @@ class DW_PLAYLIST:
|
||||
m3u_path = os.path.join(playlist_m3u_dir, f"{playlist_name_sanitized}.m3u")
|
||||
if not os.path.exists(m3u_path):
|
||||
with open(m3u_path, "w", encoding="utf-8") as m3u_file:
|
||||
m3u_file.write("#EXTM3U\\n")
|
||||
m3u_file.write("#EXTM3U\n")
|
||||
# -------------------------------------
|
||||
|
||||
playlist = Playlist()
|
||||
tracks = playlist.tracks
|
||||
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:
|
||||
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
|
||||
|
||||
# If c_song_metadata is a valid metadata dictionary (no 'error_type')
|
||||
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.json_data = self.__json_data # Pass playlist data for reporting
|
||||
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
|
||||
track = EASY_DW(c_preferences, parent='playlist').easy_dw()
|
||||
easy_dw_instance = EASY_DW(c_preferences, parent='playlist')
|
||||
track = None # Initialize track for this iteration
|
||||
|
||||
# Only log a warning if the track failed and was NOT intentionally skipped
|
||||
if not track.success and not getattr(track, 'was_skipped', False):
|
||||
song = f"{c_song_metadata['music']} - {c_song_metadata['artist']}"
|
||||
error_detail = getattr(track, 'error_message', 'Download failed for unspecified reason.')
|
||||
logger.warning(f"Cannot download '{song}' from playlist '{playlist_name}'. Reason: {error_detail} (URL: {track.link or c_preferences.link})")
|
||||
try:
|
||||
track = easy_dw_instance.easy_dw()
|
||||
except TrackNotFound as e_track_nf:
|
||||
track = easy_dw_instance.get_no_dw_track() # Retrieve the track instance from EASY_DW
|
||||
# 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)
|
||||
|
||||
# --- 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:
|
||||
# Build the relative path from the playlists directory
|
||||
|
||||
@@ -3,9 +3,9 @@ import traceback
|
||||
from os.path import isfile
|
||||
from deezspot.easy_spoty import Spo
|
||||
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.spotify_settings import stock_quality
|
||||
from deezspot.spotloader.spotify_settings import stock_quality, stock_market
|
||||
from deezspot.libutils.utils import (
|
||||
get_ids,
|
||||
link_is_valid,
|
||||
@@ -33,7 +33,8 @@ from deezspot.libutils.others_settings import (
|
||||
stock_not_interface,
|
||||
stock_zip,
|
||||
stock_save_cover,
|
||||
stock_real_time_dl
|
||||
stock_real_time_dl,
|
||||
stock_market
|
||||
)
|
||||
from deezspot.libutils.logging_utils import logger, ProgressReporter
|
||||
|
||||
@@ -98,12 +99,16 @@ class SpoLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market: list[str] | None = stock_market
|
||||
) -> Track:
|
||||
try:
|
||||
link_is_valid(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')}")
|
||||
|
||||
@@ -131,10 +136,14 @@ class SpoLogin:
|
||||
preferences.convert_to = convert_to
|
||||
preferences.bitrate = bitrate
|
||||
preferences.save_cover = save_cover
|
||||
preferences.market = market
|
||||
|
||||
track = DW_TRACK(preferences).dw()
|
||||
|
||||
return track
|
||||
except MarketAvailabilityError as e:
|
||||
logger.error(f"Track download failed due to market availability: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download track: {str(e)}")
|
||||
traceback.print_exc()
|
||||
@@ -157,14 +166,19 @@ class SpoLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market: list[str] | None = stock_market
|
||||
) -> Album:
|
||||
try:
|
||||
link_is_valid(link_album)
|
||||
ids = get_ids(link_album)
|
||||
# Use stored credentials for API calls
|
||||
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')}")
|
||||
|
||||
@@ -194,10 +208,14 @@ class SpoLogin:
|
||||
preferences.convert_to = convert_to
|
||||
preferences.bitrate = bitrate
|
||||
preferences.save_cover = save_cover
|
||||
preferences.market = market
|
||||
|
||||
album = DW_ALBUM(preferences).dw()
|
||||
|
||||
return album
|
||||
except MarketAvailabilityError as e:
|
||||
logger.error(f"Album download failed due to market availability: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download album: {str(e)}")
|
||||
traceback.print_exc()
|
||||
@@ -220,30 +238,95 @@ class SpoLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market: list[str] | None = stock_market
|
||||
) -> Playlist:
|
||||
try:
|
||||
link_is_valid(link_playlist)
|
||||
ids = get_ids(link_playlist)
|
||||
|
||||
song_metadata = []
|
||||
# Use stored credentials for API calls
|
||||
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')}")
|
||||
|
||||
for track in playlist_json['tracks']['items']:
|
||||
is_track = track['track']
|
||||
if not is_track:
|
||||
continue
|
||||
external_urls = is_track['external_urls']
|
||||
if not external_urls:
|
||||
c_song_metadata = f"The track \"{is_track['name']}\" is not available on Spotify :("
|
||||
logger.warning(f"Track not available: {is_track['name']}")
|
||||
else:
|
||||
ids = get_ids(external_urls['spotify'])
|
||||
c_song_metadata = tracking(ids)
|
||||
for track_item_wrapper in playlist_json['tracks']['items']:
|
||||
track_info = track_item_wrapper.get('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
|
||||
|
||||
track_name_for_logs = track_info.get('name', 'Unknown Track')
|
||||
track_id_for_logs = track_info.get('id', 'Unknown ID') # Track's own ID if available
|
||||
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:
|
||||
track_spotify_url = external_urls['spotify']
|
||||
track_ids_from_url = get_ids(track_spotify_url) # This is the ID used for fetching with 'tracking'
|
||||
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.real_time_dl = real_time_dl
|
||||
@@ -271,10 +354,14 @@ class SpoLogin:
|
||||
preferences.convert_to = convert_to
|
||||
preferences.bitrate = bitrate
|
||||
preferences.save_cover = save_cover
|
||||
preferences.market = market
|
||||
|
||||
playlist = DW_PLAYLIST(preferences).dw()
|
||||
|
||||
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:
|
||||
logger.error(f"Failed to download playlist: {str(e)}")
|
||||
traceback.print_exc()
|
||||
@@ -296,14 +383,19 @@ class SpoLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market: list[str] | None = stock_market
|
||||
) -> Episode:
|
||||
try:
|
||||
link_is_valid(link_episode)
|
||||
ids = get_ids(link_episode)
|
||||
# Use stored credentials for API calls
|
||||
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')}")
|
||||
|
||||
@@ -331,10 +423,14 @@ class SpoLogin:
|
||||
preferences.convert_to = convert_to
|
||||
preferences.bitrate = bitrate
|
||||
preferences.save_cover = save_cover
|
||||
preferences.market = market
|
||||
|
||||
episode = DW_EPISODE(preferences).dw()
|
||||
|
||||
return episode
|
||||
except MarketAvailabilityError as e:
|
||||
logger.error(f"Episode download failed due to market availability: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download episode: {str(e)}")
|
||||
traceback.print_exc()
|
||||
@@ -358,7 +454,9 @@ class SpoLogin:
|
||||
retry_delay_increase=30,
|
||||
max_retries=5,
|
||||
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.
|
||||
@@ -396,7 +494,9 @@ class SpoLogin:
|
||||
retry_delay_increase=retry_delay_increase,
|
||||
max_retries=max_retries,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate
|
||||
bitrate=bitrate,
|
||||
market=market,
|
||||
save_cover=save_cover
|
||||
)
|
||||
downloaded_albums.append(downloaded_album)
|
||||
return downloaded_albums
|
||||
@@ -422,7 +522,8 @@ class SpoLogin:
|
||||
max_retries=5,
|
||||
convert_to=None,
|
||||
bitrate=None,
|
||||
save_cover=stock_save_cover
|
||||
save_cover=stock_save_cover,
|
||||
market: list[str] | None = stock_market
|
||||
) -> Smart:
|
||||
try:
|
||||
link_is_valid(link)
|
||||
@@ -454,7 +555,8 @@ class SpoLogin:
|
||||
max_retries=max_retries,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate,
|
||||
save_cover=save_cover
|
||||
save_cover=save_cover,
|
||||
market=market
|
||||
)
|
||||
smart.type = "track"
|
||||
smart.track = track
|
||||
@@ -479,7 +581,8 @@ class SpoLogin:
|
||||
max_retries=max_retries,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate,
|
||||
save_cover=save_cover
|
||||
save_cover=save_cover,
|
||||
market=market
|
||||
)
|
||||
smart.type = "album"
|
||||
smart.album = album
|
||||
@@ -504,7 +607,8 @@ class SpoLogin:
|
||||
max_retries=max_retries,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate,
|
||||
save_cover=save_cover
|
||||
save_cover=save_cover,
|
||||
market=market
|
||||
)
|
||||
smart.type = "playlist"
|
||||
smart.playlist = playlist
|
||||
@@ -528,7 +632,8 @@ class SpoLogin:
|
||||
max_retries=max_retries,
|
||||
convert_to=convert_to,
|
||||
bitrate=bitrate,
|
||||
save_cover=save_cover
|
||||
save_cover=save_cover,
|
||||
market=market
|
||||
)
|
||||
smart.type = "episode"
|
||||
smart.episode = episode
|
||||
|
||||
@@ -5,6 +5,23 @@ from datetime import datetime
|
||||
from deezspot.libutils.utils import convert_to_date
|
||||
import traceback
|
||||
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):
|
||||
urls = {'image': '', 'image2': '', 'image3': ''}
|
||||
@@ -28,7 +45,7 @@ def _get_best_image_urls(images_list):
|
||||
|
||||
return urls
|
||||
|
||||
def tracking(ids, album_data_for_track=None):
|
||||
def tracking(ids, album_data_for_track=None, market: list[str] | None = None):
|
||||
datas = {}
|
||||
try:
|
||||
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.")
|
||||
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
|
||||
# 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
|
||||
@@ -129,6 +151,8 @@ def tracking(ids, album_data_for_track=None):
|
||||
datas['ids'] = 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:
|
||||
logger.error(f"Failed to track metadata for track {ids}: {str(e)}")
|
||||
logger.debug(traceback.format_exc())
|
||||
@@ -136,13 +160,18 @@ def tracking(ids, album_data_for_track=None):
|
||||
|
||||
return datas
|
||||
|
||||
def tracking_album(album_json):
|
||||
def tracking_album(album_json, market: list[str] | None = None):
|
||||
if not album_json:
|
||||
logger.error("tracking_album received None or empty album_json.")
|
||||
return None
|
||||
|
||||
song_metadata = {}
|
||||
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 = {
|
||||
"music": [], "artist": [], "tracknum": [], "discnum": [],
|
||||
"duration": [], "isrc": [], "ids": [], "explicit_list": [], "popularity_list": []
|
||||
@@ -201,7 +230,8 @@ def tracking_album(album_json):
|
||||
continue
|
||||
|
||||
# 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:
|
||||
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')}")
|
||||
|
||||
except MarketAvailabilityError: # Re-raise
|
||||
raise
|
||||
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.debug(traceback.format_exc())
|
||||
@@ -241,7 +273,7 @@ def tracking_album(album_json):
|
||||
|
||||
return song_metadata
|
||||
|
||||
def tracking_episode(ids):
|
||||
def tracking_episode(ids, market: list[str] | None = None):
|
||||
datas = {}
|
||||
try:
|
||||
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.")
|
||||
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', []))
|
||||
datas.update(image_urls)
|
||||
|
||||
@@ -305,6 +342,8 @@ def tracking_episode(ids):
|
||||
|
||||
logger.debug(f"Successfully tracked metadata for episode {ids}")
|
||||
|
||||
except MarketAvailabilityError: # Re-raise
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to track episode metadata for ID {ids}: {str(e)}")
|
||||
logger.debug(traceback.format_exc())
|
||||
|
||||
@@ -24,3 +24,5 @@ qualities = {
|
||||
"s_quality": "NORMAL"
|
||||
}
|
||||
}
|
||||
|
||||
stock_market = None
|
||||
|
||||
Reference in New Issue
Block a user