video settings: per-connection Test button (mirrors music's testConnection)

Each video connection item (TMDB/TVDB) now has a Test button that behaves like
music's: saves the key, hits POST /api/video/enrichment/<svc>/test, and toasts
the result via the shared showToast — isolated (own endpoint, own data-attr
handler, reuses the .test-button CSS).
- Client .test() pings TMDB /configuration and TVDB /login to verify the key.
- Endpoint returns {success,message,error}; unknown service -> 404.
94 tests green; music untouched.
This commit is contained in:
BoulderBadgeDad 2026-06-14 12:26:15 -07:00
parent d35bb695ae
commit 2901e4ec4d
6 changed files with 75 additions and 1 deletions

View file

@ -86,6 +86,18 @@ def register_routes(bp):
w.resume()
return jsonify({"status": "running"})
@bp.route("/enrichment/<service>/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/<service>/breakdown", methods=["GET"])
def video_enrichment_breakdown(service):
from . import get_video_db

View file

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

View file

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

View file

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

View file

@ -4856,6 +4856,7 @@
<div class="callback-help">Get your key from <a href="https://www.themoviedb.org/settings/api" target="_blank" style="color: #01b4e4;">TMDB API Settings</a></div>
<div class="callback-help">Primary movie &amp; TV metadata source.</div>
</div>
<button class="test-button" type="button" data-video-test-service="tmdb">Test TMDB</button>
</div>
</div>
<div class="api-service-frame stg-service" data-video-service="tvdb">
@ -4873,6 +4874,7 @@
<div class="callback-help">Get your key from <a href="https://thetvdb.com/dashboard/account/apikey" target="_blank" style="color: #6cd391;">TheTVDB API</a></div>
<div class="callback-help">TV series metadata source.</div>
</div>
<button class="test-button" type="button" data-video-test-service="tvdb">Test TVDB</button>
</div>
</div>
</div>

View file

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