diff --git a/streamrip/clients.py b/streamrip/clients.py index bcd6cd3..7d84cd1 100644 --- a/streamrip/clients.py +++ b/streamrip/clients.py @@ -241,6 +241,9 @@ class QobuzClient(Client): epoint = f"{media_type}/get" response, status_code = self._api_request(epoint, params) + if status_code != 200: + raise Exception(f'Error fetching metadata. "{response["message"]}"') + return response def _api_search(self, query, media_type, limit=500) -> Generator: @@ -410,6 +413,8 @@ class TidalClient(Client): source = "tidal" max_quality = 3 + # ----------- Public Methods -------------- + def __init__(self): self.logged_in = False @@ -461,7 +466,10 @@ class TidalClient(Client): } return self._api_request(f"search/{media_type}s", params=params) - def get_file_url(self, track_id, quality: int = 3): + def get_file_url(self, track_id, quality: int = 3, video=False): + if video: + return self._get_video_stream_url(track_id) + params = { "audioquality": get_quality(min(quality, TIDAL_MAX_Q), self.source), "playbackmode": "STREAM", @@ -492,6 +500,8 @@ class TidalClient(Client): ) } + # ------------ Utilities to login ------------- + def _login_new_user(self, launch=True): login_link = f"https://{self._get_device_code()}" @@ -613,6 +623,15 @@ class TidalClient(Client): self.access_token = token self._update_authorization() + def _update_authorization(self): + self.session.headers.update(self.authorization) + + @property + def authorization(self): + return {"authorization": f"Bearer {self.access_token}"} + + # ------------- Fetch data ------------------ + def _api_get(self, item_id: str, media_type: str) -> dict: url = f"{media_type}s/{item_id}" item = self._api_request(url) @@ -644,13 +663,22 @@ class TidalClient(Client): r = self.session.get(f"{TIDAL_BASE}/{path}", params=params).json() return r + def _get_video_stream_url(self, video_id) -> str: + params = { + "videoquality": "HIGH", + "playbackmode": "STREAM", + "assetpresentation": "FULL", + } + resp = self._api_request( + f"videos/{video_id}/playbackinfopostpaywall", params=params + ) + manifest = json.loads(base64.b64decode(resp['manifest']).decode("utf-8")) + return manifest['urls'][0] + def _api_post(self, url, data, auth=None): r = self.session.post(url, data=data, auth=auth, verify=False).json() return r - def _update_authorization(self): - self.session.headers.update({"authorization": f"Bearer {self.access_token}"}) - class SoundCloudClient(Client): source = "soundcloud" diff --git a/streamrip/constants.py b/streamrip/constants.py index ba673f6..76c85c3 100644 --- a/streamrip/constants.py +++ b/streamrip/constants.py @@ -158,7 +158,8 @@ TIDAL_Q_MAP = { DEEZER_MAX_Q = 6 AVAILABLE_QUALITY_IDS = (0, 1, 2, 3, 4) -MEDIA_TYPES = ("track", "album", "artist", "label", "playlist") +# video only for tidal +MEDIA_TYPES = {"track", "album", "artist", "label", "playlist", "video"} # used to homogenize cover size keys COVER_SIZES = ("thumbnail", "small", "large", "original") diff --git a/streamrip/downloader.py b/streamrip/downloader.py index 0bcc8a6..ad69f12 100644 --- a/streamrip/downloader.py +++ b/streamrip/downloader.py @@ -10,6 +10,7 @@ import re import shutil import subprocess from tempfile import gettempdir +from dataclasses import dataclass from typing import Any, Generator, Iterable, Union import click @@ -812,7 +813,7 @@ class Album(Tracklist): return Playlist.from_api(resp, client) info = cls._parse_get_resp(resp, client) - return cls(client, **info) + return cls(client, **info.asdict()) def _prepare_download(self, **kwargs): self.folder_format = kwargs.get("folder_format", FOLDER_FORMAT) diff --git a/streamrip/metadata.py b/streamrip/metadata.py index f092cd9..e2e1bd5 100644 --- a/streamrip/metadata.py +++ b/streamrip/metadata.py @@ -3,6 +3,7 @@ import logging import re from typing import Generator, Hashable, Optional, Tuple, Union +from collections import OrderedDict from .constants import ( COPYRIGHT, @@ -136,7 +137,7 @@ class TrackMetadata: # Non-embedded information self.version = resp.get("version") - self.cover_urls = resp.get("image") + self.cover_urls = OrderedDict(resp.get("image")) self.cover_urls["original"] = self.cover_urls["large"].replace("600", "org") self.streamable = resp.get("streamable", False) self.bit_depth = resp.get("maximum_bit_depth") @@ -162,10 +163,10 @@ class TrackMetadata: self.explicit = resp.get("explicit", False) # 80, 160, 320, 640, 1280 uuid = resp.get("cover") - self.cover_urls = { + self.cover_urls = OrderedDict({ sk: tidal_cover_url(uuid, size) for sk, size in zip(COVER_SIZES, (160, 320, 640, 1280)) - } + }) self.streamable = resp.get("allowStreaming", False) self.quality = TIDAL_Q_MAP[resp["audioQuality"]] @@ -185,13 +186,13 @@ class TrackMetadata: self.explicit = bool(resp.get("parental_warning")) self.quality = 2 self.bit_depth = 16 - self.cover_urls = { + self.cover_urls = OrderedDict({ sk: resp.get(rk) # size key, resp key for sk, rk in zip( COVER_SIZES, ("cover", "cover_medium", "cover_large", "cover_xl"), ) - } + }) self.sampling_rate = 44100 self.streamable = True