diff --git a/orpheus/core.py b/orpheus/core.py index 25a80e5..7c3f32f 100644 --- a/orpheus/core.py +++ b/orpheus/core.py @@ -629,5 +629,36 @@ def orpheus_core_download(orpheus_session: Orpheus, media_to_download, third_par # Show "no tracks deferred" message only for multiple track downloads downloader.print('No tracks were deferred due to rate limiting.', drop_level=0) print() # Add blank line after message + + + minutes = int(downloader.total_download_time // 60) + seconds = downloader.total_download_time % 60 + if downloader.total_download_time > 0: + time_str = f"{minutes}m {seconds:.1f}s" + else: + time_str = f"{seconds:.1f}s" + print() + print("=== TOTAL COUNTS ===") + print(f"=== Download time: {time_str}") + print(f'=== {downloader.track_download_count} tracks downloaded ===') + print(f'=== {downloader.track_skipped_count} tracks skipped ===') + print() + + if (downloader.track_not_streamable_count > 0) or (downloader.track_download_failed_count): + albumsSet = set(downloader.albums_with_failed_tracks) + print("=== ERRORS ===") + # if(downloader.track_not_streamable_count > 0): + # print(f'=== {downloader.track_not_streamable_count} tracks not streamable ===') + # for album in set(downloader.tracks_not_streamable): + # print(f' https://play.qobuz.com/album/{album}') + if(downloader.track_download_failed_count > 0): + print(f'=== {downloader.track_download_failed_count} tracks failed on {len(albumsSet)} albums ===') + for album in albumsSet: + print(f' https://play.qobuz.com/album/{album}') + print() + else: + print("=== NO ERRORS ===") + print() + if os.path.exists('temp'): shutil.rmtree('temp') \ No newline at end of file diff --git a/orpheus/music_downloader.py b/orpheus/music_downloader.py index dba171c..dda8dd3 100644 --- a/orpheus/music_downloader.py +++ b/orpheus/music_downloader.py @@ -315,6 +315,16 @@ def __init__(self, settings, module_controls, oprinter, path, third_party_module self.print = self.oprinter.oprint self.set_indent_number = self.oprinter.set_indent_number + self.track_download_count = 0 + self.track_skipped_count = 0 + + self.track_not_streamable_count = 0 + self.tracks_not_streamable = [] + + self.track_download_failed_count = 0 + self.albums_with_failed_tracks = [] + + self.total_download_time: float = 0 self._download_error_log_path = None self._download_error_log_context = None self._download_error_count = 0 @@ -1694,6 +1704,10 @@ async def bounded_download(index, args): actual_already_existed = sum(1 for r in results if r and r[2] is None and r[1] is None) # Already existed actual_failed = sum(1 for r in results if r and r[2] is not None) # Failed with error + self.track_download_count += actual_downloaded + self.track_skipped_count += actual_already_existed + self.track_download_failed_count += actual_failed + # Show final summary only when there are failures if actual_failed > 0: # Check if most failures are SoundCloud FFmpeg-related @@ -2715,6 +2729,12 @@ def download_album(self, album_id, artist_name='', path=None, indent_level=1, ex # Download tracks concurrently results = self._concurrent_download_tracks(album_info.tracks, download_args_list, concurrent_downloads, performance_summary_indent=0) + for result in results: + if(isinstance(result[2], Exception)): + # self.albums_with_failed_tracks.append(album_info.album_id) + # self.albums_with_failed_tracks.append(album_info.album_id) + self.albums_with_failed_tracks.append(list(album_info.track_extra_kwargs['data'].values())[result[0]]['album']['id']) + # Process results and collect rate-limited tracks # (Errors are already reported by concurrent download progress monitor) rate_limited_tracks = [] @@ -3722,6 +3742,18 @@ def return_with_blank_line(value, failure_reason=None): track_info = self._ensure_track_info_id(track_info, track_id) self._apply_album_context_to_track(track_info, safe_extra_kwargs) + if track_info.error: + if "is not streamable" in track_info.error: + print(f'NOT STREAMABLE: {track_info}') + self.track_not_streamable_count +=1 + self.tracks_not_streamable.append(track_info.album_id) + else: + self.track_download_failed_count +=1 + self.albums_with_failed_tracks.append(track_info.album_id) + + self.print(track_info.error) + self.print(f'=== Track {track_id} failed ===', drop_level=1) + # If we got track info, break out of retry loop if track_info is not None: break @@ -3931,6 +3963,7 @@ def return_with_blank_line(value, failure_reason=None): symbols = self._get_status_symbols() d_print(f'=== {symbols["skip"]} Track skipped ===', drop_level=header_drop_level) + self.track_skipped_count +=1 return return_with_blank_line("SKIPPED") @@ -4290,6 +4323,7 @@ def return_with_blank_line(value, failure_reason=None): symbols = self._get_status_symbols() d_print(f'=== {symbols["success"]} Track completed ===', drop_level=header_drop_level) + self.track_download_count+=1 # Clean up temporary artwork file if artwork_path and os.path.exists(artwork_path): @@ -4525,8 +4559,8 @@ def _get_artwork_settings(self, module_name = None, is_external = False): covers = self.global_settings.get('covers', {}) save_original = bool(covers.get('save_original_cover_size', False)) return { - 'should_resize': not save_original, - 'resolution': covers.get('external_resolution', 3000) if is_external else covers.get('main_resolution', 1400), + 'should_resize': ModuleFlags.needs_cover_resize in self.module_settings[module_name].flags, + 'resolution': self.global_settings['covers']['external_resolution'] if is_external else self.global_settings['covers']['main_resolution'], 'compression': self.global_settings['covers']['external_compression'] if is_external else self.global_settings['covers']['main_compression'], 'format': self.global_settings['covers']['external_format'] if is_external else 'jpg' } diff --git a/orpheus/tagging.py b/orpheus/tagging.py index 47735ff..4d9a8fa 100644 --- a/orpheus/tagging.py +++ b/orpheus/tagging.py @@ -16,6 +16,7 @@ from mutagen.oggopus import OggOpus from mutagen.oggvorbis import OggVorbis from mutagen.oggvorbis import OggVorbisHeaderError +from datetime import date import mutagen from utils.exceptions import * @@ -24,6 +25,7 @@ # Needed for Windows tagging support MP4Tags._padding = 0 +today = date.today() def _resize_image_if_needed(image_path: str, max_size_bytes: int = 16 * 1024 * 1024, target_resolution: tuple = (3000, 3000)) -> str: @@ -156,6 +158,11 @@ def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_lis if 'encoder' in tagger.tags: del tagger.tags['encoder'] + tagger['title'] = track_info.name + if track_info.album: tagger['album'] = track_info.album + if track_info.tags.album_artists: tagger['albumartist'] = track_info.tags.album_artists + tagger['artist'] = track_info.artists + if container == ContainerEnum.m4a or container == ContainerEnum.mp4: # Raw MP4 atom names for standard tags tagger['\xa9nam'] = [track_info.name] @@ -185,6 +192,10 @@ def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_lis else: tagger['artist'] = metadata_separator.join(track_info.artists) if isinstance(track_info.artists, list) else track_info.artists + tagger['title'] = track_info.name + if track_info.album: tagger['album'] = track_info.album + if track_info.tags.album_artists: tagger['albumartist'] = track_info.tags.album_artists + tagger['artist'] = track_info.artists tn = track_info.tags.track_number tt = track_info.tags.total_tracks dn = track_info.tags.disc_number @@ -320,6 +331,9 @@ def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_lis if track_info.tags.description and (container == ContainerEnum.m4a or container == ContainerEnum.mp4): tagger['desc'] = [track_info.tags.description] + tagger['comment'] = 'Qobuz OrpheusDL ' + today.strftime("%m/%d/%y") + tagger['source'] = 'Qobuz' + # add comment tag if track_info.tags.comment: if container == ContainerEnum.m4a or container == ContainerEnum.mp4: diff --git a/utils/models.py b/utils/models.py index a86ae62..aa5856a 100644 --- a/utils/models.py +++ b/utils/models.py @@ -265,6 +265,7 @@ def get_gui_handler(self, handler_name: str): @dataclass class Tags: album_artist: Optional[str] = None + album_artists: Optional[list] = None composer: Optional[str] = None track_number: Optional[int] = None total_tracks: Optional[int] = None