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