From f07547cc591da3ba89eaac5e7361425c11915f42 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 20 Dec 2025 21:20:05 +0000 Subject: [PATCH 01/87] Update Oscars year to 2026 and add admin page Co-authored-by: mathieumoran1 --- README.md | 4 +- public/checklist.html | 5 ++- public/control.html | 87 +++++++++++++++++++++++++++++++++++ public/index.html | 4 +- public/js/app_checklist.js | 6 ++- public/js/app_control.js | 92 ++++++++++++++++++++++++++++++++++++++ public/js/app_movies.js | 6 ++- public/js/app_user-stat.js | 4 ++ public/movies.html | 7 +-- public/register.html | 2 +- public/user-stat.html | 3 +- 11 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 public/control.html create mode 100644 public/js/app_control.js diff --git a/README.md b/README.md index 9289469..ee7a03f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -## Oscars Pool 2025 +## Oscars Pool 2026 Little nodejs + mongodb app that I hack together with ChatGPT to keep track of watched movies for an incoming Oscars Pool with some friends. App is not secure, using old html standard and is not the most responsive but does the job. Would probably need to revamp everything, but didn't want to spend more time on it. -![Oscars Pool 2025](https://github.com/quoije/oscar-pool/blob/prod/img/preview.png) +![Oscars Pool 2026](https://github.com/quoije/oscar-pool/blob/prod/img/preview.png) ## Setup Config those in your .env file: diff --git a/public/checklist.html b/public/checklist.html index 7952836..f510bae 100644 --- a/public/checklist.html +++ b/public/checklist.html @@ -3,7 +3,7 @@ - Pool Oscars 2025 - Checklist + Pool Oscars 2026 - Checklist + + + + + +
+

Admin - Ajouter un film

+

+ Ajoute un film via l’API (/api/movies/add). Le serveur récupère automatiquement le titre/plot/affiche depuis OMDb avec l’imdb_id. +

+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + + + + diff --git a/public/index.html b/public/index.html index fd4c59f..bcaa87b 100644 --- a/public/index.html +++ b/public/index.html @@ -3,7 +3,7 @@ - Pool Oscars 2025 + Pool Oscars 2026 + + + + +
+
+

Loading…

+
+ + +
+
+ + + +
+ + +
+ +
+ Source + — +
+
+ + + + + + + From 43925c3b60bdf5357c16162e566d4259b04791c6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 21 Dec 2025 00:26:34 +0000 Subject: [PATCH 20/87] Refactor: Add flexible video source options to Movie model Co-authored-by: mathieumoran1 --- models/Movie.js | 9 ++++- public/control.html | 53 ++++++++++++++++++++++++--- public/js/app_control.js | 26 ++++++++++++-- public/js/app_player.js | 15 +++++++- routes/movies.js | 77 +++++++++++++++++++++++++++++++++++++--- 5 files changed, 167 insertions(+), 13 deletions(-) diff --git a/models/Movie.js b/models/Movie.js index 8e8d3ef..8f7cddf 100644 --- a/models/Movie.js +++ b/models/Movie.js @@ -9,7 +9,14 @@ const movieSchema = new mongoose.Schema({ // Oscar pool year / edition (used to segment movies + stats) year: { type: Number, required: false, index: true }, category: { type: String, required: true }, - vod_link: { type: String, required: true }, + // Legacy "one field" player source. + // Kept for backwards-compat. New UI prefers video_src / embed_src. + vod_link: { type: String, required: false }, + + // Player configuration (optional; used by player.html) + player_mode: { type: String, required: false, enum: ["auto", "video", "embed"], default: "auto" }, + video_src: { type: String, required: false }, // direct video file URL or HLS (.m3u8) + embed_src: { type: String, required: false }, // URL or iframe code watchedBy: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }] }, { timestamps: true }); diff --git a/public/control.html b/public/control.html index c11baa6..825389d 100644 --- a/public/control.html +++ b/public/control.html @@ -251,8 +251,31 @@
- - + +
+
+ + +
Auto = video_src → embed_src → vod_link.
+
+
+ + +
+
+ + +
+
+ + +
Backward-compat. Prefer video_src / embed_src above.
+
+
@@ -342,8 +365,30 @@
- - + +
+
+ + +
Auto = video_src → embed_src → vod_link.
+
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/public/js/app_control.js b/public/js/app_control.js index ca9c142..0bffd55 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -72,6 +72,9 @@ window.onload = async function () { const editYearEl = document.getElementById('edit_year'); const editCategoryEl = document.getElementById('edit_category'); const editVodLinkEl = document.getElementById('edit_vod_link'); + const editPlayerModeEl = document.getElementById('edit_player_mode'); + const editVideoSrcEl = document.getElementById('edit_video_src'); + const editEmbedSrcEl = document.getElementById('edit_embed_src'); const editRefreshOmdbEl = document.getElementById('edit_refresh_omdb'); const editTitleEl = document.getElementById('edit_title'); const editRatingEl = document.getElementById('edit_rating'); @@ -485,6 +488,9 @@ window.onload = async function () { const imdb_id = document.getElementById('imdb_id').value.trim(); const category = document.getElementById('category').value.trim(); const vod_link = document.getElementById('vod_link').value.trim(); + const player_mode = (document.getElementById('player_mode')?.value || 'auto').trim(); + const video_src = document.getElementById('video_src')?.value?.trim() || ''; + const embed_src = document.getElementById('embed_src')?.value?.trim() || ''; if (!year) { showResponse('warning', 'Année invalide. Exemple attendu: 2026'); @@ -496,6 +502,11 @@ window.onload = async function () { return; } + if (!vod_link && !video_src && !embed_src) { + showResponse('warning', 'Ajoute au moins une source (VOD / video_src / embed_src).'); + return; + } + try { const res = await fetch('/api/movies/add', { method: 'POST', @@ -503,7 +514,7 @@ window.onload = async function () { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, - body: JSON.stringify({ year, imdb_id, category, vod_link }) + body: JSON.stringify({ year, imdb_id, category, vod_link, player_mode, video_src, embed_src }) }); if (res.ok) { @@ -596,6 +607,9 @@ window.onload = async function () { editYearEl.value = movie.year ? String(movie.year) : ''; editCategoryEl.value = movie.category || ''; editVodLinkEl.value = movie.vod_link || ''; + if (editPlayerModeEl) editPlayerModeEl.value = movie.player_mode || 'auto'; + if (editVideoSrcEl) editVideoSrcEl.value = movie.video_src || ''; + if (editEmbedSrcEl) editEmbedSrcEl.value = movie.embed_src || ''; editRefreshOmdbEl.checked = false; editTitleEl.value = movie.title || ''; @@ -619,6 +633,9 @@ window.onload = async function () { const yearRaw = (editYearEl.value || '').trim(); const category = (editCategoryEl.value || '').trim(); const vod_link = (editVodLinkEl.value || '').trim(); + const player_mode = (editPlayerModeEl?.value || 'auto').trim(); + const video_src = (editVideoSrcEl?.value || '').trim(); + const embed_src = (editEmbedSrcEl?.value || '').trim(); const refreshOmdb = !!editRefreshOmdbEl.checked; const title = editTitleEl.value; @@ -645,8 +662,8 @@ window.onload = async function () { return; } - if (!vod_link) { - showResponse('warning', 'Lien VOD invalide.'); + if (!vod_link && !video_src && !embed_src) { + showResponse('warning', 'Ajoute au moins une source (VOD / video_src / embed_src).'); return; } @@ -655,6 +672,9 @@ window.onload = async function () { year: yearRaw === '' ? null : year, category, vod_link, + player_mode, + video_src, + embed_src, refreshOmdb }; diff --git a/public/js/app_player.js b/public/js/app_player.js index a62517a..543df82 100644 --- a/public/js/app_player.js +++ b/public/js/app_player.js @@ -226,6 +226,18 @@ async function playVodLink(vodLink) { showEmbedPlayer(raw); } +function resolveMovieSource(movie) { + const mode = String(movie?.player_mode || 'auto').toLowerCase(); + const legacy = movie?.vod_link; + const video = movie?.video_src; + const embed = movie?.embed_src; + + if (mode === 'video') return video || legacy || embed || null; + if (mode === 'embed') return embed || legacy || video || null; + // auto + return video || embed || legacy || null; +} + window.onload = async function () { // Basic auth UX: require a valid local token (like the rest of the app). const token = localStorage.getItem('auth_token'); @@ -269,7 +281,8 @@ window.onload = async function () { } setHeader(movie); - await playVodLink(movie?.vod_link); + const src = resolveMovieSource(movie); + await playVodLink(src); } catch (err) { showAlert(err.message || 'Network error.'); } diff --git a/routes/movies.js b/routes/movies.js index f14dd50..f754618 100644 --- a/routes/movies.js +++ b/routes/movies.js @@ -19,6 +19,28 @@ function isValidImdbId(value) { return typeof value === "string" && /^tt\d{5,}$/.test(value.trim()); } +function normalizeOptionalString(v, maxLen = 2048) { + if (v === undefined) return undefined; + if (v === null) return null; + const s = String(v).trim(); + if (!s) return ""; + return s.slice(0, maxLen); +} + +function normalizePlayerMode(v) { + const s = String(v || "").trim().toLowerCase(); + if (!s) return "auto"; + if (s === "auto" || s === "video" || s === "embed") return s; + return null; +} + +function hasAnySource({ vod_link, video_src, embed_src }) { + const a = typeof vod_link === "string" ? vod_link.trim() : ""; + const b = typeof video_src === "string" ? video_src.trim() : ""; + const c = typeof embed_src === "string" ? embed_src.trim() : ""; + return !!(a || b || c); +} + // Fetch movie details from OMDb API async function fetchMovieDetailsFromOmdb(imdb_id) { const apiKey = process.env.OMDB_API; // Replace with your actual OMDb API key @@ -145,7 +167,7 @@ router.patch("/users/updateWatchedMovies", async (req, res) => { // Add movie router.post("/add", async (req, res) => { - const { imdb_id, category, vod_link, year } = req.body; + const { imdb_id, category, vod_link, year, player_mode, video_src, embed_src } = req.body; try { // Get the token from the Authorization header @@ -168,6 +190,17 @@ router.post("/add", async (req, res) => { return res.status(400).json({ message: "Année invalide (ex: 2026)" }); } + const normalizedVod = normalizeOptionalString(vod_link, 4096); + const normalizedVideo = normalizeOptionalString(video_src, 4096); + const normalizedEmbed = normalizeOptionalString(embed_src, 20000); + const normalizedMode = normalizePlayerMode(player_mode); + if (normalizedMode === null) { + return res.status(400).json({ message: "player_mode invalide (auto|video|embed)" }); + } + if (!hasAnySource({ vod_link: normalizedVod || "", video_src: normalizedVideo || "", embed_src: normalizedEmbed || "" })) { + return res.status(400).json({ message: "Ajoute au moins une source (VOD / video_src / embed_src)." }); + } + // Fetch movie details from OMDb API const movieDetails = await fetchMovieDetailsFromOmdb(imdb_id); @@ -180,7 +213,10 @@ router.post("/add", async (req, res) => { poster: movieDetails.poster, year: parsedYear, category, - vod_link + vod_link: normalizedVod || undefined, + player_mode: normalizedMode, + video_src: normalizedVideo || undefined, + embed_src: normalizedEmbed || undefined, }); await movie.save(); @@ -262,6 +298,9 @@ router.put("/:id", async (req, res) => { year, category, vod_link, + player_mode, + video_src, + embed_src, refreshOmdb, } = req.body || {}; @@ -281,7 +320,14 @@ router.put("/:id", async (req, res) => { } if (typeof vod_link === "string" && vod_link.trim() === "") { - return res.status(400).json({ message: "Lien VOD invalide." }); + // Allow clearing vod_link; enforce "at least one source" after applying changes. + } + + if (player_mode !== undefined) { + const normalizedMode = normalizePlayerMode(player_mode); + if (normalizedMode === null) { + return res.status(400).json({ message: "player_mode invalide (auto|video|embed)" }); + } } const movie = await Movie.findById(id); @@ -289,7 +335,22 @@ router.put("/:id", async (req, res) => { if (imdb_id !== undefined) movie.imdb_id = String(imdb_id).trim(); if (category !== undefined) movie.category = String(category).trim(); - if (vod_link !== undefined) movie.vod_link = String(vod_link).trim(); + if (vod_link !== undefined) { + const v = normalizeOptionalString(vod_link, 4096); + movie.vod_link = v ? v : null; + } + if (player_mode !== undefined) { + const normalizedMode = normalizePlayerMode(player_mode); + movie.player_mode = normalizedMode === null ? movie.player_mode : normalizedMode; + } + if (video_src !== undefined) { + const v = normalizeOptionalString(video_src, 4096); + movie.video_src = v ? v : null; + } + if (embed_src !== undefined) { + const v = normalizeOptionalString(embed_src, 20000); + movie.embed_src = v ? v : null; + } if (year !== undefined && year !== null && year !== "") { movie.year = parseOscarYear(year); @@ -312,6 +373,14 @@ router.put("/:id", async (req, res) => { if (poster !== undefined) movie.poster = poster; } + if (!hasAnySource({ + vod_link: movie.vod_link || "", + video_src: movie.video_src || "", + embed_src: movie.embed_src || "", + })) { + return res.status(400).json({ message: "Ajoute au moins une source (VOD / video_src / embed_src)." }); + } + await movie.save(); return res.status(200).json(movie); } catch (err) { From 5cfa35c79c55ea2a5e03e446c69668924a9b1f8d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 21 Dec 2025 00:27:58 +0000 Subject: [PATCH 21/87] Add player column and badges to movie management table Co-authored-by: mathieumoran1 --- public/control.html | 3 ++- public/js/app_control.js | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/public/control.html b/public/control.html index 825389d..96fb469 100644 --- a/public/control.html +++ b/public/control.html @@ -322,13 +322,14 @@ Sel. Titre + Player Catégorie Année Actions - Charge la liste… + Charge la liste…
diff --git a/public/js/app_control.js b/public/js/app_control.js index 0bffd55..71a7b26 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -726,12 +726,12 @@ window.onload = async function () { async function loadMoviesForManagement() { const year = manageYearSelect.value; const url = year ? `/api/movies?year=${encodeURIComponent(year)}` : '/api/movies'; - adminMoviesBody.innerHTML = `Chargement…`; + adminMoviesBody.innerHTML = `Chargement…`; try { const res = await fetch(url, { method: 'GET' }); if (!res.ok) { - adminMoviesBody.innerHTML = `Erreur lors du chargement (${res.status})`; + adminMoviesBody.innerHTML = `Erreur lors du chargement (${res.status})`; return; } const movies = await res.json(); @@ -739,13 +739,39 @@ window.onload = async function () { moviesById = new Map(movies.map((m) => [m._id, m])); if (!movies.length) { - adminMoviesBody.innerHTML = `Aucun film.`; + adminMoviesBody.innerHTML = `Aucun film.`; updateSelectionUI(); return; } + function buildPlayerBadges(m) { + const badges = []; + const hasVideo = !!(m && m.video_src); + const hasEmbed = !!(m && m.embed_src); + const hasLegacy = !!(m && m.vod_link); + + if (hasVideo) badges.push({ text: 'Video', cls: 'bg-primary' }); + if (hasEmbed) badges.push({ text: 'Embed', cls: 'bg-info text-dark' }); + if (hasLegacy) badges.push({ text: 'Legacy', cls: 'bg-secondary' }); + + const modeRaw = String(m?.player_mode || 'auto').toLowerCase(); + const mode = (modeRaw === 'video' || modeRaw === 'embed' || modeRaw === 'auto') ? modeRaw : 'auto'; + const modeText = mode === 'auto' ? 'Auto' : mode === 'video' ? 'Mode: Video' : 'Mode: Embed'; + badges.push({ text: modeText, cls: 'bg-light text-dark border' }); + + if (!hasVideo && !hasEmbed && !hasLegacy) { + return [{ text: '—', cls: 'bg-light text-dark border' }]; + } + return badges; + } + adminMoviesBody.innerHTML = ''; movies.forEach((m) => { + const badges = buildPlayerBadges(m); + const badgesHtml = badges + .map((b) => `${b.text}`) + .join(''); + const tr = document.createElement('tr'); tr.innerHTML = ` @@ -755,6 +781,7 @@ window.onload = async function () {
${m.title || '(sans titre)'}
${m.imdb_id || ''}
+ ${badgesHtml} ${m.category || ''} ${m.year || ''} @@ -776,7 +803,7 @@ window.onload = async function () { updateSelectionUI(); } catch (err) { - adminMoviesBody.innerHTML = `${err.message || 'Erreur réseau'}`; + adminMoviesBody.innerHTML = `${err.message || 'Erreur réseau'}`; } } From b72f1bde823732568716ef893f88fdc4a9edd6d1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 21 Dec 2025 00:50:45 +0000 Subject: [PATCH 22/87] Refactor movie player and navigation logic Co-authored-by: mathieumoran1 --- public/js/app_movies.js | 10 +++- public/js/app_player.js | 113 ++++++++++++++++++++++++++++++++++------ public/player.html | 47 ++++++++++++++--- 3 files changed, 146 insertions(+), 24 deletions(-) diff --git a/public/js/app_movies.js b/public/js/app_movies.js index c93c623..db29f78 100644 --- a/public/js/app_movies.js +++ b/public/js/app_movies.js @@ -117,7 +117,15 @@ window.onload = async function () { const isChecked = watchedMoviesInYear.some(watchedMovie => watchedMovie.imdb_id === movie.imdb_id); const watchedMovie = watchedMoviesInYear.find(wm => wm.imdb_id === movie.imdb_id); const watchedDate = watchedMovie ? new Date(watchedMovie.watchedDate).toLocaleString() : ''; - const playerUrl = movie && movie._id ? `/player.html?id=${encodeURIComponent(movie._id)}` : (movie.vod_link || '#'); + const hasVideoSrc = !!(movie && typeof movie.video_src === 'string' && movie.video_src.trim()); + const hasEmbedSrc = !!(movie && typeof movie.embed_src === 'string' && movie.embed_src.trim()); + const hasNewPlayerSource = hasVideoSrc || hasEmbedSrc; + const hasLegacy = !!(movie && typeof movie.vod_link === 'string' && movie.vod_link.trim()); + + // If the movie only has legacy `vod_link`, skip the player entirely and open the original URL in a new tab. + const playerUrl = (movie && movie._id && hasNewPlayerSource) + ? `/player.html?id=${encodeURIComponent(movie._id)}` + : (hasLegacy ? movie.vod_link : (movie && movie._id ? `/player.html?id=${encodeURIComponent(movie._id)}` : '#')); const movieDiv = document.createElement('div'); movieDiv.classList.add('col-md-4', 'mb-4', 'movie-card'); diff --git a/public/js/app_player.js b/public/js/app_player.js index 543df82..957ff71 100644 --- a/public/js/app_player.js +++ b/public/js/app_player.js @@ -11,9 +11,24 @@ function showAlert(message) { const el = document.getElementById('alert'); if (!el) return; el.textContent = message; + // Ensure error styling (danger) when using the simple API. + el.classList.remove('alert-primary', 'alert-secondary', 'alert-success', 'alert-danger', 'alert-warning', 'alert-info', 'alert-light', 'alert-dark'); + el.classList.add('alert-danger'); el.classList.remove('d-none'); } +function showAlertVariant(message, variant) { + const el = document.getElementById('alert'); + if (!el) return; + el.textContent = message; + el.classList.remove('d-none'); + + // Reset any prior bootstrap alert-* variants + el.classList.remove('alert-primary', 'alert-secondary', 'alert-success', 'alert-danger', 'alert-warning', 'alert-info', 'alert-light', 'alert-dark'); + const v = String(variant || 'danger').trim().toLowerCase(); + el.classList.add(`alert-${v || 'danger'}`); +} + function setSourceLabel(text) { const el = document.getElementById('source-label'); if (!el) return; @@ -140,6 +155,33 @@ function showEmbedPlayer(embedUrl) { } } +function hideAllPlayers() { + const videoEl = document.getElementById('video'); + const iframeEl = document.getElementById('embed'); + + if (videoEl) { + try { videoEl.pause(); } catch (_) {} + videoEl.classList.add('d-none'); + try { videoEl.removeAttribute('src'); videoEl.load(); } catch (_) {} + } + if (iframeEl) { + iframeEl.classList.add('d-none'); + iframeEl.removeAttribute('src'); + } +} + +function setOpenOriginal(raw) { + const openOriginal = document.getElementById('open-original'); + if (!openOriginal) return; + if (raw) { + openOriginal.href = raw; + openOriginal.classList.remove('d-none'); + } else { + openOriginal.removeAttribute('href'); + openOriginal.classList.add('d-none'); + } +} + async function playVodLink(vodLink) { const raw = normalizeVodLink(vodLink); if (!raw) { @@ -149,11 +191,7 @@ async function playVodLink(vodLink) { } // "Open original" button - const openOriginal = document.getElementById('open-original'); - if (openOriginal) { - openOriginal.href = raw; - openOriginal.classList.remove('d-none'); - } + setOpenOriginal(raw); // Prefer known embed providers const yt = toYoutubeEmbed(raw); @@ -226,16 +264,27 @@ async function playVodLink(vodLink) { showEmbedPlayer(raw); } +function cleanUrlValue(v) { + const s = typeof v === 'string' ? v.trim() : ''; + return s ? s : null; +} + function resolveMovieSource(movie) { const mode = String(movie?.player_mode || 'auto').toLowerCase(); - const legacy = movie?.vod_link; - const video = movie?.video_src; - const embed = movie?.embed_src; + const legacy = cleanUrlValue(movie?.vod_link); + const video = cleanUrlValue(movie?.video_src); + const embed = cleanUrlValue(movie?.embed_src); - if (mode === 'video') return video || legacy || embed || null; - if (mode === 'embed') return embed || legacy || video || null; - // auto - return video || embed || legacy || null; + // Legacy should never embed in the player (open in new tab only). + // So we only pick legacy if there is no other option, and mark it. + let picked = null; + if (mode === 'video') picked = video || null; + else if (mode === 'embed') picked = embed || null; + else picked = video || embed || null; // auto + + if (picked) return { src: picked, isLegacy: false }; + if (legacy) return { src: legacy, isLegacy: true }; + return { src: null, isLegacy: false }; } window.onload = async function () { @@ -258,8 +307,20 @@ window.onload = async function () { window.location.href = '/change-password.html'; return; } - const nameEl = document.getElementById('player-user-name'); + const nameEl = document.getElementById('user-name'); if (nameEl) nameEl.textContent = decoded.name || ''; + if (decoded.admin) { + const adminLink = document.getElementById('admin-control-link'); + if (adminLink) adminLink.classList.remove('d-none'); + } + + const logoffBtn = document.getElementById('log-off'); + if (logoffBtn) { + logoffBtn.addEventListener('click', function () { + localStorage.removeItem('auth_token'); + window.location.href = '/'; + }); + } } catch (_) { localStorage.removeItem('auth_token'); window.location.href = '/'; @@ -273,7 +334,10 @@ window.onload = async function () { } try { - const res = await fetch(`/api/movies/${encodeURIComponent(id)}`, { method: 'GET' }); + const res = await fetch(`/api/movies/${encodeURIComponent(id)}`, { + method: 'GET', + headers: token ? { 'Authorization': `Bearer ${token}` } : undefined, + }); const movie = await res.json().catch(() => ({})); if (!res.ok) { showAlert(movie?.message || movie?.error || `Failed to load movie (${res.status}).`); @@ -281,8 +345,25 @@ window.onload = async function () { } setHeader(movie); - const src = resolveMovieSource(movie); - await playVodLink(src); + const resolved = resolveMovieSource(movie); + if (!resolved?.src) { + setOpenOriginal(null); + setSourceLabel('—'); + hideAllPlayers(); + showAlert('No playable source configured for this movie.'); + return; + } + + if (resolved.isLegacy) { + // Legacy source: never embed it in the player. + setOpenOriginal(resolved.src); + setSourceLabel('Legacy link (opens in new tab)'); + hideAllPlayers(); + showAlertVariant('Legacy source: use “Open original” to watch in a new tab.', 'warning'); + return; + } + + await playVodLink(resolved.src); } catch (err) { showAlert(err.message || 'Network error.'); } diff --git a/public/player.html b/public/player.html index 2c0dbf3..f36ad1d 100644 --- a/public/player.html +++ b/public/player.html @@ -7,6 +7,24 @@ -