diff --git a/rip/cli.py b/rip/cli.py index 9bbf7ab..06392ce 100644 --- a/rip/cli.py +++ b/rip/cli.py @@ -46,6 +46,7 @@ class DownloadCommand(Command): # Use a thread so that it doesn't slow down startup update_check = threading.Thread(target=is_outdated, daemon=True) + update_check.start() config = Config() path, codec, quality, no_db = clean_options( @@ -83,16 +84,12 @@ class DownloadCommand(Command): elif not urls and path is None: self.line("Must pass arguments. See rip url -h.") - try: - update_check.join() - if outdated: - self.line( - "A new version of streamrip is available! Run " - "pip3 install streamrip --upgrade to update" - ) - except RuntimeError as e: - logger.debug("Update check error: %s", e) - pass + update_check.join() + if outdated: + self.line( + "A new version of streamrip is available! Run " + "pip3 install streamrip --upgrade to update" + ) return 0 diff --git a/rip/core.py b/rip/core.py index 4af44f8..cf311dc 100644 --- a/rip/core.py +++ b/rip/core.py @@ -8,6 +8,7 @@ import re from getpass import getpass from hashlib import md5 from string import Formatter +import threading from typing import Dict, Generator, List, Optional, Tuple, Type, Union import requests @@ -385,9 +386,16 @@ class RipCore(list): creds = self.config.creds(client.source) except MissingCredentials: logger.debug("Credentials are missing. Prompting..") + get_tokens = threading.Thread( + target=client._get_app_id_and_secrets, daemon=True + ) + get_tokens.start() + self.prompt_creds(client.source) creds = self.config.creds(client.source) + get_tokens.join() + if ( client.source == "qobuz" and not creds.get("secrets") diff --git a/streamrip/clients.py b/streamrip/clients.py index 6391a45..8917005 100644 --- a/streamrip/clients.py +++ b/streamrip/clients.py @@ -8,7 +8,7 @@ import logging import re import time from abc import ABC, abstractmethod -from pprint import pformat +import concurrent.futures from typing import Any, Dict, Generator, Optional, Sequence, Tuple, Union import deezer @@ -129,23 +129,17 @@ class QobuzClient(Client): logger.debug("Already logged in") return - if not (kwargs.get("app_id") or kwargs.get("secrets")): - secho("Fetching tokens — this may take a few seconds.", fg="magenta") - logger.info("Fetching tokens from Qobuz") - spoofer = Spoofer() - kwargs["app_id"] = spoofer.get_app_id() - kwargs["secrets"] = spoofer.get_secrets() - - self.app_id = str(kwargs["app_id"]) # Ensure it is a string - self.secrets = kwargs["secrets"] - - self.session = gen_threadsafe_session( - headers={"User-Agent": AGENT, "X-App-Id": self.app_id} - ) + if not kwargs.get("app_id") or not kwargs.get("secrets"): + self._get_app_id_and_secrets() # can be async + else: + self.app_id, self.secrets = str(kwargs["app_id"]), kwargs["secrets"] + self.session = gen_threadsafe_session( + headers={"User-Agent": AGENT, "X-App-Id": self.app_id} + ) + self._validate_secrets() self._api_login(email, pwd) logger.debug("Logged into Qobuz") - self._validate_secrets() logger.debug("Qobuz client is ready to use") self.logged_in = True @@ -218,6 +212,18 @@ class QobuzClient(Client): # ---------- Private Methods --------------- + def _get_app_id_and_secrets(self): + if not hasattr(self, "app_id") or not hasattr(self, "secrets"): + spoofer = Spoofer() + self.app_id, self.secrets = str(spoofer.get_app_id()), spoofer.get_secrets() + + if not hasattr(self, "sec"): + if not hasattr(self, "session"): + self.session = gen_threadsafe_session( + headers={"User-Agent": AGENT, "X-App-Id": self.app_id} + ) + self._validate_secrets() + def _gen_pages(self, epoint: str, params: dict) -> Generator: """When there are multiple pages of results, this yields them. @@ -249,11 +255,17 @@ class QobuzClient(Client): def _validate_secrets(self): """Check if the secrets are usable.""" - for secret in self.secrets: - if self._test_secret(secret): - self.sec = secret - logger.debug("Working secret and app_id: %s - %s", secret, self.app_id) - break + with concurrent.futures.ThreadPoolExecutor() as executor: + futures = [ + executor.submit(self._test_secret, secret) for secret in self.secrets + ] + + for future in concurrent.futures.as_completed(futures): + result = future.result() + if result is not None: + self.sec = result + break + if not hasattr(self, "sec"): raise InvalidAppSecretError(f"Invalid secrets: {self.secrets}") @@ -386,7 +398,7 @@ class QobuzClient(Client): else: raise InvalidAppSecretError("Cannot find app secret") - quality = int(get_quality(quality, self.source)) + quality = int(get_quality(quality, self.source)) # type: ignore r_sig = f"trackgetFileUrlformat_id{quality}intentstreamtrack_id{track_id}{unix_ts}{secret}" logger.debug("Raw request signature: %s", r_sig) r_sig_hashed = hashlib.md5(r_sig.encode("utf-8")).hexdigest() @@ -423,7 +435,7 @@ class QobuzClient(Client): logger.error("Problem getting JSON. Status code: %s", r.status_code) raise - def _test_secret(self, secret: str) -> bool: + def _test_secret(self, secret: str) -> Optional[str]: """Test the authenticity of a secret. :param secret: @@ -432,10 +444,10 @@ class QobuzClient(Client): """ try: self._api_get_file_url("19512574", sec=secret) - return True + return secret except InvalidAppSecretError as error: logger.debug("Test for %s secret didn't work: %s", secret, error) - return False + return None class DeezerClient(Client): diff --git a/streamrip/spoofbuz.py b/streamrip/spoofbuz.py index 1923cc2..5dbb4e8 100644 --- a/streamrip/spoofbuz.py +++ b/streamrip/spoofbuz.py @@ -6,6 +6,7 @@ Credits to Dash for this tool. import base64 import re from collections import OrderedDict +from typing import List import requests @@ -50,7 +51,7 @@ class Spoofer: raise Exception("Could not find app id.") - def get_secrets(self): + def get_secrets(self) -> List[str]: """Get secrets.""" seed_matches = re.finditer(self.seed_timezone_regex, self.bundle) secrets = OrderedDict() @@ -81,6 +82,6 @@ class Spoofer: "".join(secrets[secret_pair])[:-44] ).decode("utf-8") - vals = list(secrets.values()) + vals: List[str] = list(secrets.values()) vals.remove("") return vals