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:
@@ -49,7 +49,7 @@ const linked = { id: 4, name: "Mix", kind: "user", yt_playlist_id: "PL1" } as co
|
||||
describe("usePlaylistActions — deleting a list", () => {
|
||||
it("does not toast a failure the request layer already put on screen", async () => {
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(JSON.stringify({ detail: { code: "playlist", reason: "youtube_failed" } }), {
|
||||
new Response(JSON.stringify({ detail: { code: "youtube", reason: "unavailable" } }), {
|
||||
status: 422,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
|
||||
@@ -91,5 +91,10 @@
|
||||
"reload": "Reload",
|
||||
"notifTitle": "Something broke",
|
||||
"notifMessage": "Unexpected error"
|
||||
},
|
||||
"youtube": {
|
||||
"quota": "Today's YouTube budget for this instance is spent. It resets at midnight Pacific time — try again then.",
|
||||
"reconnect": "Google no longer accepts this account's YouTube connection. Sign out and sign in with Google again to reconnect it.",
|
||||
"unavailable": "YouTube didn't accept that just now. Please try again."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
"name_required": "Give the playlist a name.",
|
||||
"youtube_read_scope": "Connect YouTube (read access) to sync your playlists.",
|
||||
"not_on_youtube": "This playlist isn't on YouTube.",
|
||||
"youtube_failed": "YouTube didn't accept that just now. Please try again.",
|
||||
"watch_later_not_pushable": "The Watch later list can't be sent to YouTube.",
|
||||
"watch_later_undeletable": "The Watch later list can't be deleted.",
|
||||
"youtube_editing_off": "Enable YouTube editing in Settings first.",
|
||||
|
||||
@@ -91,5 +91,10 @@
|
||||
"reload": "Újratöltés",
|
||||
"notifTitle": "Valami elromlott",
|
||||
"notifMessage": "Váratlan hiba"
|
||||
},
|
||||
"youtube": {
|
||||
"quota": "Ennek a példánynak elfogyott a mai YouTube-kerete. Csendes-óceáni idő szerint éjfélkor újraindul — akkor próbáld újra.",
|
||||
"reconnect": "A Google már nem fogadja el ennek a fióknak a YouTube-kapcsolatát. Jelentkezz ki, majd lépj be újra Google-lel az összekötéshez.",
|
||||
"unavailable": "A YouTube most nem fogadta el. Próbáld újra."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
"name_required": "Adj nevet a lejátszási listának.",
|
||||
"youtube_read_scope": "A listák szinkronizálásához kapcsold össze a YouTube-ot (olvasási hozzáférés).",
|
||||
"not_on_youtube": "Ez a lejátszási lista nincs fent a YouTube-on.",
|
||||
"youtube_failed": "A YouTube most nem fogadta el. Próbáld újra.",
|
||||
"watch_later_not_pushable": "A „Megnézem később” listát nem lehet a YouTube-ra küldeni.",
|
||||
"watch_later_undeletable": "A „Megnézem később” lista nem törölhető.",
|
||||
"youtube_editing_off": "Előbb engedélyezd a YouTube-szerkesztést a Beállításokban.",
|
||||
|
||||
@@ -167,6 +167,23 @@ describe("req — a structured refusal reads in the user's language", () => {
|
||||
detail: localized,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["quota", "reconnect", "unavailable"])(
|
||||
"says what to do about an action YouTube refused (%s)",
|
||||
async (reason) => {
|
||||
// Waiting for tomorrow's budget, reconnecting and retrying are different advice; the manual
|
||||
// syncs used to answer a raw 500 here, and the other routes the upstream's English.
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(JSON.stringify({ detail: { code: "youtube", reason } }), {
|
||||
status: 422,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
await expect(req("/api/sync/subscriptions", { method: "POST" })).rejects.toMatchObject({
|
||||
detail: `errors.youtube.${reason}`,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("req — what it has already put in front of the user", () => {
|
||||
|
||||
@@ -129,6 +129,10 @@ function localizeDetail(d: unknown): string {
|
||||
return o.reason === "name_taken"
|
||||
? t("plex.collection.nameTaken")
|
||||
: t("plex.collection.editFailed");
|
||||
// An action YouTube refused (routes/_common.youtube_refusal): what the reader can do about it —
|
||||
// wait for the budget, reconnect, or retry. An unknown reason reads as the retryable one.
|
||||
if (o?.code === "youtube")
|
||||
return t(`errors.youtube.${o.reason}`, t("errors.youtube.unavailable"));
|
||||
// A playlist action the server refused (Watch later can't be deleted, YouTube said no, …).
|
||||
if (o?.code === "playlist") return t(`playlists.errors.${o.reason}`, t("errors.generic"));
|
||||
// An admin action on an account or an invite that the server refused: activating an address that
|
||||
|
||||
Reference in New Issue
Block a user