mirror of https://github.com/nathom/streamrip.git
TIDAL downloads working
This commit is contained in:
parent
d14fb608d3
commit
1522931f6f
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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":
|
||||
|
|
Loading…
Reference in New Issue