diff --git a/app/main.py b/app/main.py index 0157039..ad1986e 100644 --- a/app/main.py +++ b/app/main.py @@ -28,3 +28,8 @@ def index(request: Request): @app.get("/debug.html", response_class=HTMLResponse) def debug_page(request: Request): return templates.TemplateResponse("debug.html", {"request": request}) + + +@app.get("/tvdb-embed-test.html", response_class=HTMLResponse) +def tvdb_embed_test_page(request: Request): + return templates.TemplateResponse("tvdb_embed_test.html", {"request": request}) diff --git a/app/static/app.js b/app/static/app.js index 0552c4d..6a49fca 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -25,6 +25,8 @@ }, rememberedSeries: [], liveSearchItems: [], + tvdbModalUrl: "", + tvdbModalTimer: null, }; const el = { @@ -46,6 +48,12 @@ seriesStatus: document.getElementById("seriesStatus"), seriesOverview: document.getElementById("seriesOverview"), seriesTvdbLink: document.getElementById("seriesTvdbLink"), + tvdbModal: document.getElementById("tvdbModal"), + closeTvdbModalBtn: document.getElementById("closeTvdbModalBtn"), + tvdbModalOpenInTabBtn: document.getElementById("tvdbModalOpenInTabBtn"), + tvdbModalFrame: document.getElementById("tvdbModalFrame"), + tvdbModalStatus: document.getElementById("tvdbModalStatus"), + tvdbModalFallback: document.getElementById("tvdbModalFallback"), settingsBtn: document.getElementById("settingsBtn"), settingsModal: document.getElementById("settingsModal"), closeSettingsModalBtn: document.getElementById("closeSettingsModalBtn"), @@ -199,6 +207,57 @@ return ""; } + function validTvdbUrl(value) { + try { + const url = new URL((value || "").toString().trim()); + if (url.protocol !== "https:") return false; + return url.hostname === "www.thetvdb.com" || url.hostname === "thetvdb.com"; + } catch (_err) { + return false; + } + } + + function clearTvdbModalTimer() { + if (state.tvdbModalTimer) { + window.clearTimeout(state.tvdbModalTimer); + state.tvdbModalTimer = null; + } + } + + function showTvdbModalFallback(message) { + el.tvdbModalStatus.textContent = message; + el.tvdbModalFallback.classList.remove("hidden"); + } + + function openTvdbModal() { + if (!validTvdbUrl(state.tvdbModalUrl)) return; + + el.tvdbModal.classList.remove("hidden"); + el.tvdbModal.setAttribute("aria-hidden", "false"); + el.tvdbModalFallback.classList.add("hidden"); + el.tvdbModalStatus.textContent = "Loading TVDB preview..."; + + clearTvdbModalTimer(); + state.tvdbModalTimer = window.setTimeout(() => { + showTvdbModalFallback("No embed load confirmation received. Embedding is likely blocked."); + }, 5000); + el.tvdbModalFrame.src = state.tvdbModalUrl; + } + + function closeTvdbModal() { + clearTvdbModalTimer(); + el.tvdbModal.classList.add("hidden"); + el.tvdbModal.setAttribute("aria-hidden", "true"); + el.tvdbModalFrame.removeAttribute("src"); + el.tvdbModalStatus.textContent = ""; + el.tvdbModalFallback.classList.add("hidden"); + } + + function openTvdbInTab() { + if (!validTvdbUrl(state.tvdbModalUrl)) return; + window.open(state.tvdbModalUrl, "_blank", "noopener,noreferrer"); + } + function renderSelectedSeriesDetails() { const item = state.selectedSeries; if (!item) { @@ -225,9 +284,11 @@ el.seriesOverview.textContent = overview || "No overview available."; if (tvdbUrl) { + state.tvdbModalUrl = tvdbUrl; el.seriesTvdbLink.href = tvdbUrl; el.seriesTvdbLink.classList.remove("hidden"); } else { + state.tvdbModalUrl = ""; el.seriesTvdbLink.removeAttribute("href"); el.seriesTvdbLink.classList.add("hidden"); } @@ -878,6 +939,21 @@ e.stopPropagation(); }); document.addEventListener("click", closeRememberedDropdown); + el.seriesTvdbLink.addEventListener("click", (e) => { + e.preventDefault(); + openTvdbModal(); + }); + el.closeTvdbModalBtn.addEventListener("click", closeTvdbModal); + el.tvdbModalOpenInTabBtn.addEventListener("click", openTvdbInTab); + el.tvdbModal.addEventListener("click", (e) => { + if (e.target === el.tvdbModal) closeTvdbModal(); + }); + el.tvdbModalFrame.addEventListener("load", () => { + clearTvdbModalTimer(); + el.tvdbModalStatus.textContent = + "Iframe load event received. If the page is visible, embedding works in this browser."; + el.tvdbModalFallback.classList.add("hidden"); + }); el.settingsBtn.addEventListener("click", openSettingsModal); el.closeSettingsModalBtn.addEventListener("click", closeSettingsModal); el.saveSettingsBtn.addEventListener("click", () => withHandler(saveSettings, el.saveSettingsBtn)); diff --git a/app/static/styles.css b/app/static/styles.css index 011f04d..84bf239 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -412,6 +412,41 @@ button.secondary { min-height: 0; } +.tvdb-modal-card { + width: min(1500px, 92vw); + height: 86vh; +} + +.tvdb-modal-head { + margin-bottom: 6px; +} + +.tvdb-fallback { + border: 1px solid var(--border); + border-radius: 6px; + padding: 10px; + margin-bottom: 8px; + background: var(--surface-subtle); +} + +.tvdb-fallback h4 { + margin: 0 0 6px; +} + +.tvdb-fallback p { + margin: 0; + color: var(--text-muted); +} + +.tvdb-modal-frame { + width: 100%; + flex: 1; + min-height: 0; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--surface-subtle); +} + .modal-head { display: flex; align-items: center; diff --git a/app/templates/index.html b/app/templates/index.html index 2af9544..38f8372 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -192,6 +192,26 @@ + + diff --git a/app/templates/tvdb_embed_test.html b/app/templates/tvdb_embed_test.html new file mode 100644 index 0000000..e364982 --- /dev/null +++ b/app/templates/tvdb_embed_test.html @@ -0,0 +1,120 @@ + + + + + + Rename MVP - TVDB Embed Test + + + + +
+

TVDB Embed Test

+
Standalone iframe validation page
+
+
+
+
+

Embed Validation

+
+
+
+ + + + +
+

+ + +
+
+
+ + + + diff --git a/data/session_state.sqlite3 b/data/session_state.sqlite3 index eef230f..51317f7 100644 Binary files a/data/session_state.sqlite3 and b/data/session_state.sqlite3 differ diff --git a/feature_tests_tvdb_embed.sh b/feature_tests_tvdb_embed.sh new file mode 100755 index 0000000..5dda956 --- /dev/null +++ b/feature_tests_tvdb_embed.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${BASE_URL:-http://127.0.0.1:8085}" +ALT_BASE_URL="http://host.containers.internal:8085" + +detect_base_url() { + if curl -fsS --max-time 2 "$BASE_URL/api/health" >/dev/null 2>&1; then + echo "$BASE_URL" + return + fi + if curl -fsS --max-time 2 "$ALT_BASE_URL/api/health" >/dev/null 2>&1; then + echo "$ALT_BASE_URL" + return + fi + echo "$BASE_URL" +} + +BASE_URL="$(detect_base_url)" + +echo "Using BASE_URL=$BASE_URL" + +echo "== Feature test 1: tvdb embed test route is served ==" +page="$(curl -fsS "$BASE_URL/tvdb-embed-test.html")" +echo "$page" | grep -q "TVDB Embed Test" || { + echo "Expected TVDB Embed Test title missing" + exit 1 +} +echo "route availability validation passed" + +echo +echo "== Feature test 2: iframe and fallback UI exist ==" +echo "$page" | grep -q 'id="tvdbFrame"' || { + echo "Expected iframe with id tvdbFrame missing" + exit 1 +} +echo "$page" | grep -q 'id="embedFallback"' || { + echo "Expected embed fallback container missing" + exit 1 +} +echo "iframe/fallback validation passed" + +echo +echo "== Feature test 3: open-in-tab action exists ==" +echo "$page" | grep -q 'id="openInTabBtn"' || { + echo "Expected Open in tab button missing" + exit 1 +} +echo "$page" | grep -q "window.open" || { + echo "Expected window.open integration missing" + exit 1 +} +echo "open-in-tab validation passed" + +echo +echo "All TVDB embed feature tests passed."