feat(config): move youtube_api_key to DB (encrypted, admin-editable)

Register youtube_api_key in the sysconfig registry as a secret and resolve it from
the DB-override-or-env layer in YouTubeClient (self.db is in scope). Adds a YouTube
API group to the Configuration page (EN/HU/DE).

Google OAuth client id/secret stay env-only for now: the authlib client is
registered at module load (auth.py), and the token-refresh reads them too, so a
runtime DB override needs a lazy/re-registerable OAuth client — that belongs to the
install-wizard epic (first-boot OAuth config without restart). Admin-role-via-UI
moves to the auth-overhaul epic (user management lives there).
This commit is contained in:
npeter83 2026-06-19 13:19:06 +02:00
parent c953642e30
commit 2dc374cc7b
6 changed files with 16 additions and 7 deletions

View file

@ -48,6 +48,8 @@ SPECS: tuple[ConfigSpec, ...] = (
# --- Batch sizes --- # --- Batch sizes ---
ConfigSpec("enrich_batch_size", "int", "batch", "enrich_batch_size", min=1, max=50), ConfigSpec("enrich_batch_size", "int", "batch", "enrich_batch_size", min=1, max=50),
ConfigSpec("autotag_title_sample", "int", "batch", "autotag_title_sample", min=1, max=1_000), 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),
) )
_BY_KEY: dict[str, ConfigSpec] = {s.key: s for s in SPECS} _BY_KEY: dict[str, ConfigSpec] = {s.key: s for s in SPECS}

View file

@ -11,7 +11,7 @@ from datetime import datetime, timedelta, timezone
import httpx import httpx
from app import quota from app import quota, sysconfig
from app.config import settings from app.config import settings
from app.models import User from app.models import User
from app.security import decrypt from app.security import decrypt
@ -86,8 +86,9 @@ class YouTubeClient:
def _get(self, path: str, params: dict, cost: int = 1, allow_key: bool = True) -> dict: def _get(self, path: str, params: dict, cost: int = 1, allow_key: bool = True) -> dict:
p = dict(params) p = dict(params)
headers = {} headers = {}
if allow_key and settings.youtube_api_key: api_key = sysconfig.get_str(self.db, "youtube_api_key")
p["key"] = settings.youtube_api_key if allow_key and 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._http.get(f"{API_BASE}/{path}", params=p, headers=headers)

View file

@ -11,7 +11,7 @@ import Tooltip from "./Tooltip";
// applied together via one Save/Discard bar (consistent with the Settings page); nothing hits // applied together via one Save/Discard bar (consistent with the Settings page); nothing hits
// the server until Save. Group order is fixed where known so logically related settings (e.g. // the server until Save. Group order is fixed where known so logically related settings (e.g.
// Email/SMTP) stay together. // Email/SMTP) stay together.
const GROUP_ORDER = ["email", "quota", "backfill", "shorts", "batch"]; const GROUP_ORDER = ["email", "youtube", "quota", "backfill", "shorts", "batch"];
type SaveState = "idle" | "saving" | "saved" | "error"; type SaveState = "idle" | "saving" | "saved" | "error";
export default function ConfigPanel() { export default function ConfigPanel() {

View file

@ -3,6 +3,7 @@
"loading": "Konfiguration wird geladen…", "loading": "Konfiguration wird geladen…",
"groups": { "groups": {
"email": "E-Mail / SMTP", "email": "E-Mail / SMTP",
"youtube": "YouTube-API",
"quota": "Kontingent", "quota": "Kontingent",
"backfill": "Nachladen (Backfill)", "backfill": "Nachladen (Backfill)",
"shorts": "Shorts-Prüfung", "shorts": "Shorts-Prüfung",
@ -21,7 +22,8 @@
"shorts_probe_max_seconds": { "label": "Shorts-Prüfung — max. Dauer (s)", "hint": "Nur Videos bis zu dieser Länge werden als mögliche Shorts geprüft." }, "shorts_probe_max_seconds": { "label": "Shorts-Prüfung — max. Dauer (s)", "hint": "Nur Videos bis zu dieser Länge werden als mögliche Shorts geprüft." },
"shorts_probe_batch": { "label": "Shorts-Prüfung — Stapelgröße", "hint": "Wie viele Videos pro Lauf klassifiziert werden." }, "shorts_probe_batch": { "label": "Shorts-Prüfung — Stapelgröße", "hint": "Wie viele Videos pro Lauf klassifiziert werden." },
"enrich_batch_size": { "label": "Anreicherungs-Stapelgröße", "hint": "videos.list-IDs pro Aufruf (YouTube begrenzt auf 50)." }, "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." } "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." }
}, },
"save": "Speichern", "save": "Speichern",
"saving": "Speichern…", "saving": "Speichern…",

View file

@ -3,6 +3,7 @@
"loading": "Loading configuration…", "loading": "Loading configuration…",
"groups": { "groups": {
"email": "Email / SMTP", "email": "Email / SMTP",
"youtube": "YouTube API",
"quota": "Quota", "quota": "Quota",
"backfill": "Backfill", "backfill": "Backfill",
"shorts": "Shorts probe", "shorts": "Shorts probe",
@ -21,7 +22,8 @@
"shorts_probe_max_seconds": { "label": "Shorts probe — max duration (s)", "hint": "Only videos at or below this length are probed as possible Shorts." }, "shorts_probe_max_seconds": { "label": "Shorts probe — max duration (s)", "hint": "Only videos at or below this length are probed as possible Shorts." },
"shorts_probe_batch": { "label": "Shorts probe — batch size", "hint": "How many videos to classify per run." }, "shorts_probe_batch": { "label": "Shorts probe — batch size", "hint": "How many videos to classify per run." },
"enrich_batch_size": { "label": "Enrichment batch size", "hint": "videos.list ids per call (YouTube caps this at 50)." }, "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." } "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." }
}, },
"save": "Save", "save": "Save",
"saving": "Saving…", "saving": "Saving…",

View file

@ -3,6 +3,7 @@
"loading": "Konfiguráció betöltése…", "loading": "Konfiguráció betöltése…",
"groups": { "groups": {
"email": "E-mail / SMTP", "email": "E-mail / SMTP",
"youtube": "YouTube API",
"quota": "Kvóta", "quota": "Kvóta",
"backfill": "Letöltés (backfill)", "backfill": "Letöltés (backfill)",
"shorts": "Shorts-vizsgálat", "shorts": "Shorts-vizsgálat",
@ -21,7 +22,8 @@
"shorts_probe_max_seconds": { "label": "Shorts-vizsgálat — max hossz (mp)", "hint": "Csak az ennél nem hosszabb videókat vizsgáljuk lehetséges Shortsként." }, "shorts_probe_max_seconds": { "label": "Shorts-vizsgálat — max hossz (mp)", "hint": "Csak az ennél nem hosszabb videókat vizsgáljuk lehetséges Shortsként." },
"shorts_probe_batch": { "label": "Shorts-vizsgálat — kötegméret", "hint": "Futásonként ennyi videót osztályozunk." }, "shorts_probe_batch": { "label": "Shorts-vizsgálat — kötegméret", "hint": "Futásonként ennyi videót osztályozunk." },
"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)." }, "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." } "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ó." }
}, },
"save": "Mentés", "save": "Mentés",
"saving": "Mentés…", "saving": "Mentés…",