Merge: promote dev to prod

This commit is contained in:
npeter83 2026-07-09 09:46:14 +02:00
commit 9bd7e31463
3 changed files with 31 additions and 5 deletions

View file

@ -1 +1 @@
0.33.0 0.33.1

View file

@ -55,6 +55,22 @@ class YouTubeClient:
def __exit__(self, *exc) -> None: def __exit__(self, *exc) -> None:
self._http.close() self._http.close()
# --- transport ---
def _send(self, method: str, url: str, **kwargs) -> httpx.Response:
"""Issue an HTTP request, mapping any transport/network failure (egress proxy down,
DNS, timeout, connection reset) to YouTubeError. Every caller already handles
YouTubeError degrade gracefully, log, and move on so wrapping here keeps a
transient network fault (e.g. the fixed-IP egress proxy being unreachable) from
escaping as a raw httpx error and turning into an uncaught 500. HTTP status errors
are NOT touched here (we don't use raise_for_status): callers inspect resp.status_code
themselves and raise their own YouTubeError with the response body."""
try:
return self._http.request(method, url, **kwargs)
except httpx.HTTPError as exc:
raise YouTubeError(
f"YouTube API unreachable ({exc.__class__.__name__}: {exc})"
) from exc
# --- auth --- # --- auth ---
def _access_token(self) -> str: def _access_token(self) -> str:
tok = self.user.token tok = self.user.token
@ -66,7 +82,8 @@ class YouTubeClient:
refresh = decrypt(tok.refresh_token_enc) refresh = decrypt(tok.refresh_token_enc)
if not refresh: if not refresh:
raise YouTubeError("No refresh token; user must re-authenticate") raise YouTubeError("No refresh token; user must re-authenticate")
resp = self._http.post( resp = self._send(
"POST",
GOOGLE_TOKEN_URL, GOOGLE_TOKEN_URL,
data={ data={
"client_id": sysconfig.get_str(self.db, "google_client_id"), "client_id": sysconfig.get_str(self.db, "google_client_id"),
@ -94,7 +111,7 @@ class YouTubeClient:
p["key"] = api_key p["key"] = api_key
else: else:
headers["Authorization"] = f"Bearer {self._access_token()}" headers["Authorization"] = f"Bearer {self._access_token()}"
resp = self._http.get(f"{API_BASE}/{path}", params=p, headers=headers) resp = self._send("GET", f"{API_BASE}/{path}", params=p, headers=headers)
quota.record_usage(self.db, cost) quota.record_usage(self.db, cost)
if resp.status_code != 200: if resp.status_code != 200:
log.warning("YouTube API %s -> %s: %s", path, resp.status_code, resp.text[:200]) log.warning("YouTube API %s -> %s: %s", path, resp.status_code, resp.text[:200])
@ -210,7 +227,8 @@ class YouTubeClient:
def delete_subscription(self, subscription_id: str) -> None: def delete_subscription(self, subscription_id: str) -> None:
"""Unsubscribe on YouTube. Requires the write scope and the user's OAuth token """Unsubscribe on YouTube. Requires the write scope and the user's OAuth token
(never the public API key). subscriptions.delete costs 50 quota units.""" (never the public API key). subscriptions.delete costs 50 quota units."""
resp = self._http.delete( resp = self._send(
"DELETE",
f"{API_BASE}/subscriptions", f"{API_BASE}/subscriptions",
params={"id": subscription_id}, params={"id": subscription_id},
headers={"Authorization": f"Bearer {self._access_token()}"}, headers={"Authorization": f"Bearer {self._access_token()}"},
@ -268,7 +286,7 @@ class YouTubeClient:
# --- write endpoints (OAuth only, never the API key; each costs 50 quota units) --- # --- write endpoints (OAuth only, never the API key; each costs 50 quota units) ---
def _write(self, method: str, path: str, *, params: dict, json: dict | None = None) -> dict: def _write(self, method: str, path: str, *, params: dict, json: dict | None = None) -> dict:
headers = {"Authorization": f"Bearer {self._access_token()}"} headers = {"Authorization": f"Bearer {self._access_token()}"}
resp = self._http.request( resp = self._send(
method, f"{API_BASE}/{path}", params=params, json=json, headers=headers method, f"{API_BASE}/{path}", params=params, json=json, headers=headers
) )
quota.record_usage(self.db, 50) quota.record_usage(self.db, 50)

View file

@ -14,6 +14,14 @@ export interface ReleaseEntry {
} }
export const RELEASE_NOTES: ReleaseEntry[] = [ export const RELEASE_NOTES: ReleaseEntry[] = [
{
version: "0.33.1",
date: "2026-07-09",
summary: "Fix: opening a channel no longer errors when YouTube is briefly unreachable.",
fixes: [
'Opening a channel page could pop a blocking "Server error" when the connection to YouTube was momentarily unavailable. The page now loads regardless — its latest channel details simply fill in once the connection is back — instead of failing.',
],
},
{ {
version: "0.33.0", version: "0.33.0",
date: "2026-07-09", date: "2026-07-09",