Merge: promote dev to prod
This commit is contained in:
commit
9bd7e31463
3 changed files with 31 additions and 5 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.33.0
|
0.33.1
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue