diff --git a/index.js b/index.js index 2ed5a7b..877665a 100644 --- a/index.js +++ b/index.js @@ -24,6 +24,7 @@ const movieRoutes = require("./routes/movies"); const versionRoutes = require("./routes/version"); const settingsRoutes = require("./routes/settings"); const adminDbRoutes = require("./routes/adminDb"); +const videoRoutes = require("./routes/video"); app.use(express.static('public')); app.use("/api/users", userRoutes); @@ -31,6 +32,7 @@ app.use("/api/movies", movieRoutes); app.use("/api/version", versionRoutes); app.use("/api/settings", settingsRoutes); app.use("/api/admin", adminDbRoutes); +app.use("/api/video", videoRoutes); // Start Server const PORT = process.env.PORT || 5000; diff --git a/models/Movie.js b/models/Movie.js index a854260..5083547 100644 --- a/models/Movie.js +++ b/models/Movie.js @@ -17,6 +17,9 @@ const movieSchema = new mongoose.Schema({ 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 + // Server-hosted file (relative path under VIDEO_FILES_DIR, default public/video) + // If set, the backend can stream it via /api/video/:movieId (Range/seek supported). + video_file: { type: String, required: false }, watchedBy: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }] }, { timestamps: true }); diff --git a/public/control.html b/public/control.html index 7b26bab..f18a600 100644 --- a/public/control.html +++ b/public/control.html @@ -354,6 +354,14 @@
+
If you use a server file (below) and leave this empty, it will auto-use /api/video/<movieId>.
+
+
+ + +
+ Relative path under VIDEO_FILES_DIR (default: public/video). Supports seeking (Range). +
@@ -700,6 +708,12 @@
+
If you use a server file (below) and leave this empty, it will auto-use /api/video/<movieId>.
+
+
+ + +
Relative path under VIDEO_FILES_DIR (default: public/video).
diff --git a/public/js/app_control.js b/public/js/app_control.js index 0dd0cab..947f26e 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -104,6 +104,7 @@ window.onload = async function () { const editVodLinkEl = document.getElementById('edit_vod_link'); const editPlayerModeEl = document.getElementById('edit_player_mode'); const editVideoSrcEl = document.getElementById('edit_video_src'); + const editVideoFileEl = document.getElementById('edit_video_file'); const editEmbedSrcEl = document.getElementById('edit_embed_src'); const editRefreshOmdbEl = document.getElementById('edit_refresh_omdb'); const editTitleEl = document.getElementById('edit_title'); @@ -1817,6 +1818,7 @@ window.onload = async function () { 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 video_file = document.getElementById('video_file')?.value?.trim() || ''; const embed_src = document.getElementById('embed_src')?.value?.trim() || ''; if (!year) { @@ -1829,8 +1831,8 @@ window.onload = async function () { return; } - if (!vod_link && !video_src && !embed_src) { - showResponse('warning', 'Ajoute au moins une source (VOD / video_src / embed_src).'); + if (!vod_link && !video_src && !video_file && !embed_src) { + showResponse('warning', 'Ajoute au moins une source (VOD / video_src / video_file / embed_src).'); return; } @@ -1841,7 +1843,7 @@ window.onload = async function () { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, - body: JSON.stringify({ year, imdb_id, category, vod_link, player_mode, video_src, embed_src }) + body: JSON.stringify({ year, imdb_id, category, vod_link, player_mode, video_src, video_file, embed_src }) }); if (res.ok) { @@ -2025,6 +2027,7 @@ window.onload = async function () { editVodLinkEl.value = movie.vod_link || ''; if (editPlayerModeEl) editPlayerModeEl.value = movie.player_mode || 'auto'; if (editVideoSrcEl) editVideoSrcEl.value = movie.video_src || ''; + if (editVideoFileEl) editVideoFileEl.value = movie.video_file || ''; if (editEmbedSrcEl) editEmbedSrcEl.value = movie.embed_src || ''; editRefreshOmdbEl.checked = false; @@ -2051,6 +2054,7 @@ window.onload = async function () { const vod_link = (editVodLinkEl.value || '').trim(); const player_mode = (editPlayerModeEl?.value || 'auto').trim(); const video_src = (editVideoSrcEl?.value || '').trim(); + const video_file = (editVideoFileEl?.value || '').trim(); const embed_src = (editEmbedSrcEl?.value || '').trim(); const refreshOmdb = !!editRefreshOmdbEl.checked; @@ -2078,8 +2082,8 @@ window.onload = async function () { return; } - if (!vod_link && !video_src && !embed_src) { - showResponse('warning', 'Ajoute au moins une source (VOD / video_src / embed_src).'); + if (!vod_link && !video_src && !video_file && !embed_src) { + showResponse('warning', 'Ajoute au moins une source (VOD / video_src / video_file / embed_src).'); return; } @@ -2090,6 +2094,7 @@ window.onload = async function () { vod_link, player_mode, video_src, + video_file, embed_src, refreshOmdb }; @@ -2163,10 +2168,12 @@ window.onload = async function () { function buildPlayerBadges(m) { const badges = []; const hasVideo = !!(m && m.video_src); + const hasFile = !!(m && m.video_file); const hasEmbed = !!(m && m.embed_src); const hasLegacy = !!(m && m.vod_link); if (hasVideo) badges.push({ text: 'Video', cls: 'bg-primary' }); + if (hasFile) badges.push({ text: 'Server file', cls: 'bg-dark' }); if (hasEmbed) badges.push({ text: 'Embed', cls: 'bg-info text-dark' }); if (hasLegacy) badges.push({ text: 'Legacy', cls: 'bg-secondary' }); diff --git a/public/js/app_player.js b/public/js/app_player.js index 1f28919..d89d1dc 100644 --- a/public/js/app_player.js +++ b/public/js/app_player.js @@ -428,13 +428,16 @@ function resolveMovieSource(movie) { const legacy = cleanUrlValue(movie?.vod_link); const video = cleanUrlValue(movie?.video_src); const embed = cleanUrlValue(movie?.embed_src); + const serverFile = cleanUrlValue(movie?.video_file); // Legacy should never embed in the player (open in new tab only). // So we only pick legacy if there is no other option, and mark it. let picked = null; - if (mode === 'video') picked = video || null; + const serverVideoUrl = serverFile ? `/api/video/${encodeURIComponent(String(movie?._id || ''))}` : null; + + if (mode === 'video') picked = video || serverVideoUrl || null; else if (mode === 'embed') picked = embed || null; - else picked = video || embed || null; // auto + else picked = video || serverVideoUrl || embed || null; // auto if (picked) return { src: picked, isLegacy: false }; if (legacy) return { src: legacy, isLegacy: true }; @@ -500,6 +503,16 @@ window.onload = async function () { } setHeader(movie); + + // Allow