From b844e8f739e03e2e9590caf4daaec593da00e83e Mon Sep 17 00:00:00 2001 From: Xoconoch Date: Wed, 20 Aug 2025 10:30:08 -0500 Subject: [PATCH] feat: added pad_number_width, which determines the width of the padding when pad_tracks is set to True. It has an auto mode that automatically determines padding based on parent context (playlist/auto) --- deezspot/deezloader/__download__.py | 19 ++++++++- deezspot/deezloader/__init__.py | 51 ++++++++++++++++--------- deezspot/libutils/utils.py | 26 ++++++++----- deezspot/models/download/preferences.py | 1 + deezspot/spotloader/__download__.py | 24 +++++++++++- deezspot/spotloader/__init__.py | 12 ++++-- 6 files changed, 99 insertions(+), 34 deletions(-) diff --git a/deezspot/deezloader/__download__.py b/deezspot/deezloader/__download__.py index 79f5f69..11fa1d8 100644 --- a/deezspot/deezloader/__download__.py +++ b/deezspot/deezloader/__download__.py @@ -381,6 +381,18 @@ class EASY_DW: custom_track_format = getattr(self.__preferences, 'custom_track_format', None) pad_tracks = getattr(self.__preferences, 'pad_tracks', True) self.__song_metadata_dict['artist_separator'] = getattr(self.__preferences, 'artist_separator', '; ') + # Determine pad width (supports 'auto' mode) based on context + pad_number_width = None + try: + pnw = getattr(self.__preferences, 'pad_number_width', None) + if isinstance(pnw, str) and pnw.lower() == 'auto': + total = getattr(self.__preferences, 'total_tracks', None) + if isinstance(total, int) and total and total > 0: + pad_number_width = max(2, len(str(total))) + elif isinstance(pnw, int) and pnw >= 1: + pad_number_width = pnw + except Exception: + pad_number_width = None # Inject playlist placeholders if in playlist context try: if self.__parent == 'playlist' and hasattr(self.__preferences, 'json_data') and self.__preferences.json_data: @@ -404,7 +416,8 @@ class EASY_DW: self.__file_format, custom_dir_format=custom_dir_format, custom_track_format=custom_track_format, - pad_tracks=pad_tracks + pad_tracks=pad_tracks, + pad_number_width=pad_number_width ) def __set_episode_path(self) -> None: @@ -1282,6 +1295,8 @@ class DW_ALBUM: c_preferences.track_number = a + 1 # For progress reporting only c_preferences.total_tracks = total_tracks c_preferences.link = f"https://deezer.com/track/{c_preferences.ids}" + # Propagate pad width preference + c_preferences.pad_number_width = getattr(self.__preferences, 'pad_number_width', 'auto') # Inject Spotify per-track object if available without extra API calls if self.__use_spotify and spotify_tracks_in_order: spo_track_for_tag = None @@ -1456,6 +1471,8 @@ class DW_PLAYLIST: c_preferences.total_tracks = total_tracks c_preferences.json_data = self.__preferences.json_data c_preferences.link = f"https://deezer.com/track/{c_preferences.ids}" + # Propagate pad width preference + c_preferences.pad_number_width = getattr(self.__preferences, 'pad_number_width', 'auto') current_track_object = None try: diff --git a/deezspot/deezloader/__init__.py b/deezspot/deezloader/__init__.py index 97928b7..86886ba 100644 --- a/deezspot/deezloader/__init__.py +++ b/deezspot/deezloader/__init__.py @@ -139,7 +139,8 @@ class DeeLogin: market=stock_market, playlist_context=None, artist_separator: str = "; ", - spotify_metadata: bool = False + spotify_metadata: bool = False, + pad_number_width: int | str = 'auto' ) -> Track: link_is_valid(link_track) @@ -215,6 +216,7 @@ class DeeLogin: preferences.artist_separator = artist_separator preferences.spotify_metadata = bool(spotify_metadata) preferences.spotify_track_obj = playlist_context.get('spotify_track_obj') if (playlist_context and playlist_context.get('spotify_track_obj')) else None + preferences.pad_number_width = pad_number_width if playlist_context: preferences.json_data = playlist_context.get('json_data') @@ -252,7 +254,8 @@ class DeeLogin: playlist_context=None, artist_separator: str = "; ", spotify_metadata: bool = False, - spotify_album_obj=None + spotify_album_obj=None, + pad_number_width: int | str = 'auto' ) -> Album: link_is_valid(link_album) @@ -302,6 +305,7 @@ class DeeLogin: preferences.artist_separator = artist_separator preferences.spotify_metadata = bool(spotify_metadata) preferences.spotify_album_obj = spotify_album_obj + preferences.pad_number_width = pad_number_width if playlist_context: preferences.json_data = playlist_context['json_data'] @@ -335,7 +339,8 @@ class DeeLogin: bitrate=None, save_cover=stock_save_cover, market=stock_market, - artist_separator: str = "; " + artist_separator: str = "; ", + pad_number_width: int | str = 'auto' ) -> Playlist: link_is_valid(link_playlist) @@ -370,6 +375,7 @@ class DeeLogin: preferences.save_cover = save_cover preferences.market = market preferences.artist_separator = artist_separator + preferences.pad_number_width = pad_number_width playlist = DW_PLAYLIST(preferences).dw() @@ -388,7 +394,8 @@ class DeeLogin: convert_to=None, bitrate=None, save_cover=stock_save_cover, - market=stock_market + market=stock_market, + pad_number_width: int | str = 'auto' ) -> list[Track]: link_is_valid(link_artist) @@ -408,7 +415,8 @@ class DeeLogin: convert_to=convert_to, bitrate=bitrate, save_cover=save_cover, - market=market + market=market, + pad_number_width=pad_number_width ) for track in top_tracks_json ] @@ -574,7 +582,8 @@ class DeeLogin: market=stock_market, playlist_context=None, artist_separator: str = "; ", - spotify_metadata: bool = False + spotify_metadata: bool = False, + pad_number_width: int | str = 'auto' ) -> Track: link_dee = self.convert_spoty_to_dee_link_track(link_track) @@ -612,7 +621,8 @@ class DeeLogin: market=market, playlist_context=playlist_context, artist_separator=artist_separator, - spotify_metadata=spotify_metadata + spotify_metadata=spotify_metadata, + pad_number_width=pad_number_width ) return track @@ -637,7 +647,9 @@ class DeeLogin: market=stock_market, playlist_context=None, artist_separator: str = "; ", - spotify_metadata: bool = False + spotify_metadata: bool = False, + spotify_album_obj=None, + pad_number_width: int | str = 'auto' ) -> Album: link_dee = self.convert_spoty_to_dee_link_album(link_album) @@ -672,7 +684,8 @@ class DeeLogin: playlist_context=playlist_context, artist_separator=artist_separator, spotify_metadata=spotify_metadata, - spotify_album_obj=spotify_album_obj + spotify_album_obj=spotify_album_obj, + pad_number_width=pad_number_width ) return album @@ -696,7 +709,8 @@ class DeeLogin: save_cover=stock_save_cover, market=stock_market, artist_separator: str = "; ", - spotify_metadata: bool = False + spotify_metadata: bool = False, + pad_number_width: int | str = 'auto' ) -> Playlist: link_is_valid(link_playlist) @@ -832,12 +846,8 @@ class DeeLogin: 'spotify_url': link_track } # Attach Spotify track object for tagging if requested - if spotify_metadata: - try: - from deezspot.spotloader.__spo_api__ import json_to_track_playlist_object - playlist_ctx['spotify_track_obj'] = json_to_track_playlist_object(track_info) - except Exception: - pass + if False: # placeholder, will be handled via spotify_metadata in download_trackspo + pass downloaded_track = self.download_trackspo( link_track, output_dir=output_dir, quality_download=quality_download, @@ -847,7 +857,8 @@ class DeeLogin: initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, max_retries=max_retries, convert_to=convert_to, bitrate=bitrate, save_cover=save_cover, market=market, playlist_context=playlist_ctx, - artist_separator=artist_separator, spotify_metadata=spotify_metadata + artist_separator=artist_separator, spotify_metadata=False, + pad_number_width=pad_number_width ) tracks.append(downloaded_track) @@ -918,7 +929,8 @@ class DeeLogin: bitrate=None, save_cover=stock_save_cover, market=stock_market, - artist_separator: str = "; " + artist_separator: str = "; ", + pad_number_width: int | str = 'auto' ) -> Track: query = f"track:{song} artist:{artist}" @@ -949,7 +961,8 @@ class DeeLogin: bitrate=bitrate, save_cover=save_cover, market=market, - artist_separator=artist_separator + artist_separator=artist_separator, + pad_number_width=pad_number_width ) return track diff --git a/deezspot/libutils/utils.py b/deezspot/libutils/utils.py index fd4980f..70b82bd 100644 --- a/deezspot/libutils/utils.py +++ b/deezspot/libutils/utils.py @@ -135,7 +135,7 @@ def what_kind(link): def __get_tronc(string): return string[:len(string) - 1] -def apply_custom_format(format_str, metadata: dict, pad_tracks=True) -> str: +def apply_custom_format(format_str, metadata: dict, pad_tracks=True, pad_number_width: int | None = None) -> str: def replacer(match): full_key = match.group(1) # e.g., "artist", "ar_album_1" @@ -183,7 +183,7 @@ def apply_custom_format(format_str, metadata: dict, pad_tracks=True) -> str: # Handle None values safely if value is None: - if full_key in ['tracknum', 'discnum']: + if full_key in ['tracknum', 'discnum', 'playlistnum']: value = '1' if full_key == 'discnum' else '0' else: value = '' @@ -196,14 +196,18 @@ def apply_custom_format(format_str, metadata: dict, pad_tracks=True) -> str: if pad_tracks and full_key in ['tracknum', 'discnum', 'playlistnum']: str_value = str(value) - # Pad with leading zero if it's a single digit - if str_value.isdigit() and len(str_value) == 1: - return str_value.zfill(2) + if str_value.isdigit(): + if isinstance(pad_number_width, int) and pad_number_width >= 1: + return str_value.zfill(pad_number_width) + # Default legacy behavior: pad single digits to width 2 + if len(str_value) == 1: + return str_value.zfill(2) + return str_value return str(value) return re.sub(r'%([^%]+)%', replacer, format_str) -def __get_dir(song_metadata, output_dir, custom_dir_format=None, pad_tracks=True): +def __get_dir(song_metadata, output_dir, custom_dir_format=None, pad_tracks=True, pad_number_width: int | None = None): # If custom_dir_format is explicitly empty or None, use output_dir directly if not custom_dir_format: # Ensure output_dir itself exists, as __check_dir won't be called on a subpath @@ -214,7 +218,7 @@ def __get_dir(song_metadata, output_dir, custom_dir_format=None, pad_tracks=True # create directories; slashes from data are sanitized inside components. format_parts = custom_dir_format.split("/") formatted_parts = [ - apply_custom_format(part, song_metadata, pad_tracks) for part in format_parts + apply_custom_format(part, song_metadata, pad_tracks, pad_number_width) for part in format_parts ] sanitized_path_segment = "/".join( sanitize_name(part) for part in formatted_parts @@ -233,14 +237,16 @@ def set_path( is_episode=False, custom_dir_format=None, custom_track_format=None, - pad_tracks=True + pad_tracks=True, + pad_number_width: int | None = None ): # Determine the directory for the song directory = __get_dir( song_metadata, output_dir, custom_dir_format=custom_dir_format, - pad_tracks=pad_tracks + pad_tracks=pad_tracks, + pad_number_width=pad_number_width ) # Determine the filename for the song @@ -262,7 +268,7 @@ def set_path( # Apply the custom format string for the track filename. # pad_tracks is passed along for track/disc numbers in filename. - track_filename_base = apply_custom_format(custom_track_format, effective_metadata, pad_tracks) + track_filename_base = apply_custom_format(custom_track_format, effective_metadata, pad_tracks, pad_number_width) track_filename_base = sanitize_name(track_filename_base) # Add file format (extension) to the filename diff --git a/deezspot/models/download/preferences.py b/deezspot/models/download/preferences.py index d52c07c..76f8b76 100644 --- a/deezspot/models/download/preferences.py +++ b/deezspot/models/download/preferences.py @@ -17,6 +17,7 @@ class Preferences: self.custom_dir_format = None self.custom_track_format = None self.pad_tracks = True # Default to padded track numbers (01, 02, etc.) + self.pad_number_width = 'auto' # New: number of digits for padding; 'auto' uses total tracks self.initial_retry_delay = 30 # Default initial retry delay in seconds self.retry_delay_increase = 30 # Default increase in delay between retries in seconds self.max_retries = 5 # Default maximum number of retries per track diff --git a/deezspot/spotloader/__download__.py b/deezspot/spotloader/__download__.py index 1e2d8d5..394ca1a 100644 --- a/deezspot/spotloader/__download__.py +++ b/deezspot/spotloader/__download__.py @@ -145,6 +145,25 @@ class EASY_DW: pad_tracks = getattr(self.__preferences, 'pad_tracks', True) # Ensure the separator is available to formatting utils for indexed placeholders self.__song_metadata_dict['artist_separator'] = getattr(self.__preferences, 'artist_separator', '; ') + # Determine pad width (supports 'auto' mode) + pad_number_width = None + try: + pnw = getattr(self.__preferences, 'pad_number_width', None) + if isinstance(pnw, str) and pnw.lower() == 'auto': + total = None + if self.__parent == 'album' and hasattr(self.__song_metadata, 'album') and getattr(self.__song_metadata.album, 'total_tracks', None): + total = self.__song_metadata.album.total_tracks + elif self.__parent == 'playlist' and hasattr(self.__preferences, 'json_data') and self.__preferences.json_data: + try: + total = self.__preferences.json_data.get('tracks', {}).get('total') + except Exception: + total = None + if isinstance(total, int) and total and total > 0: + pad_number_width = max(2, len(str(total))) + elif isinstance(pnw, int) and pnw >= 1: + pad_number_width = pnw + except Exception: + pad_number_width = None # Inject playlist placeholders if in playlist context try: if self.__parent == 'playlist' and hasattr(self.__preferences, 'json_data') and self.__preferences.json_data: @@ -166,7 +185,8 @@ class EASY_DW: self.__file_format, custom_dir_format=custom_dir_format, custom_track_format=custom_track_format, - pad_tracks=pad_tracks + pad_tracks=pad_tracks, + pad_number_width=pad_number_width ) def __set_episode_path(self) -> None: @@ -1088,6 +1108,7 @@ class DW_ALBUM: c_preferences.link = f"https://open.spotify.com/track/{c_preferences.ids}" # Set album position for progress reporting (not for metadata - that comes from API) c_preferences.track_number = a + 1 + c_preferences.pad_number_width = getattr(self.__preferences, 'pad_number_width', 'auto') track = EASY_DW(c_preferences, parent='album').easy_dw() @@ -1241,6 +1262,7 @@ class DW_PLAYLIST: c_preferences.json_data = self.__json_data c_preferences.track_number = idx + 1 c_preferences.link = f"https://open.spotify.com/track/{c_preferences.ids}" if c_preferences.ids else None + c_preferences.pad_number_width = getattr(self.__preferences, 'pad_number_width', 'auto') easy_dw_instance = EASY_DW(c_preferences, parent='playlist') diff --git a/deezspot/spotloader/__init__.py b/deezspot/spotloader/__init__.py index 9a1b4f2..a92bbf8 100644 --- a/deezspot/spotloader/__init__.py +++ b/deezspot/spotloader/__init__.py @@ -100,7 +100,8 @@ class SpoLogin: bitrate=None, save_cover=stock_save_cover, market: list[str] | None = stock_market, - artist_separator: str = "; " + artist_separator: str = "; ", + pad_number_width: int | str = 'auto' ) -> Track: song_metadata = None try: @@ -140,6 +141,7 @@ class SpoLogin: preferences.save_cover = save_cover preferences.market = market preferences.artist_separator = artist_separator + preferences.pad_number_width = pad_number_width track = DW_TRACK(preferences).dw() @@ -186,7 +188,8 @@ class SpoLogin: bitrate=None, save_cover=stock_save_cover, market: list[str] | None = stock_market, - artist_separator: str = "; " + artist_separator: str = "; ", + pad_number_width: int | str = 'auto' ) -> Album: try: link_is_valid(link_album) @@ -230,6 +233,7 @@ class SpoLogin: preferences.save_cover = save_cover preferences.market = market preferences.artist_separator = artist_separator + preferences.pad_number_width = pad_number_width album = DW_ALBUM(preferences).dw() @@ -262,7 +266,8 @@ class SpoLogin: bitrate=None, save_cover=stock_save_cover, market: list[str] | None = stock_market, - artist_separator: str = "; " + artist_separator: str = "; ", + pad_number_width: int | str = 'auto' ) -> Playlist: try: link_is_valid(link_playlist) @@ -340,6 +345,7 @@ class SpoLogin: preferences.save_cover = save_cover preferences.market = market preferences.artist_separator = artist_separator + preferences.pad_number_width = pad_number_width playlist = DW_PLAYLIST(preferences).dw()