Tidal album download working

Bug fixes relate to:
- cover art
- quality keys

Also updated gitignore to ignore audio files.
This commit is contained in:
nathannathant 2021-03-15 22:58:24 -07:00
parent 27d58fb580
commit a57c84e2fe
5 changed files with 65 additions and 38 deletions

4
.gitignore vendored
View File

@ -149,3 +149,7 @@ dmypy.json
# Cython debug symbols
cython_debug/
*.yaml
*.flac
*.m4a
*.ogg
*.mp3

View File

@ -32,9 +32,10 @@ QOBUZ_BASE = "https://www.qobuz.com/api.json/0.2"
# Tidal
TIDAL_Q_IDS = {
4: "LOW", # AAC
5: "MEDIUM", # AAC
6: "HIGH", # Lossless, but it also could be MQA
5: "HIGH", # AAC
6: "LOSSLESS", # Lossless, but it also could be MQA
}
TIDAL_MAX_Q = max(TIDAL_Q_IDS.keys())
# Deezer
@ -80,6 +81,11 @@ class ClientInterface(ABC):
"""
pass
@property
@abstractmethod
def source(self):
pass
class SecureClientInterface(ClientInterface):
"""Identical to a ClientInterface except for a login
@ -412,8 +418,12 @@ class TidalClient(SecureClientInterface):
:param quality:
:type quality: int
"""
# Not tested
return self._get_file_url(meta_id, quality=quality)
logger.debug(f"Fetching file url with quality {quality}")
return self._get_file_url(meta_id, quality=min(TIDAL_MAX_Q, quality))
@property
def source(self):
return "tidal"
def _search(self, query, media_type="album", **kwargs):
params = {

View File

@ -10,6 +10,8 @@ LOG_DIR = appdirs.user_config_dir(APPNAME)
AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"
TIDAL_COVER_URL = "https://resources.tidal.com/images/{uuid}/{width}x{height}.jpg"
EXT = {
5: ".mp3",
6: ".flac",

View File

@ -1,7 +1,7 @@
import logging
import os
import shutil
import subprocess
from pprint import pformat
from tempfile import gettempdir
from typing import Any, Optional, Union
@ -12,7 +12,7 @@ from pathvalidate import sanitize_filename
from . import converter
from .clients import ClientInterface
from .constants import EXT, FLAC_MAX_BLOCKSIZE
from .constants import EXT, FLAC_MAX_BLOCKSIZE, TIDAL_COVER_URL
from .exceptions import (
InvalidQuality,
InvalidSourceError,
@ -32,7 +32,7 @@ TIDAL_Q_MAP = {
class Track:
"""Represents a downloadable track returned by the qobuz api.
"""Represents a downloadable track.
Loading metadata as a single track:
>>> t = Track(client, id='20252078')
@ -96,8 +96,8 @@ class Track:
) # meta dict -> TrackMetadata object
@staticmethod
def _get_tracklist(resp, client):
if client.source in ("qobuz", "tidal"):
def _get_tracklist(resp, source):
if source in ("qobuz", "tidal"):
return resp["tracks"]["items"]
else:
# TODO: implement deezer
@ -176,9 +176,8 @@ class Track:
:raises IndexError
"""
track = cls._get_tracklist(album)[pos]
track = cls._get_tracklist(album, client.source)[pos]
meta = TrackMetadata(album=album, track=track, source=client.source)
meta.add_track_meta(album["tracks"]["items"][pos])
return cls(client=client, meta=meta, id=track["id"])
def tag(self, album_meta: dict = None, cover: Union[Picture, APIC] = None):
@ -295,6 +294,9 @@ class Track:
"""
setattr(self.meta, key, val)
def __repr__(self):
return f"<Track - {self['title']}>"
class Tracklist(list):
"""A base class for tracklist-like objects.
@ -367,7 +369,7 @@ class Tracklist(list):
:param source: in ('qobuz', 'deezer', 'tidal')
:type source: str
"""
info = cls._load_get_response(item)
info = cls._parse_get_resp(item, client=client)
# equivalent to Album(client=client, **info)
return cls(client=client, **info)
@ -429,9 +431,10 @@ class Album(Tracklist):
self.load_meta()
def load_meta(self):
assert hasattr(self, "id"), "id must be set to load metadata"
self.meta = self.client.get(self.id, media_type="album")
# update attributes based on response
self.__dict__.update(self._parse_get_resp(self.meta))
self.__dict__.update(self._parse_get_resp(self.meta, self.client))
if not self.get("streamable", False):
raise NonStreamable(f"This album is not streamable ({self.id} ID)")
@ -448,6 +451,7 @@ class Album(Tracklist):
"""
if client.source == "qobuz":
info = {
"id": resp.get("id"),
"title": resp.get("title"),
"_artist": resp.get("artist") or resp.get("performer"),
"albumartist": resp.get("name"),
@ -461,11 +465,19 @@ class Album(Tracklist):
}
elif client.source == "tidal":
info = {
"id": resp.get("id"),
"title": resp.get("title"),
"_artist": safe_get(resp, "artist", "name"),
"albumartist": safe_get(resp, "artist", "name"),
"version": resp.get("version"),
"cover_urls": [resp.get("cover")],
"cover_urls": {
size: TIDAL_COVER_URL.format(
uuid=resp.get("cover").replace("-", "/"), height=x, width=x
)
for size, x in zip(
("thumbnail", "small", "large"), (160, 320, 1280)
)
},
"streamable": resp.get("allowStreaming"),
"quality": TIDAL_Q_MAP[resp.get("audioQuality")],
"tracktotal": resp.get("numberOfTracks"),
@ -491,6 +503,8 @@ class Album(Tracklist):
This uses a classmethod to convert an item into a Track object, which
stores the metadata inside a TrackMetadata object.
"""
logging.debug("Loading tracks to album")
logging.debug(pformat(self.meta))
for i in range(self.tracktotal):
# append method inherited from superclass list
self.append(
@ -639,7 +653,7 @@ class Playlist(Tracklist):
"id": item.id,
}
else:
raise ValueError(f"invalid source '{source}'")
raise InvalidSourceError(source)
# equivalent to Playlist(client=client, **info)
return cls(client=client, **info)
@ -708,7 +722,7 @@ class Artist(Tracklist):
"id": item.id,
}
else:
raise ValueError(f"invalid source '{source}'")
raise InvalidSourceError(source)
logging.debug(f"Loaded info {info}")
# equivalent to Artist(client=client, **info)

View File

@ -73,37 +73,34 @@ class TrackMetadata:
if track is not None:
self.add_track_meta(track)
# prefer track['album'] over album
if track.get("album"):
album = track.get("album")
if album is not None:
self.add_album_meta(album)
def add_album_meta(self, album: dict):
"""Parse the metadata from an album dict returned by the
def add_album_meta(self, resp: dict):
"""Parse the metadata from an resp dict returned by the
Qobuz API.
:param dict album: from the Qobuz API
:param dict resp: from the Qobuz API
"""
if self.__source == "qobuz":
self.album = album.get("title")
self.tracktotal = str(album.get("tracks_count", 1))
self.genre = album.get("genres_list", [])
self.date = album.get("release_date_original") or album.get("release_date")
self.copyright = album.get("copyright")
self.albumartist = album.get("artist", {}).get("name")
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 = album.get("label")
self.label = resp.get("label")
if isinstance(self.label, dict):
self.label = self.label.get("name")
elif self.__source == "tidal":
self.album = album.get("title")
self.tracktotal = album.get("numberOfTracks")
self.date = album.get("releaseDate")
self.copyright = album.get("copyright")
self.albumartist = album.get("artist", {}).get("name")
self.album = resp.get("title")
self.tracktotal = resp.get("numberOfTracks")
self.date = resp.get("releaseDate")
self.copyright = resp.get("copyright")
self.albumartist = resp.get("artist", {}).get("name")
elif self.__source == "deezer":
raise NotImplementedError
else:
@ -130,8 +127,8 @@ class TrackMetadata:
elif self.__source == "tidal":
self.title = track.get("title").strip()
self._mod_title(track.get("version"), None)
self.tracknumber = track.get("trackNumber")
self.discnumber = track.get("volumeNumber")
self.tracknumber = str(track.get("trackNumber"))
self.discnumber = str(track.get("volumeNumber"))
self.artist = track.get("artist", {}).get("name")
elif self.__source == "deezer":
raise NotImplementedError
@ -284,7 +281,7 @@ class TrackMetadata:
"""
for k, v in FLAC_KEY.items():
tag = getattr(self, k)
if tag is not None and not k.startswith("_"):
if tag is not None:
yield (v, tag)
def __gen_mp3_tags(self) -> Tuple[str, str]:
@ -300,7 +297,7 @@ class TrackMetadata:
else:
text = getattr(self, k)
if text is not None and not k.startswith("_"):
if text is not None:
yield (v.__name__, v(encoding=3, text=text))
def __mp4_tags(self) -> Tuple[str, str]: