diff --git a/streamrip/clients.py b/streamrip/clients.py index f1c75f7..f7f6412 100644 --- a/streamrip/clients.py +++ b/streamrip/clients.py @@ -4,14 +4,15 @@ import hashlib import json import logging import os -import sys +# import sys import time from abc import ABC, abstractmethod -from pprint import pformat, pprint +from pprint import pformat # , pprint from typing import Generator, Sequence, Tuple, Union import click import requests +from requests.packages import urllib3 import tidalapi from dogpile.cache import make_region @@ -33,6 +34,9 @@ from .exceptions import ( ) from .spoofbuz import Spoofer +urllib3.disable_warnings() +requests.adapters.DEFAULT_RETRIES = 5 + os.makedirs(CACHE_DIR, exist_ok=True) region = make_region().configure( "dogpile.cache.dbm", @@ -427,6 +431,7 @@ class DeezerClient(ClientInterface): return url +''' class TidalClient(ClientInterface): source = "tidal" @@ -510,12 +515,15 @@ class TidalClient(ClientInterface): resp = self.session.request("GET", f"tracks/{track_id}/streamUrl", params) resp.raise_for_status() return resp.json() +''' -class TidalMQAClient(ClientInterface): +class TidalClient(ClientInterface): source = "tidal" def __init__(self): + self.logged_in = False + self.device_code = None self.user_code = None self.verification_url = None @@ -538,26 +546,54 @@ class TidalMQAClient(ClientInterface): if access_token is not None: self.token_expiry = token_expiry self.refresh_token = refresh_token - self.login_by_access_token(access_token, user_id) + if self.token_expiry - time.time() < 86400: # 1 day + self._refresh_access_token() + else: + self._login_by_access_token(access_token, user_id) else: - self.login_new_user() + self._login_new_user() + + self.logged_in = True + click.secho("Logged into Tidal", fg='green') def get(self, item_id, media_type): return self._api_get(item_id, media_type) - def search(self, query, media_type="album"): - raise NotImplementedError + def search(self, query, media_type="album", limit: int = 100): + params = { + "query": query, + "limit": limit, + } + return self._api_get(f"search/{media_type}s", params=params) + + def get_file_url(self, track_id, quality: int = 7): + params = { + "audioquality": TIDAL_Q_IDS[quality], + "playbackmode": "STREAM", + "assetpresentation": "FULL", + } + resp = self._api_request(f"tracks/{track_id}/playbackinfopostpaywall", params) + manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8")) + return { + "url": manifest["urls"][0], + "enc_key": manifest.get("keyId"), + "codec": manifest["codecs"], + } + + def _login_new_user(self, launch=True): + login_link = f"https://{self._get_device_code()}" - def login_new_user(self): - login_link = self.get_device_code() click.secho( f"Go to {login_link} to log into Tidal within 5 minutes.", fg="blue" ) + if launch: + click.launch(login_link) + start = time.time() elapsed = 0 - while elapsed < 600: # change later + while elapsed < 600: # 5 mins to login elapsed = time.time() - start - status = self.check_auth_status() + status = self._check_auth_status() if status == 2: # pending time.sleep(4) @@ -571,7 +607,7 @@ class TidalMQAClient(ClientInterface): else: raise Exception - def get_device_code(self): + def _get_device_code(self): data = { "client_id": TIDAL_CLIENT_INFO["id"], "scope": "r_usr+w_usr+w_sub", @@ -588,7 +624,7 @@ class TidalMQAClient(ClientInterface): self.auth_interval = resp["interval"] return resp["verificationUriComplete"] - def check_auth_status(self): + def _check_auth_status(self): data = { "client_id": TIDAL_CLIENT_INFO["id"], "device_code": self.device_code, @@ -616,7 +652,7 @@ class TidalMQAClient(ClientInterface): self.token_expiry = resp["expires_in"] + time.time() return 0 - def verify_access_token(self, token): + def _verify_access_token(self, token): headers = { "authorization": f"Bearer {token}", } @@ -626,7 +662,7 @@ class TidalMQAClient(ClientInterface): return True - def refresh_access_token(self): + def _refresh_access_token(self): data = { "client_id": TIDAL_CLIENT_INFO["id"], "refresh_token": self.refresh_token, @@ -639,7 +675,7 @@ class TidalMQAClient(ClientInterface): (TIDAL_CLIENT_INFO["id"], TIDAL_CLIENT_INFO["secret"]), ) - if resp.get("status") != 200: + if resp.get("status", 200) != 200: raise Exception("Refresh failed") self.user_id = resp["user"]["userId"] @@ -647,7 +683,7 @@ class TidalMQAClient(ClientInterface): self.access_token = resp["access_token"] self.token_expiry = resp["expires_in"] + time.time() - def login_by_access_token(self, token, user_id=None): + def _login_by_access_token(self, token, user_id=None): headers = {"authorization": f"Bearer {token}"} resp = requests.get("https://api.tidal.com/v1/sessions", headers=headers).json() if resp.get("status", 200) != 200: @@ -693,17 +729,3 @@ class TidalMQAClient(ClientInterface): def _api_post(self, url, data, auth=None): r = requests.post(url, data=data, auth=auth, verify=False).json() return r - - def get_file_url(self, track_id, quality: int = 7): - params = { - "audioquality": TIDAL_Q_IDS[quality], - "playbackmode": "STREAM", - "assetpresentation": "FULL", - } - resp = self._api_request(f"tracks/{track_id}/playbackinfopostpaywall", params) - manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8")) - return { - "url": manifest["urls"][0], - "enc_key": manifest.get("keyId", ""), - "codec": manifest["codecs"], - } diff --git a/streamrip/constants.py b/streamrip/constants.py index db4960b..7270890 100644 --- a/streamrip/constants.py +++ b/streamrip/constants.py @@ -8,7 +8,7 @@ APPNAME = "streamrip" CACHE_DIR = click.get_app_dir(APPNAME) CONFIG_DIR = click.get_app_dir(APPNAME) -CONFIG_PATH = os.path.join(CONFIG_DIR, "configmqa.yaml") +CONFIG_PATH = os.path.join(CONFIG_DIR, "config.yaml") LOG_DIR = click.get_app_dir(APPNAME) DB_PATH = os.path.join(LOG_DIR, "downloads.db") diff --git a/streamrip/core.py b/streamrip/core.py index ffd3327..e07f5ca 100644 --- a/streamrip/core.py +++ b/streamrip/core.py @@ -63,18 +63,21 @@ class MusicDL(list): :param source: :type source: str """ - click.secho(f"Enter {capitalize(source)} email:", fg="green") - self.config.file[source]["email"] = input() - click.secho( - f"Enter {capitalize(source)} password (will not show on screen):", - fg="green", - ) - self.config.file[source]["password"] = getpass( - prompt="" - ) # does hashing work for tidal? + if source == "qobuz": + click.secho(f"Enter {capitalize(source)} email:", fg="green") + self.config.file[source]["email"] = input() + click.secho( + f"Enter {capitalize(source)} password (will not show on screen):", + fg="green", + ) + self.config.file[source]["password"] = getpass( + prompt="" + ) # does hashing work for tidal? - self.config.save() - click.secho(f'Credentials saved to config file at "{self.config._path}"') + self.config.save() + click.secho(f'Credentials saved to config file at "{self.config._path}"') + else: + raise Exception def assert_creds(self, source: str): assert source in ("qobuz", "tidal", "deezer"), f"Invalid source {source}" @@ -82,7 +85,7 @@ class MusicDL(list): # no login for deezer return - if ( + if source == "qobuz" and ( self.config.file[source]["email"] is None or self.config.file[source]["password"] is None ): @@ -185,6 +188,9 @@ class MusicDL(list): self.config.file["qobuz"]["secrets"], ) = client.get_tokens() self.config.save() + elif client.source == 'tidal': + self.config.file['tidal'] = client.get_tokens() + self.config.save() def parse_urls(self, url: str) -> Tuple[str, str]: """Returns the type of the url and the id. diff --git a/streamrip/downloader.py b/streamrip/downloader.py index df22388..2d5f23b 100644 --- a/streamrip/downloader.py +++ b/streamrip/downloader.py @@ -33,11 +33,11 @@ from .exceptions import ( from .metadata import TrackMetadata from .utils import ( clean_format, + decrypt_mqa_file, quality_id, safe_get, tidal_cover_url, tqdm_download, - decrypt_mqa_file, ) logger = logging.getLogger(__name__) @@ -229,7 +229,7 @@ class Track: raise InvalidSourceError(self.client.source) if dl_info.get("enc_key"): - decrypt_mqa_file(temp_file, self.final_path, dl_info['enc_key']) + decrypt_mqa_file(temp_file, self.final_path, dl_info["enc_key"]) else: shutil.move(temp_file, self.final_path) diff --git a/streamrip/utils.py b/streamrip/utils.py index 8750d82..2d24cba 100644 --- a/streamrip/utils.py +++ b/streamrip/utils.py @@ -1,5 +1,5 @@ -import logging import base64 +import logging import logging.handlers as handlers import os from string import Formatter