From 6c4c33f9566fd92747bfc3bcc73a63ae9a826263 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 26 Jun 2026 00:32:17 +0200 Subject: [PATCH 1/3] fix(notifications): name the entity in action toasts Eight confirmations stated a bare fact without saying what they acted on. Pass the entity through the mutation variables and interpolate it: channel unsubscribe/reset name the channel, whitelist/demo-whitelist adds name the email, tag delete names the tag, and playlist revert/push name the playlist. EN/HU/DE. --- frontend/src/components/AdminUsers.tsx | 8 ++++---- frontend/src/components/Channels.tsx | 16 ++++++++-------- frontend/src/components/Playlists.tsx | 8 +++++--- frontend/src/components/TagManager.tsx | 8 ++++---- frontend/src/i18n/locales/de/channels.json | 4 ++-- frontend/src/i18n/locales/de/playlists.json | 6 +++--- frontend/src/i18n/locales/de/settings.json | 4 ++-- frontend/src/i18n/locales/de/tagManager.json | 2 +- frontend/src/i18n/locales/en/channels.json | 4 ++-- frontend/src/i18n/locales/en/playlists.json | 6 +++--- frontend/src/i18n/locales/en/settings.json | 4 ++-- frontend/src/i18n/locales/en/tagManager.json | 2 +- frontend/src/i18n/locales/hu/channels.json | 4 ++-- frontend/src/i18n/locales/hu/playlists.json | 6 +++--- frontend/src/i18n/locales/hu/settings.json | 4 ++-- frontend/src/i18n/locales/hu/tagManager.json | 2 +- 16 files changed, 45 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/AdminUsers.tsx b/frontend/src/components/AdminUsers.tsx index 20165b6..8c6aeae 100644 --- a/frontend/src/components/AdminUsers.tsx +++ b/frontend/src/components/AdminUsers.tsx @@ -242,10 +242,10 @@ function AdminInvites() { }); const add = useMutation({ mutationFn: (email: string) => api.addInvite(email), - onSuccess: () => { + onSuccess: (_d, email) => { setNewEmail(""); refresh(); - notify({ level: "success", message: t("settings.invites.addedToWhitelist") }); + notify({ level: "success", message: t("settings.invites.addedToWhitelist", { email }) }); }, onError: () => notify({ level: "error", message: t("settings.invites.addFailed") }), }); @@ -324,10 +324,10 @@ function AdminDemo() { const add = useMutation({ mutationFn: (email: string) => api.addDemoWhitelist(email), - onSuccess: () => { + onSuccess: (_d, email) => { setNewEmail(""); qc.invalidateQueries({ queryKey: ["demo-whitelist"] }); - notify({ level: "success", message: t("settings.demo.added") }); + notify({ level: "success", message: t("settings.demo.added", { email }) }); }, onError: () => notify({ level: "error", message: t("settings.demo.addFailed") }), }); diff --git a/frontend/src/components/Channels.tsx b/frontend/src/components/Channels.tsx index 76447a7..ddcdfa4 100644 --- a/frontend/src/components/Channels.tsx +++ b/frontend/src/components/Channels.tsx @@ -157,20 +157,20 @@ export default function Channels({ onError: (e) => notifyActionError(e, "channels.notify.syncFailed"), }); const unsubscribe = useMutation({ - mutationFn: (id: string) => api.unsubscribeChannel(id), - onSuccess: () => { + mutationFn: (v: { id: string; name: string }) => api.unsubscribeChannel(v.id), + onSuccess: (_d, v) => { qc.invalidateQueries({ queryKey: ["channels"] }); qc.invalidateQueries({ queryKey: ["my-status"] }); - notify({ level: "success", message: t("channels.notify.unsubscribed") }); + notify({ level: "success", message: t("channels.notify.unsubscribed", { name: v.name }) }); }, onError: (e) => notifyActionError(e, "channels.notify.unsubscribeFailed"), }); const resetBackfill = useMutation({ - mutationFn: (id: string) => api.resetChannelBackfill(id), - onSuccess: () => { + mutationFn: (v: { id: string; name: string }) => api.resetChannelBackfill(v.id), + onSuccess: (_d, v) => { qc.invalidateQueries({ queryKey: ["channels"] }); qc.invalidateQueries({ queryKey: ["my-status"] }); - notify({ level: "success", message: t("channels.notify.resetDone") }); + notify({ level: "success", message: t("channels.notify.resetDone", { name: v.name }) }); }, onError: (e) => notifyActionError(e, "channels.notify.resetFailed"), }); @@ -210,7 +210,7 @@ export default function Channels({ confirmLabel: t("channels.row.unsubscribeOnYoutube"), danger: true, }); - if (ok) unsubscribe.mutate(c.id); + if (ok) unsubscribe.mutate({ id: c.id, name: c.title ?? c.id }); }; const onReset = async (c: ManagedChannel) => { const ok = await confirm({ @@ -218,7 +218,7 @@ export default function Channels({ message: t("channels.confirmReset", { name: c.title ?? c.id }), confirmLabel: t("channels.row.backfillThis"), }); - if (ok) resetBackfill.mutate(c.id); + if (ok) resetBackfill.mutate({ id: c.id, name: c.title ?? c.id }); }; const columns: Column[] = [ diff --git a/frontend/src/components/Playlists.tsx b/frontend/src/components/Playlists.tsx index 8bf346e..8c49f64 100644 --- a/frontend/src/components/Playlists.tsx +++ b/frontend/src/components/Playlists.tsx @@ -353,11 +353,12 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) { danger: true, }); if (!ok) return; + const plName = detail.kind === "watch_later" ? t("playlists.watchLater") : detail.name; setReverting(true); try { await api.revertPlaylist(selectedId); refreshAll(); - notify({ level: "success", message: t("playlists.revertDone") }); + notify({ level: "success", message: t("playlists.revertDone", { name: plName }) }); } catch { notify({ level: "warning", message: t("playlists.revertFailed") }); } finally { @@ -367,11 +368,12 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) { async function pushToYoutube() { if (selectedId == null || !detail || pushing) return; + const plName = detail.kind === "watch_later" ? t("playlists.watchLater") : detail.name; setPushing(true); try { const plan = await api.playlistPushPlan(selectedId); if (plan.action === "update" && !plan.to_insert && !plan.to_delete && !plan.to_reorder) { - notify({ level: "info", message: t("playlists.pushUpToDate") }); + notify({ level: "info", message: t("playlists.pushUpToDate", { name: plName }) }); return; } if (!plan.affordable) { @@ -410,7 +412,7 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) { if (r.failures.length) { notify({ level: "warning", message: t("playlists.pushPartial", { count: r.failures.length }) }); } else { - notify({ level: "success", message: t("playlists.pushDone") }); + notify({ level: "success", message: t("playlists.pushDone", { name: plName }) }); } } catch { notify({ level: "warning", message: t("playlists.pushFailed") }); diff --git a/frontend/src/components/TagManager.tsx b/frontend/src/components/TagManager.tsx index 5999728..72bd36a 100644 --- a/frontend/src/components/TagManager.tsx +++ b/frontend/src/components/TagManager.tsx @@ -137,10 +137,10 @@ export default function TagManager({ onSuccess: invalidate, }); const del = useMutation({ - mutationFn: (id: number) => api.deleteTag(id), - onSuccess: () => { + mutationFn: (v: { id: number; name: string }) => api.deleteTag(v.id), + onSuccess: (_d, v) => { invalidate(); - notify({ level: "success", message: t("tagManager.deleted") }); + notify({ level: "success", message: t("tagManager.deleted", { name: v.name }) }); }, }); @@ -179,7 +179,7 @@ export default function TagManager({ }); if (!ok) return; } - del.mutate(tag.id); + del.mutate({ id: tag.id, name: tag.name }); }} /> ))} diff --git a/frontend/src/i18n/locales/de/channels.json b/frontend/src/i18n/locales/de/channels.json index 7898cba..93cafc2 100644 --- a/frontend/src/i18n/locales/de/channels.json +++ b/frontend/src/i18n/locales/de/channels.json @@ -111,14 +111,14 @@ "notify": { "synced": "{{count}} Abos synchronisiert", "syncFailed": "Abo-Synchronisierung fehlgeschlagen", - "unsubscribed": "Bei YouTube abbestellt", + "unsubscribed": "{{name}} bei YouTube abbestellt", "unsubscribeFailed": "Abbestellen fehlgeschlagen", "fullHistoryRequested": "Vollständiger Verlauf für {{count}} Kanäle angefordert", "fullHistoryFailed": "Vollständiger Verlauf konnte nicht angefordert werden", "needYouTube": "Verbinde dein YouTube-Konto, um dies zu tun.", "connect": "Verbinden", "filteredByTag": "Feed nach Tag gefiltert: {{name}}", - "resetDone": "Kanal zurückgesetzt — wird neu geladen", + "resetDone": "{{name}} zurückgesetzt — wird neu geladen", "resetFailed": "Kanal konnte nicht zurückgesetzt werden" }, "confirmUnsubscribe": "Kanal „{{name}}“ bei YouTube abbestellen? Dies ändert dein echtes YouTube-Konto. Um ihn nur aus deinem Feed zu entfernen, blende ihn stattdessen aus.", diff --git a/frontend/src/i18n/locales/de/playlists.json b/frontend/src/i18n/locales/de/playlists.json index e23f006..706522b 100644 --- a/frontend/src/i18n/locales/de/playlists.json +++ b/frontend/src/i18n/locales/de/playlists.json @@ -26,7 +26,7 @@ "revertTitle": "Lokale Änderungen verwerfen?", "revertMsg": "Dies lädt die Wiedergabeliste von YouTube neu und verwirft deine nicht synchronisierten lokalen Änderungen (Reihenfolge, Hinzufügungen, Entfernungen, Umbenennung). Fortfahren?", "revertConfirm": "Verwerfen & neu laden", - "revertDone": "Von YouTube neu geladen ✓", + "revertDone": "„{{name}}“ von YouTube neu geladen ✓", "revertFailed": "Neuladen von YouTube fehlgeschlagen.", "pushTitle": "Mit YouTube synchronisieren", "pushConfirm": "Synchronisieren", @@ -34,8 +34,8 @@ "pushPlanUpdate": "Auf YouTube aktualisieren — {{insert}} hinzufügen, {{del}} entfernen, {{reorder}} neu anordnen? (~{{units}} Kontingenteinheiten; heute noch {{left}} übrig.)", "pushDiverged": "{{count}} Video(s), die derzeit auf YouTube sind, werden entfernt.", "pushNoQuota": "Nicht genug YouTube-Kontingent für heute ({{left}}) für diese Synchronisierung (~{{units}} benötigt). Versuche es morgen erneut.", - "pushUpToDate": "Bereits mit YouTube synchronisiert.", - "pushDone": "Mit YouTube synchronisiert ✓", + "pushUpToDate": "„{{name}}“ ist bereits mit YouTube synchronisiert.", + "pushDone": "„{{name}}“ mit YouTube synchronisiert ✓", "pushPartial": "Synchronisiert, aber {{count}} Element(e) wurden übersprungen.", "pushFailed": "Synchronisierung mit YouTube fehlgeschlagen.", "deleteOnYoutubeTitle": "Auch auf YouTube löschen?", diff --git a/frontend/src/i18n/locales/de/settings.json b/frontend/src/i18n/locales/de/settings.json index 0f7bced..228d491 100644 --- a/frontend/src/i18n/locales/de/settings.json +++ b/frontend/src/i18n/locales/de/settings.json @@ -93,7 +93,7 @@ "intro": "E-Mails hier können das gemeinsame Demo-Konto direkt von der Anmeldeseite aus betreten — ohne Google-Anmeldung. Sie tippen die Adresse einfach ins Feld und werden nach einem Moment eingelassen.", "addPlaceholder": "E-Mail für die Demo auf die Whitelist…", "add": "Hinzufügen", - "added": "Zur Demo-Whitelist hinzugefügt", + "added": "{{email}} zur Demo-Whitelist hinzugefügt", "addFailed": "Diese E-Mail konnte nicht hinzugefügt werden", "removeFailed": "Diese E-Mail konnte nicht entfernt werden", "remove": "Entfernen", @@ -115,7 +115,7 @@ "approved": "{{email}} genehmigt — sie können sich jetzt anmelden", "approveFailed": "Genehmigung fehlgeschlagen", "denyFailed": "Ablehnung fehlgeschlagen", - "addedToWhitelist": "Zur Whitelist hinzugefügt", + "addedToWhitelist": "{{email}} zur Whitelist hinzugefügt", "addFailed": "Diese E-Mail konnte nicht hinzugefügt werden", "requested": "angefragt {{time}}", "approveHint": "Genehmigen — setzt diese E-Mail auf die Whitelist und benachrichtigt sie per E-Mail, dass sie dabei sind.", diff --git a/frontend/src/i18n/locales/de/tagManager.json b/frontend/src/i18n/locales/de/tagManager.json index a5a86d3..7042cf1 100644 --- a/frontend/src/i18n/locales/de/tagManager.json +++ b/frontend/src/i18n/locales/de/tagManager.json @@ -3,7 +3,7 @@ "channels": "{{count}} Kan.", "onChannels": "Getaggte Kanäle", "delete": "Löschen", - "deleted": "Tag gelöscht", + "deleted": "Tag „{{name}}“ gelöscht", "deleteTitle": "Tag löschen", "confirmDelete": "Tag „{{name}}“ löschen? Er wird von allen Kanälen entfernt, auf denen er liegt.", "empty": "Noch keine Tags — füge unten einen hinzu.", diff --git a/frontend/src/i18n/locales/en/channels.json b/frontend/src/i18n/locales/en/channels.json index fed9566..eef0389 100644 --- a/frontend/src/i18n/locales/en/channels.json +++ b/frontend/src/i18n/locales/en/channels.json @@ -111,14 +111,14 @@ "notify": { "synced": "Synced {{count}} subscriptions", "syncFailed": "Subscription sync failed", - "unsubscribed": "Unsubscribed on YouTube", + "unsubscribed": "Unsubscribed from {{name}} on YouTube", "unsubscribeFailed": "Unsubscribe failed", "fullHistoryRequested": "Full history requested for {{count}} channels", "fullHistoryFailed": "Couldn't request full history", "needYouTube": "Connect your YouTube account to do this.", "connect": "Connect", "filteredByTag": "Feed filtered by tag: {{name}}", - "resetDone": "Channel reset — re-fetching now", + "resetDone": "{{name}} reset — re-fetching now", "resetFailed": "Couldn't reset this channel" }, "confirmUnsubscribe": "Unsubscribe from \"{{name}}\" on YouTube? This changes your real YouTube account. To just remove it from your feed, hide it instead.", diff --git a/frontend/src/i18n/locales/en/playlists.json b/frontend/src/i18n/locales/en/playlists.json index ca48e4c..179101e 100644 --- a/frontend/src/i18n/locales/en/playlists.json +++ b/frontend/src/i18n/locales/en/playlists.json @@ -26,7 +26,7 @@ "revertTitle": "Discard local changes?", "revertMsg": "This reloads the playlist from YouTube and discards your unsynced local changes (order, additions, removals, rename). Continue?", "revertConfirm": "Discard & reload", - "revertDone": "Reloaded from YouTube ✓", + "revertDone": "“{{name}}” reloaded from YouTube ✓", "revertFailed": "Couldn't reload from YouTube.", "pushTitle": "Sync to YouTube", "pushConfirm": "Sync", @@ -34,8 +34,8 @@ "pushPlanUpdate": "Update on YouTube — add {{insert}}, remove {{del}}, reorder {{reorder}}? (~{{units}} quota units; {{left}} left today.)", "pushDiverged": "{{count}} video(s) currently on YouTube will be removed.", "pushNoQuota": "Not enough YouTube quota left today ({{left}}) for this sync (~{{units}} needed). Try again tomorrow.", - "pushUpToDate": "Already in sync with YouTube.", - "pushDone": "Synced to YouTube ✓", + "pushUpToDate": "“{{name}}” is already in sync with YouTube.", + "pushDone": "“{{name}}” synced to YouTube ✓", "pushPartial": "Synced, but {{count}} item(s) were skipped.", "pushFailed": "Couldn't sync to YouTube.", "deleteOnYoutubeTitle": "Delete on YouTube too?", diff --git a/frontend/src/i18n/locales/en/settings.json b/frontend/src/i18n/locales/en/settings.json index 91c992b..21fb2e7 100644 --- a/frontend/src/i18n/locales/en/settings.json +++ b/frontend/src/i18n/locales/en/settings.json @@ -93,7 +93,7 @@ "intro": "Emails here can enter the shared demo account straight from the login page — no Google sign-in. They just type the address into the field and are let in after a moment.", "addPlaceholder": "Whitelist an email for demo…", "add": "Add", - "added": "Added to the demo whitelist", + "added": "{{email}} added to the demo whitelist", "addFailed": "Couldn't add that email", "removeFailed": "Couldn't remove that email", "remove": "Remove", @@ -115,7 +115,7 @@ "approved": "Approved {{email}} — they can sign in now", "approveFailed": "Approve failed", "denyFailed": "Deny failed", - "addedToWhitelist": "Added to the whitelist", + "addedToWhitelist": "{{email}} added to the whitelist", "addFailed": "Couldn't add that email", "requested": "requested {{time}}", "approveHint": "Approve — whitelist this email and email them they're in.", diff --git a/frontend/src/i18n/locales/en/tagManager.json b/frontend/src/i18n/locales/en/tagManager.json index 89ffd64..64b33ae 100644 --- a/frontend/src/i18n/locales/en/tagManager.json +++ b/frontend/src/i18n/locales/en/tagManager.json @@ -3,7 +3,7 @@ "channels": "{{count}} ch.", "onChannels": "Tagged channels", "delete": "Delete", - "deleted": "Tag deleted", + "deleted": "Tag “{{name}}” deleted", "deleteTitle": "Delete tag", "confirmDelete": "Delete the tag “{{name}}”? It will be removed from every channel it's on.", "empty": "No tags yet — add one below.", diff --git a/frontend/src/i18n/locales/hu/channels.json b/frontend/src/i18n/locales/hu/channels.json index 8595dfc..6b9fa1e 100644 --- a/frontend/src/i18n/locales/hu/channels.json +++ b/frontend/src/i18n/locales/hu/channels.json @@ -111,14 +111,14 @@ "notify": { "synced": "{{count}} feliratkozás szinkronizálva", "syncFailed": "A feliratkozások szinkronizálása sikertelen", - "unsubscribed": "Leiratkozva a YouTube-on", + "unsubscribed": "Leiratkozva a(z) {{name}} csatornáról a YouTube-on", "unsubscribeFailed": "A leiratkozás sikertelen", "fullHistoryRequested": "Teljes előzmény kérve {{count}} csatornához", "fullHistoryFailed": "Nem sikerült teljes előzményt kérni", "needYouTube": "Ehhez csatlakoztasd a YouTube-fiókod.", "connect": "Csatlakoztatás", "filteredByTag": "Hírfolyam szűrve címkére: {{name}}", - "resetDone": "Csatorna resetelve — újraletöltés folyamatban", + "resetDone": "{{name}} resetelve — újraletöltés folyamatban", "resetFailed": "Nem sikerült resetelni a csatornát" }, "confirmUnsubscribe": "Leiratkozol a(z) „{{name}}” csatornáról a YouTube-on? Ez megváltoztatja a valódi YouTube-fiókodat. Ha csak a hírfolyamodból szeretnéd eltávolítani, inkább rejtsd el.", diff --git a/frontend/src/i18n/locales/hu/playlists.json b/frontend/src/i18n/locales/hu/playlists.json index b9f6835..c7104c9 100644 --- a/frontend/src/i18n/locales/hu/playlists.json +++ b/frontend/src/i18n/locales/hu/playlists.json @@ -26,7 +26,7 @@ "revertTitle": "Eldobod a helyi módosításokat?", "revertMsg": "Ez újratölti a listát a YouTube-ról, és eldobja a nem szinkronizált helyi módosításaidat (sorrend, hozzáadás, eltávolítás, átnevezés). Folytatod?", "revertConfirm": "Eldobás és újratöltés", - "revertDone": "Újratöltve a YouTube-ról ✓", + "revertDone": "„{{name}}” újratöltve a YouTube-ról ✓", "revertFailed": "Nem sikerült újratölteni a YouTube-ról.", "pushTitle": "Szinkron YouTube-ra", "pushConfirm": "Szinkron", @@ -34,8 +34,8 @@ "pushPlanUpdate": "Frissítés a YouTube-on — {{insert}} hozzáadása, {{del}} eltávolítása, {{reorder}} átrendezése? (~{{units}} kvótaegység; ma még {{left}} maradt.)", "pushDiverged": "{{count}} videó, amely jelenleg a YouTube-on van, el lesz távolítva.", "pushNoQuota": "Nincs elég YouTube-kvóta mára ({{left}}) ehhez a szinkronhoz (~{{units}} kell). Próbáld újra holnap.", - "pushUpToDate": "Már szinkronban van a YouTube-bal.", - "pushDone": "Szinkronizálva a YouTube-ra ✓", + "pushUpToDate": "A(z) „{{name}}” már szinkronban van a YouTube-bal.", + "pushDone": "„{{name}}” szinkronizálva a YouTube-ra ✓", "pushPartial": "Szinkronizálva, de {{count}} elem kimaradt.", "pushFailed": "Nem sikerült a YouTube-ra szinkronizálás.", "deleteOnYoutubeTitle": "Törlöd a YouTube-on is?", diff --git a/frontend/src/i18n/locales/hu/settings.json b/frontend/src/i18n/locales/hu/settings.json index 77c5119..6ae1077 100644 --- a/frontend/src/i18n/locales/hu/settings.json +++ b/frontend/src/i18n/locales/hu/settings.json @@ -93,7 +93,7 @@ "intro": "Az itt szereplő e-mailek a bejelentkező oldalról közvetlenül beléphetnek a közös demo fiókba — Google-bejelentkezés nélkül. Csak beírják a címet a mezőbe, és pár pillanat múlva bekerülnek.", "addPlaceholder": "E-mail fehérlistára a demóhoz…", "add": "Hozzáadás", - "added": "Hozzáadva a demo fehérlistához", + "added": "{{email}} hozzáadva a demo fehérlistához", "addFailed": "Nem sikerült hozzáadni ezt az e-mailt", "removeFailed": "Nem sikerült eltávolítani ezt az e-mailt", "remove": "Eltávolítás", @@ -115,7 +115,7 @@ "approved": "Jóváhagyva: {{email}} — most már be tud jelentkezni", "approveFailed": "A jóváhagyás sikertelen", "denyFailed": "Az elutasítás sikertelen", - "addedToWhitelist": "Hozzáadva a fehérlistához", + "addedToWhitelist": "{{email}} hozzáadva a fehérlistához", "addFailed": "Nem sikerült hozzáadni ezt az e-mailt", "requested": "kérve: {{time}}", "approveHint": "Jóváhagyás — fehérlistára teszi ezt az e-mailt és e-mailben értesíti őket, hogy bekerültek.", diff --git a/frontend/src/i18n/locales/hu/tagManager.json b/frontend/src/i18n/locales/hu/tagManager.json index 6ee27a5..4a57382 100644 --- a/frontend/src/i18n/locales/hu/tagManager.json +++ b/frontend/src/i18n/locales/hu/tagManager.json @@ -3,7 +3,7 @@ "channels": "{{count}} csat.", "onChannels": "Címkézett csatornák", "delete": "Törlés", - "deleted": "Címke törölve", + "deleted": "„{{name}}” címke törölve", "deleteTitle": "Címke törlése", "confirmDelete": "Törlöd a(z) „{{name}}” címkét? Minden csatornáról lekerül, amin rajta van.", "empty": "Még nincs címke — adj hozzá lentebb.", From d047ed7e1553284daae95aea9623ad13841b7641 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 26 Jun 2026 00:32:17 +0200 Subject: [PATCH 2/3] feat(youtube): optional egress proxy for YouTube API calls New youtube_api_proxy setting (env fallback + admin Config UI, env YOUTUBE_API_PROXY): when set, the YouTubeClient routes all its httpx traffic through that HTTP(S) proxy. Lets a dynamic-IP host send API calls through a fixed-IP host (e.g. the server over private tunnel) so an IP-restricted API key keeps working. Empty = direct. --- backend/app/config.py | 4 ++++ backend/app/sysconfig.py | 2 ++ backend/app/youtube/client.py | 5 ++++- frontend/src/i18n/locales/de/config.json | 1 + frontend/src/i18n/locales/en/config.json | 1 + frontend/src/i18n/locales/hu/config.json | 1 + 6 files changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/app/config.py b/backend/app/config.py index cce9b1c..5564ee0 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -65,6 +65,10 @@ class Settings(BaseSettings): # Optional API key for public reads (channels/videos/playlistItems). When set it is # preferred for shared backfill/enrichment so it doesn't depend on a specific user. youtube_api_key: str = "" + # Optional HTTP(S) proxy for outbound YouTube Data API calls. Set this to send the + # scheduler's googleapis traffic through a fixed-IP host (e.g. the VPS over WireGuard) so an + # IP-restricted API key keeps working even when this host's public IP is dynamic. + youtube_api_proxy: str = "" # Daily quota budget (units). Default leaves headroom under the 10,000/day free limit. quota_daily_budget: int = 9000 # Recent-first backfill: how far back to fetch on the first pass per channel. diff --git a/backend/app/sysconfig.py b/backend/app/sysconfig.py index 51b3ad3..8ad8c53 100644 --- a/backend/app/sysconfig.py +++ b/backend/app/sysconfig.py @@ -50,6 +50,8 @@ SPECS: tuple[ConfigSpec, ...] = ( ConfigSpec("autotag_title_sample", "int", "batch", "autotag_title_sample", min=1, max=1_000), # --- YouTube Data API key (secret; optional — public reads prefer it over OAuth) --- ConfigSpec("youtube_api_key", "str", "youtube", "youtube_api_key", secret=True), + # Optional egress proxy for YouTube API calls (fixed-IP host for an IP-restricted key). + ConfigSpec("youtube_api_proxy", "str", "youtube", "youtube_api_proxy"), # --- Google OAuth client (Sign in with Google; both encrypted, env fallback). Set from the # install wizard / Configuration page so no restart is needed when the creds change. --- ConfigSpec("google_client_id", "str", "google", "google_client_id", secret=True), diff --git a/backend/app/youtube/client.py b/backend/app/youtube/client.py index aecd93b..aa1490a 100644 --- a/backend/app/youtube/client.py +++ b/backend/app/youtube/client.py @@ -44,7 +44,10 @@ class YouTubeClient: def __init__(self, db, user: User): self.db = db self.user = user - self._http = httpx.Client(timeout=30.0) + # Optionally route all YouTube traffic through a fixed-IP egress proxy (e.g. the VPS + # over WireGuard) so an IP-restricted API key keeps working from a dynamic-IP host. + proxy = sysconfig.get_str(db, "youtube_api_proxy") or None + self._http = httpx.Client(timeout=30.0, proxy=proxy) def __enter__(self) -> "YouTubeClient": return self diff --git a/frontend/src/i18n/locales/de/config.json b/frontend/src/i18n/locales/de/config.json index e2bed7a..571c8ae 100644 --- a/frontend/src/i18n/locales/de/config.json +++ b/frontend/src/i18n/locales/de/config.json @@ -26,6 +26,7 @@ "enrich_batch_size": { "label": "Anreicherungs-Stapelgröße", "hint": "videos.list-IDs pro Aufruf (YouTube begrenzt auf 50)." }, "autotag_title_sample": { "label": "Auto-Tag-Titelstichprobe", "hint": "Pro Kanal abgetastete aktuelle Videotitel zur Spracherkennung." }, "youtube_api_key": { "label": "YouTube-API-Schlüssel", "hint": "Optional. Für öffentliche Lesezugriffe (Kanäle/Videos), damit gemeinsames Nachladen nicht vom OAuth eines Nutzers abhängt. Verschlüsselt gespeichert; nur schreibbar." }, + "youtube_api_proxy": { "label": "YouTube-API-Egress-Proxy", "hint": "Optional. HTTP(S)-Proxy-URL für ausgehende YouTube-API-Aufrufe — über einen Host mit fester IP leiten, damit ein IP-beschränkter Schlüssel von einem Server mit dynamischer IP funktioniert. Leer = direkt." }, "google_client_id": { "label": "Google-Client-ID", "hint": "OAuth-2.0-Client-ID für die Google-Anmeldung. Verschlüsselt gespeichert; nur schreibbar. Beide Google-Felder leer lassen, um die Google-Anmeldung zu deaktivieren (E-Mail+Passwort funktioniert weiter)." }, "google_client_secret": { "label": "Google-Client-Secret", "hint": "OAuth-2.0-Client-Secret. Verschlüsselt gespeichert; nur schreibbar." }, "allow_registration": { "label": "Registrierung erlauben", "hint": "Wenn aktiviert, kann jeder eine E-Mail+Passwort-Registrierung einreichen. Für die Anmeldung sind weiterhin E-Mail-Bestätigung und Admin-Freigabe nötig." } diff --git a/frontend/src/i18n/locales/en/config.json b/frontend/src/i18n/locales/en/config.json index 656a3e4..f67175d 100644 --- a/frontend/src/i18n/locales/en/config.json +++ b/frontend/src/i18n/locales/en/config.json @@ -26,6 +26,7 @@ "enrich_batch_size": { "label": "Enrichment batch size", "hint": "videos.list ids per call (YouTube caps this at 50)." }, "autotag_title_sample": { "label": "Auto-tag title sample", "hint": "Recent video titles sampled per channel for language detection." }, "youtube_api_key": { "label": "YouTube API key", "hint": "Optional. Used for public reads (channels/videos) so shared backfill doesn't depend on a user's OAuth. Stored encrypted; write-only." }, + "youtube_api_proxy": { "label": "YouTube API egress proxy", "hint": "Optional. HTTP(S) proxy URL for outbound YouTube API calls — route them through a fixed-IP host so an IP-restricted key keeps working from a dynamic-IP server. Leave empty for direct." }, "google_client_id": { "label": "Google client ID", "hint": "OAuth 2.0 client ID for Sign in with Google. Stored encrypted; write-only. Leave both Google fields empty to disable Google sign-in (email+password still works)." }, "google_client_secret": { "label": "Google client secret", "hint": "OAuth 2.0 client secret. Stored encrypted; write-only." }, "allow_registration": { "label": "Allow registration", "hint": "When on, anyone can submit an email+password registration. They still need to verify their email and be approved by an admin before they can sign in." } diff --git a/frontend/src/i18n/locales/hu/config.json b/frontend/src/i18n/locales/hu/config.json index 651200a..9d40c0c 100644 --- a/frontend/src/i18n/locales/hu/config.json +++ b/frontend/src/i18n/locales/hu/config.json @@ -26,6 +26,7 @@ "enrich_batch_size": { "label": "Gazdagítási kötegméret", "hint": "videos.list azonosítók hívásonként (a YouTube 50-ben maximálja)." }, "autotag_title_sample": { "label": "Auto-címke címmintavétel", "hint": "Csatornánként ennyi friss videócímet mintázunk a nyelvfelismeréshez." }, "youtube_api_key": { "label": "YouTube API-kulcs", "hint": "Opcionális. Publikus olvasásokhoz (csatornák/videók), hogy a megosztott letöltés ne függjön egy felhasználó OAuth-jától. Titkosítva tárolva; csak írható." }, + "youtube_api_proxy": { "label": "YouTube API egress proxy", "hint": "Opcionális. HTTP(S) proxy URL a kimenő YouTube API-hívásokhoz — fix IP-jű hoston átengedve egy IP-korlátozott kulcs dinamikus IP-jű szerverről is működik. Üresen = közvetlen." }, "google_client_id": { "label": "Google kliens-azonosító", "hint": "OAuth 2.0 kliens-azonosító a Google-bejelentkezéshez. Titkosítva tárolva; csak írható. Hagyd üresen mindkét Google-mezőt a Google-bejelentkezés kikapcsolásához (az e-mail+jelszó továbbra is működik)." }, "google_client_secret": { "label": "Google kliens-titok", "hint": "OAuth 2.0 kliens-titok. Titkosítva tárolva; csak írható." }, "allow_registration": { "label": "Regisztráció engedélyezése", "hint": "Bekapcsolva bárki beküldhet email+jelszó regisztrációt. Belépéshez továbbra is email-igazolás és admin-jóváhagyás szükséges." } From 6286dcc3c2604054c1f406ba1a867e8bf5a1577f Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 26 Jun 2026 00:36:46 +0200 Subject: [PATCH 3/3] =?UTF-8?q?chore(release):=200.16.1=20=E2=80=94=20noti?= =?UTF-8?q?fication=20context=20+=20YouTube=20API=20egress=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- frontend/src/lib/releaseNotes.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 04a373e..2a0970c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.16.0 +0.16.1 diff --git a/frontend/src/lib/releaseNotes.ts b/frontend/src/lib/releaseNotes.ts index 3dd62a7..b1a5e4a 100644 --- a/frontend/src/lib/releaseNotes.ts +++ b/frontend/src/lib/releaseNotes.ts @@ -14,6 +14,15 @@ export interface ReleaseEntry { } export const RELEASE_NOTES: ReleaseEntry[] = [ + { + version: "0.16.1", + date: "2026-06-26", + summary: "Clearer action messages, and a fix for background syncing on self-hosted setups.", + fixes: [ + "Action confirmations now name what they acted on — which channel was unsubscribed or reset, which address was added to a list, which tag was deleted, and which playlist was reloaded or synced.", + "Background syncing keeps working when the server's public IP changes: an instance can now be pointed at a fixed-IP egress proxy, so an IP-restricted YouTube API key no longer breaks after a network change.", + ], + }, { version: "0.16.0", date: "2026-06-26",