mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-13 22:54:55 -04:00
Use asynchronous requests for QobuzClient startup/login
This commit is contained in:
parent
e9f40923ba
commit
6e0731ed0b
4 changed files with 54 additions and 36 deletions
17
rip/cli.py
17
rip/cli.py
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue