qobuz-dl/qobuz_dl/metadata.py

230 lines
7.0 KiB
Python

import re
import os
import logging
from mutagen.flac import FLAC, Picture
import mutagen.id3 as id3
from mutagen.id3 import ID3NoHeaderError
logger = logging.getLogger(__name__)
# unicode symbols
COPYRIGHT, PHON_COPYRIGHT = "\u2117", "\u00a9"
# if a metadata block exceeds this, mutagen will raise error
# and the file won't be tagged
FLAC_MAX_BLOCKSIZE = 16777215
ID3_LEGEND = {
"album": id3.TALB,
"albumartist": id3.TPE2,
"artist": id3.TPE1,
"comment": id3.COMM,
"composer": id3.TCOM,
"copyright": id3.TCOP,
"date": id3.TDAT,
"genre": id3.TCON,
"isrc": id3.TSRC,
"label": id3.TPUB,
"performer": id3.TOPE,
"title": id3.TIT2,
"year": id3.TYER,
}
def _get_title(track_dict):
title = track_dict["title"]
version = track_dict.get("version")
if version:
title = f"{title} ({version})"
# for classical works
if track_dict.get("work"):
title = f"{track_dict['work']}: {title}"
return title
def _format_copyright(s: str) -> str:
if s:
s = s.replace("(P)", PHON_COPYRIGHT)
s = s.replace("(C)", COPYRIGHT)
return s
def _format_genres(genres: list) -> str:
"""Fixes the weirdly formatted genre lists returned by the API.
>>> g = ['Pop/Rock', 'Pop/Rock→Rock', 'Pop/Rock→Rock→Alternatif et Indé']
>>> _format_genres(g)
'Pop, Rock, Alternatif et Indé'
"""
genres = re.findall(r"([^\u2192\/]+)", "/".join(genres))
no_repeats = []
[no_repeats.append(g) for g in genres if g not in no_repeats]
return ", ".join(no_repeats)
def _embed_flac_img(root_dir, audio: FLAC):
emb_image = os.path.join(root_dir, "cover.jpg")
multi_emb_image = os.path.join(
os.path.abspath(os.path.join(root_dir, os.pardir)), "cover.jpg"
)
if os.path.isfile(emb_image):
cover_image = emb_image
else:
cover_image = multi_emb_image
try:
# rest of the metadata still gets embedded
# when the image size is too big
if os.path.getsize(cover_image) > FLAC_MAX_BLOCKSIZE:
raise Exception(
"downloaded cover size too large to embed. "
"turn off `og_cover` to avoid error"
)
image = Picture()
image.type = 3
image.mime = "image/jpeg"
image.desc = "cover"
with open(cover_image, "rb") as img:
image.data = img.read()
audio.add_picture(image)
except Exception as e:
logger.error(f"Error embedding image: {e}", exc_info=True)
def _embed_id3_img(root_dir, audio: id3.ID3):
emb_image = os.path.join(root_dir, "cover.jpg")
multi_emb_image = os.path.join(
os.path.abspath(os.path.join(root_dir, os.pardir)), "cover.jpg"
)
if os.path.isfile(emb_image):
cover_image = emb_image
else:
cover_image = multi_emb_image
with open(cover_image, "rb") as cover:
audio.add(id3.APIC(3, "image/jpeg", 3, "", cover.read()))
# Use KeyError catching instead of dict.get to avoid empty tags
def tag_flac(
filename, root_dir, final_name, d: dict, album, istrack=True, em_image=False
):
"""
Tag a FLAC file
:param str filename: FLAC file path
:param str root_dir: Root dir used to get the cover art
:param str final_name: Final name of the FLAC file (complete path)
:param dict d: Track dictionary from Qobuz_client
:param dict album: Album dictionary from Qobuz_client
:param bool istrack
:param bool em_image: Embed cover art into file
"""
audio = FLAC(filename)
audio["TITLE"] = _get_title(d)
audio["TRACKNUMBER"] = str(d["track_number"]) # TRACK NUMBER
if "Disc " in final_name:
audio["DISCNUMBER"] = str(d["media_number"])
try:
audio["COMPOSER"] = d["composer"]["name"] # COMPOSER
except KeyError:
pass
artist_ = d.get("performer", {}).get("name") # TRACK ARTIST
if istrack:
audio["ARTIST"] = artist_ or d["album"]["artist"]["name"] # TRACK ARTIST
else:
audio["ARTIST"] = artist_ or album["artist"]["name"]
audio["LABEL"] = album.get("label", {}).get("name", "n/a")
if istrack:
audio["GENRE"] = _format_genres(d["album"]["genres_list"])
audio["ALBUMARTIST"] = d["album"]["artist"]["name"]
audio["TRACKTOTAL"] = str(d["album"]["tracks_count"])
audio["ALBUM"] = d["album"]["title"]
audio["DATE"] = d["album"]["release_date_original"]
audio["COPYRIGHT"] = _format_copyright(d.get("copyright") or "n/a")
else:
audio["GENRE"] = _format_genres(album["genres_list"])
audio["ALBUMARTIST"] = album["artist"]["name"]
audio["TRACKTOTAL"] = str(album["tracks_count"])
audio["ALBUM"] = album["title"]
audio["DATE"] = album["release_date_original"]
audio["COPYRIGHT"] = _format_copyright(album.get("copyright") or "n/a")
if em_image:
_embed_flac_img(root_dir, audio)
audio.save()
os.rename(filename, final_name)
def tag_mp3(filename, root_dir, final_name, d, album, istrack=True, em_image=False):
"""
Tag an mp3 file
:param str filename: mp3 temporary file path
:param str root_dir: Root dir used to get the cover art
:param str final_name: Final name of the mp3 file (complete path)
:param dict d: Track dictionary from Qobuz_client
:param bool istrack
:param bool em_image: Embed cover art into file
"""
try:
audio = id3.ID3(filename)
except ID3NoHeaderError:
audio = id3.ID3()
# temporarily holds metadata
tags = dict()
tags["title"] = _get_title(d)
try:
tags["label"] = album["label"]["name"]
except KeyError:
pass
artist_ = d.get("performer", {}).get("name") # TRACK ARTIST
if istrack:
tags["artist"] = artist_ or d["album"]["artist"]["name"] # TRACK ARTIST
else:
tags["artist"] = artist_ or album["artist"]["name"]
if istrack:
tags["genre"] = _format_genres(d["album"]["genres_list"])
tags["albumartist"] = d["album"]["artist"]["name"]
tags["album"] = d["album"]["title"]
tags["date"] = d["album"]["release_date_original"]
tags["copyright"] = _format_copyright(d["copyright"])
tracktotal = str(d["album"]["tracks_count"])
else:
tags["genre"] = _format_genres(album["genres_list"])
tags["albumartist"] = album["artist"]["name"]
tags["album"] = album["title"]
tags["date"] = album["release_date_original"]
tags["copyright"] = _format_copyright(album["copyright"])
tracktotal = str(album["tracks_count"])
tags["year"] = tags["date"][:4]
audio["TRCK"] = id3.TRCK(encoding=3, text=f'{d["track_number"]}/{tracktotal}')
audio["TPOS"] = id3.TPOS(encoding=3, text=str(d["media_number"]))
# write metadata in `tags` to file
for k, v in tags.items():
id3tag = ID3_LEGEND[k]
audio[id3tag.__name__] = id3tag(encoding=3, text=v)
if em_image:
_embed_id3_img(root_dir, audio)
audio.save(filename, "v2_version=3")
os.rename(filename, final_name)