Merge feature/config-db-4c

Move youtube_api_key into the DB-backed system_config (encrypted). Google OAuth
creds + admin-role UI deferred to the wizard/auth epics (see commit body).
This commit is contained in:
npeter83 2026-06-19 13:19:06 +02:00
commit b8223c5741
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…",