diff --git a/tiddl/cli/download/__init__.py b/tiddl/cli/download/__init__.py index ceeb81e..886fbb9 100644 --- a/tiddl/cli/download/__init__.py +++ b/tiddl/cli/download/__init__.py @@ -10,6 +10,7 @@ from ..ctx import Context, passContext from tiddl.download import downloadTrackStream from tiddl.models import TrackArg, ARG_TO_QUALITY, Track, PlaylistTrack, Album from tiddl.utils import formatTrack, trackExists +from tiddl.metadata import addMetadata, Cover @click.command("download") @@ -33,7 +34,7 @@ def DownloadCommand( api = ctx.obj.getApi() - def downloadTrack(track: Track, file_name: str) -> None: + def downloadTrack(track: Track, file_name: str, cover_data=b"") -> None: if not track.allowStreaming: click.echo( f"{click.style('✖', 'yellow')} Track {click.style(file_name, 'yellow')} does not allow streaming" @@ -66,12 +67,21 @@ def DownloadCommand( with full_path.open("wb") as f: f.write(stream_data) + # TODO: add track credits fetching to fill more metadata + + if not cover_data and track.album.cover: + cover_data = Cover(track.album.cover).content + + addMetadata(full_path, track, cover_data) + def downloadAlbum(album: Album): click.echo(f"★ Album {album.title}") # TODO: fetch all items album_items = api.getAlbumItems(album.id, limit=100) + cover_data = Cover(album.cover).content if album.cover else b"" + for item in album_items.items: if isinstance(item.item, Track): track = item.item @@ -82,7 +92,7 @@ def DownloadCommand( album_artist=album.artist.name, ) - downloadTrack(track=track, file_name=file_name) + downloadTrack(track=track, file_name=file_name, cover_data=cover_data) for resource in ctx.obj.resources: match resource.type: diff --git a/tiddl/metadata.py b/tiddl/metadata.py new file mode 100644 index 0000000..8f8a3b7 --- /dev/null +++ b/tiddl/metadata.py @@ -0,0 +1,100 @@ +import logging +import requests + +from pathlib import Path + +from mutagen.flac import FLAC as MutagenFLAC, Picture +from mutagen.easymp4 import EasyMP4 as MutagenEasyMP4 +from mutagen.mp4 import MP4Cover, MP4 as MutagenMP4 + +from tiddl.models import Track + + +logger = logging.getLogger(__name__) + + +def addMetadata(track_path: Path, track: Track, cover_data=b""): + extension = track_path.suffix + + if extension == ".flac": + metadata = MutagenFLAC(track_path) + if cover_data: + picture = Picture() + picture.data = cover_data + picture.mime = "image/jpeg" + metadata.add_picture(picture) + elif extension == ".m4a": + if cover_data: + metadata = MutagenMP4(track_path) + metadata["covr"] = [MP4Cover(cover_data, imageformat=MP4Cover.FORMAT_JPEG)] + metadata.save(track_path) + metadata = MutagenEasyMP4(track_path) + else: + raise ValueError(f"Unknown file extension: {extension}") + + new_metadata: dict[str, str] = { + "title": track.title, + "trackNumber": str(track.trackNumber), + "discnumber": str(track.volumeNumber), + "copyright": track.copyright, + "albumartist": track.artist.name if track.artist else "", + "artist": ";".join([artist.name.strip() for artist in track.artists]), + "album": track.album.title, + "date": str(track.streamStartDate) if track.streamStartDate else "", + } + + metadata.update(new_metadata) + + try: + metadata.save(track_path) + except Exception as e: + logger.error(f"Failed to set metadata for {extension}: {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(f"got cover: {self.uid}") + + return req.content + + def save(self, directory_path: Path): + if not self.content: + logger.error("cover file content is empty") + return + + file = directory_path / "cover.jpg" + + if file.exists(): + logger.debug(f"cover already exists ({file})") + return + + try: + with file.open("wb") as f: + f.write(self.content) + + except FileNotFoundError as e: + logger.error(f"could not save cover. {file} -> {e}")