mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-17 08:35:08 -04:00
Renamed music_dl to streamrip
This commit is contained in:
parent
0c8933420a
commit
1ce080fbd2
13 changed files with 0 additions and 0 deletions
391
streamrip/metadata.py
Normal file
391
streamrip/metadata.py
Normal file
|
@ -0,0 +1,391 @@
|
|||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import Generator, Optional, Tuple, Union
|
||||
|
||||
from .constants import (
|
||||
COPYRIGHT,
|
||||
FLAC_KEY,
|
||||
MP3_KEY,
|
||||
MP4_KEY,
|
||||
PHON_COPYRIGHT,
|
||||
TRACK_KEYS,
|
||||
)
|
||||
from .exceptions import InvalidContainerError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TrackMetadata:
|
||||
"""Contains all of the metadata needed to tag the file.
|
||||
Available attributes:
|
||||
|
||||
* title
|
||||
* artist
|
||||
* album
|
||||
* albumartist
|
||||
* composer
|
||||
* year
|
||||
* comment
|
||||
* description
|
||||
* purchase_date
|
||||
* grouping
|
||||
* genre
|
||||
* lyrics
|
||||
* encoder
|
||||
* copyright
|
||||
* compilation
|
||||
* cover
|
||||
* tracknumber
|
||||
* discnumber
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, track: Optional[dict] = None, album: Optional[dict] = None, source="qobuz"
|
||||
):
|
||||
"""Creates a TrackMetadata object optionally initialized with
|
||||
dicts returned by the Qobuz API.
|
||||
|
||||
:param track: track dict from API
|
||||
:type track: Optional[dict]
|
||||
:param album: album dict from API
|
||||
:type album: Optional[dict]
|
||||
"""
|
||||
self.album = None
|
||||
self.albumartist = None
|
||||
self.composer = None
|
||||
self.comment = None
|
||||
self.description = None
|
||||
self.purchase_date = None
|
||||
self.grouping = None
|
||||
self.lyrics = None
|
||||
self.encoder = None
|
||||
self.compilation = None
|
||||
self.cover = None
|
||||
self.tracknumber = None
|
||||
self.discnumber = None
|
||||
|
||||
self.__source = source # not included in tags
|
||||
|
||||
if track is None and album is None:
|
||||
logger.debug("No params passed, returning")
|
||||
return
|
||||
|
||||
if track is not None:
|
||||
self.add_track_meta(track)
|
||||
|
||||
if album is not None:
|
||||
self.add_album_meta(album)
|
||||
|
||||
def add_album_meta(self, resp: dict):
|
||||
"""Parse the metadata from an resp dict returned by the
|
||||
Qobuz API.
|
||||
|
||||
:param dict resp: from the Qobuz API
|
||||
"""
|
||||
if self.__source == "qobuz":
|
||||
self.album = resp.get("title")
|
||||
self.tracktotal = str(resp.get("tracks_count", 1))
|
||||
self.genre = resp.get("genres_list", [])
|
||||
self.date = resp.get("release_date_original") or resp.get("release_date")
|
||||
self.copyright = resp.get("copyright")
|
||||
self.albumartist = resp.get("artist", {}).get("name")
|
||||
self.label = resp.get("label")
|
||||
|
||||
if isinstance(self.label, dict):
|
||||
self.label = self.label.get("name")
|
||||
|
||||
elif self.__source == "tidal":
|
||||
self.album = resp.get("title")
|
||||
self.tracktotal = resp.get("numberOfTracks")
|
||||
# genre not returned by API
|
||||
self.date = resp.get("releaseDate")
|
||||
self.copyright = resp.get("copyright")
|
||||
self.albumartist = resp.get("artist", {}).get("name")
|
||||
# label not returned by API
|
||||
|
||||
elif self.__source == "deezer":
|
||||
self.album = resp.get("title")
|
||||
self.tracktotal = resp.get("track_total")
|
||||
self.genre = resp.get("genres", {}).get("data")
|
||||
self.date = resp.get("release_date")
|
||||
self.albumartist = resp.get("artist", {}).get("name")
|
||||
self.label = resp.get("label")
|
||||
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
def add_track_meta(self, track: dict):
|
||||
"""Parse the metadata from a track dict returned by the
|
||||
Qobuz API.
|
||||
|
||||
:param track:
|
||||
"""
|
||||
if self.__source == "qobuz":
|
||||
self.title = track.get("title").strip()
|
||||
self._mod_title(track.get("version"), track.get("work"))
|
||||
self.composer = track.get("composer", {}).get("name")
|
||||
|
||||
self.tracknumber = f"{int(track.get('track_number', 1)):02}"
|
||||
self.discnumber = str(track.get("media_number", 1))
|
||||
try:
|
||||
self.artist = track["performer"]["name"]
|
||||
except KeyError:
|
||||
if hasattr(self, "albumartist"):
|
||||
self.artist = self.albumartist
|
||||
|
||||
elif self.__source == "tidal":
|
||||
self.title = track.get("title").strip()
|
||||
self._mod_title(track.get("version"), None)
|
||||
self.tracknumber = f"{int(track.get('trackNumber', 1)):02}"
|
||||
self.discnumber = str(track.get("volumeNumber"))
|
||||
self.artist = track.get("artist", {}).get("name")
|
||||
|
||||
elif self.__source == "deezer":
|
||||
self.title = track.get("title").strip()
|
||||
self._mod_title(track.get("version"), None)
|
||||
self.tracknumber = f"{int(track.get('track_position', 1)):02}"
|
||||
self.discnumber = track.get("disk_number")
|
||||
self.artist = track.get("artist", {}).get("name")
|
||||
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
if track.get("album"):
|
||||
self.add_album_meta(track["album"])
|
||||
|
||||
def _mod_title(self, version, work):
|
||||
if version is not None:
|
||||
logger.debug("Version found: %s", version)
|
||||
self.title = f"{self.title} ({version})"
|
||||
if work is not None:
|
||||
logger.debug("Work found: %s", work)
|
||||
self.title = f"{work}: {self.title}"
|
||||
|
||||
@property
|
||||
def artist(self) -> Union[str, None]:
|
||||
"""Returns the value to set for the artist tag. Defaults to
|
||||
`self.albumartist` if there is no track artist.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._artist is None and self.albumartist is not None:
|
||||
return self.albumartist
|
||||
|
||||
if self._artist is not None:
|
||||
return self._artist
|
||||
|
||||
@artist.setter
|
||||
def artist(self, val: str):
|
||||
"""Sets the internal artist variable to val.
|
||||
|
||||
:param val:
|
||||
:type val: str
|
||||
"""
|
||||
self._artist = val
|
||||
|
||||
@property
|
||||
def genre(self) -> Union[str, None]:
|
||||
"""Formats the genre list returned by the Qobuz API.
|
||||
>>> g = ['Pop/Rock', 'Pop/Rock→Rock', 'Pop/Rock→Rock→Alternatif et Indé']
|
||||
>>> _format_genres(g)
|
||||
'Pop, Rock, Alternatif et Indé'
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if not self.get("_genres"):
|
||||
return None
|
||||
|
||||
if isinstance(self._genres, list):
|
||||
genres = re.findall(r"([^\u2192\/]+)", "/".join(self._genres))
|
||||
no_repeats = []
|
||||
[no_repeats.append(g) for g in genres if g not in no_repeats]
|
||||
return ", ".join(no_repeats)
|
||||
elif isinstance(self._genres, str):
|
||||
return self._genres
|
||||
|
||||
raise TypeError(f"Genre must be list or str, not {type(self._genres)}")
|
||||
|
||||
@genre.setter
|
||||
def genre(self, val: Union[str, list]):
|
||||
"""Sets the internal `genre` field to the given list.
|
||||
It is not formatted until it is requested with `meta.genre`.
|
||||
|
||||
:param val:
|
||||
:type val: Union[str, list]
|
||||
"""
|
||||
self._genres = val
|
||||
|
||||
@property
|
||||
def copyright(self) -> Union[str, None]:
|
||||
"""Formats the copyright string to use nice-looking unicode
|
||||
characters.
|
||||
|
||||
:rtype: str, None
|
||||
"""
|
||||
if hasattr(self, "_copyright"):
|
||||
if self._copyright is None:
|
||||
return None
|
||||
cr = self._copyright.replace("(P)", PHON_COPYRIGHT)
|
||||
cr = cr.replace("(C)", COPYRIGHT)
|
||||
return cr
|
||||
|
||||
logger.debug("Accessed copyright tag before setting, return None")
|
||||
return None
|
||||
|
||||
@copyright.setter
|
||||
def copyright(self, val: str):
|
||||
"""Sets the internal copyright variable to the given value.
|
||||
Only formatted when requested.
|
||||
|
||||
:param val:
|
||||
:type val: str
|
||||
"""
|
||||
self._copyright = val
|
||||
|
||||
@property
|
||||
def year(self) -> Union[str, None]:
|
||||
"""Returns the year published of the track.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if hasattr(self, "_year"):
|
||||
return self._year
|
||||
|
||||
if hasattr(self, "date"):
|
||||
if self.date is not None:
|
||||
return self.date[:4]
|
||||
|
||||
@year.setter
|
||||
def year(self, val):
|
||||
"""Sets the internal year variable to val.
|
||||
|
||||
:param val:
|
||||
"""
|
||||
self._year = val
|
||||
|
||||
def get_formatter(self) -> dict:
|
||||
"""Returns a dict that is used to apply values to file format strings.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
# the keys in the tuple are the possible keys for format strings
|
||||
return {k: getattr(self, k) for k in TRACK_KEYS}
|
||||
|
||||
def tags(self, container: str = "flac") -> Generator:
|
||||
"""Return a generator of (key, value) pairs to use for tagging
|
||||
files with mutagen. The *_KEY dicts are organized in the format
|
||||
|
||||
>>> {attribute_name: key_to_use_for_metadata}
|
||||
|
||||
They are then converted to the format
|
||||
|
||||
>>> {key_to_use_for_metadata: value_of_attribute}
|
||||
|
||||
so that they can be used like this:
|
||||
|
||||
>>> audio = MP4(path)
|
||||
>>> for k, v in meta.tags(container='MP4'):
|
||||
... audio[k] = v
|
||||
>>> audio.save()
|
||||
|
||||
:param container: the container format
|
||||
:type container: str
|
||||
:rtype: Generator
|
||||
"""
|
||||
container = container.lower()
|
||||
if container in ("flac", "vorbis"):
|
||||
return self.__gen_flac_tags()
|
||||
elif container in ("mp3", "id3"):
|
||||
return self.__gen_mp3_tags()
|
||||
elif container in ("alac", "m4a", "mp4", "aac"):
|
||||
return self.__gen_mp4_tags()
|
||||
else:
|
||||
raise InvalidContainerError(f"Invalid container {container}")
|
||||
|
||||
def __gen_flac_tags(self) -> Tuple[str, str]:
|
||||
"""Generate key, value pairs to tag FLAC files.
|
||||
|
||||
:rtype: Tuple[str, str]
|
||||
"""
|
||||
for k, v in FLAC_KEY.items():
|
||||
tag = getattr(self, k)
|
||||
if tag:
|
||||
logger.debug(f"Adding tag {v}: {repr(tag)}")
|
||||
yield (v, tag)
|
||||
|
||||
def __gen_mp3_tags(self) -> Tuple[str, str]:
|
||||
"""Generate key, value pairs to tag MP3 files.
|
||||
|
||||
:rtype: Tuple[str, str]
|
||||
"""
|
||||
for k, v in MP3_KEY.items():
|
||||
if k == "tracknumber":
|
||||
text = f"{self.tracknumber}/{self.tracktotal}"
|
||||
elif k == "discnumber":
|
||||
text = str(self.discnumber)
|
||||
else:
|
||||
text = getattr(self, k)
|
||||
|
||||
if text is not None:
|
||||
yield (v.__name__, v(encoding=3, text=text))
|
||||
|
||||
def __mp4_tags(self) -> Tuple[str, str]:
|
||||
"""Generate key, value pairs to tag ALAC or AAC files in
|
||||
an MP4 container.
|
||||
|
||||
:rtype: Tuple[str, str]
|
||||
"""
|
||||
for k, v in MP4_KEY.items():
|
||||
return (v, getattr(self, k))
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
"""Dict-like access for tags.
|
||||
|
||||
:param key:
|
||||
:param val:
|
||||
"""
|
||||
setattr(self, key, val)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Dict-like access for tags.
|
||||
|
||||
:param key:
|
||||
"""
|
||||
return getattr(self, key)
|
||||
|
||||
def get(self, key, default=None) -> str:
|
||||
"""Returns the requested attribute of the object, with
|
||||
a default value.
|
||||
|
||||
:param key:
|
||||
:param default:
|
||||
"""
|
||||
if hasattr(self, key):
|
||||
res = self.__getitem__(key)
|
||||
if res is not None:
|
||||
return res
|
||||
|
||||
return default
|
||||
|
||||
return default
|
||||
|
||||
def set(self, key, val) -> str:
|
||||
"""Equivalent to
|
||||
>>> meta[key] = val
|
||||
|
||||
:param key:
|
||||
:param val:
|
||||
:rtype: str
|
||||
"""
|
||||
return self.__setitem__(key, val)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Returns the string representation of the metadata object.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
# TODO: make a more readable repr
|
||||
return json.dumps(self.__dict__, indent=2)
|
Loading…
Add table
Add a link
Reference in a new issue