From 3b12f92bd2f06091cd7a2318397384e726c9a121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Dudzi=C5=84ski?= <56404247+oskvr37@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:51:15 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Added=20config=20and=20flag=20for?= =?UTF-8?q?=20saving=20m3u=20file=20(#158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Added `save_playlist_m3u` flag * ✨ Changed scan path flag to `--scan-path` * ♻️ Refactored, edited logs --- tiddl/cli/download/__init__.py | 19 +++++++++++++----- tiddl/config.py | 1 + tiddl/utils.py | 36 +++++++++++++++++++++++----------- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/tiddl/cli/download/__init__.py b/tiddl/cli/download/__init__.py index 25f8e5e..2d4286c 100644 --- a/tiddl/cli/download/__init__.py +++ b/tiddl/cli/download/__init__.py @@ -96,10 +96,17 @@ from typing import List, Union help="Enable downloading videos", ) @click.option( - "--scan_path", + "--scan-path", "SCAN_PATH", type=str, - help="Base music directory to scan for existing. Default is 'path'", + help="Base directory to scan for existing tracks. Default is 'path'", +) +@click.option( + "--save-m3u", + "-m3u", + "SAVE_M3U", + is_flag=True, + help="Save M3U file for playlists.", ) @passContext def DownloadCommand( @@ -113,6 +120,7 @@ def DownloadCommand( EMBED_LYRICS: bool, DOWNLOAD_VIDEO: bool, SCAN_PATH: str | None, + SAVE_M3U: bool, ): """Download resources""" DOWNLOAD_VIDEO = DOWNLOAD_VIDEO or ctx.obj.config.download.download_video @@ -131,6 +139,7 @@ def DownloadCommand( EMBED_LYRICS, DOWNLOAD_VIDEO, SCAN_PATH, + SAVE_M3U, ) ) @@ -371,7 +380,7 @@ def DownloadCommand( offset += album_items.limit def handleResource(resource: TidalResource) -> None: - logging.debug(f"Handling Resource '{resource}'") + logging.debug(f"'{resource}'") match resource.type: case "track": @@ -428,7 +437,7 @@ def DownloadCommand( case "playlist": playlist = api.getPlaylist(resource.id) - logging.info(f"Playlist {playlist.title!r}") + logging.info(f"downloading playlist {playlist.title!r}") offset = 0 playlist_path = None playlist_tracks: dict[str, Track] = {} @@ -462,7 +471,7 @@ def DownloadCommand( path = Path(PATH) if PATH else ctx.obj.config.download.path - if playlist_path: + if playlist_path and SAVE_M3U: savePlaylistM3U( playlist_tracks=playlist_tracks, path=path / playlist_path, diff --git a/tiddl/config.py b/tiddl/config.py index 5ca007f..60a224e 100644 --- a/tiddl/config.py +++ b/tiddl/config.py @@ -31,6 +31,7 @@ class DownloadConfig(BaseModel): embed_lyrics: bool = False download_video: bool = False scan_path: Path | None = path + save_playlist_m3u: bool = False class AuthConfig(BaseModel): diff --git a/tiddl/utils.py b/tiddl/utils.py index f976957..76ee06d 100644 --- a/tiddl/utils.py +++ b/tiddl/utils.py @@ -3,6 +3,7 @@ import os import logging from ffmpeg_asyncio import FFmpeg +from ffmpeg_asyncio.types import Option as FFmpegOption from pydantic import BaseModel from urllib.parse import urlparse @@ -184,7 +185,6 @@ def findTrackFilename( return full_file_name - async def convertFileExtension( source_file: Path, extension: str, @@ -210,9 +210,11 @@ async def convertFileExtension( logging.debug("Conversion not required, already %s", extension) return source_file - ffmpeg_args = {"loglevel": "error"} + ffmpeg_args: dict[str, FFmpegOption | None] = {"loglevel": "error"} + if copy_audio: ffmpeg_args["acodec"] = "copy" + if is_video: ffmpeg_args["vcodec"] = "copy" @@ -220,21 +222,23 @@ async def convertFileExtension( logging.debug("Trying conversion") ffmpeg = FFmpeg().option("y") ffmpeg.input(str(source_file)) - ffmpeg.output(str(output_file), **ffmpeg_args) + ffmpeg.output(str(output_file), ffmpeg_args) @ffmpeg.on("completed") def on_completed(): - logging.debug("Conversion successful for: %s", output_file) + logging.debug(f"converted {output_file}") if remove_source: try: os.remove(source_file) except OSError as e: - logging.error(f"Error removing source file {source_file}: {e}") + logging.error(f"can't remove source file {source_file}: {e}") await ffmpeg.execute() + except Exception as e: - logging.error(f"FFMPEG Error during conversion of {source_file}: {e}") + logging.error(f"can't convert file {source_file}: {e}") return source_file + return output_file @@ -242,13 +246,23 @@ def savePlaylistM3U( playlist_tracks: dict[str, Track], path: Path, filename="playlist.m3u" ): file = path / filename + logging.debug(f"saving m3u file at {file}") if not playlist_tracks: + logging.warning(f"playlist {file} is empty") return - with file.open("w", encoding="utf-8") as f: - f.write("#EXTM3U\n") - for track_path, track in playlist_tracks.items(): - f.write( - f"#EXTINF:{track.duration},{track.artist.name} - {track.title}\n{track_path}\n" + try: + with file.open("w", encoding="utf-8") as f: + f.write("#EXTM3U\n") + for track_path, track in playlist_tracks.items(): + f.write( + f"#EXTINF:{track.duration},{track.artist.name if track.artist else ''} - {track.title}\n{track_path}\n" + ) + + logging.debug( + f"saved m3u file as {file} with {len(playlist_tracks)} tracks" ) + + except Exception as e: + logging.error(f"can't save playlist m3u file: {e}")