From 3465885d52c62a19c0e7cacc8d265f29fe547d68 Mon Sep 17 00:00:00 2001 From: oskvr37 Date: Fri, 9 Aug 2024 18:56:07 +0200 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20#29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tiddl/__init__.py | 64 ++++++++++++++++++++++++++++++++++++++--------- tiddl/download.py | 46 ++++++++++++++++++++++++++++++++++ tiddl/utils.py | 11 +++++--- 3 files changed, 106 insertions(+), 15 deletions(-) diff --git a/tiddl/__init__.py b/tiddl/__init__.py index 0900c57..528b4de 100644 --- a/tiddl/__init__.py +++ b/tiddl/__init__.py @@ -6,7 +6,7 @@ from .api import TidalApi from .auth import getDeviceAuth, getToken, refreshToken from .config import Config, HOME_DIRECTORY -from .download import downloadTrackStream, downloadCover +from .download import downloadTrackStream, Cover from .parser import QUALITY_ARGS, parser from .types import TRACK_QUALITY, TrackQuality, Track from .utils import ( @@ -19,6 +19,8 @@ initLogging, ) +SAVE_COVER = True + def main(): args = parser.parse_args() @@ -121,7 +123,12 @@ def main(): ) def downloadTrack( - track: Track, file_template: str, skip_existing=True, sleep=False, playlist="" + track: Track, + file_template: str, + skip_existing=True, + sleep=False, + playlist="", + cover_data=b"", ) -> tuple[str, str]: file_dir, file_name = formatFilename(file_template, track, playlist) @@ -138,7 +145,11 @@ def downloadTrack( if sleep: sleep_time = randint(5, 15) / 10 + 1 logger.info(f"sleeping for {sleep_time}s") - time.sleep(sleep_time) + try: + time.sleep(sleep_time) + except KeyboardInterrupt: + logger.info("stopping...") + exit() stream = api.getTrackStream(track["id"], track_quality) quality = TRACK_QUALITY[stream["audioQuality"]] @@ -162,13 +173,14 @@ def downloadTrack( stream["manifest"], stream["manifestMimeType"], ) - try: - setMetadata(track_path, track) - except ValueError as e: - logger.error(f"setMetadata error: {e}") track_path = convertToFlac(track_path) + try: + setMetadata(track_path, track, cover_data) + except ValueError as e: + logger.error(f"could not set metadata. {e}") + logger.info(f"track saved as {track_path}") return file_dir, file_name @@ -182,6 +194,8 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): album_items = api.getAlbumItems(album_id, limit=100) file_dir = "" + cover = Cover(album["cover"]) + for item in album_items["items"]: track = item["item"] file_dir, file_name = downloadTrack( @@ -189,10 +203,12 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): file_template=config["settings"]["album_template"], skip_existing=skip_existing, sleep=True, + cover_data=cover.content, ) - if file_dir: - downloadCover(album["cover"], f"{download_path}/{file_dir}") + # spaghetti + if SAVE_COVER and file_dir: + cover.save(f"{download_path}/{file_dir}") skip_existing = not args.no_skip failed_input = [] @@ -215,9 +231,24 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): match input_type: case "track": track = api.getTrack(input_id) - downloadTrack( - track, file_template=track_template, skip_existing=skip_existing + cover = Cover(track["album"]["cover"]) + + file_dir, file_name = downloadTrack( + track, + file_template=track_template, + skip_existing=skip_existing, + cover_data=cover.content, ) + + # spaghetti + if SAVE_COVER and file_dir: + cover.save(f"{download_path}/{file_dir}") + + # saving cover as `cover.jpg` does not make sense + # as it will be overwrited with other track covers. + # we would need to save it as `{track_id}.jpg` + # but is this feature needed? + continue case "album": @@ -243,9 +274,14 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): playlist = api.getPlaylist(input_id) logger.info(f"playlist: {playlist['title']} ({playlist['url']})") + cover = Cover(playlist["squareImage"]) + playlist_items = api.getPlaylistItems(input_id) + + file_dir = "" + for item in playlist_items["items"]: - downloadTrack( + file_dir, file_name = downloadTrack( item["item"], file_template=config["settings"]["playlist_template"], skip_existing=skip_existing, @@ -253,6 +289,10 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): playlist=playlist["title"], ) + # spaghetti + if SAVE_COVER and file_dir: + cover.save(f"{download_path}/{file_dir}") + continue case _: diff --git a/tiddl/download.py b/tiddl/download.py index b2b20cb..7558474 100644 --- a/tiddl/download.py +++ b/tiddl/download.py @@ -243,3 +243,49 @@ def downloadCover(uid: str, path: str, size=640): f.write(req.content) except FileNotFoundError as e: logger.error(f"could not save cover. {file} -> {e}") + + +class Cover: + def __init__(self, uid: str, size=1280) -> None: + if size > 1280: + logger.warning( + f"can not set cover size higher than 1280 (user set: {size})" + ) + size = 1280 + + self.uid = uid + + formatted_uid = uid.replace("-", "/") + self.url = ( + f"https://resources.tidal.com/images/{formatted_uid}/{size}x{size}.jpg" + ) + + logger.debug((self.uid, self.url)) + self.content = self.get() + + def get(self) -> bytes: + req = requests.get(self.url) + + if req.status_code != 200: + logger.error(f"could not download cover. ({req.status_code}) {self.url}") + return b"" + + logger.debug("got cover") + + return req.content + + def save(self, path: str): + logger.debug(path) + + if not self.content: + logger.error("cover file content is empty") + return + + file = f"{path}/cover.jpg" + + try: + with open(file, "wb") as f: + logger.debug(file) + f.write(self.content) + except FileNotFoundError as e: + logger.error(f"could not save cover. {file} -> {e}") diff --git a/tiddl/utils.py b/tiddl/utils.py index 9387827..6b8d67e 100644 --- a/tiddl/utils.py +++ b/tiddl/utils.py @@ -4,7 +4,7 @@ import subprocess from typing import TypedDict, Literal, List, get_args -from mutagen.flac import FLAC as MutagenFLAC +from mutagen.flac import FLAC as MutagenFLAC, Picture from mutagen.easymp4 import EasyMP4 as MutagenMP4 from .types.track import Track @@ -83,13 +83,18 @@ def loadingSymbol(i: int, text: str): print(f"\r{text} {symbol}", end="\r") -def setMetadata(file_path: str, track: Track): +def setMetadata(file_path: str, track: Track, cover_data=b""): _, extension = os.path.splitext(file_path) if extension == ".flac": metadata = MutagenFLAC(file_path) + if cover_data: + picture = Picture() + picture.data = cover_data + metadata.add_picture(picture) elif extension == ".m4a": metadata = MutagenMP4(file_path) + # i dont know if there is a way to add cover for m4a file else: raise ValueError(f"Unknown file extension: {extension}") @@ -124,7 +129,7 @@ def convertToFlac(source_path: str, remove_source=True): if source_extension != ".m4a": return source_path - logger.info(f"converting `{source_path}` to FLAC") + logger.debug(f"converting `{source_path}` to FLAC") command = ["ffmpeg", "-i", source_path, dest_path] result = subprocess.run( command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL From fc29c5370bcfbdf13a72746c829789f0a7c66982 Mon Sep 17 00:00:00 2001 From: oskvr37 Date: Sat, 10 Aug 2024 17:58:46 +0200 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=90=9B=20add=20mime=20type=20to=20cov?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tiddl/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tiddl/utils.py b/tiddl/utils.py index 6b8d67e..51f3eba 100644 --- a/tiddl/utils.py +++ b/tiddl/utils.py @@ -91,6 +91,7 @@ def setMetadata(file_path: str, track: Track, cover_data=b""): if cover_data: picture = Picture() picture.data = cover_data + picture.mime = "image/jpeg" metadata.add_picture(picture) elif extension == ".m4a": metadata = MutagenMP4(file_path) From 23e849739767136073aa2c175d0cec9ddeb16c81 Mon Sep 17 00:00:00 2001 From: oskvr37 Date: Sat, 10 Aug 2024 18:04:59 +0200 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=90=9B=20add=20correct=20size=20for?= =?UTF-8?q?=20playlists=20cover?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tiddl/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tiddl/__init__.py b/tiddl/__init__.py index 528b4de..4f08645 100644 --- a/tiddl/__init__.py +++ b/tiddl/__init__.py @@ -274,13 +274,16 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): playlist = api.getPlaylist(input_id) logger.info(f"playlist: {playlist['title']} ({playlist['url']})") - cover = Cover(playlist["squareImage"]) + cover = Cover( + playlist["squareImage"], 1080 + ) # playlists have 1080x1080 size playlist_items = api.getPlaylistItems(input_id) file_dir = "" for item in playlist_items["items"]: + # tracks arent getting cover when downloaded from playlists file_dir, file_name = downloadTrack( item["item"], file_template=config["settings"]["playlist_template"], From 7e59440429ec77a4e54bbfb0a9f0a3173afe28f5 Mon Sep 17 00:00:00 2001 From: oskvr37 Date: Sun, 11 Aug 2024 23:37:28 +0200 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=90=9B=20fix=20#32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tiddl/utils.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tiddl/utils.py b/tiddl/utils.py index 51f3eba..4fe60f7 100644 --- a/tiddl/utils.py +++ b/tiddl/utils.py @@ -58,7 +58,7 @@ def formatFilename(template: str, track: Track, playlist=""): } dirs = template.split("/") - filename = dirs.pop().format(**formatted_track) + filename = sanitizeFileName(dirs.pop().format(**formatted_track)) template_without_filename = "/".join(dirs) formatted_dir = template_without_filename.format(**formatted_track) @@ -70,11 +70,20 @@ def formatFilename(template: str, track: Track, playlist=""): def sanitizeDirName(dir_name: str): # replace invalid characters with an underscore - sanitized_dir = re.sub(r'[<>:"|?*]', "_", dir_name) + sanitized = re.sub(r'[<>:"|?*]', "_", dir_name) # strip whitespace - sanitized_dir = sanitized_dir.strip() + sanitized = sanitized.strip() - return sanitized_dir + return sanitized + + +def sanitizeFileName(file_name: str): + # replace invalid characters with an underscore + sanitized = re.sub(r'[<>:"|?*/\\]', "_", file_name) + # strip whitespace + sanitized = sanitized.strip() + + return sanitized def loadingSymbol(i: int, text: str): From e1382934077f12b4d90d3ffab0b50f8d8d8add8f Mon Sep 17 00:00:00 2001 From: oskvr37 Date: Mon, 12 Aug 2024 20:27:08 +0200 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9C=A8=20close=20#33?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tiddl/__init__.py | 33 +++++++++++++-------------------- tiddl/download.py | 16 +++++++++++----- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/tiddl/__init__.py b/tiddl/__init__.py index 4f08645..3069898 100644 --- a/tiddl/__init__.py +++ b/tiddl/__init__.py @@ -132,9 +132,6 @@ def downloadTrack( ) -> tuple[str, str]: file_dir, file_name = formatFilename(file_template, track, playlist) - # it will stop detecting existing file for other extensions. - # we need to store track `id + quality` in metadata to differentiate tracks - # TODO: create better existing file detecting ✨ file_path = f"{download_path}/{file_dir}/{file_name}" if skip_existing and ( os.path.isfile(file_path + ".m4a") or os.path.isfile(file_path + ".flac") @@ -192,9 +189,7 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): # i dont know if limit 100 is suspicious # but i will leave it here album_items = api.getAlbumItems(album_id, limit=100) - file_dir = "" - - cover = Cover(album["cover"]) + album_cover = Cover(album["cover"]) for item in album_items["items"]: track = item["item"] @@ -203,12 +198,11 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): file_template=config["settings"]["album_template"], skip_existing=skip_existing, sleep=True, - cover_data=cover.content, + cover_data=album_cover.content, ) - # spaghetti - if SAVE_COVER and file_dir: - cover.save(f"{download_path}/{file_dir}") + if SAVE_COVER: + album_cover.save(f"{download_path}/{file_dir}") skip_existing = not args.no_skip failed_input = [] @@ -240,8 +234,7 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): cover_data=cover.content, ) - # spaghetti - if SAVE_COVER and file_dir: + if SAVE_COVER: cover.save(f"{download_path}/{file_dir}") # saving cover as `cover.jpg` does not make sense @@ -274,27 +267,27 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): playlist = api.getPlaylist(input_id) logger.info(f"playlist: {playlist['title']} ({playlist['url']})") - cover = Cover( + playlist_cover = Cover( playlist["squareImage"], 1080 ) # playlists have 1080x1080 size playlist_items = api.getPlaylistItems(input_id) - file_dir = "" - for item in playlist_items["items"]: - # tracks arent getting cover when downloaded from playlists + track = item["item"] + cover = Cover(track["album"]["cover"]) + file_dir, file_name = downloadTrack( - item["item"], + track, file_template=config["settings"]["playlist_template"], skip_existing=skip_existing, sleep=True, playlist=playlist["title"], + cover_data=cover.content, ) - # spaghetti - if SAVE_COVER and file_dir: - cover.save(f"{download_path}/{file_dir}") + if SAVE_COVER: + playlist_cover.save(f"{download_path}/{file_dir}") continue diff --git a/tiddl/download.py b/tiddl/download.py index 7558474..6d0d0d3 100644 --- a/tiddl/download.py +++ b/tiddl/download.py @@ -226,7 +226,13 @@ def downloadTrackStream( return file_path -def downloadCover(uid: str, path: str, size=640): +def downloadCover(uid: str, path: str, size=1280): + file = f"{path}/cover.jpg" + + if os.path.isfile(file): + logger.debug(f"cover already exists ({file})") + return + formatted_uid = uid.replace("-", "/") url = f"https://resources.tidal.com/images/{formatted_uid}/{size}x{size}.jpg" @@ -236,8 +242,6 @@ def downloadCover(uid: str, path: str, size=640): logger.error(f"could not download cover. ({req.status_code}) {url}") return - file = f"{path}/cover.jpg" - try: with open(file, "wb") as f: f.write(req.content) @@ -275,14 +279,16 @@ def get(self) -> bytes: return req.content def save(self, path: str): - logger.debug(path) - if not self.content: logger.error("cover file content is empty") return file = f"{path}/cover.jpg" + if os.path.isfile(file): + logger.debug(f"cover already exists ({file})") + return + try: with open(file, "wb") as f: logger.debug(file) From e591253adab4ca1ea465051fb6e8be320df94f1f Mon Sep 17 00:00:00 2001 From: oskvr37 Date: Mon, 12 Aug 2024 20:40:43 +0200 Subject: [PATCH 6/7] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20dont=20download=20cove?= =?UTF-8?q?r=20if=20track=20already=20exists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tiddl/__init__.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/tiddl/__init__.py b/tiddl/__init__.py index 3069898..ee58645 100644 --- a/tiddl/__init__.py +++ b/tiddl/__init__.py @@ -173,6 +173,10 @@ def downloadTrack( track_path = convertToFlac(track_path) + if not cover_data: + cover = Cover(track["album"]["cover"]) + cover_data = cover.content + try: setMetadata(track_path, track, cover_data) except ValueError as e: @@ -225,23 +229,13 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): match input_type: case "track": track = api.getTrack(input_id) - cover = Cover(track["album"]["cover"]) - file_dir, file_name = downloadTrack( + downloadTrack( track, file_template=track_template, skip_existing=skip_existing, - cover_data=cover.content, ) - if SAVE_COVER: - cover.save(f"{download_path}/{file_dir}") - - # saving cover as `cover.jpg` does not make sense - # as it will be overwrited with other track covers. - # we would need to save it as `{track_id}.jpg` - # but is this feature needed? - continue case "album": @@ -275,7 +269,6 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): for item in playlist_items["items"]: track = item["item"] - cover = Cover(track["album"]["cover"]) file_dir, file_name = downloadTrack( track, @@ -283,7 +276,6 @@ def downloadAlbum(album_id: str | int, skip_existing: bool): skip_existing=skip_existing, sleep=True, playlist=playlist["title"], - cover_data=cover.content, ) if SAVE_COVER: From 02bcce0806cf84f4f66a4defcdc64a14f093d025 Mon Sep 17 00:00:00 2001 From: oskvr37 Date: Mon, 12 Aug 2024 20:42:44 +0200 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=9A=80=20bump=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 58372e0..03ace49 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="tiddl", - version="1.6.0", + version="1.7.0", description="TIDDL (Tidal Downloader) is a Python CLI application that allows downloading Tidal tracks.", long_description=open('README.md', encoding="utf-8").read(), long_description_content_type='text/markdown',