feat: add user resolution and likes fetching with 401 retry
Made-with: Cursor
This commit is contained in:
@@ -50,5 +50,103 @@ class SoundCloudClient:
|
||||
def invalidate_client_id(self) -> None:
|
||||
self._client_id = None
|
||||
|
||||
async def _api_get(self, url: str, params: dict | None = None) -> httpx.Response:
|
||||
client_id = await self._extract_client_id()
|
||||
params = dict(params or {})
|
||||
params["client_id"] = client_id
|
||||
|
||||
for attempt in range(3):
|
||||
resp = await self._http.get(url, params=params)
|
||||
if resp.status_code == 401:
|
||||
logger.warning("Got 401 from SoundCloud API, refreshing client_id (attempt %d)", attempt + 1)
|
||||
self.invalidate_client_id()
|
||||
client_id = await self._extract_client_id()
|
||||
params["client_id"] = client_id
|
||||
continue
|
||||
resp.raise_for_status()
|
||||
return resp
|
||||
|
||||
raise httpx.HTTPStatusError(
|
||||
"Failed after 3 attempts (401)",
|
||||
request=resp.request,
|
||||
response=resp,
|
||||
)
|
||||
|
||||
async def resolve_user(self, username: str) -> int:
|
||||
resp = await self._api_get(
|
||||
f"{API_BASE}/resolve",
|
||||
params={"url": f"{SOUNDCLOUD_BASE}/{username}"},
|
||||
)
|
||||
return resp.json()["id"]
|
||||
|
||||
async def fetch_likes(
|
||||
self,
|
||||
user_id: int,
|
||||
since: datetime,
|
||||
until: datetime,
|
||||
limit: int = 50,
|
||||
) -> list[Track]:
|
||||
cursor = _build_cursor(until, user_id)
|
||||
collected: list[Track] = []
|
||||
|
||||
while True:
|
||||
params: dict = {"limit": limit}
|
||||
if cursor:
|
||||
params["offset"] = cursor
|
||||
|
||||
resp = await self._api_get(f"{API_BASE}/users/{user_id}/likes", params=params)
|
||||
data = resp.json()
|
||||
collection = data.get("collection", [])
|
||||
|
||||
if not collection:
|
||||
break
|
||||
|
||||
stop = False
|
||||
for item in collection:
|
||||
liked_at_str = item.get("created_at", "")
|
||||
liked_at = datetime.fromisoformat(liked_at_str.replace("Z", "+00:00"))
|
||||
|
||||
if liked_at < since:
|
||||
stop = True
|
||||
break
|
||||
|
||||
if liked_at > until:
|
||||
continue
|
||||
|
||||
track_data = item.get("track")
|
||||
if track_data is None:
|
||||
continue
|
||||
|
||||
user_data = track_data.get("user", {})
|
||||
collected.append(
|
||||
Track(
|
||||
id=track_data["id"],
|
||||
title=track_data["title"],
|
||||
artist=user_data.get("username", "Unknown"),
|
||||
permalink_url=track_data["permalink_url"],
|
||||
artwork_url=track_data.get("artwork_url"),
|
||||
duration_ms=track_data.get("full_duration", track_data.get("duration", 0)),
|
||||
license=track_data.get("license", ""),
|
||||
liked_at=liked_at,
|
||||
raw_json=json.dumps(track_data),
|
||||
)
|
||||
)
|
||||
|
||||
if stop:
|
||||
break
|
||||
|
||||
next_href = data.get("next_href")
|
||||
if not next_href:
|
||||
break
|
||||
|
||||
parsed = urlparse(next_href)
|
||||
qs = parse_qs(parsed.query)
|
||||
cursor = qs.get("offset", [None])[0]
|
||||
if cursor is None:
|
||||
break
|
||||
|
||||
collected.sort(key=lambda t: t.liked_at)
|
||||
return collected
|
||||
|
||||
async def close(self) -> None:
|
||||
await self._http.aclose()
|
||||
|
||||
Reference in New Issue
Block a user