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..96fb469 100644
--- a/public/control.html
+++ b/public/control.html
@@ -251,8 +251,31 @@
diff --git a/public/js/app_control.js b/public/js/app_control.js
index ca9c142..71a7b26 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
};
@@ -706,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();
@@ -719,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 = `
@@ -735,6 +781,7 @@ window.onload = async function () {
${m.title || '(sans titre)'}
${m.imdb_id || ''}
|
+
${badgesHtml} |
${m.category || ''} |
${m.year || ''} |
@@ -756,7 +803,7 @@ window.onload = async function () {
updateSelectionUI();
} catch (err) {
- adminMoviesBody.innerHTML = ` | | ${err.message || 'Erreur réseau'} |
`;
+ adminMoviesBody.innerHTML = `
| ${err.message || 'Erreur réseau'} |
`;
}
}
diff --git a/public/js/app_movies.js b/public/js/app_movies.js
index 8dfa399..c93c623 100644
--- a/public/js/app_movies.js
+++ b/public/js/app_movies.js
@@ -117,6 +117,7 @@ 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 movieDiv = document.createElement('div');
movieDiv.classList.add('col-md-4', 'mb-4', 'movie-card');
@@ -124,10 +125,12 @@ window.onload = async function () {
movieDiv.innerHTML = `
${isChecked ? '
VISIONNÉ
' : ''}
-

+
+
+
- ${movie.title}
+ ${movie.title}
⭐ ${movie.rating}
${movie.category}
diff --git a/public/js/app_player.js b/public/js/app_player.js
new file mode 100644
index 0000000..543df82
--- /dev/null
+++ b/public/js/app_player.js
@@ -0,0 +1,290 @@
+function decodeJwt(token) {
+ return JSON.parse(atob(token.split('.')[1]));
+}
+
+function getQueryParam(name) {
+ const url = new URL(window.location.href);
+ return url.searchParams.get(name);
+}
+
+function showAlert(message) {
+ const el = document.getElementById('alert');
+ if (!el) return;
+ el.textContent = message;
+ el.classList.remove('d-none');
+}
+
+function setSourceLabel(text) {
+ const el = document.getElementById('source-label');
+ if (!el) return;
+ el.textContent = text || '—';
+ el.title = text || '';
+}
+
+function setHeader(movie) {
+ const titleEl = document.getElementById('movie-title');
+ const categoryEl = document.getElementById('movie-category');
+ const ratingEl = document.getElementById('movie-rating');
+
+ if (titleEl) titleEl.textContent = movie?.title || 'Player';
+
+ if (categoryEl) {
+ const cat = movie?.category ? String(movie.category) : '';
+ categoryEl.textContent = cat ? `${cat} · ` : '';
+ }
+
+ if (ratingEl) {
+ const r = movie?.rating ? String(movie.rating) : '';
+ ratingEl.textContent = r ? `⭐ ${r}` : '';
+ }
+
+ document.title = movie?.title ? `Player - ${movie.title}` : 'Player';
+}
+
+function normalizeVodLink(raw) {
+ const v = String(raw || '').trim();
+ if (!v) return null;
+
+ // Support pasted iframe embed code (extract src)
+ if (v.toLowerCase().includes('