Option added to embed and download album art.

This commit is contained in:
Robert Honz
2024-11-11 10:05:32 +01:00
parent 6732c5e15e
commit d7112063a0
6 changed files with 93 additions and 53 deletions
+1 -1
View File
@@ -171,7 +171,7 @@ profile = "black"
multi_line_output = 3
indent = 4
color_output = true
known_third_party = ["Crypto", "PySide6", "coloredlogs", "dataclasses_json", "ffmpeg", "m3u8", "mutagen", "pathvalidate", "requests", "rich", "tidalapi", "toml", "typer"]
known_third_party = ["Crypto", "PySide6", "coloredlogs", "constants", "dataclasses_json", "ffmpeg", "m3u8", "mutagen", "pathvalidate", "requests", "rich", "tidalapi", "toml", "typer"]
[tool.mypy]
# https://mypy.readthedocs.io/en/latest/config_file.html#using-a-pyproject-toml-file
+1
View File
@@ -5,6 +5,7 @@ REQUESTS_TIMEOUT_SEC: int = 45
EXTENSION_LYRICS: str = ".lrc"
UNIQUIFY_THRESHOLD: int = 99
FILENAME_SANITIZE_PLACEHOLDER: str = "_"
COVER_NAME: str = "cover.jpg"
class QualityVideo(StrEnum):
+79 -15
View File
@@ -10,6 +10,7 @@ from uuid import uuid4
import ffmpeg
import m3u8
import requests
from constants import COVER_NAME
from requests.exceptions import HTTPError
from rich.progress import Progress, TaskID
from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video
@@ -189,6 +190,7 @@ class Download:
download_delay: bool = False,
quality_audio: Quality | None = None,
quality_video: QualityVideo | None = None,
is_parent_album: bool = False,
) -> (bool, str):
try:
if media_id and media_type:
@@ -278,10 +280,13 @@ class Download:
tmp_path_file = self._extract_flac(tmp_path_file)
tmp_path_lyrics: pathlib.Path | None = None
tmp_path_cover: pathlib.Path | None = None
# Write metadata to file.
if not isinstance(media, Video):
result_metadata, tmp_path_lyrics = self.metadata_write(media, tmp_path_file)
result_metadata, tmp_path_lyrics, tmp_path_cover = self.metadata_write(
media, tmp_path_file, is_parent_album
)
# Move final file to the configured destination directory.
os.makedirs(os.path.dirname(path_media_dst), exist_ok=True)
@@ -289,7 +294,11 @@ class Download:
# Move lyrics file
if self.settings.data.lyrics_file and not isinstance(media, Video):
self._move_lyrics(path_media_dst, tmp_path_lyrics)
self._move_lyrics(tmp_path_lyrics, path_media_dst)
# Move cover file
if self.settings.data.cover_album_file:
self._move_cover(tmp_path_cover, path_media_dst)
else:
self.fn_logger.debug(f"Download skipped, since file exists: '{path_media_dst}'")
@@ -327,13 +336,13 @@ class Download:
return quality_old
def _move_lyrics(self, file_media_dst: str, path_lyrics: pathlib.Path) -> bool:
def _move_file(self, path_file_source: pathlib.Path, path_file_destination: str | pathlib.Path) -> bool:
result: bool
# Build tmp lyrics filename
# Check if the file was downloaded
if path_lyrics and os.path.isfile(path_lyrics):
if path_file_source and os.path.isfile(path_file_source):
# Move it.
shutil.move(path_lyrics, os.path.splitext(file_media_dst)[0] + EXTENSION_LYRICS)
shutil.move(path_file_source, path_file_destination)
result = True
else:
@@ -341,20 +350,64 @@ class Download:
return result
def lyrics_write_file(self, dir_destination: pathlib.Path, lyrics: str) -> str:
def _move_lyrics(self, path_lyrics: pathlib.Path, file_media_dst: str) -> bool:
# Build tmp lyrics filename
path_file_lyrics: str = os.path.splitext(file_media_dst)[0] + EXTENSION_LYRICS
result: bool = self._move_file(path_lyrics, path_file_lyrics)
return result
def _move_cover(self, path_cover: pathlib.Path, file_media_dst: str) -> bool:
# Build tmp lyrics filename
path_file_cover: pathlib.Path = pathlib.Path(file_media_dst).parent.absolute() / COVER_NAME
result: bool = self._move_file(path_cover, path_file_cover)
return result
def lyrics_to_file(self, dir_destination: pathlib.Path, lyrics: str) -> str:
return self.write_to_tmp_file(dir_destination, mode="x", content=lyrics)
def cover_to_file(self, dir_destination: pathlib.Path, image: bytes) -> str:
return self.write_to_tmp_file(dir_destination, mode="xb", content=image)
def write_to_tmp_file(self, dir_destination: pathlib.Path, mode: str, content: str | bytes) -> str:
result: str = dir_destination / str(uuid4())
encoding: str | None = "utf-8" if isinstance(content, str) else None
try:
with open(result, "x", encoding="utf-8") as f:
f.write(lyrics)
with open(result, mode=mode, encoding=encoding) as f:
f.write(content)
except:
result = ""
return result
def metadata_write(self, track: Track, path_media: str) -> (bool, pathlib.Path | None):
@staticmethod
def cover_data(url: str = None, path_file: str = None) -> str | bytes:
result: str | bytes = ""
if url:
try:
result = requests.get(url, timeout=REQUESTS_TIMEOUT_SEC).content
except Exception as e:
# TODO: Implement propper logging.
print(e)
elif path_file:
try:
with open(path_file, "rb") as f:
result = f.read()
except OSError as e:
# TODO: Implement propper logging.
print(e)
return result
def metadata_write(
self, track: Track, path_media: str, is_parent_album: str
) -> (bool, pathlib.Path | None, pathlib.Path | None):
result: bool = False
path_lyrics: pathlib.Path | None = None
path_cover: pathlib.Path | None = None
release_date: str = (
track.album.available_release_date.strftime("%Y-%m-%d")
if track.album.available_release_date
@@ -363,6 +416,7 @@ class Download:
copy_right: str = track.copyright if hasattr(track, "copyright") and track.copyright else ""
isrc: str = track.isrc if hasattr(track, "isrc") and track.isrc else ""
lyrics: str = ""
cover_data: bytes = None
if self.settings.data.lyrics_embed or self.settings.data.lyrics_file:
# Try to retrieve lyrics.
@@ -379,7 +433,14 @@ class Download:
print(f"Could not retrieve lyrics for `{name_builder_item(track)}`.")
if lyrics and self.settings.data.lyrics_file:
path_lyrics = self.lyrics_write_file(pathlib.Path(path_media).parent, lyrics)
path_lyrics = self.lyrics_to_file(pathlib.Path(path_media).parent, lyrics)
if self.settings.data.metadata_cover_embed or (self.settings.data.cover_album_file and is_parent_album):
url_cover = track.album.image(int(self.settings.data.metadata_cover_dimension))
cover_data = self.cover_data(url=url_cover)
if cover_data and self.settings.data.cover_album_file and is_parent_album:
path_cover = self.cover_to_file(pathlib.Path(path_media).parent, cover_data)
# `None` values are not allowed.
m: Metadata = Metadata(
@@ -396,14 +457,14 @@ class Download:
totaltrack=track.album.num_tracks if track.album and track.album.num_tracks else 1,
totaldisc=track.album.num_volumes if track.album and track.album.num_volumes else 1,
discnumber=track.volume_num if track.volume_num else 1,
url_cover=track.album.image(int(self.settings.data.metadata_cover_dimension)),
cover_data=cover_data if self.settings.data.metadata_cover_embed else None,
)
m.save()
result = True
return result, path_lyrics
return result, path_lyrics, path_cover
def items(
self,
@@ -451,16 +512,19 @@ class Download:
f"[green]List '{list_media_name}'", total=len(items), visible=progress_stdout
)
is_album: bool = isinstance(media, Album)
# Iterate through list items
while not self.progress.finished:
for media in items:
for item_media in items:
# Download the item.
status_download, result_path_file = self.item(
media=media,
media=item_media,
file_template=file_name_relative,
quality_audio=quality_audio,
quality_video=quality_video,
download_delay=download_delay,
is_parent_album=is_album,
)
# Advance progress bar.
+1 -2
View File
@@ -31,7 +31,6 @@ from tidal_dl_ng.helper.tidal import (
search_results_all,
user_media_lists,
)
from tidal_dl_ng.metadata import Metadata
try:
import qdarktheme
@@ -757,7 +756,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
if cover_url and self.cover_url_current != cover_url:
self.cover_url_current = cover_url
data_cover: bytes = Metadata.cover_data(cover_url)
data_cover: bytes = Download.cover_data(cover_url)
pixmap: QtGui.QPixmap = QtGui.QPixmap()
pixmap.loadFromData(data_cover)
self.l_pm_cover.setPixmap(pixmap)
+7 -33
View File
@@ -1,10 +1,7 @@
import mutagen
import requests
from mutagen import flac, id3, mp4
from mutagen.id3 import APIC, TALB, TCOM, TCOP, TDRC, TIT2, TOPE, TPE1, TRCK, TSRC, USLT
from tidal_dl_ng.constants import REQUESTS_TIMEOUT_SEC
class Metadata:
path_file: str
@@ -22,7 +19,7 @@ class Metadata:
isrc: str
lyrics: str
path_cover: str
url_cover: str
cover_data: bytes
m: mutagen.mp4.MP4 | mutagen.mp4.MP4 | mutagen.flac.FLAC
def __init__(
@@ -41,8 +38,7 @@ class Metadata:
albumartist: str = "",
date: str = "",
lyrics: str = "",
path_cover: str = "",
url_cover: str = "",
cover_data: bytes = None,
):
self.path_file = path_file
self.title = title
@@ -58,27 +54,25 @@ class Metadata:
self.composer = composer
self.isrc = isrc
self.lyrics = lyrics
self.path_cover = path_cover
self.url_cover = url_cover
self.cover_data = cover_data
self.m: mutagen.mp4.MP4 | mutagen.flac.FLAC | mutagen.mp3.MP3 = mutagen.File(self.path_file)
def _cover(self) -> bool:
result: bool = False
data_cover: str | bytes = self.cover_data(url=self.url_cover, path_file=self.path_cover)
if data_cover:
if self.cover_data:
if isinstance(self.m, mutagen.flac.FLAC):
flac_cover = flac.Picture()
flac_cover.type = id3.PictureType.COVER_FRONT
flac_cover.data = data_cover
flac_cover.data = self.cover_data
flac_cover.mime = "image/jpeg"
self.m.clear_pictures()
self.m.add_picture(flac_cover)
elif isinstance(self.m, mutagen.mp3.MP3):
self.m.tags.add(APIC(encoding=3, data=data_cover))
self.m.tags.add(APIC(encoding=3, data=self.cover_data))
elif isinstance(self.m, mutagen.mp4.MP4):
cover_mp4 = mp4.MP4Cover(data_cover)
cover_mp4 = mp4.MP4Cover(self.cover_data)
self.m.tags["covr"] = [cover_mp4]
result = True
@@ -142,23 +136,3 @@ class Metadata:
self.m.tags["\xa9wrt"] = ", ".join(self.composer) if self.composer else ""
self.m.tags["\xa9lyr"] = self.lyrics
self.m.tags["isrc"] = self.isrc
@staticmethod
def cover_data(url: str = None, path_file: str = None) -> str | bytes:
result: str | bytes = ""
if url:
try:
result = requests.get(url, timeout=REQUESTS_TIMEOUT_SEC).content
except Exception as e:
# TODO: Implement propper logging.
print(e)
elif path_file:
try:
with open(path_file, "rb") as f:
result = f.read()
except OSError as e:
# TODO: Implement propper logging.
print(e)
return result
+4 -2
View File
@@ -10,8 +10,6 @@ from tidal_dl_ng.constants import CoverDimensions, QualityVideo, SkipExisting
@dataclass
class Settings:
skip_existing: SkipExisting = SkipExisting.Disabled
# TODO: Implement cover download to a separate file.
# album_cover_save: bool = True
lyrics_embed: bool = False
lyrics_file: bool = False
# TODO: Implement API KEY selection.
@@ -36,6 +34,8 @@ class Settings:
video_convert_mp4: bool = True
path_binary_ffmpeg: str = ""
metadata_cover_dimension: CoverDimensions = CoverDimensions.Px320
metadata_cover_embed: bool = True
cover_album_file: bool = True
extract_flac: bool = True
@@ -79,6 +79,8 @@ class HelpSettings:
metadata_cover_dimension: str = (
"The dimensions of the cover image embedded into the track. Possible values: 320x320, 640x640x 1280x1280."
)
metadata_cover_embed: str = "Embed album cover into file."
cover_album_file: str = "Save cover to 'cover.jpg', if an album is downloaded."
extract_flac: str = "Extract FLAC audio tracks from MP4 containers and save them as `*.flac` (uses FFmpeg)."