Movie player integration #15
+8
-1
@@ -9,7 +9,14 @@ const movieSchema = new mongoose.Schema({
|
|||||||
// Oscar pool year / edition (used to segment movies + stats)
|
// Oscar pool year / edition (used to segment movies + stats)
|
||||||
year: { type: Number, required: false, index: true },
|
year: { type: Number, required: false, index: true },
|
||||||
category: { type: String, required: 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' }]
|
watchedBy: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]
|
||||||
}, { timestamps: true });
|
}, { timestamps: true });
|
||||||
|
|
||||||
|
|||||||
+49
-4
@@ -251,8 +251,31 @@
|
|||||||
<input type="text" class="form-control" id="category" placeholder="Best Picture, etc." required>
|
<input type="text" class="form-control" id="category" placeholder="Best Picture, etc." required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="vod_link" class="form-label">Lien VOD</label>
|
<label class="form-label">Lecteur (source)</label>
|
||||||
<input type="url" class="form-control" id="vod_link" placeholder="https://..." required>
|
<div class="row g-2">
|
||||||
|
<div class="col-12 col-lg-4">
|
||||||
|
<label for="player_mode" class="form-label">Mode</label>
|
||||||
|
<select id="player_mode" class="form-select">
|
||||||
|
<option value="auto" selected>Auto</option>
|
||||||
|
<option value="video">Video</option>
|
||||||
|
<option value="embed">Embed</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">Auto = video_src → embed_src → vod_link.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-8">
|
||||||
|
<label for="video_src" class="form-label">Video URL (mp4/webm/m3u8)</label>
|
||||||
|
<input type="text" class="form-control" id="video_src" placeholder="https://.../movie.mp4 or .../playlist.m3u8">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="embed_src" class="form-label">Embed URL or iframe code</label>
|
||||||
|
<textarea class="form-control font-monospace" id="embed_src" rows="3" placeholder="https://www.youtube.com/watch?v=... OR <iframe src='...'></iframe>"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="vod_link" class="form-label">Legacy VOD link (optional)</label>
|
||||||
|
<input type="text" class="form-control" id="vod_link" placeholder="https://...">
|
||||||
|
<div class="form-text">Backward-compat. Prefer video_src / embed_src above.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Ajouter</button>
|
<button type="submit" class="btn btn-primary">Ajouter</button>
|
||||||
@@ -342,8 +365,30 @@
|
|||||||
<input type="text" class="form-control" id="edit_category" placeholder="Best Picture, etc." required>
|
<input type="text" class="form-control" id="edit_category" placeholder="Best Picture, etc." required>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="edit_vod_link" class="form-label">Lien VOD</label>
|
<label class="form-label">Lecteur (source)</label>
|
||||||
<input type="url" class="form-control" id="edit_vod_link" placeholder="https://..." required>
|
<div class="row g-2">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="edit_player_mode" class="form-label">Mode</label>
|
||||||
|
<select id="edit_player_mode" class="form-select">
|
||||||
|
<option value="auto">Auto</option>
|
||||||
|
<option value="video">Video</option>
|
||||||
|
<option value="embed">Embed</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">Auto = video_src → embed_src → vod_link.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="edit_video_src" class="form-label">Video URL (mp4/webm/m3u8)</label>
|
||||||
|
<input type="text" class="form-control" id="edit_video_src" placeholder="https://.../movie.mp4 or .../playlist.m3u8">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="edit_embed_src" class="form-label">Embed URL or iframe code</label>
|
||||||
|
<textarea class="form-control font-monospace" id="edit_embed_src" rows="3" placeholder="https://... OR <iframe ...>"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="edit_vod_link" class="form-label">Legacy VOD link (optional)</label>
|
||||||
|
<input type="text" class="form-control" id="edit_vod_link" placeholder="https://...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ window.onload = async function () {
|
|||||||
const editYearEl = document.getElementById('edit_year');
|
const editYearEl = document.getElementById('edit_year');
|
||||||
const editCategoryEl = document.getElementById('edit_category');
|
const editCategoryEl = document.getElementById('edit_category');
|
||||||
const editVodLinkEl = document.getElementById('edit_vod_link');
|
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 editRefreshOmdbEl = document.getElementById('edit_refresh_omdb');
|
||||||
const editTitleEl = document.getElementById('edit_title');
|
const editTitleEl = document.getElementById('edit_title');
|
||||||
const editRatingEl = document.getElementById('edit_rating');
|
const editRatingEl = document.getElementById('edit_rating');
|
||||||
@@ -485,6 +488,9 @@ window.onload = async function () {
|
|||||||
const imdb_id = document.getElementById('imdb_id').value.trim();
|
const imdb_id = document.getElementById('imdb_id').value.trim();
|
||||||
const category = document.getElementById('category').value.trim();
|
const category = document.getElementById('category').value.trim();
|
||||||
const vod_link = document.getElementById('vod_link').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) {
|
if (!year) {
|
||||||
showResponse('warning', 'Année invalide. Exemple attendu: 2026');
|
showResponse('warning', 'Année invalide. Exemple attendu: 2026');
|
||||||
@@ -496,6 +502,11 @@ window.onload = async function () {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!vod_link && !video_src && !embed_src) {
|
||||||
|
showResponse('warning', 'Ajoute au moins une source (VOD / video_src / embed_src).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/movies/add', {
|
const res = await fetch('/api/movies/add', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -503,7 +514,7 @@ window.onload = async function () {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`
|
'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) {
|
if (res.ok) {
|
||||||
@@ -596,6 +607,9 @@ window.onload = async function () {
|
|||||||
editYearEl.value = movie.year ? String(movie.year) : '';
|
editYearEl.value = movie.year ? String(movie.year) : '';
|
||||||
editCategoryEl.value = movie.category || '';
|
editCategoryEl.value = movie.category || '';
|
||||||
editVodLinkEl.value = movie.vod_link || '';
|
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;
|
editRefreshOmdbEl.checked = false;
|
||||||
|
|
||||||
editTitleEl.value = movie.title || '';
|
editTitleEl.value = movie.title || '';
|
||||||
@@ -619,6 +633,9 @@ window.onload = async function () {
|
|||||||
const yearRaw = (editYearEl.value || '').trim();
|
const yearRaw = (editYearEl.value || '').trim();
|
||||||
const category = (editCategoryEl.value || '').trim();
|
const category = (editCategoryEl.value || '').trim();
|
||||||
const vod_link = (editVodLinkEl.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 refreshOmdb = !!editRefreshOmdbEl.checked;
|
||||||
|
|
||||||
const title = editTitleEl.value;
|
const title = editTitleEl.value;
|
||||||
@@ -645,8 +662,8 @@ window.onload = async function () {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!vod_link) {
|
if (!vod_link && !video_src && !embed_src) {
|
||||||
showResponse('warning', 'Lien VOD invalide.');
|
showResponse('warning', 'Ajoute au moins une source (VOD / video_src / embed_src).');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,6 +672,9 @@ window.onload = async function () {
|
|||||||
year: yearRaw === '' ? null : year,
|
year: yearRaw === '' ? null : year,
|
||||||
category,
|
category,
|
||||||
vod_link,
|
vod_link,
|
||||||
|
player_mode,
|
||||||
|
video_src,
|
||||||
|
embed_src,
|
||||||
refreshOmdb
|
refreshOmdb
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -226,6 +226,18 @@ async function playVodLink(vodLink) {
|
|||||||
showEmbedPlayer(raw);
|
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 () {
|
window.onload = async function () {
|
||||||
// Basic auth UX: require a valid local token (like the rest of the app).
|
// Basic auth UX: require a valid local token (like the rest of the app).
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
@@ -269,7 +281,8 @@ window.onload = async function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setHeader(movie);
|
setHeader(movie);
|
||||||
await playVodLink(movie?.vod_link);
|
const src = resolveMovieSource(movie);
|
||||||
|
await playVodLink(src);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlert(err.message || 'Network error.');
|
showAlert(err.message || 'Network error.');
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-4
@@ -19,6 +19,28 @@ function isValidImdbId(value) {
|
|||||||
return typeof value === "string" && /^tt\d{5,}$/.test(value.trim());
|
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
|
// Fetch movie details from OMDb API
|
||||||
async function fetchMovieDetailsFromOmdb(imdb_id) {
|
async function fetchMovieDetailsFromOmdb(imdb_id) {
|
||||||
const apiKey = process.env.OMDB_API; // Replace with your actual OMDb API key
|
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
|
// Add movie
|
||||||
router.post("/add", async (req, res) => {
|
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 {
|
try {
|
||||||
// Get the token from the Authorization header
|
// 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)" });
|
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
|
// Fetch movie details from OMDb API
|
||||||
const movieDetails = await fetchMovieDetailsFromOmdb(imdb_id);
|
const movieDetails = await fetchMovieDetailsFromOmdb(imdb_id);
|
||||||
|
|
||||||
@@ -180,7 +213,10 @@ router.post("/add", async (req, res) => {
|
|||||||
poster: movieDetails.poster,
|
poster: movieDetails.poster,
|
||||||
year: parsedYear,
|
year: parsedYear,
|
||||||
category,
|
category,
|
||||||
vod_link
|
vod_link: normalizedVod || undefined,
|
||||||
|
player_mode: normalizedMode,
|
||||||
|
video_src: normalizedVideo || undefined,
|
||||||
|
embed_src: normalizedEmbed || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await movie.save();
|
await movie.save();
|
||||||
@@ -262,6 +298,9 @@ router.put("/:id", async (req, res) => {
|
|||||||
year,
|
year,
|
||||||
category,
|
category,
|
||||||
vod_link,
|
vod_link,
|
||||||
|
player_mode,
|
||||||
|
video_src,
|
||||||
|
embed_src,
|
||||||
refreshOmdb,
|
refreshOmdb,
|
||||||
} = req.body || {};
|
} = req.body || {};
|
||||||
|
|
||||||
@@ -281,7 +320,14 @@ router.put("/:id", async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof vod_link === "string" && vod_link.trim() === "") {
|
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);
|
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 (imdb_id !== undefined) movie.imdb_id = String(imdb_id).trim();
|
||||||
if (category !== undefined) movie.category = String(category).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 !== "") {
|
if (year !== undefined && year !== null && year !== "") {
|
||||||
movie.year = parseOscarYear(year);
|
movie.year = parseOscarYear(year);
|
||||||
@@ -312,6 +373,14 @@ router.put("/:id", async (req, res) => {
|
|||||||
if (poster !== undefined) movie.poster = poster;
|
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();
|
await movie.save();
|
||||||
return res.status(200).json(movie);
|
return res.status(200).json(movie);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user