From 3e77876c6dbacf29d53e4a7c923a923e1714eb7d Mon Sep 17 00:00:00 2001 From: Xoconoch Date: Mon, 9 Jun 2025 12:57:52 -0600 Subject: [PATCH] implemented summary at the end of every download fixed year placeholder --- deezspot/deezloader/__download__.py | 489 +++++++++++++--------- deezspot/deezloader/__init__.py | 136 ++++-- deezspot/libutils/logging_utils.py | 146 ++++++- deezspot/libutils/utils.py | 7 + deezspot/spotloader/__download__.py | 614 +++++++++++++++------------- deezspot/spotloader/__init__.py | 58 ++- 6 files changed, 934 insertions(+), 516 deletions(-) diff --git a/deezspot/deezloader/__download__.py b/deezspot/deezloader/__download__.py index 06c0ed8..dd995ea 100644 --- a/deezspot/deezloader/__download__.py +++ b/deezspot/deezloader/__download__.py @@ -42,7 +42,7 @@ from mutagen.mp3 import MP3 from mutagen.id3 import ID3 from mutagen.mp4 import MP4 from mutagen import File -from deezspot.libutils.logging_utils import logger, ProgressReporter +from deezspot.libutils.logging_utils import logger, ProgressReporter, report_progress from deezspot.libutils.skip_detection import check_track_exists from deezspot.libutils.cleanup_utils import register_active_download, unregister_active_download from deezspot.libutils.audio_converter import AUDIO_FORMATS # Added for parse_format_string @@ -54,15 +54,6 @@ class Download_JOB: def set_progress_reporter(cls, reporter): cls.progress_reporter = reporter - @classmethod - def report_progress(cls, progress_data): - """Report progress if a reporter is configured.""" - if cls.progress_reporter: - cls.progress_reporter.report(progress_data) - else: - # Fallback to logger if no reporter is configured - logger.info(json.dumps(progress_data)) - @classmethod def __get_url(cls, c_track: Track, quality_download: str) -> dict: if c_track.get('__TYPE__') == 'episode': @@ -324,17 +315,11 @@ class EASY_DW: self.__c_track.success = True self.__c_track.was_skipped = True - progress_data = { - "type": "track", - "song": current_title, - "artist": self.__song_metadata['artist'], - "status": "skipped", - "url": self.__link, - "reason": f"Track already exists in desired format at {existing_file_path}", - "convert_to": self.__convert_to, - "bitrate": self.__bitrate - } - + parent = None + current_track = None + total_tracks = None + summary = None + # Add parent info based on parent type if self.__parent == "playlist" and hasattr(self.__preferences, "json_data"): playlist_data = self.__preferences.json_data @@ -343,17 +328,13 @@ class EASY_DW: current_track = getattr(self.__preferences, 'track_number', 0) # Format for playlist-parented tracks exactly as required - progress_data.update({ - "current_track": current_track, + parent = { + "type": "playlist", + "name": playlist_name, + "owner": playlist_data.get('creator', {}).get('name', 'unknown'), "total_tracks": total_tracks, - "parent": { - "type": "playlist", - "name": playlist_name, - "owner": playlist_data.get('creator', {}).get('name', 'unknown'), - "total_tracks": total_tracks, - "url": f"https://deezer.com/playlist/{self.__preferences.json_data.get('id', '')}" - } - }) + "url": f"https://deezer.com/playlist/{self.__preferences.json_data.get('id', '')}" + } elif self.__parent == "album": album_name = self.__song_metadata.get('album', '') album_artist = self.__song_metadata.get('album_artist', self.__song_metadata.get('album_artist', '')) @@ -361,19 +342,43 @@ class EASY_DW: current_track = getattr(self.__preferences, 'track_number', 0) # Format for album-parented tracks exactly as required - progress_data.update({ - "current_track": current_track, + parent = { + "type": "album", + "title": album_name, + "artist": album_artist, "total_tracks": total_tracks, - "parent": { - "type": "album", - "title": album_name, - "artist": album_artist, - "total_tracks": total_tracks, - "url": f"https://deezer.com/album/{self.__preferences.song_metadata.get('album_id', '')}" - } - }) + "url": f"https://deezer.com/album/{self.__preferences.song_metadata.get('album_id', '')}" + } - Download_JOB.report_progress(progress_data) + if self.__parent is None: + track_info = { + "name": current_title, + "artist": current_artist + } + summary = { + "successful_tracks": [], + "skipped_tracks": [f"{track_info['name']} - {track_info['artist']}"], + "failed_tracks": [], + "total_successful": 0, + "total_skipped": 1, + "total_failed": 0, + } + + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="track", + song=current_title, + artist=self.__song_metadata['artist'], + status="skipped", + url=self.__link, + reason=f"Track already exists in desired format at {existing_file_path}", + convert_to=self.__convert_to, + bitrate=self.__bitrate, + parent=parent, + current_track=current_track, + total_tracks=total_tracks, + summary=summary + ) # self.__c_track might not be fully initialized here if __write_track() hasn't been called # Create a minimal track object for skipped scenario skipped_item = Track( @@ -407,45 +412,49 @@ class EASY_DW: # Create done status report using the new required format (only if download_try didn't fail) # This part should only execute if download_try itself was successful (i.e., no exception) if self.__c_track.success : # Check if download_try marked it as successful - progress_data = { - "type": "track", - "song": self.__song_metadata['music'], - "artist": self.__song_metadata['artist'], - "status": "done", - "convert_to": self.__convert_to - } + parent = None + current_track = None + total_tracks = None + spotify_url = getattr(self.__preferences, 'spotify_url', None) - progress_data["url"] = spotify_url if spotify_url else self.__link + url = spotify_url if spotify_url else self.__link + if self.__parent == "playlist" and hasattr(self.__preferences, "json_data"): playlist_data = self.__preferences.json_data - # ... (rest of playlist parent data) ... - progress_data.update({ - "current_track": getattr(self.__preferences, 'track_number', 0), - "total_tracks": getattr(self.__preferences, 'total_tracks', 0), - "parent": { - "type": "playlist", - "name": playlist_data.get('title', 'unknown'), - "owner": playlist_data.get('creator', {}).get('name', 'unknown') - } - }) - elif self.__parent == "album": - album_name = self.__song_metadata.get('album', '') - album_artist = self.__song_metadata.get('album_artist', self.__song_metadata.get('album_artist', '')) - total_tracks = getattr(self.__preferences, 'total_tracks', 0) - current_track = getattr(self.__preferences, 'track_number', 0) - - progress_data.update({ - "current_track": current_track, - "total_tracks": total_tracks, - "parent": { + total_tracks = getattr(self.__preferences, 'total_tracks', 0) + current_track = getattr(self.__preferences, 'track_number', 0) + parent = { + "type": "playlist", + "name": playlist_data.get('title', 'unknown'), + "owner": playlist_data.get('creator', {}).get('name', 'unknown'), + "total_tracks": total_tracks, + "url": f"https://deezer.com/playlist/{playlist_data.get('id', '')}" + } + elif self.__parent == "album": + album_name = self.__song_metadata.get('album', '') + album_artist = self.__song_metadata.get('album_artist', self.__song_metadata.get('album_artist', '')) + total_tracks = getattr(self.__preferences, 'total_tracks', 0) + current_track = getattr(self.__preferences, 'track_number', 0) + parent = { "type": "album", "title": album_name, "artist": album_artist, "total_tracks": total_tracks, "url": f"https://deezer.com/album/{self.__preferences.song_metadata.get('album_id', '')}" } - }) - Download_JOB.report_progress(progress_data) + + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="track", + song=self.__song_metadata['music'], + artist=self.__song_metadata['artist'], + status="done", + url=url, + parent=parent, + current_track=current_track, + total_tracks=total_tracks, + convert_to=self.__convert_to + ) except Exception as e: # Covers failures within download_try or download_episode_try item_type = "Episode" if self.__infos_dw.get('__TYPE__') == 'episode' else "Track" @@ -572,47 +581,48 @@ class EASY_DW: self.__write_track() # Send immediate progress status for the track - progress_data = { - "type": "track", - "song": self.__song_metadata.get("music", ""), - "artist": self.__song_metadata.get("artist", ""), - "status": "progress" - } + parent = None + current_track = None + total_tracks = None spotify_url = getattr(self.__preferences, 'spotify_url', None) - progress_data["url"] = spotify_url if spotify_url else self.__link + url = spotify_url if spotify_url else self.__link + if self.__parent == "playlist" and hasattr(self.__preferences, "json_data"): playlist_data = self.__preferences.json_data playlist_name = playlist_data.get('title', 'unknown') total_tracks = getattr(self.__preferences, 'total_tracks', 0) current_track = getattr(self.__preferences, 'track_number', 0) - progress_data.update({ - "current_track": current_track, + parent = { + "type": "playlist", + "name": playlist_name, + "owner": playlist_data.get('creator', {}).get('name', 'unknown'), "total_tracks": total_tracks, - "parent": { - "type": "playlist", - "name": playlist_name, - "owner": playlist_data.get('creator', {}).get('name', 'unknown'), - "total_tracks": total_tracks, - "url": f"https://deezer.com/playlist/{self.__preferences.json_data.get('id', '')}" - } - }) + "url": f"https://deezer.com/playlist/{self.__preferences.json_data.get('id', '')}" + } elif self.__parent == "album": album_name = self.__song_metadata.get('album', '') album_artist = self.__song_metadata.get('album_artist', self.__song_metadata.get('album_artist', '')) total_tracks = getattr(self.__preferences, 'total_tracks', 0) current_track = getattr(self.__preferences, 'track_number', 0) - progress_data.update({ - "current_track": current_track, + parent = { + "type": "album", + "title": album_name, + "artist": album_artist, "total_tracks": total_tracks, - "parent": { - "type": "album", - "title": album_name, - "artist": album_artist, - "total_tracks": total_tracks, - "url": f"https://deezer.com/album/{self.__preferences.song_metadata.get('album_id', '')}" - } - }) - Download_JOB.report_progress(progress_data) + "url": f"https://deezer.com/album/{self.__preferences.song_metadata.get('album_id', '')}" + } + + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="track", + song=self.__song_metadata.get("music", ""), + artist=self.__song_metadata.get("artist", ""), + status="progress", + url=url, + parent=parent, + current_track=current_track, + total_tracks=total_tracks, + ) # Start of processing block (decryption, tagging, cover, conversion) register_active_download(self.__song_path) @@ -704,21 +714,46 @@ class EASY_DW: elif "404" in error_msg or "Not Found" in error_msg: error_msg = "Track not found - It might have been removed" # (Error reporting code as it exists) - error_progress_data = { - "type": "track", "status": "error", - "song": self.__song_metadata.get('music', ''), "artist": self.__song_metadata.get('artist', ''), - "error": error_msg, "url": getattr(self.__preferences, 'spotify_url', None) or self.__link, - "convert_to": self.__convert_to - } + parent = None + current_track = None + total_tracks = None + if self.__parent == "playlist" and hasattr(self.__preferences, "json_data"): - playlist_data = self.__preferences.json_data; playlist_name = playlist_data.get('title', 'unknown') - total_tracks = getattr(self.__preferences, 'total_tracks', 0); current_track = getattr(self.__preferences, 'track_number', 0) - error_progress_data.update({"current_track": current_track, "total_tracks": total_tracks, "parent": {"type": "playlist", "name": playlist_name, "owner": playlist_data.get('creator', {}).get('name', 'unknown'), "total_tracks": total_tracks, "url": f"https://deezer.com/playlist/{playlist_data.get('id', '')}"}}) + playlist_data = self.__preferences.json_data + playlist_name = playlist_data.get('title', 'unknown') + total_tracks = getattr(self.__preferences, 'total_tracks', 0) + current_track = getattr(self.__preferences, 'track_number', 0) + parent = { + "type": "playlist", "name": playlist_name, + "owner": playlist_data.get('creator', {}).get('name', 'unknown'), + "total_tracks": total_tracks, + "url": f"https://deezer.com/playlist/{playlist_data.get('id', '')}" + } elif self.__parent == "album": - album_name = self.__song_metadata.get('album', ''); album_artist = self.__song_metadata.get('album_artist', self.__song_metadata.get('album_artist', '')) - total_tracks = getattr(self.__preferences, 'total_tracks', 0); current_track = getattr(self.__preferences, 'track_number', 0) - error_progress_data.update({"current_track": current_track, "total_tracks": total_tracks, "parent": {"type": "album", "title": album_name, "artist": album_artist, "total_tracks": total_tracks, "url": f"https://deezer.com/album/{self.__preferences.song_metadata.get('album_id', '')}"}}) - Download_JOB.report_progress(error_progress_data) + album_name = self.__song_metadata.get('album', '') + album_artist = self.__song_metadata.get('album_artist', self.__song_metadata.get('album_artist', '')) + total_tracks = getattr(self.__preferences, 'total_tracks', 0) + current_track = getattr(self.__preferences, 'track_number', 0) + parent = { + "type": "album", "title": album_name, + "artist": album_artist, + "total_tracks": total_tracks, + "url": f"https://deezer.com/album/{self.__preferences.song_metadata.get('album_id', '')}" + } + + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="track", + status="error", + song=self.__song_metadata.get('music', ''), + artist=self.__song_metadata.get('artist', ''), + error=error_msg, + url=getattr(self.__preferences, 'spotify_url', None) or self.__link, + convert_to=self.__convert_to, + parent=parent, + current_track=current_track, + total_tracks=total_tracks + ) logger.error(f"Failed to process track: {error_msg}") self.__c_track.success = False @@ -781,7 +816,7 @@ class EASY_DW: spotify_url = getattr(self.__preferences, 'spotify_url', None) progress_data["url"] = spotify_url if spotify_url else self.__link - Download_JOB.report_progress(progress_data) + Download_JOB.progress_reporter.report(progress_data) self.__c_track.success = True self.__write_episode() @@ -933,6 +968,29 @@ class DW_TRACK: logger.error(f"{error_msg} (Link: {current_link})") raise TrackNotFound(message=error_msg, url=current_link) + if track.success and not getattr(track, 'was_skipped', False): + track_info = { + "name": track.tags.get('music', 'Unknown Track'), + "artist": track.tags.get('artist', 'Unknown Artist') + } + summary = { + "successful_tracks": [f"{track_info['name']} - {track_info['artist']}"], + "skipped_tracks": [], + "failed_tracks": [], + "total_successful": 1, + "total_skipped": 0, + "total_failed": 0, + } + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="track", + song=track_info['name'], + artist=track_info['artist'], + status="done", + url=track.link, + summary=summary + ) + return track class DW_ALBUM: @@ -985,14 +1043,15 @@ class DW_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", - "artist": derived_album_artist_from_contributors, - "status": "initializing", - "total_tracks": total_tracks_for_report, - "title": album_name_for_report, - "url": album_link_for_report # Use the actual album link - }) + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="album", + artist=derived_album_artist_from_contributors, + status="initializing", + total_tracks=total_tracks_for_report, + title=album_name_for_report, + url=album_link_for_report + ) infos_dw = API_GW.get_album_data(self.__ids)['data'] @@ -1183,14 +1242,42 @@ class DW_ALBUM: album_name = self.__song_metadata.get('album', 'Unknown Album') total_tracks = self.__song_metadata.get('nb_tracks', 0) - Download_JOB.report_progress({ - "type": "album", - "artist": derived_album_artist_from_contributors, - "status": "done", - "total_tracks": total_tracks, - "title": album_name, - "url": album_link_for_report # Use the actual album link - }) + successful_tracks = [] + failed_tracks = [] + skipped_tracks = [] + for track in tracks: + track_info = { + "name": track.tags.get('music') or track.tags.get('name', 'Unknown Track'), + "artist": track.tags.get('artist', 'Unknown Artist') + } + if getattr(track, 'was_skipped', False): + skipped_tracks.append(track_info) + elif track.success: + successful_tracks.append(track_info) + else: + track_info["reason"] = getattr(track, 'error_message', 'Unknown reason') + failed_tracks.append(track_info) + + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="album", + artist=derived_album_artist_from_contributors, + status="done", + total_tracks=total_tracks, + title=album_name, + url=album_link_for_report, # Use the actual album link + summary={ + "successful_tracks": [f"{t['name']} - {t['artist']}" for t in successful_tracks], + "skipped_tracks": [f"{t['name']} - {t['artist']}" for t in skipped_tracks], + "failed_tracks": [{ + "track": f"{t['name']} - {t['artist']}", + "reason": t['reason'] + } for t in failed_tracks], + "total_successful": len(successful_tracks), + "total_skipped": len(skipped_tracks), + "total_failed": len(failed_tracks) + } + ) return album @@ -1215,14 +1302,15 @@ class DW_PLAYLIST: total_tracks = self.__json_data.get('nb_tracks', 0) # Report playlist initializing status - Download_JOB.report_progress({ - "type": "playlist", - "owner": playlist_owner, - "status": "initializing", - "total_tracks": total_tracks, - "name": playlist_name, - "url": f"https://deezer.com/playlist/{self.__ids}" - }) + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="playlist", + owner=playlist_owner, + status="initializing", + total_tracks=total_tracks, + name=playlist_name, + url=f"https://deezer.com/playlist/{self.__ids}" + ) # Retrieve playlist data from API infos_dw = API_GW.get_playlist_data(self.__ids)['data'] @@ -1378,14 +1466,44 @@ class DW_PLAYLIST: playlist_owner = self.__json_data.get('creator', {}).get('name', 'Unknown Owner') total_tracks = self.__json_data.get('nb_tracks', 0) - Download_JOB.report_progress({ - "type": "playlist", - "owner": playlist_owner, - "status": "done", - "total_tracks": total_tracks, - "name": playlist_name, - "url": f"https://deezer.com/playlist/{self.__ids}" - }) + successful_tracks = [] + failed_tracks = [] + skipped_tracks = [] + for track in tracks: + track_name = track.tags.get('music') or track.tags.get('name', 'Unknown Track') + artist_name = track.tags.get('artist', 'Unknown Artist') + track_info = { + "name": track_name, + "artist": artist_name + } + if getattr(track, 'was_skipped', False): + skipped_tracks.append(track_info) + elif track.success: + successful_tracks.append(track_info) + else: + track_info["reason"] = getattr(track, 'error_message', 'Unknown reason') + failed_tracks.append(track_info) + + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="playlist", + owner=playlist_owner, + status="done", + total_tracks=total_tracks, + name=playlist_name, + url=f"https://deezer.com/playlist/{self.__ids}", + summary={ + "successful_tracks": [f"{t['name']} - {t['artist']}" for t in successful_tracks], + "skipped_tracks": [f"{t['name']} - {t['artist']}" for t in skipped_tracks], + "failed_tracks": [{ + "track": f"{t['name']} - {t['artist']}", + "reason": t['reason'] + } for t in failed_tracks], + "total_successful": len(successful_tracks), + "total_skipped": len(skipped_tracks), + "total_failed": len(failed_tracks) + } + ) return playlist @@ -1435,19 +1553,20 @@ class DW_EPISODE: total_size = int(response.headers.get('content-length', 0)) # Send initial progress status - progress_data = { - "type": "episode", - "song": self.__preferences.song_metadata.get('name', ''), - "artist": self.__preferences.song_metadata.get('publisher', ''), - "status": "progress", - "url": f"https://www.deezer.com/episode/{self.__ids}", - "parent": { - "type": "show", - "title": self.__preferences.song_metadata.get('show', ''), - "artist": self.__preferences.song_metadata.get('publisher', '') - } + parent = { + "type": "show", + "title": self.__preferences.song_metadata.get('artist', ''), + "artist": self.__preferences.song_metadata.get('artist', '') } - Download_JOB.report_progress(progress_data) + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="episode", + song=self.__preferences.song_metadata.get('music', ''), + artist=self.__preferences.song_metadata.get('artist', ''), + status="progress", + url=f"https://www.deezer.com/episode/{self.__ids}", + parent=parent + ) with open(output_path, 'wb') as f: start_time = time.time() @@ -1464,21 +1583,22 @@ class DW_EPISODE: last_report_time = current_time percentage = round((downloaded / total_size) * 100, 2) - progress_data = { - "type": "episode", - "song": self.__preferences.song_metadata.get('name', ''), - "artist": self.__preferences.song_metadata.get('publisher', ''), - "status": "real-time", - "url": f"https://www.deezer.com/episode/{self.__ids}", - "time_elapsed": int((current_time - start_time) * 1000), - "progress": percentage, - "parent": { - "type": "show", - "title": self.__preferences.song_metadata.get('show', ''), - "artist": self.__preferences.song_metadata.get('publisher', '') - } + parent = { + "type": "show", + "title": self.__preferences.song_metadata.get('artist', ''), + "artist": self.__preferences.song_metadata.get('artist', '') } - Download_JOB.report_progress(progress_data) + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="episode", + song=self.__preferences.song_metadata.get('music', ''), + artist=self.__preferences.song_metadata.get('artist', ''), + status="real-time", + url=f"https://www.deezer.com/episode/{self.__ids}", + time_elapsed=int((current_time - start_time) * 1000), + progress=percentage, + parent=parent + ) episode = Track( self.__preferences.song_metadata, @@ -1491,19 +1611,20 @@ class DW_EPISODE: episode.success = True # Send completion status - progress_data = { - "type": "episode", - "song": self.__preferences.song_metadata.get('name', ''), - "artist": self.__preferences.song_metadata.get('publisher', ''), - "status": "done", - "url": f"https://www.deezer.com/episode/{self.__ids}", - "parent": { - "type": "show", - "title": self.__preferences.song_metadata.get('show', ''), - "artist": self.__preferences.song_metadata.get('publisher', '') - } + parent = { + "type": "show", + "title": self.__preferences.song_metadata.get('artist', ''), + "artist": self.__preferences.song_metadata.get('artist', '') } - Download_JOB.report_progress(progress_data) + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="episode", + song=self.__preferences.song_metadata.get('music', ''), + artist=self.__preferences.song_metadata.get('artist', ''), + status="done", + url=f"https://www.deezer.com/episode/{self.__ids}", + parent=parent + ) # Save cover image for the episode if self.__preferences.save_cover: diff --git a/deezspot/deezloader/__init__.py b/deezspot/deezloader/__init__.py index b7908ff..5be79a9 100644 --- a/deezspot/deezloader/__init__.py +++ b/deezspot/deezloader/__init__.py @@ -43,7 +43,7 @@ from deezspot.libutils.others_settings import ( stock_save_cover, stock_market ) -from deezspot.libutils.logging_utils import ProgressReporter, logger +from deezspot.libutils.logging_utils import ProgressReporter, logger, report_progress import requests API() @@ -89,10 +89,6 @@ class DeeLogin: # Set the progress reporter for Download_JOB Download_JOB.set_progress_reporter(self.progress_reporter) - def report_progress(self, progress_data): - """Report progress using the configured reporter.""" - self.progress_reporter.report(progress_data) - def download_trackdee( self, link_track, output_dir=stock_output, @@ -125,6 +121,20 @@ class DeeLogin: 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}") + summary = { + "successful_tracks": [], "skipped_tracks": [], "total_successful": 0, "total_skipped": 0, "total_failed": 1, + "failed_tracks": [{"track": f"Track ID {ids}", "reason": str(e)}] + } + report_progress( + reporter=self.progress_reporter, + report_type="track", + status="error", + song="Unknown Track", + artist="Unknown Artist", + url=link_track, + error=str(e), + summary=summary + ) raise TrackNotFound(url=link_track, message=e.message) from e except NoDataApi: infos = self.__gw_api.get_song_data(ids) @@ -161,9 +171,28 @@ class DeeLogin: preferences.save_cover = save_cover preferences.market = market - track = DW_TRACK(preferences).dw() - - return track + try: + track = DW_TRACK(preferences).dw() + return track + except Exception as e: + logger.error(f"Failed to download track: {str(e)}") + if song_metadata: + track_info = {"name": song_metadata.get("music", "Unknown Track"), "artist": song_metadata.get("artist", "Unknown Artist")} + summary = { + "successful_tracks": [], "skipped_tracks": [], "total_successful": 0, "total_skipped": 0, "total_failed": 1, + "failed_tracks": [{"track": f"{track_info['name']} - {track_info['artist']}", "reason": str(e)}] + } + report_progress( + reporter=self.progress_reporter, + report_type="track", + status="error", + song=track_info['name'], + artist=track_info['artist'], + url=link_track, + error=str(e), + summary=summary + ) + raise e def download_albumdee( self, link_album, @@ -695,29 +724,26 @@ class DeeLogin: # Use stored credentials for API calls playlist_json = Spo.get_playlist(ids) playlist_name = playlist_json['name'] + playlist_owner = playlist_json.get('owner', {}).get('display_name', 'Unknown Owner') total_tracks = playlist_json['tracks']['total'] playlist_tracks = playlist_json['tracks']['items'] playlist = Playlist() tracks = playlist.tracks # Initializing status - replaced print with report_progress - self.report_progress({ - "status": "initializing", - "type": "playlist", - "name": playlist_name, - "total_tracks": total_tracks - }) + report_progress( + reporter=self.progress_reporter, + report_type="playlist", + status="initializing", + name=playlist_name, + owner=playlist_owner, + total_tracks=total_tracks, + url=link_playlist + ) for index, item in enumerate(playlist_tracks, 1): is_track = item.get('track') if not is_track: - # Progress status for an invalid track item - self.report_progress({ - "status": "progress", - "type": "playlist", - "track": "Unknown Track", - "current_track": f"{index}/{total_tracks}" - }) continue track_info = is_track @@ -727,24 +753,9 @@ class DeeLogin: external_urls = track_info.get('external_urls', {}) if not external_urls: - # Progress status for unavailable track - self.report_progress({ - "status": "progress", - "type": "playlist", - "track": track_name, - "current_track": f"{index}/{total_tracks}" - }) logger.warning(f"The track \"{track_name}\" is not available on Spotify :(") continue - # Progress status before download attempt - self.report_progress({ - "status": "progress", - "type": "playlist", - "track": track_name, - "current_track": f"{index}/{total_tracks}" - }) - link_track = external_urls['spotify'] try: @@ -773,12 +784,49 @@ class DeeLogin: tracks.append(f"{track_name} - {artist_name}") # Done status - self.report_progress({ - "status": "done", - "type": "playlist", - "name": playlist_name, - "total_tracks": total_tracks - }) + successful_tracks_list = [] + failed_tracks_list = [] + skipped_tracks_list = [] + for track in tracks: + if isinstance(track, Track): + track_info = { + "name": track.tags.get('music', 'Unknown Track'), + "artist": track.tags.get('artist', 'Unknown Artist') + } + if getattr(track, 'was_skipped', False): + skipped_tracks_list.append(f"{track_info['name']} - {track_info['artist']}") + elif track.success: + successful_tracks_list.append(f"{track_info['name']} - {track_info['artist']}") + else: + failed_tracks_list.append({ + "track": f"{track_info['name']} - {track_info['artist']}", + "reason": getattr(track, 'error_message', 'Unknown reason') + }) + elif isinstance(track, str): # It can be a string for failed tracks + failed_tracks_list.append({ + "track": track, + "reason": "Failed to download or convert." + }) + + summary = { + "successful_tracks": successful_tracks_list, + "skipped_tracks": skipped_tracks_list, + "failed_tracks": failed_tracks_list, + "total_successful": len(successful_tracks_list), + "total_skipped": len(skipped_tracks_list), + "total_failed": len(failed_tracks_list), + } + + report_progress( + reporter=self.progress_reporter, + report_type="playlist", + status="done", + name=playlist_name, + owner=playlist_owner, + total_tracks=total_tracks, + url=link_playlist, + summary=summary + ) # === New m3u File Creation Section === # Create a subfolder "playlists" inside the output directory @@ -974,7 +1022,7 @@ class DeeLogin: smart.source = source # Add progress reporting for the smart downloader - self.report_progress({ + self.progress_reporter.report({ "status": "initializing", "type": "smart_download", "link": link, @@ -1071,7 +1119,7 @@ class DeeLogin: smart.playlist = playlist # Report completion - self.report_progress({ + self.progress_reporter.report({ "status": "done", "type": "smart_download", "source": source, diff --git a/deezspot/libutils/logging_utils.py b/deezspot/libutils/logging_utils.py index 25aadca..e0df632 100644 --- a/deezspot/libutils/logging_utils.py +++ b/deezspot/libutils/logging_utils.py @@ -66,4 +66,148 @@ class ProgressReporter: self.callback(progress_data) elif not self.silent: # Log using JSON format - logger.log(self.log_level, json.dumps(progress_data)) \ No newline at end of file + logger.log(self.log_level, json.dumps(progress_data)) + +# --- Standardized Progress Report Format --- +# The report_progress function generates a standardized dictionary (JSON object) +# to provide detailed feedback about the download process. +# +# Base Structure: +# { +# "type": "track" | "album" | "playlist" | "episode", +# "status": "initializing" | "skipped" | "retrying" | "real-time" | "error" | "done" +# ... other fields based on type and status +# } +# +# --- Field Definitions --- +# +# [ General Fields ] +# - url: (str) The URL of the item being processed. +# - convert_to: (str) Target audio format for conversion (e.g., "mp3"). +# - bitrate: (str) Target bitrate for conversion (e.g., "320"). +# +# [ Type: "track" ] +# - song: (str) The name of the track. +# - artist: (str) The artist of the track. +# - album: (str, optional) The album of the track. +# - parent: (dict, optional) Information about the container (album/playlist). +# { "type": "album"|"playlist", "name": str, "owner": str, "artist": str, ... } +# - current_track: (int, optional) The track number in the context of a parent. +# - total_tracks: (int, optional) The total tracks in the context of a parent. +# +# [ Status: "skipped" ] +# - reason: (str) The reason for skipping (e.g., "Track already exists..."). +# +# [ Status: "retrying" ] +# - retry_count: (int) The current retry attempt number. +# - seconds_left: (int) The time in seconds until the next retry attempt. +# - error: (str) The error message that caused the retry. +# +# [ Status: "real-time" ] +# - time_elapsed: (int) Time in milliseconds since the download started. +# - progress: (int) Download percentage (0-100). +# +# [ Status: "error" ] +# - error: (str) The detailed error message. +# +# [ Status: "done" (for single track downloads) ] +# - summary: (dict) A summary of the operation. +# +# [ Type: "album" | "playlist" ] +# - title / name: (str) The title of the album or name of the playlist. +# - artist / owner: (str) The artist of the album or owner of the playlist. +# - total_tracks: (int) The total number of tracks. +# +# [ Status: "done" ] +# - summary: (dict) A detailed summary of the entire download operation. +# { +# "successful_tracks": [str], +# "skipped_tracks": [str], +# "failed_tracks": [{"track": str, "reason": str}], +# "total_successful": int, +# "total_skipped": int, +# "total_failed": int +# } +# + +def report_progress( + reporter: Optional["ProgressReporter"], + report_type: str, + status: str, + song: Optional[str] = None, + artist: Optional[str] = None, + album: Optional[str] = None, + url: Optional[str] = None, + convert_to: Optional[str] = None, + bitrate: Optional[str] = None, + parent: Optional[Dict[str, Any]] = None, + current_track: Optional[int] = None, + total_tracks: Optional[Union[int, str]] = None, + reason: Optional[str] = None, + summary: Optional[Dict[str, Any]] = None, + error: Optional[str] = None, + retry_count: Optional[int] = None, + seconds_left: Optional[int] = None, + time_elapsed: Optional[int] = None, + progress: Optional[int] = None, + owner: Optional[str] = None, + name: Optional[str] = None, + title: Optional[str] = None, +): + """Builds and reports a standardized progress dictionary after validating the input.""" + + # --- Input Validation --- + # Enforce the standardized format to ensure consistent reporting. + if report_type == "track": + if not all([song, artist]): + raise ValueError("For report_type 'track', 'song' and 'artist' parameters are required.") + if status == "skipped" and reason is None: + raise ValueError("For a 'skipped' track, a 'reason' is required.") + if status == "retrying" and not all(p is not None for p in [retry_count, seconds_left, error]): + raise ValueError("For a 'retrying' track, 'retry_count', 'seconds_left', and 'error' are required.") + if status == "real-time" and not all(p is not None for p in [time_elapsed, progress]): + raise ValueError("For a 'real-time' track, 'time_elapsed' and 'progress' are required.") + if status == "error" and error is None: + raise ValueError("For an 'error' track, an 'error' message is required.") + + elif report_type == "album": + if not all(p is not None for p in [title, artist, total_tracks]): + raise ValueError("For report_type 'album', 'title', 'artist', and 'total_tracks' are required.") + if status == "done" and summary is None: + raise ValueError("For an 'album' with status 'done', a 'summary' is required.") + + elif report_type == "playlist": + if not all(p is not None for p in [name, owner, total_tracks]): + raise ValueError("For report_type 'playlist', 'name', 'owner', and 'total_tracks' are required.") + if status == "done" and summary is None: + raise ValueError("For a 'playlist' with status 'done', a 'summary' is required.") + + elif report_type == "episode": + if not all([song, artist]): # song=episode_title, artist=show_name + raise ValueError("For report_type 'episode', 'song' and 'artist' parameters are required.") + if status == "retrying" and not all(p is not None for p in [retry_count, seconds_left, error]): + raise ValueError("For a 'retrying' episode, 'retry_count', 'seconds_left', and 'error' are required.") + if status == "error" and error is None: + raise ValueError("For an 'error' episode, an 'error' message is required.") + + # --- Report Building --- + report = {"type": report_type, "status": status} + + data_fields = { + "song": song, "artist": artist, "album": album, "url": url, + "convert_to": convert_to, "bitrate": bitrate, "parent": parent, + "current_track": current_track, "total_tracks": total_tracks, + "reason": reason, "summary": summary, "error": error, + "retry_count": retry_count, "seconds_left": seconds_left, + "time_elapsed": time_elapsed, "progress": progress, + "owner": owner, "name": name, "title": title + } + + for key, value in data_fields.items(): + if value is not None: + report[key] = value + + if reporter: + reporter.report(report) + else: + logger.info(json.dumps(report)) \ No newline at end of file diff --git a/deezspot/libutils/utils.py b/deezspot/libutils/utils.py index 5ad911e..20019cf 100644 --- a/deezspot/libutils/utils.py +++ b/deezspot/libutils/utils.py @@ -163,6 +163,13 @@ def apply_custom_format(format_str, metadata: dict, pad_tracks=True) -> str: else: # Original non-indexed placeholder logic (for %album%, %title%, %artist%, %ar_album%, etc.) value = metadata.get(full_key, '') + + if full_key == 'year' and value: + if isinstance(value, datetime): + return str(value.year) + # Fallback for string-based dates like "YYYY-MM-DD" or just "YYYY" + return str(value).split('-')[0] + if pad_tracks and full_key in ['tracknum', 'discnum']: str_value = str(value) # Pad with leading zero if it's a single digit diff --git a/deezspot/spotloader/__download__.py b/deezspot/spotloader/__download__.py index 46c1e11..a0f90c3 100644 --- a/deezspot/spotloader/__download__.py +++ b/deezspot/spotloader/__download__.py @@ -32,7 +32,7 @@ from deezspot.libutils.utils import ( save_cover_image, __get_dir as get_album_directory, ) -from deezspot.libutils.logging_utils import logger +from deezspot.libutils.logging_utils import logger, report_progress from deezspot.libutils.cleanup_utils import ( register_active_download, unregister_active_download, @@ -57,15 +57,6 @@ class Download_JOB: @classmethod def set_progress_reporter(cls, reporter): cls.progress_reporter = reporter - - @classmethod - def report_progress(cls, progress_data): - """Report progress if a reporter is configured.""" - if cls.progress_reporter: - cls.progress_reporter.report(progress_data) - else: - # Fallback to logger if no reporter is configured - logger.info(json.dumps(progress_data)) class EASY_DW: def __init__( @@ -336,47 +327,50 @@ class EASY_DW: self.__c_track.success = True # Mark as success because the desired file is available self.__c_track.was_skipped = True - progress_data = { - "type": "track", - "song": current_title, - "artist": current_artist, - "status": "skipped", - "url": self.__link, - "reason": f"Track already exists in desired format at {existing_file_path}", - "convert_to": self.__preferences.convert_to, # Reflect user's conversion preference - "bitrate": self.__preferences.bitrate # Reflect user's bitrate preference - } - + parent_info = None + playlist_data = None + total_tracks_val = None + if self.__parent == "playlist" and hasattr(self.__preferences, "json_data"): playlist_data = self.__preferences.json_data - playlist_name = playlist_data.get('name', 'unknown') - total_tracks = playlist_data.get('tracks', {}).get('total', 'unknown') - current_track_num = getattr(self.__preferences, 'track_number', 0) - progress_data.update({ - "current_track": current_track_num, - "total_tracks": total_tracks, - "parent": { - "type": "playlist", - "name": playlist_name, - "owner": playlist_data.get('owner', {}).get('display_name', 'unknown') - } - }) + total_tracks_val = playlist_data.get('tracks', {}).get('total', 'unknown') + parent_info = { + "type": "playlist", + "name": playlist_data.get('name', 'unknown'), + "owner": playlist_data.get('owner', {}).get('display_name', 'unknown') + } elif self.__parent == "album": - album_name_meta = self.__song_metadata.get('album', '') - album_artist_meta = self.__song_metadata.get('album_artist', self.__song_metadata.get('ar_album', '')) - total_tracks_meta = self.__song_metadata.get('nb_tracks', 0) - current_track_num = getattr(self.__preferences, 'track_number', 0) - progress_data.update({ - "current_track": current_track_num, - "total_tracks": total_tracks_meta, - "parent": { - "type": "album", - "title": album_name_meta, - "artist": album_artist_meta - } - }) - - Download_JOB.report_progress(progress_data) + total_tracks_val = self.__song_metadata.get('nb_tracks', 0) + parent_info = { + "type": "album", + "title": self.__song_metadata.get('album', ''), + "artist": self.__song_metadata.get('album_artist', self.__song_metadata.get('ar_album', '')) + } + + summary_data = { + "successful_tracks": [], + "skipped_tracks": [f"{current_title} - {current_artist}"], + "failed_tracks": [], + "total_successful": 0, + "total_skipped": 1, + "total_failed": 0, + } if self.__parent is None else None + + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="track", + status="skipped", + song=current_title, + artist=current_artist, + url=self.__link, + reason=f"Track already exists in desired format at {existing_file_path}", + convert_to=self.__preferences.convert_to, + bitrate=self.__preferences.bitrate, + current_track=getattr(self.__preferences, 'track_number', None), + total_tracks=total_tracks_val, + parent=parent_info, + summary=summary_data + ) return self.__c_track # If track does not exist in the desired final format, proceed with download/conversion @@ -432,59 +426,44 @@ class EASY_DW: if current_percentage > self._last_reported_percentage: self._last_reported_percentage = current_percentage - # Create real-time progress data - progress_data = { - "type": "track", - "song": self.__song_metadata.get("music", ""), - "artist": self.__song_metadata.get("artist", ""), - "status": "real-time", - "url": self.__link, - "time_elapsed": int((current_time - start_time) * 1000), - "progress": current_percentage, - "convert_to": self.__convert_to, - "bitrate": self.__bitrate - } - - # Add parent info based on parent type + parent_info = None + playlist_data = None + total_tracks_val = None if self.__parent == "playlist" and hasattr(self.__preferences, "json_data"): playlist_data = self.__preferences.json_data - playlist_name = playlist_data.get('name', 'unknown') - total_tracks = playlist_data.get('tracks', {}).get('total', 'unknown') - current_track = getattr(self.__preferences, 'track_number', 0) - playlist_owner = playlist_data.get('owner', {}).get('display_name', 'unknown') - playlist_id = playlist_data.get('id', '') - - progress_data.update({ - "current_track": current_track, - "total_tracks": total_tracks, - "parent": { - "type": "playlist", - "name": playlist_name, - "owner": playlist_owner, - "total_tracks": total_tracks, - "url": f"https://open.spotify.com/playlist/{playlist_id}" - } - }) + total_tracks_val = playlist_data.get('tracks', {}).get('total', 'unknown') + parent_info = { + "type": "playlist", + "name": playlist_data.get('name', 'unknown'), + "owner": playlist_data.get('owner', {}).get('display_name', 'unknown'), + "total_tracks": total_tracks_val, + "url": f"https://open.spotify.com/playlist/{playlist_data.get('id', '')}" + } elif self.__parent == "album": - album_name = self.__song_metadata.get('album', '') - album_artist = self.__song_metadata.get('album_artist', self.__song_metadata.get('ar_album', '')) - total_tracks = self.__song_metadata.get('nb_tracks', 0) - current_track = getattr(self.__preferences, 'track_number', 0) - - progress_data.update({ - "current_track": current_track, - "total_tracks": total_tracks, - "parent": { - "type": "album", - "title": album_name, - "artist": album_artist, - "total_tracks": total_tracks, - "url": f"https://open.spotify.com/album/{self.__song_metadata.get('album_id', '')}" - } - }) - - # Report the progress - Download_JOB.report_progress(progress_data) + total_tracks_val = self.__song_metadata.get('nb_tracks', 0) + parent_info = { + "type": "album", + "title": self.__song_metadata.get('album', ''), + "artist": self.__song_metadata.get('album_artist', self.__song_metadata.get('ar_album', '')), + "total_tracks": total_tracks_val, + "url": f"https://open.spotify.com/album/{self.__song_metadata.get('album_id', '')}" + } + + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="track", + status="real-time", + song=self.__song_metadata.get("music", ""), + artist=self.__song_metadata.get("artist", ""), + url=self.__link, + time_elapsed=int((current_time - start_time) * 1000), + progress=current_percentage, + convert_to=self.__convert_to, + bitrate=self.__bitrate, + current_track=getattr(self.__preferences, 'track_number', None), + total_tracks=total_tracks_val, + parent=parent_info + ) # Rate limiting (if needed) expected_time = bytes_written / rate_limit @@ -591,7 +570,24 @@ class EASY_DW: } }) - Download_JOB.report_progress(progress_data) + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="track", + status="retrying", + retry_count=retries, + seconds_left=retry_delay, + song=self.__song_metadata.get('music', ''), + artist=self.__song_metadata.get('artist', ''), + album=self.__song_metadata.get('album', ''), + error=str(e), + url=self.__link, + convert_to=self.__convert_to, + bitrate=self.__bitrate, + current_track=getattr(self.__preferences, 'track_number', None), + total_tracks=total_tracks_val, + parent=parent_info + ) + if retries >= max_retries or GLOBAL_RETRY_COUNT >= GLOBAL_MAX_RETRIES: # Final cleanup before giving up if os.path.exists(self.__song_path): @@ -630,59 +626,43 @@ class EASY_DW: else: error_msg = f"Audio conversion failed: {original_error_str}" - # Create standardized error format - progress_data = { - "type": "track", - "status": "error", - "song": self.__song_metadata.get('music', ''), - "artist": self.__song_metadata.get('artist', ''), - "error": error_msg, - "url": self.__link, - "convert_to": self.__convert_to, - "bitrate": self.__bitrate - } - - # Add parent info based on parent type + parent_info = None + playlist_data = None + total_tracks_val = None if self.__parent == "playlist" and hasattr(self.__preferences, "json_data"): playlist_data = self.__preferences.json_data - playlist_name = playlist_data.get('name', 'unknown') - total_tracks = playlist_data.get('tracks', {}).get('total', 'unknown') - current_track = getattr(self.__preferences, 'track_number', 0) - playlist_owner = playlist_data.get('owner', {}).get('display_name', 'unknown') - playlist_id = playlist_data.get('id', '') - - progress_data.update({ - "current_track": current_track, - "total_tracks": total_tracks, - "parent": { - "type": "playlist", - "name": playlist_name, - "owner": playlist_owner, - "total_tracks": total_tracks, - "url": f"https://open.spotify.com/playlist/{playlist_id}" - } - }) + total_tracks_val = playlist_data.get('tracks', {}).get('total', 'unknown') + parent_info = { + "type": "playlist", + "name": playlist_data.get('name', 'unknown'), + "owner": playlist_data.get('owner', {}).get('display_name', 'unknown'), + "total_tracks": total_tracks_val, + "url": f"https://open.spotify.com/playlist/{playlist_data.get('id', '')}" + } elif self.__parent == "album": - album_name = self.__song_metadata.get('album', '') - album_artist = self.__song_metadata.get('album_artist', self.__song_metadata.get('ar_album', '')) - total_tracks = self.__song_metadata.get('nb_tracks', 0) - current_track = getattr(self.__preferences, 'track_number', 0) - album_id = self.__song_metadata.get('album_id', '') - - progress_data.update({ - "current_track": current_track, - "total_tracks": total_tracks, - "parent": { - "type": "album", - "title": album_name, - "artist": album_artist, - "total_tracks": total_tracks, - "url": f"https://open.spotify.com/album/{album_id}" - } - }) + total_tracks_val = self.__song_metadata.get('nb_tracks', 0) + parent_info = { + "type": "album", + "title": self.__song_metadata.get('album', ''), + "artist": self.__song_metadata.get('album_artist', self.__song_metadata.get('ar_album', '')), + "total_tracks": total_tracks_val, + "url": f"https://open.spotify.com/album/{self.__song_metadata.get('album_id', '')}" + } - # Report the error - Download_JOB.report_progress(progress_data) + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="track", + status="error", + song=self.__song_metadata.get('music', ''), + artist=self.__song_metadata.get('artist', ''), + error=error_msg, + url=self.__link, + convert_to=self.__convert_to, + bitrate=self.__bitrate, + current_track=getattr(self.__preferences, 'track_number', None), + total_tracks=total_tracks_val, + parent=parent_info + ) logger.error(f"Audio conversion error: {error_msg}") # If conversion fails, clean up the .ogg file @@ -696,10 +676,21 @@ class EASY_DW: self.__convert_audio() except Exception as conv_e: # If conversion fails twice, create a final error report - error_msg = f"Audio conversion failed after retry for '{self.__song_metadata.get('music', 'Unknown Track')}'. Original error: {str(conv_e)}" - progress_data["error"] = error_msg - progress_data["status"] = "error" - Download_JOB.report_progress(progress_data) + error_msg_2 = f"Audio conversion failed after retry for '{self.__song_metadata.get('music', 'Unknown Track')}'. Original error: {str(conv_e)}" + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="track", + status="error", + song=self.__song_metadata.get('music', 'Unknown Track'), + artist=self.__song_metadata.get('artist', ''), + error=error_msg_2, + url=self.__link, + convert_to=self.__convert_to, + bitrate=self.__bitrate, + parent=parent_info, + current_track=getattr(self.__preferences, 'track_number', None), + total_tracks=total_tracks_val + ) logger.error(error_msg) if os.path.exists(self.__song_path): @@ -714,58 +705,63 @@ class EASY_DW: self.__c_track.success = True write_tags(self.__c_track) - # Create done status report using the same format as progress status - progress_data = { - "type": "track", - "song": self.__song_metadata.get("music", ""), - "artist": self.__song_metadata.get("artist", ""), - "status": "done", - "url": self.__link, - "convert_to": self.__convert_to, - "bitrate": self.__bitrate - } - - # Add parent info based on parent type + # Create done status report + song = self.__song_metadata.get("music", "") + artist = self.__song_metadata.get("artist", "") + parent_info = None + total_tracks_val = None + current_track_val = None + summary_data = None + if self.__parent == "playlist" and hasattr(self.__preferences, "json_data"): playlist_data = self.__preferences.json_data - playlist_name = playlist_data.get('name', 'unknown') - total_tracks = playlist_data.get('tracks', {}).get('total', 'unknown') - current_track = getattr(self.__preferences, 'track_number', 0) - - progress_data.update({ - "current_track": current_track, - "total_tracks": total_tracks, - "parent": { - "type": "playlist", - "name": playlist_name, - "owner": playlist_data.get('owner', {}).get('display_name', 'unknown'), - "total_tracks": total_tracks, - "url": f"https://open.spotify.com/playlist/{playlist_data.get('id', '')}" - } - }) + total_tracks_val = playlist_data.get('tracks', {}).get('total', 'unknown') + current_track_val = getattr(self.__preferences, 'track_number', 0) + parent_info = { + "type": "playlist", + "name": playlist_data.get('name', 'unknown'), + "owner": playlist_data.get('owner', {}).get('display_name', 'unknown'), + "total_tracks": total_tracks_val, + "url": f"https://open.spotify.com/playlist/{playlist_data.get('id', '')}" + } elif self.__parent == "album": - album_name = self.__song_metadata.get('album', '') - album_artist = self.__song_metadata.get('album_artist', self.__song_metadata.get('ar_album', '')) - total_tracks = self.__song_metadata.get('nb_tracks', 0) - current_track = getattr(self.__preferences, 'track_number', 0) - - progress_data.update({ - "current_track": current_track, - "total_tracks": total_tracks, - "parent": { - "type": "album", - "title": album_name, - "artist": album_artist, - "total_tracks": total_tracks, - "url": f"https://open.spotify.com/album/{self.__song_metadata.get('album_id', '')}" - } - }) - - Download_JOB.report_progress(progress_data) + total_tracks_val = self.__song_metadata.get('nb_tracks', 0) + current_track_val = getattr(self.__preferences, 'track_number', 0) + parent_info = { + "type": "album", + "title": self.__song_metadata.get('album', ''), + "artist": self.__song_metadata.get('album_artist', self.__song_metadata.get('ar_album', '')), + "total_tracks": total_tracks_val, + "url": f"https://open.spotify.com/album/{self.__song_metadata.get('album_id', '')}" + } + + if self.__parent is None: + summary_data = { + "successful_tracks": [f"{song} - {artist}"], + "skipped_tracks": [], + "failed_tracks": [], + "total_successful": 1, + "total_skipped": 0, + "total_failed": 0, + } + + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="track", + status="done", + song=song, + artist=artist, + url=self.__link, + convert_to=self.__convert_to, + bitrate=self.__bitrate, + parent=parent_info, + current_track=current_track_val, + total_tracks=total_tracks_val, + summary=summary_data, + ) if hasattr(self, '_EASY_DW__c_track') and self.__c_track and self.__c_track.success: # Unregister the final successful file path after all operations are done. - # self.__c_track.song_path would have been updated by __convert_audio__ if conversion occurred. unregister_active_download(self.__c_track.song_path) return self.__c_track @@ -806,21 +802,22 @@ class EASY_DW: GLOBAL_RETRY_COUNT += 1 retries += 1 # Log retry attempt with structured data - print(json.dumps({ - "status": "retrying", - "retry_count": retries, - "seconds_left": retry_delay, - "song": self.__song_metadata.get('music', 'Unknown Episode'), - "artist": self.__song_metadata.get('artist', 'Unknown Show'), - "album": self.__song_metadata.get('album', 'N/A'), # Episodes don't typically have albums - "error": str(e), - "convert_to": self.__convert_to, - "bitrate": self.__bitrate - })) + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="episode", + status="retrying", + retry_count=retries, + seconds_left=retry_delay, + song=self.__song_metadata.get('music', 'Unknown Episode'), + artist=self.__song_metadata.get('artist', 'Unknown Show'), + error=str(e), + url=self.__link, + convert_to=self.__convert_to, + bitrate=self.__bitrate + ) if retries >= max_retries or GLOBAL_RETRY_COUNT >= GLOBAL_MAX_RETRIES: if os.path.exists(self.__song_path): os.remove(self.__song_path) # Clean up partial file - unregister_active_download(self.__song_path) # Unregister it track_name = self.__song_metadata.get('music', 'Unknown Episode') artist_name = self.__song_metadata.get('artist', 'Unknown Show') final_error_msg = f"Maximum retry limit reached for '{track_name}' by '{artist_name}' (local: {max_retries}, global: {GLOBAL_MAX_RETRIES}). Last error: {str(e)}" @@ -941,23 +938,24 @@ class EASY_DW: # Conversion failed. __convert_audio or underlying convert_audio should have cleaned up its own temps. # The original downloaded file (if __convert_audio started from it) might still exist or be the self.__song_path. # Or self.__song_path might be a partially converted file if convert_audio failed mid-way and didn't cleanup perfectly. - logger.error(json.dumps({ - "status": "error", - "action": "convert_audio", - "song": self.__song_metadata.get('music', 'Unknown Episode'), - "artist": self.__song_metadata.get('artist', 'Unknown Show'), - "album": self.__song_metadata.get('album', 'N/A'), - "error": str(conv_e), - "convert_to": self.__convert_to, - "bitrate": self.__bitrate - })) + episode_title = self.__song_metadata.get('music', 'Unknown Episode') + error_message = f"Audio conversion for episode '{episode_title}' failed. Original error: {str(conv_e)}" + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="episode", + status="error", + song=episode_title, + artist=self.__song_metadata.get('artist', 'Unknown Show'), + error=error_message, + url=self.__link, + convert_to=self.__convert_to, + bitrate=self.__bitrate + ) # Attempt to remove self.__song_path, which is the latest known path for this episode if os.path.exists(self.__song_path): os.remove(self.__song_path) unregister_active_download(self.__song_path) # Unregister it as it failed/was removed - episode_title = self.__song_metadata.get('music', 'Unknown Episode') - error_message = f"Audio conversion for episode '{episode_title}' failed. Original error: {str(conv_e)}" logger.error(error_message) if hasattr(self, '_EASY_DW__c_episode') and self.__c_episode: self.__c_episode.success = False @@ -1045,14 +1043,15 @@ class DW_ALBUM: total_tracks = self.__song_metadata.get('nb_tracks', 0) album_id = self.__ids - Download_JOB.report_progress({ - "type": "album", - "artist": album_artist, - "status": "initializing", - "total_tracks": total_tracks, - "title": album_name, - "url": f"https://open.spotify.com/album/{album_id}" - }) + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="album", + artist=album_artist, + status="initializing", + total_tracks=total_tracks, + title=album_name, + url=f"https://open.spotify.com/album/{album_id}", + ) pic_url = self.__song_metadata['image'] # This is URL for spotify image_bytes = request(pic_url).content @@ -1095,7 +1094,7 @@ class DW_ALBUM: album_name_for_log = c_song_metadata.get('album', self.__song_metadata.get('album', 'Unknown Album')) logger.warning( f"In album '{album_name_for_log}', metadata list for key '{key}' is too short. " - f"Expected at least {a + 1} elements for track {a + 1}/{total_tracks} " + f"Expected at least {a + 1} elements for track {a + 1} " f"(list has {len(metadata_value_for_key)}). Assigning None to '{key}' for this track." ) c_song_metadata[key] = None @@ -1158,14 +1157,44 @@ class DW_ALBUM: total_tracks = self.__song_metadata.get('nb_tracks', 0) album_id = self.__ids - Download_JOB.report_progress({ - "type": "album", - "artist": album_artist, - "status": "done", - "total_tracks": total_tracks, - "title": album_name, - "url": f"https://open.spotify.com/album/{album_id}" - }) + successful_tracks = [] + failed_tracks = [] + skipped_tracks = [] + for track in tracks: + track_info = { + "name": track.tags.get('music', 'Unknown Track'), + "artist": track.tags.get('artist', 'Unknown Artist') + } + if getattr(track, 'was_skipped', False): + skipped_tracks.append(track_info) + elif track.success: + successful_tracks.append(track_info) + else: + track_info["reason"] = getattr(track, 'error_message', 'Unknown reason') + failed_tracks.append(track_info) + + summary = { + "successful_tracks": [f"{t['name']} - {t['artist']}" for t in successful_tracks], + "skipped_tracks": [f"{t['name']} - {t['artist']}" for t in skipped_tracks], + "failed_tracks": [{ + "track": f"{t['name']} - {t['artist']}", + "reason": t['reason'] + } for t in failed_tracks], + "total_successful": len(successful_tracks), + "total_skipped": len(skipped_tracks), + "total_failed": len(failed_tracks) + } + + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="album", + artist=album_artist, + status="done", + total_tracks=total_tracks, + title=album_name, + url=f"https://open.spotify.com/album/{album_id}", + summary=summary, + ) return album @@ -1188,14 +1217,15 @@ class DW_PLAYLIST: playlist_id = self.__ids # Report playlist initializing status - Download_JOB.report_progress({ - "type": "playlist", - "owner": playlist_owner, - "status": "initializing", - "total_tracks": total_tracks, - "name": playlist_name, - "url": f"https://open.spotify.com/playlist/{playlist_id}" - }) + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="playlist", + owner=playlist_owner, + status="initializing", + total_tracks=total_tracks, + name=playlist_name, + url=f"https://open.spotify.com/playlist/{playlist_id}", + ) # --- Prepare the m3u playlist file --- playlist_m3u_dir = os.path.join(self.__output_dir, "playlists") @@ -1344,14 +1374,43 @@ class DW_PLAYLIST: total_tracks = self.__json_data.get('tracks', {}).get('total', 0) playlist_id = self.__ids - Download_JOB.report_progress({ - "type": "playlist", - "owner": playlist_owner, - "status": "done", - "total_tracks": total_tracks, - "name": playlist_name, - "url": f"https://open.spotify.com/playlist/{playlist_id}" - }) + successful_tracks = [] + failed_tracks = [] + skipped_tracks = [] + for track in tracks: + track_info = { + "name": track.tags.get('music') or track.tags.get('name', 'Unknown Track'), + "artist": track.tags.get('artist', 'Unknown Artist') + } + if getattr(track, 'was_skipped', False): + skipped_tracks.append(track_info) + elif track.success: + successful_tracks.append(track_info) + else: + track_info["reason"] = getattr(track, 'error_message', 'Unknown reason') + failed_tracks.append(track_info) + + summary = { + "successful_tracks": [f"{t['name']} - {t['artist']}" for t in successful_tracks], + "skipped_tracks": [f"{t['name']} - {t['artist']}" for t in skipped_tracks], + "failed_tracks": [{ + "track": f"{t['name']} - {t['artist']}", + "reason": t['reason'] + } for t in failed_tracks], + "total_successful": len(successful_tracks), + "total_skipped": len(skipped_tracks), + "total_failed": len(failed_tracks) + } + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="playlist", + owner=playlist_owner, + status="done", + total_tracks=total_tracks, + name=playlist_name, + url=f"https://open.spotify.com/playlist/{playlist_id}", + summary=summary, + ) return playlist @@ -1363,36 +1422,27 @@ class DW_EPISODE: self.__preferences = preferences def dw(self) -> Episode: - # Using standardized episode progress format - progress_data = { - "type": "episode", - "song": self.__preferences.song_metadata.get('name', 'Unknown Episode'), - "artist": self.__preferences.song_metadata.get('show', 'Unknown Show'), - "status": "initializing" - } - - # Set URL if available episode_id = self.__preferences.ids - if episode_id: - progress_data["url"] = f"https://open.spotify.com/episode/{episode_id}" - - Download_JOB.report_progress(progress_data) + url = f"https://open.spotify.com/episode/{episode_id}" if episode_id else None + + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="episode", + song=self.__preferences.song_metadata.get('name', 'Unknown Episode'), + artist=self.__preferences.song_metadata.get('show', 'Unknown Show'), + status="initializing", + url=url, + ) episode = EASY_DW(self.__preferences).download_eps() - # Using standardized episode progress format - progress_data = { - "type": "episode", - "song": self.__preferences.song_metadata.get('name', 'Unknown Episode'), - "artist": self.__preferences.song_metadata.get('show', 'Unknown Show'), - "status": "done" - } - - # Set URL if available - episode_id = self.__preferences.ids - if episode_id: - progress_data["url"] = f"https://open.spotify.com/episode/{episode_id}" - - Download_JOB.report_progress(progress_data) + report_progress( + reporter=Download_JOB.progress_reporter, + report_type="episode", + song=self.__preferences.song_metadata.get('name', 'Unknown Episode'), + artist=self.__preferences.song_metadata.get('show', 'Unknown Show'), + status="done", + url=url, + ) return episode diff --git a/deezspot/spotloader/__init__.py b/deezspot/spotloader/__init__.py index 532dfb2..59d1c66 100644 --- a/deezspot/spotloader/__init__.py +++ b/deezspot/spotloader/__init__.py @@ -36,7 +36,7 @@ from deezspot.libutils.others_settings import ( stock_real_time_dl, stock_market ) -from deezspot.libutils.logging_utils import logger, ProgressReporter +from deezspot.libutils.logging_utils import logger, ProgressReporter, report_progress class SpoLogin: def __init__( @@ -61,10 +61,6 @@ class SpoLogin: self.__initialize_session() - def report_progress(self, progress_data): - """Report progress using the configured reporter.""" - self.progress_reporter.report(progress_data) - def __initialize_session(self) -> None: try: session_builder = Session.Builder() @@ -143,10 +139,62 @@ class SpoLogin: return track except MarketAvailabilityError as e: logger.error(f"Track download failed due to market availability: {str(e)}") + if song_metadata: + track_info = { + "name": song_metadata.get("music", "Unknown Track"), + "artist": song_metadata.get("artist", "Unknown Artist"), + } + summary = { + "successful_tracks": [], + "skipped_tracks": [], + "failed_tracks": [{ + "track": f"{track_info['name']} - {track_info['artist']}", + "reason": str(e) + }], + "total_successful": 0, + "total_skipped": 0, + "total_failed": 1 + } + report_progress( + reporter="ProgressReporter", + report_type="track", + song=track_info['name'], + artist=track_info['artist'], + status="failed", + url=link_track, + error=str(e), + summary=summary + ) raise except Exception as e: logger.error(f"Failed to download track: {str(e)}") traceback.print_exc() + if song_metadata: + track_info = { + "name": song_metadata.get("music", "Unknown Track"), + "artist": song_metadata.get("artist", "Unknown Artist"), + } + summary = { + "successful_tracks": [], + "skipped_tracks": [], + "failed_tracks": [{ + "track": f"{track_info['name']} - {track_info['artist']}", + "reason": str(e) + }], + "total_successful": 0, + "total_skipped": 0, + "total_failed": 1 + } + report_progress( + reporter="ProgressReporter", + report_type="track", + song=track_info['name'], + artist=track_info['artist'], + status="failed", + url=link_track, + error=str(e), + summary=summary + ) raise e def download_album(