From dd5a402dcebd79e1efe1b643ec874ade42683271 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Fri, 17 Oct 2025 13:13:19 +0200 Subject: [PATCH] =?UTF-8?q?feat(path):=20=E2=9C=A8=20add=20customizable=20?= =?UTF-8?q?delimiters=20for=20artist=20names=20in=20file=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated `format_path_media` and related functions to accept customizable delimiters for artist names. * Modified `Settings` class to include `filename_delimiter_artist` and `filename_delimiter_album_artist`. * Enhanced `name_builder_artist` and `name_builder_album_artist` functions to utilize the new delimiters. --- tidal_dl_ng/download.py | 33 ++++++++++++--- tidal_dl_ng/helper/path.py | 80 ++++++++++++++++++++++++++++++------- tidal_dl_ng/helper/tidal.py | 27 ++++++++++++- tidal_dl_ng/model/cfg.py | 12 ++++-- 4 files changed, 125 insertions(+), 27 deletions(-) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index a4e4bfc..2d6f6ed 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -653,7 +653,13 @@ class Download: ) file_name_relative: str = format_path_media( - file_template, media, self.settings.data.album_track_num_pad_min, list_position, list_total + file_template, + media, + self.settings.data.album_track_num_pad_min, + list_position, + list_total, + delimiter_artist=self.settings.data.filename_delimiter_artist, + delimiter_album_artist=self.settings.data.filename_delimiter_album_artist, ) path_media_dst: pathlib.Path = ( @@ -671,7 +677,12 @@ class Download: if self.settings.data.symlink_to_track and not isinstance(media, Video): # Compute symlink tracks path, sanitize and check if file exists - file_name_track_dir_relative: str = format_path_media(self.settings.data.format_track, media) + file_name_track_dir_relative: str = format_path_media( + self.settings.data.format_track, + media, + delimiter_artist=self.settings.data.filename_delimiter_artist, + delimiter_album_artist=self.settings.data.filename_delimiter_album_artist, + ) path_media_track_dir: pathlib.Path = ( pathlib.Path(self.path_base).expanduser() / (file_name_track_dir_relative + file_extension_dummy) ).absolute() @@ -947,7 +958,12 @@ class Download: pathlib.Path: Destination path. """ # Compute tracks path, sanitize and ensure path exists - file_name_relative: str = format_path_media(self.settings.data.format_track, media) + file_name_relative: str = format_path_media( + self.settings.data.format_track, + media, + delimiter_artist=self.settings.data.filename_delimiter_artist, + delimiter_album_artist=self.settings.data.filename_delimiter_album_artist, + ) path_media_dst: pathlib.Path = ( pathlib.Path(self.path_base).expanduser() / (file_name_relative + file_extension) ).absolute() @@ -1315,7 +1331,12 @@ class Download: tuple[str, str, str, list, bool]: (file_name_relative, list_media_name, list_media_name_short, items, progress_stdout) """ # Create file name and path - file_name_relative: str = format_path_media(file_template, media) + file_name_relative: str = format_path_media( + file_template, + media, + delimiter_artist=self.settings.data.filename_delimiter_artist, + delimiter_album_artist=self.settings.data.filename_delimiter_album_artist, + ) # Get the name of the list and check, if videos should be included. list_media_name: str = name_builder_title(media) @@ -1522,8 +1543,8 @@ class Download: if sort_alphabetically: path_tracks.sort() elif not is_album: - # If it is not an album sort by modification time - path_tracks.sort(key=lambda x: os.path.getmtime(x)) + # If it is not an album sort by creation time + path_tracks.sort(key=lambda x: os.path.getctime(x)) # Write data to m3u file with path_playlist.open(mode="w", encoding="utf-8") as f: diff --git a/tidal_dl_ng/helper/path.py b/tidal_dl_ng/helper/path.py index 13bf88a..d6c23b0 100644 --- a/tidal_dl_ng/helper/path.py +++ b/tidal_dl_ng/helper/path.py @@ -87,7 +87,25 @@ def format_path_media( album_track_num_pad_min: int = 0, list_pos: int = 0, list_total: int = 0, + delimiter_artist: str = ", ", + delimiter_album_artist: str = ", ", ) -> str: + """Formats a media path string using a template and media attributes. + + Replaces placeholders in the format template with sanitized media attribute values to generate a valid file path. + + Args: + fmt_template (str): The format template string containing placeholders. + media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract values from. + album_track_num_pad_min (int, optional): Minimum padding for track numbers. Defaults to 0. + list_pos (int, optional): Position in a list. Defaults to 0. + list_total (int, optional): Total items in a list. Defaults to 0. + delimiter_artist (str, optional): Delimiter for artist names. Defaults to ", ". + delimiter_album_artist (str, optional): Delimiter for album artist names. Defaults to ", ". + + Returns: + str: The formatted and sanitized media path string. + """ result = fmt_template # Search track format template for placeholder. @@ -96,7 +114,15 @@ def format_path_media( for _matchNum, match in enumerate(matches, start=1): template_str = match.group() - result_fmt = format_str_media(match.group(1), media, album_track_num_pad_min, list_pos, list_total) + result_fmt = format_str_media( + match.group(1), + media, + album_track_num_pad_min, + list_pos, + list_total, + delimiter_artist=delimiter_artist, + delimiter_album_artist=delimiter_album_artist, + ) if result_fmt != match.group(1): # Sanitize here, in case of the filename has slashes or something, which will be recognized later as a directory separator. @@ -115,18 +141,24 @@ def format_str_media( album_track_num_pad_min: int = 0, list_pos: int = 0, list_total: int = 0, + delimiter_artist: str = ", ", + delimiter_album_artist: str = ", ", ) -> str: - """Format a string based on media attributes. + """Formats a string for media attributes based on the provided name. + + Attempts to format the given name using a sequence of formatter functions, returning the first successful result. Args: - name (str): The format template name. - media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object. - album_track_num_pad_min (int): Minimum padding for track numbers. Defaults to 0. - list_pos (int): Position in a list. Defaults to 0. - list_total (int): Total items in a list. Defaults to 0. + name (str): The format string name to process. + media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract values from. + album_track_num_pad_min (int, optional): Minimum padding for track numbers. Defaults to 0. + list_pos (int, optional): Position in a list. Defaults to 0. + list_total (int, optional): Total items in a list. Defaults to 0. + delimiter_artist (str, optional): Delimiter for artist names. Defaults to ", ". + delimiter_album_artist (str, optional): Delimiter for album artist names. Defaults to ", ". Returns: - str: The formatted string. + str: The formatted string for the media attribute, or the original name if no formatter matches. """ try: # Try each formatter function in sequence @@ -139,7 +171,15 @@ def format_str_media( _format_metadata, _format_volumes, ): - result = formatter(name, media, album_track_num_pad_min, list_pos, list_total) + result = formatter( + name, + media, + album_track_num_pad_min, + list_pos, + list_total, + delimiter_artist=delimiter_artist, + delimiter_album_artist=delimiter_album_artist, + ) if result is not None: return result except Exception as e: @@ -152,6 +192,8 @@ def format_str_media( def _format_artist_names( name: str, media: Track | Album | Playlist | UserPlaylist | Video | Mix, + delimiter_artist: str = ", ", + delimiter_album_artist: str = ", ", *_args, ) -> str | None: """Handle artist name-related format strings. @@ -166,13 +208,13 @@ def _format_artist_names( """ if name == "artist_name" and isinstance(media, Track | Video): if hasattr(media, "artists"): - return name_builder_artist(media) + return name_builder_artist(media, delimiter=delimiter_artist) elif hasattr(media, "artist"): return media.artist.name elif name == "album_artist": return name_builder_album_artist(media, first_only=True) elif name == "album_artists": - return name_builder_album_artist(media) + return name_builder_album_artist(media, delimiter=delimiter_album_artist) return None @@ -208,20 +250,28 @@ def _format_titles( def _format_names( name: str, media: Track | Album | Playlist | UserPlaylist | Video | Mix, - *_args, + *args, + delimiter_artist: str = ", ", + delimiter_album_artist: str = ", ", ) -> str | None: - """Handle name-related format strings. + """Handles name-related format strings for media. + + Tries to format the provided name as an artist or title, returning the first matching result. Args: name (str): The format string name to check. media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract name information from. - *_args (Any): Additional arguments (not used). + *args: Additional arguments (not used). + delimiter_artist (str, optional): Delimiter for artist names. Defaults to ", ". + delimiter_album_artist (str, optional): Delimiter for album artist names. Defaults to ", ". Returns: str | None: The formatted name or None if the format string is not name-related. """ # First try artist name formats - result = _format_artist_names(name, media) + result = _format_artist_names( + name, media, delimiter_artist=delimiter_artist, delimiter_album_artist=delimiter_album_artist + ) if result is not None: return result diff --git a/tidal_dl_ng/helper/tidal.py b/tidal_dl_ng/helper/tidal.py index 22d8f00..4a06d21 100644 --- a/tidal_dl_ng/helper/tidal.py +++ b/tidal_dl_ng/helper/tidal.py @@ -10,11 +10,34 @@ from tidal_dl_ng.constants import FAVORITES, MediaType from tidal_dl_ng.helper.exceptions import MediaUnknown -def name_builder_artist(media: Track | Video | Album, delimiter: str = "; ") -> str: +def name_builder_artist(media: Track | Video | Album, delimiter: str = ", ") -> str: + """Builds a string of artist names for a track, video, or album. + + Returns a delimited string of all artist names associated with the given media. + + Args: + media (Track | Video | Album): The media object to extract artist names from. + delimiter (str, optional): The delimiter to use between artist names. Defaults to ", ". + + Returns: + str: A delimited string of artist names. + """ return delimiter.join(artist.name for artist in media.artists) -def name_builder_album_artist(media: Track | Album, first_only: bool = False, delimiter: str = "; ") -> str: +def name_builder_album_artist(media: Track | Album, first_only: bool = False, delimiter: str = ", ") -> str: + """Builds a string of main album artist names for a track or album. + + Returns a delimited string of main artist names from the album, optionally including only the first main artist. + + Args: + media (Track | Album): The media object to extract artist names from. + first_only (bool, optional): If True, only the first main artist is included. Defaults to False. + delimiter (str, optional): The delimiter to use between artist names. Defaults to ", ". + + Returns: + str: A delimited string of main album artist names. + """ artists_tmp: [str] = [] artists: [Artist] = media.album.artists if isinstance(media, Track) else media.artists diff --git a/tidal_dl_ng/model/cfg.py b/tidal_dl_ng/model/cfg.py index 22e8aa8..dc3b69d 100644 --- a/tidal_dl_ng/model/cfg.py +++ b/tidal_dl_ng/model/cfg.py @@ -50,8 +50,10 @@ class Settings: window_y: int = 50 window_w: int = 1200 window_h: int = 800 - metadata_delimiter_artist: str = "; " - metadata_delimiter_album_artist: str = "; " + metadata_delimiter_artist: str = ", " + metadata_delimiter_album_artist: str = ", " + filename_delimiter_artist: str = ", " + filename_delimiter_album_artist: str = ", " @dataclass_json @@ -111,8 +113,10 @@ class HelpSettings: window_y: str = "Y-Coordinate of saved window location." window_w: str = "Width of saved window size." window_h: str = "Height of saved window size." - metadata_delimiter_artist: str = "Metadata tag delimiter for multiple artists. Default: '; '" - metadata_delimiter_album_artist: str = "Metadata tag delimiter for multiple album artists. Default: '; '" + metadata_delimiter_artist: str = "Metadata tag delimiter for multiple artists. Default: ', '" + metadata_delimiter_album_artist: str = "Metadata tag delimiter for multiple album artists. Default: ', '" + filename_delimiter_artist: str = "Filename delimiter for multiple artists. Default: ', '" + filename_delimiter_album_artist: str = "Filename delimiter for multiple album artists. Default: ', '" @dataclass_json