diff --git a/api/video/enrichment.py b/api/video/enrichment.py index 2f6b3ace..b8ef869c 100644 --- a/api/video/enrichment.py +++ b/api/video/enrichment.py @@ -86,6 +86,18 @@ def register_routes(bp): w.resume() return jsonify({"status": "running"}) + @bp.route("/enrichment//test", methods=["POST"]) + def video_enrichment_test(service): + w = engine().worker(service) + if not w: + return jsonify({"success": False, "error": "unknown service"}), 404 + try: + ok, msg = w.client.test() + return jsonify({"success": bool(ok), "message": msg, "error": None if ok else msg}) + except Exception: + logger.exception("video enrichment test failed for %s", service) + return jsonify({"success": False, "error": "Test failed"}) + @bp.route("/enrichment//breakdown", methods=["GET"]) def video_enrichment_breakdown(service): from . import get_video_db diff --git a/core/video/enrichment/clients.py b/core/video/enrichment/clients.py index 4bbf3bec..19c9b99e 100644 --- a/core/video/enrichment/clients.py +++ b/core/video/enrichment/clients.py @@ -31,6 +31,21 @@ class TMDBClient: def enabled(self): return bool(self.api_key) + def test(self): + if not self.api_key: + return False, "No TMDB API key set" + import requests + try: + r = requests.get(self.BASE + "/configuration", params={"api_key": self.api_key}, timeout=12) + if r.status_code == 200: + return True, "TMDB connection OK" + if r.status_code == 401: + return False, "Invalid TMDB API key" + return False, "TMDB returned HTTP " + str(r.status_code) + except Exception: + logger.exception("TMDB test failed") + return False, "Could not reach TMDB" + def match(self, kind, title, year): if not self.api_key or not title: return None @@ -77,6 +92,18 @@ class TVDBClient: def enabled(self): return bool(self.api_key) + def test(self): + if not self.api_key: + return False, "No TVDB API key set" + try: + token = self._auth() + if token: + return True, "TVDB connection OK" + return False, "TVDB login failed — check the key" + except Exception: + logger.exception("TVDB test failed") + return False, "Could not reach TVDB" + def _auth(self): if self._token: return self._token diff --git a/tests/test_video_api.py b/tests/test_video_api.py index d5d165f6..0912c5a1 100644 --- a/tests/test_video_api.py +++ b/tests/test_video_api.py @@ -39,6 +39,7 @@ def test_blueprint_exposes_dashboard_route(): assert "/api/video/enrichment//status" in rules assert "/api/video/enrichment//unmatched" in rules assert "/api/video/enrichment/config" in rules + assert "/api/video/enrichment//test" in rules def test_dashboard_endpoint_returns_zeroed_json(tmp_path): @@ -100,6 +101,7 @@ def test_enrichment_endpoints(tmp_path): class FakeClient: enabled = True def match(self, *a): return None + def test(self): return (True, "ok") db = VideoDatabase(database_path=str(tmp_path / "video_library.db")) videoapi._video_db = db @@ -125,6 +127,8 @@ def test_enrichment_endpoints(tmp_path): assert client.post("/api/video/enrichment/tmdb/resume").get_json()["status"] == "running" assert client.post("/api/video/enrichment/tmdb/retry", json={"kind": "movie", "scope": "failed"}).get_json()["reset"] == 1 + assert client.post("/api/video/enrichment/tmdb/test").get_json()["success"] is True + assert client.post("/api/video/enrichment/nope/test").status_code == 404 assert client.get("/api/video/enrichment/nope/status").status_code == 404 finally: videoapi._video_db = None diff --git a/tests/test_video_side_shell.py b/tests/test_video_side_shell.py index 31a01513..cace4d05 100644 --- a/tests/test_video_side_shell.py +++ b/tests/test_video_side_shell.py @@ -237,6 +237,8 @@ def test_video_side_hides_music_api_config_and_shows_placeholders(): assert 'class="api-service-frame stg-service" data-video-service="tvdb"' in _INDEX # They must NOT carry the music-hooked data-service attribute. assert 'data-service="tmdb"' not in _INDEX and 'data-service="tvdb"' not in _INDEX + # Each connection item has a Test button (like music's connections). + assert 'data-video-test-service="tmdb"' in _INDEX and 'data-video-test-service="tvdb"' in _INDEX def test_dashboard_enrichment_buttons_present(): diff --git a/webui/index.html b/webui/index.html index 6895484f..9228a6ed 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4856,6 +4856,7 @@
Get your key from TMDB API Settings
Primary movie & TV metadata source.
+
@@ -4873,6 +4874,7 @@
Get your key from TheTVDB API
TV series metadata source.
+ diff --git a/webui/static/video/video-settings.js b/webui/static/video/video-settings.js index 7d1ecab0..6d7309b0 100644 --- a/webui/static/video/video-settings.js +++ b/webui/static/video/video-settings.js @@ -80,13 +80,31 @@ function saveKeys() { var t = document.getElementById('tmdb-api-key'); var v = document.getElementById('tvdb-api-key'); - fetch(CONFIG_URL, { + return 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 toast(msg, type) { + if (typeof showToast === 'function') showToast(msg, type); // shared shell helper + } + + // Mirrors music's testConnection(): save the key, then hit the test + // endpoint, then toast the result. Isolated -> /api/video/enrichment//test. + function testConnection(svc) { + var name = svc.toUpperCase(); + toast('Testing ' + name + ' connection…', 'info'); + saveKeys().then(function () { + return fetch('/api/video/enrichment/' + svc + '/test', + { method: 'POST', headers: { 'Accept': 'application/json' } }); + }).then(function (r) { return r.json(); }).then(function (res) { + if (res && res.success) toast(res.message || (name + ' connection successful'), 'success'); + else toast(name + ' connection failed: ' + ((res && res.error) || 'unknown'), 'error'); + }).catch(function () { toast('Failed to test ' + name + ' connection', 'error'); }); + } + function onPageShown(e) { if (e && e.detail !== PAGE_ID) return; load(); @@ -105,6 +123,15 @@ var el = document.getElementById(id); if (el) el.addEventListener('change', saveKeys); }); + // Per-connection Test buttons (same behaviour as music's testConnection). + var testBtns = document.querySelectorAll('[data-video-test-service]'); + for (var k = 0; k < testBtns.length; k++) { + (function (b) { + b.addEventListener('click', function () { + testConnection(b.getAttribute('data-video-test-service')); + }); + })(testBtns[k]); + } document.addEventListener('soulsync:video-page-shown', onPageShown); }