diff --git a/tidal_dl_ng/helper/path.py b/tidal_dl_ng/helper/path.py index 61e2dae..12ffdce 100644 --- a/tidal_dl_ng/helper/path.py +++ b/tidal_dl_ng/helper/path.py @@ -116,119 +116,282 @@ def format_str_media( list_pos: int = 0, list_total: int = 0, ) -> str: - result: str = name + """Format a string based on media attributes. + 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. + + Returns: + str: The formatted string. + """ try: - match name: - case "artist_name": - if isinstance(media, Track | Video): - if hasattr(media, "artists"): - result = name_builder_artist(media) - elif hasattr(media, "artist"): - result = media.artist.name - case "album_artist": - result = name_builder_album_artist(media, first_only=True) - case "album_artists": - result = name_builder_album_artist(media) - case "track_title": - if isinstance(media, Track | Video): - result = name_builder_title(media) - case "mix_name": - if isinstance(media, Mix): - result = media.title - case "playlist_name": - if isinstance(media, Playlist | UserPlaylist): - result = media.name - case "album_title": - if isinstance(media, Album): - result = media.name - elif isinstance(media, Track): - result = media.album.name - case "album_track_num": - if isinstance(media, Track | Video): - result = calculate_number_padding( - album_track_num_pad_min, - media.track_num, - media.album.num_tracks if hasattr(media, "album") else 1, - ) - case "album_num_tracks": - if isinstance(media, Track | Video): - result = str(media.album.num_tracks if hasattr(media, "album") else 1) - case "track_id": - if isinstance(media, Track | Video): - result = str(media.id) - case "playlist_id": - if isinstance(media, Playlist): - result = str(media.id) - case "album_id": - if isinstance(media, Album): - result = str(media.id) - elif isinstance(media, Track): - result = str(media.album.id) - case "track_duration_seconds": - if isinstance(media, Track | Video): - result = str(media.duration) - case "track_duration_minutes": - if isinstance(media, Track | Video): - m, s = divmod(media.duration, 60) - result = f"{m:01d}:{s:02d}" - case "album_duration_seconds": - if isinstance(media, Album): - result = str(media.duration) - case "album_duration_minutes": - if isinstance(media, Album): - m, s = divmod(media.duration, 60) - result = f"{m:01d}:{s:02d}" - case "playlist_duration_seconds": - if isinstance(media, Album): - result = str(media.duration) - case "playlist_duration_minutes": - if isinstance(media, Album): - m, s = divmod(media.duration, 60) - result = f"{m:01d}:{s:02d}" - case "album_year": - if isinstance(media, Album): - result = str(media.year) - elif isinstance(media, Track): - result = str(media.album.year) - case "video_quality": - if isinstance(media, Video): - result = media.video_quality - case "track_quality": - if isinstance(media, Track): - result = ", ".join(tag for tag in media.media_metadata_tags if tag is not None) - case "track_explicit": - if isinstance(media, Track | Video): - result = FORMAT_TEMPLATE_EXPLICIT if media.explicit else "" - case "album_explicit": - if isinstance(media, Album): - result = FORMAT_TEMPLATE_EXPLICIT if media.explicit else "" - case "album_num_volumes": - if isinstance(media, Album): - result = str(media.num_volumes) - case "track_volume_num": - if isinstance(media, Track | Video): - result = str(media.volume_num) - case "track_volume_num_optional": - if isinstance(media, Track | Video): - num_volumes: int = media.album.num_volumes if hasattr(media, "album") else 1 - result = "" if num_volumes == 1 else str(media.volume_num) - case "track_volume_num_optional_CD": - if isinstance(media, Track | Video): - num_volumes: int = media.album.num_volumes if hasattr(media, "album") else 1 - result = "" if num_volumes == 1 else f"CD{media.volume_num!s}" - case "isrc": - if isinstance(media, Track): - result = media.isrc - case "list_pos": - if isinstance(media, Track | Video): - # TODO: Rename `album_track_num_pad_min` globally. - result = calculate_number_padding(album_track_num_pad_min, list_pos, list_total) + # Try each formatter function in sequence + for formatter in ( + _format_names, + _format_numbers, + _format_ids, + _format_durations, + _format_metadata, + _format_volumes, + ): + result = formatter(name, media, album_track_num_pad_min, list_pos, list_total) + if result is not None: + return result except Exception as e: # TODO: Implement better exception logging. print(e) - return result + return name + + +def _format_artist_names( + name: str, + media: Track | Album | Playlist | UserPlaylist | Video | Mix, + *_args, +) -> str | None: + """Handle artist name-related format strings. + + Args: + name (str): The format string name to check. + media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract artist information from. + *_args (Any): Additional arguments (not used). + + Returns: + str | None: The formatted artist name or None if the format string is not artist-related. + """ + if name == "artist_name" and isinstance(media, Track | Video): + if hasattr(media, "artists"): + return name_builder_artist(media) + 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 None + + +def _format_titles( + name: str, + media: Track | Album | Playlist | UserPlaylist | Video | Mix, + *_args, +) -> str | None: + """Handle title-related format strings. + + Args: + name (str): The format string name to check. + media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract title information from. + *_args (Any): Additional arguments (not used). + + Returns: + str | None: The formatted title or None if the format string is not title-related. + """ + if name == "track_title" and isinstance(media, Track | Video): + return name_builder_title(media) + elif name == "mix_name" and isinstance(media, Mix): + return media.title + elif name == "playlist_name" and isinstance(media, Playlist | UserPlaylist): + return media.name + elif name == "album_title": + if isinstance(media, Album): + return media.name + elif isinstance(media, Track): + return media.album.name + return None + + +def _format_names( + name: str, + media: Track | Album | Playlist | UserPlaylist | Video | Mix, + *_args, +) -> str | None: + """Handle name-related format strings. + + 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). + + 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) + if result is not None: + return result + + # Then try title formats + return _format_titles(name, media) + + +def _format_numbers( + name: str, + media: Track | Album | Playlist | UserPlaylist | Video | Mix, + album_track_num_pad_min: int, + list_pos: int, + list_total: int, + *_args, +) -> str | None: + """Handle number-related format strings. + + Args: + name (str): The format string name to check. + media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract number information from. + album_track_num_pad_min (int): Minimum padding for track numbers. + list_pos (int): Position in a list. + list_total (int): Total items in a list. + *_args (Any): Additional arguments (not used). + + Returns: + str | None: The formatted number or None if the format string is not number-related. + """ + if name == "album_track_num" and isinstance(media, Track | Video): + return calculate_number_padding( + album_track_num_pad_min, + media.track_num, + media.album.num_tracks if hasattr(media, "album") else 1, + ) + elif name == "album_num_tracks" and isinstance(media, Track | Video): + return str(media.album.num_tracks if hasattr(media, "album") else 1) + elif name == "list_pos" and isinstance(media, Track | Video): + # TODO: Rename `album_track_num_pad_min` globally. + return calculate_number_padding(album_track_num_pad_min, list_pos, list_total) + return None + + +def _format_ids( + name: str, + media: Track | Album | Playlist | UserPlaylist | Video | Mix, + *_args, +) -> str | None: + """Handle ID-related format strings. + + Args: + name (str): The format string name to check. + media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract ID information from. + *_args (Any): Additional arguments (not used). + + Returns: + str | None: The formatted ID or None if the format string is not ID-related. + """ + # Handle track and playlist IDs + if (name == "track_id" and isinstance(media, Track | Video)) or ( + name == "playlist_id" and isinstance(media, Playlist) + ): + return str(media.id) + # Handle album IDs + elif name == "album_id": + if isinstance(media, Album): + return str(media.id) + elif isinstance(media, Track): + return str(media.album.id) + # Handle ISRC + elif name == "isrc" and isinstance(media, Track): + return media.isrc + return None + + +def _format_durations( + name: str, + media: Track | Album | Playlist | UserPlaylist | Video | Mix, + *_args, +) -> str | None: + """Handle duration-related format strings. + + Args: + name (str): The format string name to check. + media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract duration information from. + *_args (Any): Additional arguments (not used). + + Returns: + str | None: The formatted duration or None if the format string is not duration-related. + """ + # Format track durations + if name == "track_duration_seconds" and isinstance(media, Track | Video): + return str(media.duration) + elif name == "track_duration_minutes" and isinstance(media, Track | Video): + m, s = divmod(media.duration, 60) + return f"{m:01d}:{s:02d}" + + # Format album durations + elif name == "album_duration_seconds" and isinstance(media, Album): + return str(media.duration) + elif name == "album_duration_minutes" and isinstance(media, Album): + m, s = divmod(media.duration, 60) + return f"{m:01d}:{s:02d}" + + # Format playlist durations + elif name == "playlist_duration_seconds" and isinstance(media, Album): + return str(media.duration) + elif name == "playlist_duration_minutes" and isinstance(media, Album): + m, s = divmod(media.duration, 60) + return f"{m:01d}:{s:02d}" + + # Format year + elif name == "album_year": + if isinstance(media, Album): + return str(media.year) + elif isinstance(media, Track): + return str(media.album.year) + return None + + +def _format_metadata( + name: str, + media: Track | Album | Playlist | UserPlaylist | Video | Mix, + *_args, +) -> str | None: + """Handle metadata-related format strings. + + Args: + name (str): The format string name to check. + media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract metadata information from. + *_args (Any): Additional arguments (not used). + + Returns: + str | None: The formatted metadata or None if the format string is not metadata-related. + """ + if name == "video_quality" and isinstance(media, Video): + return media.video_quality + elif name == "track_quality" and isinstance(media, Track): + return ", ".join(tag for tag in media.media_metadata_tags if tag is not None) + elif (name == "track_explicit" and isinstance(media, Track | Video)) or ( + name == "album_explicit" and isinstance(media, Album) + ): + return FORMAT_TEMPLATE_EXPLICIT if media.explicit else "" + return None + + +def _format_volumes( + name: str, + media: Track | Album | Playlist | UserPlaylist | Video | Mix, + *_args, +) -> str | None: + """Handle volume-related format strings. + + Args: + name (str): The format string name to check. + media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract volume information from. + *_args (Any): Additional arguments (not used). + + Returns: + str | None: The formatted volume information or None if the format string is not volume-related. + """ + if name == "album_num_volumes" and isinstance(media, Album): + return str(media.num_volumes) + elif name == "track_volume_num" and isinstance(media, Track | Video): + return str(media.volume_num) + elif name == "track_volume_num_optional" and isinstance(media, Track | Video): + num_volumes: int = media.album.num_volumes if hasattr(media, "album") else 1 + return "" if num_volumes == 1 else str(media.volume_num) + elif name == "track_volume_num_optional_CD" and isinstance(media, Track | Video): + num_volumes: int = media.album.num_volumes if hasattr(media, "album") else 1 + return "" if num_volumes == 1 else f"CD{media.volume_num!s}" + return None def calculate_number_padding(padding_minimum: int, item_position: int, items_max: int) -> str: @@ -350,12 +513,12 @@ def path_file_uniquify(path_file: pathlib.Path) -> pathlib.Path: return path_file -def file_unique_suffix(path_file: pathlib.Path, seperator: str = "_") -> str: +def file_unique_suffix(path_file: pathlib.Path, separator: str = "_") -> str: """Generate a unique suffix for a file path. Args: path_file (pathlib.Path): The file path to check for uniqueness. - seperator (str, optional): The separator to use for the suffix. Defaults to "_". + separator (str, optional): The separator to use for the suffix. Defaults to "_". Returns: str: The unique suffix, or an empty string if not needed. @@ -367,7 +530,7 @@ def file_unique_suffix(path_file: pathlib.Path, seperator: str = "_") -> str: while check_file_exists(path_file_tmp) and count < UNIQUIFY_THRESHOLD: count += 1 - unique_suffix = seperator + str(count).zfill(threshold_zfill) + unique_suffix = separator + str(count).zfill(threshold_zfill) path_file_tmp = path_file.parent / (path_file.stem + unique_suffix + path_file.suffix) return unique_suffix @@ -386,31 +549,26 @@ def check_file_exists(path_file: pathlib.Path, extension_ignore: bool = False) - if extension_ignore: path_file_stem: str = pathlib.Path(path_file).stem path_parent: pathlib.Path = pathlib.Path(path_file).parent - path_files: [str] = [] + path_files: list[str] = [] - for extension in AudioExtensions: - path_files.append(str(path_parent.joinpath(path_file_stem + extension))) + path_files.extend(str(path_parent.joinpath(path_file_stem + extension)) for extension in AudioExtensions) else: - path_files: [str] = [path_file] + path_files: list[str] = [str(path_file)] - result = any(os.path.isfile(_file) for _file in path_files) - - return result + return any(os.path.isfile(_file) for _file in path_files) -def resource_path(relative_path): +def resource_path(relative_path: str) -> str: """Get the absolute path to a resource. Args: - relative_path (str): The relative path to the resource. + relative_path: The relative path to the resource. Returns: str: The absolute path to the resource. """ - try: - base_path = sys._MEIPASS - except Exception: - base_path = os.path.abspath(".") + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = getattr(sys, "_MEIPASS", os.path.abspath(".")) return os.path.join(base_path, relative_path)