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.
This commit is contained in:
BoulderBadgeDad 2026-06-14 11:26:09 -07:00
parent 80679b02ba
commit 65ff84aa6a
4 changed files with 96 additions and 1 deletions

View file

@ -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/<service>/status", methods=["GET"])
def video_enrichment_status(service):
w = engine().worker(service)

View file

@ -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()

View file

@ -38,6 +38,7 @@ def test_blueprint_exposes_dashboard_route():
assert "/api/video/enrichment/services" in rules
assert "/api/video/enrichment/<service>/status" in rules
assert "/api/video/enrichment/<service>/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"):

View file

@ -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);
}