Movie player integration #15
+8
-1
@@ -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 });
|
||||
|
||||
|
||||
+49
-4
@@ -251,8 +251,31 @@
|
||||
<input type="text" class="form-control" id="category" placeholder="Best Picture, etc." required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="vod_link" class="form-label">Lien VOD</label>
|
||||
<input type="url" class="form-control" id="vod_link" placeholder="https://..." required>
|
||||
<label class="form-label">Lecteur (source)</label>
|
||||
<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 class="d-flex gap-2">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="edit_vod_link" class="form-label">Lien VOD</label>
|
||||
<input type="url" class="form-control" id="edit_vod_link" placeholder="https://..." required>
|
||||
<label class="form-label">Lecteur (source)</label>
|
||||
<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 class="col-12">
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
+14
-1
@@ -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.');
|
||||
}
|
||||
|
||||
+73
-4
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user