video: autoplay billboard trailer (opt-in setting)
After a couple seconds on a detail page, a muted trailer plays behind the hero (Netflix/Disney+ style) with mute/unmute + stop controls; the backdrop fades back when stopped. Stops on navigate-away/modal-open (no orphaned audio). Gated by a 'Autoplay trailers in the billboard' toggle in video Settings → Detail Pages (default on). Backed by billboard_autoplay in video_settings, read via a lightweight /api/video/prefs. Tests updated for the new config field.
This commit is contained in:
parent
be54fccc63
commit
2f3e4b128b
6 changed files with 122 additions and 3 deletions
|
|
@ -46,8 +46,16 @@ def register_routes(bp):
|
||||||
"tmdb_api_key": db.get_setting("tmdb_api_key") or "",
|
"tmdb_api_key": db.get_setting("tmdb_api_key") or "",
|
||||||
"tvdb_api_key": db.get_setting("tvdb_api_key") or "",
|
"tvdb_api_key": db.get_setting("tvdb_api_key") or "",
|
||||||
"omdb_api_key": db.get_setting("omdb_api_key") or "",
|
"omdb_api_key": db.get_setting("omdb_api_key") or "",
|
||||||
|
"billboard_autoplay": (db.get_setting("billboard_autoplay") or "1") == "1",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@bp.route("/prefs", methods=["GET"])
|
||||||
|
def video_prefs():
|
||||||
|
# Lightweight UI prefs for the detail page (no API keys).
|
||||||
|
from . import get_video_db
|
||||||
|
db = get_video_db()
|
||||||
|
return jsonify({"billboard_autoplay": (db.get_setting("billboard_autoplay") or "1") == "1"})
|
||||||
|
|
||||||
@bp.route("/enrichment/config", methods=["POST"])
|
@bp.route("/enrichment/config", methods=["POST"])
|
||||||
def video_enrichment_config_save():
|
def video_enrichment_config_save():
|
||||||
from . import get_video_db
|
from . import get_video_db
|
||||||
|
|
@ -57,6 +65,8 @@ def register_routes(bp):
|
||||||
db.set_setting("tmdb_api_key", body.get("tmdb_api_key") or "")
|
db.set_setting("tmdb_api_key", body.get("tmdb_api_key") or "")
|
||||||
if "tvdb_api_key" in body:
|
if "tvdb_api_key" in body:
|
||||||
db.set_setting("tvdb_api_key", body.get("tvdb_api_key") or "")
|
db.set_setting("tvdb_api_key", body.get("tvdb_api_key") or "")
|
||||||
|
if "billboard_autoplay" in body:
|
||||||
|
db.set_setting("billboard_autoplay", "1" if body.get("billboard_autoplay") else "0")
|
||||||
if "omdb_api_key" in body:
|
if "omdb_api_key" in body:
|
||||||
new_key = body.get("omdb_api_key") or ""
|
new_key = body.get("omdb_api_key") or ""
|
||||||
changed = new_key != (db.get_setting("omdb_api_key") or "")
|
changed = new_key != (db.get_setting("omdb_api_key") or "")
|
||||||
|
|
|
||||||
|
|
@ -267,12 +267,14 @@ def test_enrichment_config_save_load(tmp_path, monkeypatch):
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
try:
|
try:
|
||||||
assert client.get("/api/video/enrichment/config").get_json() == {
|
assert client.get("/api/video/enrichment/config").get_json() == {
|
||||||
"tmdb_api_key": "", "tvdb_api_key": "", "omdb_api_key": ""}
|
"tmdb_api_key": "", "tvdb_api_key": "", "omdb_api_key": "", "billboard_autoplay": True}
|
||||||
client.post("/api/video/enrichment/config",
|
client.post("/api/video/enrichment/config",
|
||||||
json={"tmdb_api_key": "abc", "tvdb_api_key": "xyz", "omdb_api_key": "om"})
|
json={"tmdb_api_key": "abc", "tvdb_api_key": "xyz", "omdb_api_key": "om",
|
||||||
|
"billboard_autoplay": False})
|
||||||
assert client.get("/api/video/enrichment/config").get_json() == {
|
assert client.get("/api/video/enrichment/config").get_json() == {
|
||||||
"tmdb_api_key": "abc", "tvdb_api_key": "xyz", "omdb_api_key": "om"}
|
"tmdb_api_key": "abc", "tvdb_api_key": "xyz", "omdb_api_key": "om", "billboard_autoplay": False}
|
||||||
assert db.get_setting("tmdb_api_key") == "abc" and db.get_setting("omdb_api_key") == "om"
|
assert db.get_setting("tmdb_api_key") == "abc" and db.get_setting("omdb_api_key") == "om"
|
||||||
|
assert client.get("/api/video/prefs").get_json() == {"billboard_autoplay": False}
|
||||||
finally:
|
finally:
|
||||||
videoapi._video_db = None
|
videoapi._video_db = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5191,6 +5191,22 @@
|
||||||
<button class="test-button" type="button" data-video-test-service="omdb">Test OMDb</button>
|
<button class="test-button" type="button" data-video-test-service="omdb">Test OMDb</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="api-service-frame stg-service" data-video-service="prefs">
|
||||||
|
<div class="stg-service-header" onclick="toggleStgService(this)">
|
||||||
|
<span class="stg-service-dot" style="color: #38bdf8;"></span>
|
||||||
|
<h4 class="service-title">Detail Pages</h4>
|
||||||
|
<span class="stg-service-chevron">›</span>
|
||||||
|
</div>
|
||||||
|
<div class="stg-service-body">
|
||||||
|
<label class="vid-pref-row">
|
||||||
|
<input type="checkbox" id="video-billboard-autoplay" data-video-pref="billboard_autoplay">
|
||||||
|
<span>Autoplay trailers in the billboard</span>
|
||||||
|
</label>
|
||||||
|
<div class="callback-info">
|
||||||
|
<div class="callback-help">Plays a muted trailer behind the hero after a moment (uses more bandwidth).</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Server Connections -->
|
<!-- Server Connections -->
|
||||||
|
|
|
||||||
|
|
@ -375,6 +375,7 @@
|
||||||
var n = q(s); if (n) n.hidden = true;
|
var n = q(s); if (n) n.hidden = true;
|
||||||
});
|
});
|
||||||
galleryImages = [];
|
galleryImages = [];
|
||||||
|
stopBillboardTrailer();
|
||||||
}
|
}
|
||||||
function loadExtras(kind, id) {
|
function loadExtras(kind, id) {
|
||||||
fetch(DETAIL_URL + kind + '/' + id + '/extras', { headers: { 'Accept': 'application/json' } })
|
fetch(DETAIL_URL + kind + '/' + id + '/extras', { headers: { 'Accept': 'application/json' } })
|
||||||
|
|
@ -471,6 +472,7 @@
|
||||||
renderVideos(ex.videos);
|
renderVideos(ex.videos);
|
||||||
renderGallery(ex.gallery);
|
renderGallery(ex.gallery);
|
||||||
renderReview(ex.review);
|
renderReview(ex.review);
|
||||||
|
maybeAutoplayBillboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderReview(review) {
|
function renderReview(review) {
|
||||||
|
|
@ -641,6 +643,7 @@
|
||||||
// ── trailer modal (YouTube embed) ─────────────────────────────────────────
|
// ── trailer modal (YouTube embed) ─────────────────────────────────────────
|
||||||
function openTrailer(key) {
|
function openTrailer(key) {
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
|
stopBillboardTrailer(); // don't double up audio with the billboard
|
||||||
var ov = document.getElementById('vd-trailer-overlay');
|
var ov = document.getElementById('vd-trailer-overlay');
|
||||||
if (!ov) {
|
if (!ov) {
|
||||||
ov = document.createElement('div');
|
ov = document.createElement('div');
|
||||||
|
|
@ -662,6 +665,56 @@
|
||||||
if (ov) { ov.classList.remove('vd-trailer-overlay--open'); ov.innerHTML = ''; }
|
if (ov) { ov.classList.remove('vd-trailer-overlay--open'); ov.innerHTML = ''; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── billboard autoplay trailer (opt-in setting) ───────────────────────────
|
||||||
|
var prefs = null, bbTrailerTimer = null, bbMuted = true;
|
||||||
|
function loadPrefs(cb) {
|
||||||
|
if (prefs) { cb(prefs); return; }
|
||||||
|
fetch('/api/video/prefs', { headers: { 'Accept': 'application/json' } })
|
||||||
|
.then(function (r) { return r.ok ? r.json() : null; })
|
||||||
|
.then(function (d) { prefs = d || {}; cb(prefs); })
|
||||||
|
.catch(function () { prefs = {}; cb(prefs); });
|
||||||
|
}
|
||||||
|
function maybeAutoplayBillboard() {
|
||||||
|
stopBillboardTrailer();
|
||||||
|
if (!data || !data.trailer || !data.trailer.key) return;
|
||||||
|
var key = data.trailer.key, id = currentId, kind = currentKind;
|
||||||
|
loadPrefs(function (p) {
|
||||||
|
if (!p || !p.billboard_autoplay || currentId !== id || currentKind !== kind) return;
|
||||||
|
bbTrailerTimer = setTimeout(function () {
|
||||||
|
if (currentId === id && currentKind === kind) startBillboardTrailer(key);
|
||||||
|
}, 2600);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function startBillboardTrailer(key) {
|
||||||
|
var bb = q('.vd-billboard'); if (!bb || bb.querySelector('[data-vd-bb-trailer]')) return;
|
||||||
|
bbMuted = true;
|
||||||
|
var wrap = document.createElement('div');
|
||||||
|
wrap.className = 'vd-bb-trailer'; wrap.setAttribute('data-vd-bb-trailer', '');
|
||||||
|
wrap.innerHTML = '<iframe allow="autoplay; encrypted-media" frameborder="0" ' +
|
||||||
|
'src="https://www.youtube.com/embed/' + encodeURIComponent(key) +
|
||||||
|
'?autoplay=1&mute=1&controls=0&modestbranding=1&rel=0&playsinline=1&enablejsapi=1"></iframe>' +
|
||||||
|
'<div class="vd-bb-tctrls"><button class="vd-bb-tbtn" type="button" data-vd-bb-mute aria-label="Unmute">🔇</button>' +
|
||||||
|
'<button class="vd-bb-tbtn" type="button" data-vd-bb-stop aria-label="Stop">✕</button></div>';
|
||||||
|
bb.appendChild(wrap);
|
||||||
|
bb.classList.add('vd-billboard--trailer');
|
||||||
|
}
|
||||||
|
function stopBillboardTrailer() {
|
||||||
|
clearTimeout(bbTrailerTimer); bbTrailerTimer = null;
|
||||||
|
var ws = document.querySelectorAll('[data-vd-bb-trailer]');
|
||||||
|
for (var i = 0; i < ws.length; i++) ws[i].remove();
|
||||||
|
var bbs = document.querySelectorAll('.vd-billboard--trailer');
|
||||||
|
for (var j = 0; j < bbs.length; j++) bbs[j].classList.remove('vd-billboard--trailer');
|
||||||
|
}
|
||||||
|
function toggleBillboardMute(btn) {
|
||||||
|
bbMuted = !bbMuted;
|
||||||
|
var iframe = document.querySelector('[data-vd-bb-trailer] iframe');
|
||||||
|
if (iframe && iframe.contentWindow) {
|
||||||
|
iframe.contentWindow.postMessage(JSON.stringify(
|
||||||
|
{ event: 'command', func: bbMuted ? 'mute' : 'unMute', args: [] }), '*');
|
||||||
|
}
|
||||||
|
btn.textContent = bbMuted ? '🔇' : '🔊';
|
||||||
|
}
|
||||||
|
|
||||||
// ── season selector (4 views) ─────────────────────────────────────────────
|
// ── season selector (4 views) ─────────────────────────────────────────────
|
||||||
function renderViewToggle() {
|
function renderViewToggle() {
|
||||||
var host = q('[data-vd-view-toggle]');
|
var host = q('[data-vd-view-toggle]');
|
||||||
|
|
@ -990,6 +1043,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClick(e) {
|
function onClick(e) {
|
||||||
|
var muteBtn = e.target.closest('[data-vd-bb-mute]');
|
||||||
|
if (muteBtn) { toggleBillboardMute(muteBtn); return; }
|
||||||
|
var stopBtn = e.target.closest('[data-vd-bb-stop]');
|
||||||
|
if (stopBtn) { stopBillboardTrailer(); return; }
|
||||||
var r = root(); if (!r) return;
|
var r = root(); if (!r) return;
|
||||||
// In-app drill-ins (real <a> links → modified clicks open new tabs).
|
// In-app drill-ins (real <a> links → modified clicks open new tabs).
|
||||||
var sim = e.target.closest('[data-vd-sim]');
|
var sim = e.target.closest('[data-vd-sim]');
|
||||||
|
|
@ -1051,6 +1108,10 @@
|
||||||
function init() {
|
function init() {
|
||||||
document.addEventListener('soulsync:video-open-detail', onOpen);
|
document.addEventListener('soulsync:video-open-detail', onOpen);
|
||||||
document.addEventListener('click', onClick);
|
document.addEventListener('click', onClick);
|
||||||
|
// Kill the billboard trailer (audio!) when navigating to a non-detail page.
|
||||||
|
document.addEventListener('soulsync:video-page-shown', function (e) {
|
||||||
|
if (e && e.detail !== 'video-movie-detail' && e.detail !== 'video-show-detail') stopBillboardTrailer();
|
||||||
|
});
|
||||||
document.addEventListener('keydown', function (e) {
|
document.addEventListener('keydown', function (e) {
|
||||||
if (e.key === 'Escape') { closeTrailer(); closeLightbox(); closeCastModal(); }
|
if (e.key === 'Escape') { closeTrailer(); closeLightbox(); closeCastModal(); }
|
||||||
else if (lightboxOpen()) {
|
else if (lightboxOpen()) {
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,20 @@
|
||||||
if (t && d.tmdb_api_key != null) t.value = d.tmdb_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;
|
if (v && d.tvdb_api_key != null) v.value = d.tvdb_api_key;
|
||||||
if (o && d.omdb_api_key != null) o.value = d.omdb_api_key;
|
if (o && d.omdb_api_key != null) o.value = d.omdb_api_key;
|
||||||
|
var ap = document.getElementById('video-billboard-autoplay');
|
||||||
|
if (ap && d.billboard_autoplay != null) ap.checked = !!d.billboard_autoplay;
|
||||||
})
|
})
|
||||||
.catch(function () { /* ignore */ });
|
.catch(function () { /* ignore */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveAutoplay() {
|
||||||
|
var ap = document.getElementById('video-billboard-autoplay');
|
||||||
|
fetch(CONFIG_URL, {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||||
|
body: JSON.stringify({ billboard_autoplay: ap ? ap.checked : true })
|
||||||
|
}).catch(function () { /* ignore */ });
|
||||||
|
}
|
||||||
|
|
||||||
function saveKeys() {
|
function saveKeys() {
|
||||||
var t = document.getElementById('tmdb-api-key');
|
var t = document.getElementById('tmdb-api-key');
|
||||||
var v = document.getElementById('tvdb-api-key');
|
var v = document.getElementById('tvdb-api-key');
|
||||||
|
|
@ -129,6 +139,8 @@
|
||||||
var el = document.getElementById(id);
|
var el = document.getElementById(id);
|
||||||
if (el) el.addEventListener('change', saveKeys);
|
if (el) el.addEventListener('change', saveKeys);
|
||||||
});
|
});
|
||||||
|
var autoplay = document.getElementById('video-billboard-autoplay');
|
||||||
|
if (autoplay) autoplay.addEventListener('change', saveAutoplay);
|
||||||
// Per-connection Test buttons (same behaviour as music's testConnection).
|
// Per-connection Test buttons (same behaviour as music's testConnection).
|
||||||
var testBtns = document.querySelectorAll('[data-video-test-service]');
|
var testBtns = document.querySelectorAll('[data-video-test-service]');
|
||||||
for (var k = 0; k < testBtns.length; k++) {
|
for (var k = 0; k < testBtns.length; k++) {
|
||||||
|
|
|
||||||
|
|
@ -1395,3 +1395,21 @@ a.vd-prov:hover img, a.vd-prov:hover .vd-prov-ph { border-color: rgba(var(--vd-a
|
||||||
background: none; border: 0; color: rgb(var(--vd-accent-rgb)); font-weight: 700; font-size: 13.5px;
|
background: none; border: 0; color: rgb(var(--vd-accent-rgb)); font-weight: 700; font-size: 13.5px;
|
||||||
cursor: pointer; padding: 8px 0 0;
|
cursor: pointer; padding: 8px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── billboard autoplay trailer ──────────────────────────────────────────── */
|
||||||
|
.vd-bb-trailer { position: absolute; inset: 0; z-index: 0; overflow: hidden; background: #000; animation: vd-fade 0.6s ease; }
|
||||||
|
.vd-bb-trailer iframe {
|
||||||
|
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||||
|
width: 100vw; height: 56.25vw; min-height: 100%; min-width: 177.78vh; border: 0; pointer-events: none;
|
||||||
|
}
|
||||||
|
.vd-billboard--trailer .vd-bb-bg { opacity: 0; transition: opacity 0.6s ease; }
|
||||||
|
.vd-bb-tctrls { position: absolute; bottom: 20px; right: 24px; z-index: 4; display: flex; gap: 8px; }
|
||||||
|
.vd-bb-tbtn {
|
||||||
|
width: 38px; height: 38px; border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.25); cursor: pointer;
|
||||||
|
background: rgba(0, 0, 0, 0.5); color: #fff; font-size: 15px; backdrop-filter: blur(8px); transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.vd-bb-tbtn:hover { background: rgba(0, 0, 0, 0.78); transform: translateY(-1px); }
|
||||||
|
|
||||||
|
/* settings: detail-pages preference toggle */
|
||||||
|
.vid-pref-row { display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 14px; color: var(--text-primary, #fff); }
|
||||||
|
.vid-pref-row input { width: 16px; height: 16px; cursor: pointer; accent-color: #38bdf8; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue