diff --git a/.gitignore b/.gitignore index db04c5b..a6e8b77 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ test.py StreamripDownloads *.wav *.log +*.mp4 diff --git a/streamrip/clients.py b/streamrip/clients.py index 7d84cd1..0016bd5 100644 --- a/streamrip/clients.py +++ b/streamrip/clients.py @@ -2,6 +2,7 @@ import base64 import hashlib import json import logging +import re import time from abc import ABC, abstractmethod from typing import Generator, Sequence, Tuple, Union @@ -16,6 +17,11 @@ from .constants import ( SOUNDCLOUD_CLIENT_ID, TIDAL_CLIENT_INFO, TIDAL_MAX_Q, + QOBUZ_BASE, + TIDAL_BASE, + TIDAL_AUTH_URL, + DEEZER_BASE, + SOUNDCLOUD_BASE, ) from .exceptions import ( AuthenticationError, @@ -27,16 +33,6 @@ from .exceptions import ( from .spoofbuz import Spoofer from .utils import gen_threadsafe_session, get_quality -QOBUZ_BASE = "https://www.qobuz.com/api.json/0.2" - -TIDAL_BASE = "https://api.tidalhifi.com/v1" -TIDAL_AUTH_URL = "https://auth.tidal.com/v1/oauth2" - -DEEZER_BASE = "https://api.deezer.com" -DEEZER_DL = "http://dz.loaderapp.info/deezer" - -SOUNDCLOUD_BASE = "https://api-v2.soundcloud.com" - logger = logging.getLogger(__name__) @@ -672,8 +668,16 @@ class TidalClient(Client): 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] + stream_url_regex = ( + r'#EXT-X-STREAM-INF:BANDWIDTH=\d+,AVERAGE-BANDWIDTH=\d+,CODECS="[^"]+"' + r",RESOLUTION=\d+x\d+\n(.+)" + ) + manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8")) + available_urls = self.session.get(manifest["urls"][0]) + url_info = re.findall(stream_url_regex, available_urls.text) + + # highest resolution is last + return url_info[-1] def _api_post(self, url, data, auth=None): r = self.session.post(url, data=data, auth=auth, verify=False).json() diff --git a/streamrip/constants.py b/streamrip/constants.py index 76c85c3..490f9fc 100644 --- a/streamrip/constants.py +++ b/streamrip/constants.py @@ -168,3 +168,14 @@ TIDAL_CLIENT_INFO = { "id": "aR7gUaTK1ihpXOEP", "secret": "eVWBEkuL2FCjxgjOkR3yK0RYZEbcrMXRc2l8fU3ZCdE=", } + +QOBUZ_BASE = "https://www.qobuz.com/api.json/0.2" + +TIDAL_BASE = "https://api.tidalhifi.com/v1" +TIDAL_AUTH_URL = "https://auth.tidal.com/v1/oauth2" + +DEEZER_BASE = "https://api.deezer.com" +DEEZER_DL = "http://dz.loaderapp.info/deezer" + +SOUNDCLOUD_BASE = "https://api-v2.soundcloud.com" + diff --git a/streamrip/metadata.py b/streamrip/metadata.py index e2e1bd5..f00b29b 100644 --- a/streamrip/metadata.py +++ b/streamrip/metadata.py @@ -2,8 +2,8 @@ import logging import re -from typing import Generator, Hashable, Optional, Tuple, Union from collections import OrderedDict +from typing import Generator, Hashable, Optional, Tuple, Union from .constants import ( COPYRIGHT, @@ -163,10 +163,12 @@ class TrackMetadata: self.explicit = resp.get("explicit", False) # 80, 160, 320, 640, 1280 uuid = resp.get("cover") - self.cover_urls = OrderedDict({ - sk: tidal_cover_url(uuid, size) - for sk, size in zip(COVER_SIZES, (160, 320, 640, 1280)) - }) + 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"]] @@ -186,13 +188,15 @@ class TrackMetadata: self.explicit = bool(resp.get("parental_warning")) self.quality = 2 self.bit_depth = 16 - 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.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