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) {