From 0c367884e38aab2846129aa48b4fb4f4573c4c60 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Wed, 10 Apr 2024 18:18:52 +0200 Subject: [PATCH] feat: implemented cover artwork taggin --- .vscode/settings.json | 4 +- music_kraken/audio/metadata.py | 50 +++++++++++++------ music_kraken/connection/connection.py | 19 ++++--- music_kraken/objects/__init__.py | 2 + music_kraken/objects/artwork.py | 4 +- music_kraken/objects/formatted_text.py | 2 +- music_kraken/objects/target.py | 10 +++- music_kraken/pages/abstract.py | 2 +- .../pages/youtube_music/youtube_music.py | 5 +- .../utils/config/config_files/main_config.py | 2 +- pyproject.toml | 1 + 11 files changed, 73 insertions(+), 28 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4ee5e04..675fd7d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,9 +16,11 @@ }, "python.formatting.provider": "none", "cSpell.words": [ + "APIC", "Bandcamp", "dotenv", "levenshtein", - "OKBLUE" + "OKBLUE", + "Referer" ] } \ No newline at end of file diff --git a/music_kraken/audio/metadata.py b/music_kraken/audio/metadata.py index 1ad1969..045638d 100644 --- a/music_kraken/audio/metadata.py +++ b/music_kraken/audio/metadata.py @@ -1,16 +1,20 @@ import mutagen -from mutagen.id3 import ID3, Frame +from mutagen.id3 import ID3, Frame, APIC from pathlib import Path from typing import List import logging +from PIL import Image from ..utils.config import logging_settings from ..objects import Song, Target, Metadata - +from ..connection import Connection LOGGER = logging_settings["tagging_logger"] +artwork_connection: Connection = Connection() + + class AudioMetadata: def __init__(self, file_location: str = None) -> None: self._file_location = None @@ -52,11 +56,39 @@ class AudioMetadata: file_location = property(fget=lambda self: self._file_location, fset=set_file_location) -def write_metadata_to_target(metadata: Metadata, target: Target): +def write_metadata_to_target(metadata: Metadata, target: Target, song: Song): if not target.exists: + LOGGER.warning(f"file {target.file_path} not found") return id3_object = AudioMetadata(file_location=target.file_path) + + if song.artwork.best_variant is not None: + r = artwork_connection.get( + url=song.artwork.best_variant["url"], + disable_cache=False, + ) + + temp_target: Target = Target.temp() + with temp_target.open("wb") as f: + f.write(r.content) + + converted_target: Target = Target.temp(name=f"{song.title}.jpeg") + with Image.open(temp_target.file_path) as img: + img.save(converted_target.file_path, "JPEG") + + id3_object.frames.add( + APIC( + encoding=3, + mime="image/jpeg", + type=3, + desc="Cover", + data=converted_target.read_bytes(), + ) + ) + + mutagen_file = mutagen.File(target.file_path) + id3_object.add_metadata(metadata) id3_object.save() @@ -70,19 +102,9 @@ def write_metadata(song: Song, ignore_file_not_found: bool = True): else: raise ValueError(f"{song.target.file} not found") - id3_object = AudioMetadata(file_location=target.file_path) - id3_object.add_song_metadata(song=song) - id3_object.save() + write_metadata_to_target(metadata=song.metadata, target=target, song=song) def write_many_metadata(song_list: List[Song]): for song in song_list: write_metadata(song=song, ignore_file_not_found=True) - - -if __name__ == "__main__": - print("called directly") - filepath = "/home/lars/Music/deathcore/Archspire/Bleed the Future/Bleed the Future.mp3" - - audio_metadata = AudioMetadata(file_location=filepath) - print(audio_metadata.frames.pprint()) diff --git a/music_kraken/connection/connection.py b/music_kraken/connection/connection.py index 991c712..41b5c07 100644 --- a/music_kraken/connection/connection.py +++ b/music_kraken/connection/connection.py @@ -23,7 +23,7 @@ from ..utils.hacking import merge_args class Connection: def __init__( self, - host: str, + host: str = None, proxies: List[dict] = None, tries: int = (len(main_settings["proxies"]) + 1) * main_settings["tries_per_proxy"], timeout: int = 7, @@ -45,7 +45,7 @@ class Connection: self.HEADER_VALUES = dict() if header_values is None else header_values self.LOGGER = logger - self.HOST = urlparse(host) + self.HOST = host if host is None else urlparse(host) self.TRIES = tries self.TIMEOUT = timeout self.rotating_proxy = RotatingProxy(proxy_list=proxies) @@ -87,22 +87,27 @@ class Connection: time.sleep(interval) def base_url(self, url: ParseResult = None): - if url is None: + if url is None and self.HOST is not None: url = self.HOST return urlunsplit((url.scheme, url.netloc, "", "", "")) def get_header(self, **header_values) -> Dict[str, str]: - return { + headers = { "user-agent": main_settings["user_agent"], "User-Agent": main_settings["user_agent"], "Connection": "keep-alive", - "Host": self.HOST.netloc, - "Referer": self.base_url(), "Accept-Language": main_settings["language"], - **header_values } + if self.HOST is not None: + headers["Host"] = self.HOST.netloc + headers["Referer"] = self.base_url(url=self.HOST) + + headers.update(header_values) + + return headers + def rotate(self): self.session.proxies = self.rotating_proxy.rotate() diff --git a/music_kraken/objects/__init__.py b/music_kraken/objects/__init__.py index b4e75cb..da5b9aa 100644 --- a/music_kraken/objects/__init__.py +++ b/music_kraken/objects/__init__.py @@ -22,4 +22,6 @@ from .contact import Contact from .parents import OuterProxy +from .artwork import Artwork + DatabaseObject = TypeVar('T', bound=OuterProxy) diff --git a/music_kraken/objects/artwork.py b/music_kraken/objects/artwork.py index 94e4b58..4f103ee 100644 --- a/music_kraken/objects/artwork.py +++ b/music_kraken/objects/artwork.py @@ -33,7 +33,7 @@ class Artwork: def _calculate_deviation(*dimensions: List[int]) -> float: return sum(abs(d - main_settings["preferred_artwork_resolution"]) for d in dimensions) / len(dimensions) - def append(self, url: str, width: int, height: int) -> None: + def append(self, url: str, width: int, height: int, **kwargs) -> None: self._variant_mapping[hash_url(url=url)] = { "url": url, "width": width, @@ -43,6 +43,8 @@ class Artwork: @property def best_variant(self) -> ArtworkVariant: + if len(self._variant_mapping) == 0: + return None return min(self._variant_mapping.values(), key=lambda x: x["deviation"]) def __merge__(self, other: Artwork, override: bool = False) -> None: diff --git a/music_kraken/objects/formatted_text.py b/music_kraken/objects/formatted_text.py index 7a828fe..c7ee042 100644 --- a/music_kraken/objects/formatted_text.py +++ b/music_kraken/objects/formatted_text.py @@ -16,7 +16,7 @@ class FormattedText: @property def is_empty(self) -> bool: - return self.doc is None + return self.html == "" def __eq__(self, other) -> False: if type(other) != type(self): diff --git a/music_kraken/objects/target.py b/music_kraken/objects/target.py index 35afd4a..2ba1cfe 100644 --- a/music_kraken/objects/target.py +++ b/music_kraken/objects/target.py @@ -3,11 +3,12 @@ from __future__ import annotations from pathlib import Path from typing import List, Tuple, TextIO, Union import logging - +import random import requests from tqdm import tqdm from .parents import OuterProxy +from ..utils.shared import HIGHEST_ID from ..utils.config import main_settings, logging_settings from ..utils.string_processing import fit_to_file_system @@ -29,6 +30,10 @@ class Target(OuterProxy): _default_factories = { } + @classmethod + def temp(cls, name: str = str(random.randint(0, HIGHEST_ID))) -> P: + return cls(main_settings["temp_directory"] / name) + # This is automatically generated def __init__(self, file_path: Union[Path, str], relative_to_music_dir: bool = False, **kwargs) -> None: if not isinstance(file_path, Path): @@ -106,3 +111,6 @@ class Target(OuterProxy): def delete(self): self.file_path.unlink(missing_ok=True) + + def read_bytes(self) -> bytes: + return self.file_path.read_bytes() diff --git a/music_kraken/pages/abstract.py b/music_kraken/pages/abstract.py index 9547298..818e989 100644 --- a/music_kraken/pages/abstract.py +++ b/music_kraken/pages/abstract.py @@ -464,7 +464,7 @@ class Page: self.post_process_hook(song, temp_target) - write_metadata_to_target(song.metadata, temp_target) + write_metadata_to_target(song.metadata, temp_target, song) r = DownloadResult() diff --git a/music_kraken/pages/youtube_music/youtube_music.py b/music_kraken/pages/youtube_music/youtube_music.py index 31dae1e..c2913ab 100644 --- a/music_kraken/pages/youtube_music/youtube_music.py +++ b/music_kraken/pages/youtube_music/youtube_music.py @@ -20,7 +20,7 @@ from ...utils import get_current_millis from ...utils import dump_to_file -from ...objects import Source, DatabaseObject, ID3Timestamp +from ...objects import Source, DatabaseObject, ID3Timestamp, Artwork from ..abstract import Page from ...objects import ( Artist, @@ -501,6 +501,7 @@ class YoutubeMusic(SuperYouTube): note=ydl_res.get("descriptions"), album_list=album_list, length=int(ydl_res.get("duration", 0)) * 1000, + artwork=Artwork(*ydl_res.get("thumbnails", [])), main_artist_list=[Artist( name=artist_name, source_list=[Source( @@ -551,6 +552,7 @@ class YoutubeMusic(SuperYouTube): return self.download_values_by_url[source.url] + def download_song_to_target(self, source: Source, target: Target, desc: str = None) -> DownloadResult: media = self.fetch_media_url(source) @@ -571,5 +573,6 @@ class YoutubeMusic(SuperYouTube): return result + def __del__(self): self.ydl.__exit__() diff --git a/music_kraken/utils/config/config_files/main_config.py b/music_kraken/utils/config/config_files/main_config.py index 6a55a0a..bae8618 100644 --- a/music_kraken/utils/config/config_files/main_config.py +++ b/music_kraken/utils/config/config_files/main_config.py @@ -27,7 +27,7 @@ The further you choose to be able to go back, the higher the memory usage. EmptyLine(), - Attribute(name="preferred_artwork_resolution", default_value=100), + Attribute(name="preferred_artwork_resolution", default_value=1000), EmptyLine(), diff --git a/pyproject.toml b/pyproject.toml index 0dd521b..3ac5165 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ "pyffmpeg~=2.4.2.18.1", "ffmpeg-progress-yield~=0.7.8", "mutagen~=1.46.0", + "pillow~=10.3.0", "rich~=13.7.1", "mistune~=3.0.2",