mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-09 14:11:55 -04:00
127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
import re
|
|
|
|
from .client import Client, NonStreamable
|
|
from .config import Config
|
|
from .downloadable import SoundcloudDownloadable
|
|
|
|
BASE = "https://api-v2.soundcloud.com"
|
|
SOUNDCLOUD_USER_ID = "672320-86895-162383-801513"
|
|
|
|
|
|
class SoundcloudClient(Client):
|
|
source = "soundcloud"
|
|
logged_in = False
|
|
|
|
def __init__(self, config: Config):
|
|
self.global_config = config
|
|
self.config = config.session.soundcloud
|
|
self.session = self.get_session()
|
|
self.rate_limiter = self.get_rate_limiter(
|
|
config.session.downloads.requests_per_minute
|
|
)
|
|
|
|
async def login(self):
|
|
client_id, app_version = self.config.client_id, self.config.app_version
|
|
if not client_id or not app_version or not self._announce():
|
|
client_id, app_version = await self._refresh_tokens()
|
|
|
|
# update file and session configs and save to disk
|
|
c = self.global_config.file.soundcloud
|
|
self.config.client_id = c.client_id = client_id
|
|
self.config.client_id = c.app_version = app_version
|
|
self.global_config.file.set_modified()
|
|
|
|
async def _announce(self):
|
|
resp = await self._api_request("announcements")
|
|
return resp.status == 200
|
|
|
|
async def _refresh_tokens(self) -> tuple[str, str]:
|
|
"""Return a valid client_id, app_version pair."""
|
|
STOCK_URL = "https://soundcloud.com/"
|
|
async with self.session.get(STOCK_URL) as resp:
|
|
page_text = await resp.text(encoding="utf-8")
|
|
|
|
*_, client_id_url_match = re.finditer(
|
|
r"<script\s+crossorigin\s+src=\"([^\"]+)\"", page_text
|
|
)
|
|
|
|
if client_id_url_match is None:
|
|
raise Exception("Could not find client ID in %s" % STOCK_URL)
|
|
|
|
client_id_url = client_id_url_match.group(1)
|
|
|
|
app_version_match = re.search(
|
|
r'<script>window\.__sc_version="(\d+)"</script>', page_text
|
|
)
|
|
if app_version_match is None:
|
|
raise Exception("Could not find app version in %s" % client_id_url_match)
|
|
app_version = app_version_match.group(1)
|
|
|
|
async with self.session.get(client_id_url) as resp:
|
|
page_text2 = await resp.text(encoding="utf-8")
|
|
|
|
client_id_match = re.search(r'client_id:\s*"(\w+)"', page_text2)
|
|
assert client_id_match is not None
|
|
client_id = client_id_match.group(1)
|
|
|
|
return client_id, app_version
|
|
|
|
async def get_downloadable(self, item: dict, _) -> SoundcloudDownloadable:
|
|
if not item["streamable"] or item["policy"] == "BLOCK":
|
|
raise NonStreamable(item)
|
|
|
|
if item["downloadable"] and item["has_downloads_left"]:
|
|
resp = await self._api_request(f"tracks/{item['id']}/download")
|
|
resp_json = await resp.json()
|
|
return SoundcloudDownloadable(
|
|
{"url": resp_json["redirectUri"], "type": "original"}
|
|
)
|
|
|
|
else:
|
|
url = None
|
|
for tc in item["media"]["transcodings"]:
|
|
fmt = tc["format"]
|
|
if fmt["protocol"] == "hls" and fmt["mime_type"] == "audio/mpeg":
|
|
url = tc["url"]
|
|
break
|
|
|
|
assert url is not None
|
|
|
|
resp = await self._request(url)
|
|
resp_json = await resp.json()
|
|
return SoundcloudDownloadable({"url": resp_json["url"], "type": "mp3"})
|
|
|
|
async def search(
|
|
self, query: str, media_type: str, limit: int = 50, offset: int = 0
|
|
):
|
|
params = {
|
|
"q": query,
|
|
"facet": "genre",
|
|
"user_id": SOUNDCLOUD_USER_ID,
|
|
"limit": limit,
|
|
"offset": offset,
|
|
"linked_partitioning": "1",
|
|
}
|
|
resp = await self._api_request(f"search/{media_type}s", params=params)
|
|
return await resp.json()
|
|
|
|
async def _api_request(self, path, params=None, headers=None):
|
|
url = f"{BASE}/{path}"
|
|
return await self._request(url, params=params, headers=headers)
|
|
|
|
async def _request(self, url, params=None, headers=None):
|
|
c = self.config
|
|
_params = {
|
|
"client_id": c.client_id,
|
|
"app_version": c.app_version,
|
|
"app_locale": "en",
|
|
}
|
|
if params is not None:
|
|
_params.update(params)
|
|
|
|
async with self.session.get(url, params=_params, headers=headers) as resp:
|
|
return resp
|
|
|
|
async def _resolve_url(self, url: str) -> dict:
|
|
resp = await self._api_request(f"resolve?url={url}")
|
|
return await resp.json()
|