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 @@
@@ -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