From 14f58de9e54a42a14d7cfbab3016bbf55d69a51c Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Sat, 9 Aug 2025 23:18:56 +0200 Subject: [PATCH] Add Metadata --- qobuz-dl-remote.py | 51 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 5 deletions(-) mode change 100644 => 100755 qobuz-dl-remote.py diff --git a/qobuz-dl-remote.py b/qobuz-dl-remote.py old mode 100644 new mode 100755 index 77aa8cc..e13ec73 --- a/qobuz-dl-remote.py +++ b/qobuz-dl-remote.py @@ -1,8 +1,13 @@ +#!/usr/bin/env python3 from typing import Any, Literal import requests import tqdm +from mutagen.flac import FLAC, Picture import sys from dataclasses import dataclass +import datetime +from PIL import Image +from io import BytesIO QOBUZ_API_BASE = "https://us.qobuz.squid.wtf/api" QUALITY = 27 @@ -11,7 +16,12 @@ QUALITY = 27 @dataclass class Track: title: str + artist: str + nr: int id: str + album_name: str + release_date: datetime.date + cover: Image.Image | None @dataclass @@ -44,14 +54,35 @@ class Album: maximum_sampling_rate: int id: str image: ImageLink + release_date: datetime.date @classmethod def from_dict(cls, data: dict[Any, Any]) -> "Album": tracks = None + release_date = datetime.date.fromtimestamp(data["released_at"]) + image = ImageLink.from_dict(data["image"]) + image_data = None + if image.large is not None: + # Fetch image + print(f"Fetching image from {image.large}") + image_content = BytesIO(requests.get(image.large).content) + + image_data = Image.open(image_content) + if "tracks" in data and "track_ids" in data: tracks = [ - Track(id=tid, title=track["title"]) - for tid, track in zip(data["track_ids"], data["tracks"]["items"]) + Track( + nr=tnr + 1, + artist=data["artist"]["name"], + id=tid, + title=track["title"], + album_name=data["title"], + release_date=release_date, + cover=image_data, + ) + for tnr, (tid, track) in enumerate( + zip(data["track_ids"], data["tracks"]["items"]) + ) ] return cls( artist=data["artist"]["name"], @@ -61,6 +92,7 @@ class Album: maximum_sampling_rate=data["maximum_sampling_rate"], id=data["id"], image=ImageLink.from_dict(data["image"]), + release_date=release_date, ) @@ -80,8 +112,8 @@ class Qobuz: return result_json["data"]["url"] raise RuntimeError(f"Failed to get download URL, result: {result}") - def download_track(self, track_id: str, filename: str) -> None: - download_url = self.get_download_url(track_id) + def download_track(self, track: Track, filename: str) -> None: + download_url = self.get_download_url(track.id) response = requests.get(download_url, stream=True) if response.status_code == 200: total_size = int(response.headers.get("content-length", 0)) @@ -92,6 +124,15 @@ class Qobuz: for chunk in response.iter_content(chunk_size=8192): file.write(chunk) pbar.update(len(chunk)) + # Set metadata using mutagen + audio = FLAC(filename) + audio["title"] = track.title + audio["artist"] = track.artist + audio["tracknumber"] = str(track.nr) + audio["album"] = track.album_name + audio["date"] = track.release_date.isoformat() + audio.save() + print(f"Downloaded {filename}") else: raise RuntimeError("Failed to download track") @@ -112,7 +153,7 @@ class Qobuz: for nr, track in enumerate(tracks): print(f"Downloading track #{nr + 1:02d} {track.title}...") filename = f"{artist} - {album_title} - {nr + 1:02d} {track.title}.flac" - self.download_track(track.id, filename) + self.download_track(track, filename) def search_album(self, query: str) -> Album: print(f'Searching for "{query}"')