mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-09 14:11:55 -04:00
100 lines
3.4 KiB
Python
100 lines
3.4 KiB
Python
"""Get app id and secrets for Qobuz.
|
|
|
|
Credits to Dash for this tool.
|
|
"""
|
|
|
|
import base64
|
|
import re
|
|
from collections import OrderedDict
|
|
from typing import List
|
|
|
|
import aiohttp
|
|
|
|
|
|
class QobuzSpoofer:
|
|
"""Spoofs the information required to stream tracks from Qobuz."""
|
|
|
|
def __init__(self):
|
|
"""Create a Spoofer."""
|
|
self.seed_timezone_regex = (
|
|
r'[a-z]\.initialSeed\("(?P<seed>[\w=]+)",window\.ut'
|
|
r"imezone\.(?P<timezone>[a-z]+)\)"
|
|
)
|
|
# note: {timezones} should be replaced with every capitalized timezone joined by a |
|
|
self.info_extras_regex = (
|
|
r'name:"\w+/(?P<timezone>{timezones})",info:"'
|
|
r'(?P<info>[\w=]+)",extras:"(?P<extras>[\w=]+)"'
|
|
)
|
|
self.app_id_regex = (
|
|
r'production:{api:{appId:"(?P<app_id>\d{9})",appSecret:"(\w{32})'
|
|
)
|
|
self.session = None
|
|
|
|
async def get_app_id_and_secrets(self) -> tuple[str, list[str]]:
|
|
assert self.session is not None
|
|
async with self.session.get("https://play.qobuz.com/login") as req:
|
|
login_page = await req.text()
|
|
|
|
bundle_url_match = re.search(
|
|
r'<script src="(/resources/\d+\.\d+\.\d+-[a-z]\d{3}/bundle\.js)"></script>',
|
|
login_page,
|
|
)
|
|
assert bundle_url_match is not None
|
|
bundle_url = bundle_url_match.group(1)
|
|
|
|
async with self.session.get("https://play.qobuz.com" + bundle_url) as req:
|
|
self.bundle = await req.text()
|
|
|
|
match = re.search(self.app_id_regex, self.bundle)
|
|
if match is None:
|
|
raise Exception("Could not find app id.")
|
|
|
|
app_id = str(match.group("app_id"))
|
|
|
|
# get secrets
|
|
seed_matches = re.finditer(self.seed_timezone_regex, self.bundle)
|
|
secrets = OrderedDict()
|
|
for match in seed_matches:
|
|
seed, timezone = match.group("seed", "timezone")
|
|
secrets[timezone] = [seed]
|
|
|
|
"""
|
|
The code that follows switches around the first and second timezone.
|
|
Qobuz uses two ternary (a shortened if statement) conditions that
|
|
should always return false. The way Javascript's ternary syntax
|
|
works, the second option listed is what runs if the condition returns
|
|
false. Because of this, we must prioritize the *second* seed/timezone
|
|
pair captured, not the first.
|
|
"""
|
|
|
|
keypairs = list(secrets.items())
|
|
secrets.move_to_end(keypairs[1][0], last=False)
|
|
|
|
info_extras_regex = self.info_extras_regex.format(
|
|
timezones="|".join(timezone.capitalize() for timezone in secrets)
|
|
)
|
|
info_extras_matches = re.finditer(info_extras_regex, self.bundle)
|
|
for match in info_extras_matches:
|
|
timezone, info, extras = match.group("timezone", "info", "extras")
|
|
secrets[timezone.lower()] += [info, extras]
|
|
|
|
for secret_pair in secrets:
|
|
secrets[secret_pair] = base64.standard_b64decode(
|
|
"".join(secrets[secret_pair])[:-44]
|
|
).decode("utf-8")
|
|
|
|
vals: List[str] = list(secrets.values())
|
|
vals.remove("")
|
|
|
|
secrets_list = vals
|
|
|
|
return app_id, secrets_list
|
|
|
|
async def __aenter__(self):
|
|
self.session = aiohttp.ClientSession()
|
|
return self
|
|
|
|
async def __aexit__(self, *_):
|
|
if self.session is not None:
|
|
await self.session.close()
|
|
self.session = None
|