TIDAL downloads working

This commit is contained in:
Nathan Thomas 2023-12-21 13:13:01 -08:00
parent d14fb608d3
commit 1522931f6f
6 changed files with 115 additions and 38 deletions

View File

@ -1,5 +1,6 @@
"""The clients that interact with the streaming service APIs."""
import contextlib
import logging
from abc import ABC, abstractmethod
@ -40,11 +41,11 @@ class Client(ABC):
@staticmethod
def get_rate_limiter(
requests_per_min: int,
) -> aiolimiter.AsyncLimiter | None:
) -> aiolimiter.AsyncLimiter | contextlib.nullcontext:
return (
aiolimiter.AsyncLimiter(requests_per_min, 60)
if requests_per_min > 0
else None
else contextlib.nullcontext()
)
@staticmethod

View File

@ -16,6 +16,17 @@ logging.captureWarnings(True)
class DeezerClient(Client):
"""Client to handle deezer API. Does not do rate limiting.
Attributes:
global_config: Entire config object
client: client from deezer py used for API requests
logged_in: True if logged in
config: deezer local config
session: aiohttp.ClientSession, used only for track downloads not API requests
"""
source = "deezer"
max_quality = 2
@ -59,7 +70,6 @@ class DeezerClient(Client):
)
except Exception as e:
logger.error("Got exception from deezer API %s", e)
# item["album"] = {"readable": False, "tracks": [], "track_total": 0}
return item
album_metadata["tracks"] = album_tracks["data"]
@ -131,28 +141,12 @@ class DeezerClient(Client):
(1, "FLAC"), # quality 2
]
# available_formats = [
# "AAC_64",
# "MP3_64",
# "MP3_128",
# "MP3_256",
# "MP3_320",
# "FLAC",
# ]
_, format_str = quality_map[quality]
dl_info["quality_to_size"] = [
track_info[f"FILESIZE_{format}"] for _, format in quality_map
]
# dl_info["size_to_quality"] = {
# int(track_info.get(f"FILESIZE_{format}")): self._quality_id_from_filetype(
# format
# )
# for format in available_formats
# }
token = track_info["TRACK_TOKEN"]
try:
logger.debug("Fetching deezer url with token %s", token)

View File

@ -390,14 +390,9 @@ class QobuzClient(Client):
"""
url = f"{QOBUZ_BASE_URL}/{epoint}"
logger.debug("api_request: endpoint=%s, params=%s", epoint, params)
if self.rate_limiter is not None:
async with self.rate_limiter:
async with self.session.get(url, params=params) as response:
return response.status, await response.json()
# return await self.session.get(url, params=params)
async with self.session.get(url, params=params) as response:
resp_json = await response.json()
return response.status, resp_json
async with self.rate_limiter:
async with self.session.get(url, params=params) as response:
return response.status, await response.json()
@staticmethod
def get_quality(quality: int):

View File

@ -1,3 +1,4 @@
import asyncio
import base64
import json
import logging
@ -71,10 +72,12 @@ class TidalClient(Client):
:type media_type: str
:rtype: dict
"""
assert media_type in ("track", "playlist", "album", "artist"), media_type
url = f"{media_type}s/{item_id}"
item = await self._api_request(url)
if media_type in ("playlist", "album"):
# TODO: move into new method
# TODO: move into new method and make concurrent
resp = await self._api_request(f"{url}/items")
tracks_left = item["numberOfTracks"]
if tracks_left > 100:
@ -90,9 +93,9 @@ class TidalClient(Client):
item["tracks"] = [item["item"] for item in resp["items"]]
elif media_type == "artist":
logger.debug("filtering eps")
album_resp = await self._api_request(f"{url}/albums")
ep_resp = await self._api_request(
f"{url}/albums", params={"filter": "EPSANDSINGLES"}
album_resp, ep_resp = await asyncio.gather(
self._api_request(f"{url}/albums"),
self._api_request(f"{url}/albums", params={"filter": "EPSANDSINGLES"}),
)
item["albums"] = album_resp["items"]
@ -295,8 +298,9 @@ class TidalClient(Client):
:param data:
:param auth:
"""
async with self.session.post(url, data=data, auth=auth) as resp:
return await resp.json()
async with self.rate_limiter:
async with self.session.post(url, data=data, auth=auth) as resp:
return await resp.json()
async def _api_request(self, path: str, params=None) -> dict:
"""Handle Tidal API requests.
@ -312,6 +316,7 @@ class TidalClient(Client):
params["countryCode"] = self.config.country_code
params["limit"] = 100
async with self.session.get(f"{BASE}/{path}", params=params) as resp:
resp.raise_for_status()
return await resp.json()
async with self.rate_limiter:
async with self.session.get(f"{BASE}/{path}", params=params) as resp:
resp.raise_for_status()
return await resp.json()

View File

@ -13,7 +13,7 @@ concurrency = true
# A value that is too high for your bandwidth may cause slowdowns
# Set to -1 for no limit
max_connections = 6
# Max number of API requests to handle per minute
# Max number of API requests per source to handle per minute
# Set to -1 for no limit
requests_per_minute = 60

View File

@ -366,12 +366,94 @@ class AlbumMetadata:
tracktotal=tracktotal,
)
@classmethod
def from_tidal_playlist_track_resp(cls, resp) -> AlbumMetadata | None:
album_resp = resp["album"]
streamable = resp.get("allowStreaming", False)
if not streamable:
return None
item_id = str(resp["id"])
album = typed(album_resp.get("title", "Unknown Album"), str)
tracktotal = 1
# genre not returned by API
date = typed(resp.get("streamStartDate"), str | None)
if date is not None:
year = date[:4]
else:
year = "Unknown Year"
_copyright = typed(resp.get("copyright"), str)
artists = typed(resp.get("artists", []), list)
albumartist = ", ".join(a["name"] for a in artists)
if not albumartist:
albumartist = typed(safe_get(resp, "artist", "name"), str)
disctotal = typed(resp.get("volumeNumber", 1), int)
# label not returned by API
# non-embedded
explicit = typed(resp.get("explicit", False), bool)
covers = Covers.from_tidal(album_resp)
if covers is None:
covers = Covers()
quality_map: dict[str, int] = {
"LOW": 0,
"HIGH": 1,
"LOSSLESS": 2,
"HI_RES": 3,
}
tidal_quality = resp.get("audioQuality", "LOW")
quality = quality_map[tidal_quality]
if quality >= 2:
sampling_rate = 44100
if quality == 3:
bit_depth = 24
else:
bit_depth = 16
else:
sampling_rate = None
bit_depth = None
info = AlbumInfo(
id=item_id,
quality=quality,
container="MP4",
label=None,
explicit=explicit,
sampling_rate=sampling_rate,
bit_depth=bit_depth,
booklets=None,
)
return AlbumMetadata(
info,
album,
albumartist,
year,
genre=[],
covers=covers,
albumcomposer=None,
comment=None,
compilation=None,
copyright=_copyright,
date=date,
description=None,
disctotal=disctotal,
encoder=None,
grouping=None,
lyrics=None,
purchase_date=None,
tracktotal=tracktotal,
)
@classmethod
def from_track_resp(cls, resp: dict, source: str) -> AlbumMetadata | None:
if source == "qobuz":
return cls.from_qobuz(resp["album"])
if source == "tidal":
return cls.from_tidal(resp)
return cls.from_tidal_playlist_track_resp(resp)
if source == "soundcloud":
return cls.from_soundcloud(resp)
if source == "deezer":