From 65ff84aa6a20d97f6f8c5820933dcfbff93fb06e Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sun, 14 Jun 2026 11:26:09 -0700 Subject: [PATCH] video enrichment 1d: wire TMDB/TVDB API keys (workers turn on) - GET/POST /api/video/enrichment/config saves the keys into video_settings; POST rebuilds the engine (stop old workers, rebuild clients with new keys) so they pick up the change live. - video-settings.js loads the saved keys into the TMDB/TVDB fields on the Connections tab and saves them on change (workers enable once a key is set). Backend is now end-to-end: key -> client.enabled -> worker matches the library to TMDB/TVDB and fills ids + metadata. 91 tests green; real DB untouched. --- api/video/enrichment.py | 25 ++++++++++++++++++++ core/video/enrichment/engine.py | 14 ++++++++++++ tests/test_video_api.py | 24 ++++++++++++++++++++ webui/static/video/video-settings.js | 34 +++++++++++++++++++++++++++- 4 files changed, 96 insertions(+), 1 deletion(-) diff --git a/api/video/enrichment.py b/api/video/enrichment.py index 70281854..2f6b3ace 100644 --- a/api/video/enrichment.py +++ b/api/video/enrichment.py @@ -38,6 +38,31 @@ def register_routes(bp): logger.exception("video enrichment services failed") return jsonify({"services": []}) + @bp.route("/enrichment/config", methods=["GET"]) + def video_enrichment_config(): + from . import get_video_db + db = get_video_db() + return jsonify({ + "tmdb_api_key": db.get_setting("tmdb_api_key") or "", + "tvdb_api_key": db.get_setting("tvdb_api_key") or "", + }) + + @bp.route("/enrichment/config", methods=["POST"]) + def video_enrichment_config_save(): + from . import get_video_db + db = get_video_db() + body = request.get_json(silent=True) or {} + if "tmdb_api_key" in body: + db.set_setting("tmdb_api_key", body.get("tmdb_api_key") or "") + if "tvdb_api_key" in body: + db.set_setting("tvdb_api_key", body.get("tvdb_api_key") or "") + try: + from core.video.enrichment.engine import rebuild_video_enrichment_engine + rebuild_video_enrichment_engine() + except Exception: + logger.exception("video enrichment: engine rebuild after key change failed") + return jsonify({"status": "saved"}) + @bp.route("/enrichment//status", methods=["GET"]) def video_enrichment_status(service): w = engine().worker(service) diff --git a/core/video/enrichment/engine.py b/core/video/enrichment/engine.py index 9f03f9e9..bbc10008 100644 --- a/core/video/enrichment/engine.py +++ b/core/video/enrichment/engine.py @@ -58,3 +58,17 @@ def get_video_enrichment_engine(): eng.start_all() _engine = eng return _engine + + +def rebuild_video_enrichment_engine(): + """Rebuild the engine so workers pick up changed API keys (stops the old + workers first so threads don't leak).""" + global _engine + with _lock: + if _engine is not None: + try: + _engine.stop_all() + except Exception: + logger.exception("video enrichment: stopping old engine failed") + _engine = None + return get_video_enrichment_engine() diff --git a/tests/test_video_api.py b/tests/test_video_api.py index 44a46946..d5d165f6 100644 --- a/tests/test_video_api.py +++ b/tests/test_video_api.py @@ -38,6 +38,7 @@ def test_blueprint_exposes_dashboard_route(): assert "/api/video/enrichment/services" in rules assert "/api/video/enrichment//status" in rules assert "/api/video/enrichment//unmatched" in rules + assert "/api/video/enrichment/config" in rules def test_dashboard_endpoint_returns_zeroed_json(tmp_path): @@ -130,6 +131,29 @@ def test_enrichment_endpoints(tmp_path): eng_mod._engine = None +def test_enrichment_config_save_load(tmp_path, monkeypatch): + import api.video as videoapi + from database.video_database import VideoDatabase + import core.video.enrichment.engine as eng_mod + # Don't build a real engine (would open the default-path DB + start threads). + monkeypatch.setattr(eng_mod, "rebuild_video_enrichment_engine", lambda: None) + + db = VideoDatabase(database_path=str(tmp_path / "video_library.db")) + videoapi._video_db = db + app = Flask(__name__) + app.register_blueprint(videoapi.create_video_blueprint(), url_prefix="/api/video") + client = app.test_client() + try: + assert client.get("/api/video/enrichment/config").get_json() == { + "tmdb_api_key": "", "tvdb_api_key": ""} + client.post("/api/video/enrichment/config", json={"tmdb_api_key": "abc", "tvdb_api_key": "xyz"}) + assert client.get("/api/video/enrichment/config").get_json() == { + "tmdb_api_key": "abc", "tvdb_api_key": "xyz"} + assert db.get_setting("tmdb_api_key") == "abc" + finally: + videoapi._video_db = None + + def test_video_api_imports_nothing_from_music(): base = Path(__file__).resolve().parent.parent / "api" / "video" for py in base.glob("*.py"): diff --git a/webui/static/video/video-settings.js b/webui/static/video/video-settings.js index 6530483e..7d1ecab0 100644 --- a/webui/static/video/video-settings.js +++ b/webui/static/video/video-settings.js @@ -12,6 +12,7 @@ var PAGE_ID = 'video-settings'; var URL = '/api/video/libraries'; + var CONFIG_URL = '/api/video/enrichment/config'; function status(text) { var n = document.querySelector('[data-video-lib-status]'); @@ -62,8 +63,34 @@ .catch(function () { status('Save failed'); }); } + // ── Enrichment API keys (TMDB / TVDB) ─────────────────────────────────── + function loadKeys() { + fetch(CONFIG_URL, { headers: { 'Accept': 'application/json' } }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (d) { + if (!d) return; + var t = document.getElementById('tmdb-api-key'); + var v = document.getElementById('tvdb-api-key'); + if (t && d.tmdb_api_key != null) t.value = d.tmdb_api_key; + if (v && d.tvdb_api_key != null) v.value = d.tvdb_api_key; + }) + .catch(function () { /* ignore */ }); + } + + function saveKeys() { + var t = document.getElementById('tmdb-api-key'); + var v = document.getElementById('tvdb-api-key'); + fetch(CONFIG_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, + body: JSON.stringify({ tmdb_api_key: t ? t.value : '', tvdb_api_key: v ? v.value : '' }) + }).catch(function () { /* ignore */ }); + } + function onPageShown(e) { - if (e && e.detail === PAGE_ID) load(); + if (e && e.detail !== PAGE_ID) return; + load(); + loadKeys(); } function init() { @@ -73,6 +100,11 @@ for (var i = 0; i < selects.length; i++) { selects[i].addEventListener('change', save); } + // Enrichment keys save on blur/change (turns the workers on). + ['tmdb-api-key', 'tvdb-api-key'].forEach(function (id) { + var el = document.getElementById(id); + if (el) el.addEventListener('change', saveKeys); + }); document.addEventListener('soulsync:video-page-shown', onPageShown); }