Use asynchronous requests for QobuzClient startup/login

This commit is contained in:
nathom 2021-08-12 10:50:11 -07:00
parent e9f40923ba
commit 6e0731ed0b
4 changed files with 54 additions and 36 deletions

View file

@ -46,6 +46,7 @@ class DownloadCommand(Command):
# Use a thread so that it doesn't slow down startup # Use a thread so that it doesn't slow down startup
update_check = threading.Thread(target=is_outdated, daemon=True) update_check = threading.Thread(target=is_outdated, daemon=True)
update_check.start()
config = Config() config = Config()
path, codec, quality, no_db = clean_options( path, codec, quality, no_db = clean_options(
@ -83,16 +84,12 @@ class DownloadCommand(Command):
elif not urls and path is None: elif not urls and path is None:
self.line("<error>Must pass arguments. See </><cmd>rip url -h</cmd>.") self.line("<error>Must pass arguments. See </><cmd>rip url -h</cmd>.")
try: update_check.join()
update_check.join() if outdated:
if outdated: self.line(
self.line( "<info>A new version of streamrip is available! Run</info> "
"<info>A new version of streamrip is available! Run</info> " "<cmd>pip3 install streamrip --upgrade to update</cmd>"
"<cmd>pip3 install streamrip --upgrade to update</cmd>" )
)
except RuntimeError as e:
logger.debug("Update check error: %s", e)
pass
return 0 return 0

View file

@ -8,6 +8,7 @@ import re
from getpass import getpass from getpass import getpass
from hashlib import md5 from hashlib import md5
from string import Formatter from string import Formatter
import threading
from typing import Dict, Generator, List, Optional, Tuple, Type, Union from typing import Dict, Generator, List, Optional, Tuple, Type, Union
import requests import requests
@ -385,9 +386,16 @@ class RipCore(list):
creds = self.config.creds(client.source) creds = self.config.creds(client.source)
except MissingCredentials: except MissingCredentials:
logger.debug("Credentials are missing. Prompting..") 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) self.prompt_creds(client.source)
creds = self.config.creds(client.source) creds = self.config.creds(client.source)
get_tokens.join()
if ( if (
client.source == "qobuz" client.source == "qobuz"
and not creds.get("secrets") and not creds.get("secrets")

View file

@ -8,7 +8,7 @@ import logging
import re import re
import time import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pprint import pformat import concurrent.futures
from typing import Any, Dict, Generator, Optional, Sequence, Tuple, Union from typing import Any, Dict, Generator, Optional, Sequence, Tuple, Union
import deezer import deezer
@ -129,23 +129,17 @@ class QobuzClient(Client):
logger.debug("Already logged in") logger.debug("Already logged in")
return return
if not (kwargs.get("app_id") or kwargs.get("secrets")): if not kwargs.get("app_id") or not kwargs.get("secrets"):
secho("Fetching tokens — this may take a few seconds.", fg="magenta") self._get_app_id_and_secrets() # can be async
logger.info("Fetching tokens from Qobuz") else:
spoofer = Spoofer() self.app_id, self.secrets = str(kwargs["app_id"]), kwargs["secrets"]
kwargs["app_id"] = spoofer.get_app_id() self.session = gen_threadsafe_session(
kwargs["secrets"] = spoofer.get_secrets() headers={"User-Agent": AGENT, "X-App-Id": self.app_id}
)
self.app_id = str(kwargs["app_id"]) # Ensure it is a string self._validate_secrets()
self.secrets = kwargs["secrets"]
self.session = gen_threadsafe_session(
headers={"User-Agent": AGENT, "X-App-Id": self.app_id}
)
self._api_login(email, pwd) self._api_login(email, pwd)
logger.debug("Logged into Qobuz") logger.debug("Logged into Qobuz")
self._validate_secrets()
logger.debug("Qobuz client is ready to use") logger.debug("Qobuz client is ready to use")
self.logged_in = True self.logged_in = True
@ -218,6 +212,18 @@ class QobuzClient(Client):
# ---------- Private Methods --------------- # ---------- 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: def _gen_pages(self, epoint: str, params: dict) -> Generator:
"""When there are multiple pages of results, this yields them. """When there are multiple pages of results, this yields them.
@ -249,11 +255,17 @@ class QobuzClient(Client):
def _validate_secrets(self): def _validate_secrets(self):
"""Check if the secrets are usable.""" """Check if the secrets are usable."""
for secret in self.secrets: with concurrent.futures.ThreadPoolExecutor() as executor:
if self._test_secret(secret): futures = [
self.sec = secret executor.submit(self._test_secret, secret) for secret in self.secrets
logger.debug("Working secret and app_id: %s - %s", secret, self.app_id) ]
break
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"): if not hasattr(self, "sec"):
raise InvalidAppSecretError(f"Invalid secrets: {self.secrets}") raise InvalidAppSecretError(f"Invalid secrets: {self.secrets}")
@ -386,7 +398,7 @@ class QobuzClient(Client):
else: else:
raise InvalidAppSecretError("Cannot find app secret") 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}" r_sig = f"trackgetFileUrlformat_id{quality}intentstreamtrack_id{track_id}{unix_ts}{secret}"
logger.debug("Raw request signature: %s", r_sig) logger.debug("Raw request signature: %s", r_sig)
r_sig_hashed = hashlib.md5(r_sig.encode("utf-8")).hexdigest() 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) logger.error("Problem getting JSON. Status code: %s", r.status_code)
raise raise
def _test_secret(self, secret: str) -> bool: def _test_secret(self, secret: str) -> Optional[str]:
"""Test the authenticity of a secret. """Test the authenticity of a secret.
:param secret: :param secret:
@ -432,10 +444,10 @@ class QobuzClient(Client):
""" """
try: try:
self._api_get_file_url("19512574", sec=secret) self._api_get_file_url("19512574", sec=secret)
return True return secret
except InvalidAppSecretError as error: except InvalidAppSecretError as error:
logger.debug("Test for %s secret didn't work: %s", secret, error) logger.debug("Test for %s secret didn't work: %s", secret, error)
return False return None
class DeezerClient(Client): class DeezerClient(Client):

View file

@ -6,6 +6,7 @@ Credits to Dash for this tool.
import base64 import base64
import re import re
from collections import OrderedDict from collections import OrderedDict
from typing import List
import requests import requests
@ -50,7 +51,7 @@ class Spoofer:
raise Exception("Could not find app id.") raise Exception("Could not find app id.")
def get_secrets(self): def get_secrets(self) -> List[str]:
"""Get secrets.""" """Get secrets."""
seed_matches = re.finditer(self.seed_timezone_regex, self.bundle) seed_matches = re.finditer(self.seed_timezone_regex, self.bundle)
secrets = OrderedDict() secrets = OrderedDict()
@ -81,6 +82,6 @@ class Spoofer:
"".join(secrets[secret_pair])[:-44] "".join(secrets[secret_pair])[:-44]
).decode("utf-8") ).decode("utf-8")
vals = list(secrets.values()) vals: List[str] = list(secrets.values())
vals.remove("") vals.remove("")
return vals return vals