fix(youtube): a refused YouTube action answers a 422 that says what to do

The two manual syncs (subscriptions import, playlist pull) had no YouTubeError
handler, so a quota, token or transport failure was a raw 500 and the blocking
"Server error" dialog (E9 F1). The other YouTube routes answered either the
upstream's English or a playlist-scoped "youtube_failed".

One helper, routes/_common.youtube_refusal: 422 {code: "youtube", reason} with
the reason sorted by what the user can do — quota (wait for the reset),
reconnect (the token step refused), unavailable (retry). Used by every route
that surfaces a YouTube failure; localizeDetail translates it.

"Reconnect" had no path: a grant Google revoked still read as granted, so
Settings and the wizard offered no connect button while every call failed.
An invalid_grant from the token endpoint now clears the stored grant.
This commit is contained in:
2026-09-19 06:55:40 +02:00
parent 1cfb19bdc7
commit fad301e7a4
15 changed files with 221 additions and 27 deletions
+10
View File
@@ -13,6 +13,7 @@ from sqlalchemy.orm import Session
from app.db import Base
from app.models import User
from app.youtube.client import YouTubeError, refusal_reason
M = TypeVar("M", bound=Base)
@@ -51,3 +52,12 @@ def owned_or_404(
if getattr(obj, owner_attr) != user.id:
raise not_found(what)
return obj
def youtube_refusal(exc: YouTubeError) -> HTTPException:
"""The answer to an action YouTube refused: a 422 whose reason the UI translates
(`errors.youtube.<reason>`). One definition, because the routes had grown three shapes of it —
a bare English string quoting the upstream message, a playlist-scoped reason, and nothing at
all (a raw 500 from the manual syncs). The upstream failure is already logged where it
happened (`youtube.api.error`)."""
return HTTPException(status_code=422, detail={"code": "youtube", "reason": refusal_reason(exc)})