we subbing now bois #73
@@ -21,6 +21,9 @@ const movieSchema = new mongoose.Schema({
|
|||||||
// Server-hosted file (relative path under VIDEO_FILES_DIR, default public/video)
|
// 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).
|
// If set, the backend can stream it via /api/video/:movieId (Range/seek supported).
|
||||||
video_file: { type: String, required: false },
|
video_file: { type: String, required: false },
|
||||||
|
// Optional low-quality server file (relative path under VIDEO_FILES_DIR, default public/video)
|
||||||
|
// Served when the player requests ?quality=low on /api/video/:movieId.
|
||||||
|
video_file_low: { type: String, required: false },
|
||||||
watchedBy: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]
|
watchedBy: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]
|
||||||
}, { timestamps: true });
|
}, { timestamps: true });
|
||||||
|
|
||||||
|
|||||||
@@ -690,6 +690,11 @@
|
|||||||
<input type="text" class="form-control font-monospace" id="video_file" placeholder="my_movie.mp4 or 2026/my_movie.mp4">
|
<input type="text" class="form-control font-monospace" id="video_file" placeholder="my_movie.mp4 or 2026/my_movie.mp4">
|
||||||
<div class="form-text">Chemin relatif sous <code>VIDEO_FILES_DIR</code> (défaut: <code>public/video</code>).</div>
|
<div class="form-text">Chemin relatif sous <code>VIDEO_FILES_DIR</code> (défaut: <code>public/video</code>).</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="video_file_low" class="form-label">Server video file (basse qualité, optionnel)</label>
|
||||||
|
<input type="text" class="form-control font-monospace" id="video_file_low" placeholder="LOW/my_movie.LOW.mp4">
|
||||||
|
<div class="form-text">Chemin relatif sous <code>VIDEO_FILES_DIR</code>. Ex: <code>LOW/123.LOW.mp4</code>.</div>
|
||||||
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label for="embed_src" class="form-label">Embed URL ou iframe</label>
|
<label for="embed_src" class="form-label">Embed URL ou iframe</label>
|
||||||
<textarea class="form-control font-monospace" id="embed_src" rows="3" placeholder="https://... OR <iframe src='...'></iframe>"></textarea>
|
<textarea class="form-control font-monospace" id="embed_src" rows="3" placeholder="https://... OR <iframe src='...'></iframe>"></textarea>
|
||||||
@@ -723,6 +728,7 @@
|
|||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
<li><strong>Auto</strong>: utilise <code>video_src</code> → <code>embed_src</code> → <code>vod_link</code>.</li>
|
<li><strong>Auto</strong>: utilise <code>video_src</code> → <code>embed_src</code> → <code>vod_link</code>.</li>
|
||||||
<li><strong>Server file</strong>: mets <code>video_file</code> (chemin relatif sous <code>public/video</code>).</li>
|
<li><strong>Server file</strong>: mets <code>video_file</code> (chemin relatif sous <code>public/video</code>).</li>
|
||||||
|
<li><strong>Low quality</strong>: mets <code>video_file_low</code> (ex: <code>LOW/mon_film.LOW.mp4</code>).</li>
|
||||||
<li><strong>Édition</strong>: le bouton “Éditer” ouvre une modale avec des onglets Info / Player.</li>
|
<li><strong>Édition</strong>: le bouton “Éditer” ouvre une modale avec des onglets Info / Player.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -1385,6 +1391,11 @@
|
|||||||
<input type="text" class="form-control font-monospace" id="edit_video_file" placeholder="my_movie.mp4 or 2026/my_movie.mp4">
|
<input type="text" class="form-control font-monospace" id="edit_video_file" placeholder="my_movie.mp4 or 2026/my_movie.mp4">
|
||||||
<div class="form-text">Relative path under <code>VIDEO_FILES_DIR</code> (default: <code>public/video</code>).</div>
|
<div class="form-text">Relative path under <code>VIDEO_FILES_DIR</code> (default: <code>public/video</code>).</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="edit_video_file_low" class="form-label">Server video file (low quality, optional)</label>
|
||||||
|
<input type="text" class="form-control font-monospace" id="edit_video_file_low" placeholder="LOW/my_movie.LOW.mp4">
|
||||||
|
<div class="form-text">Relative path under <code>VIDEO_FILES_DIR</code>. Example: <code>LOW/123.LOW.mp4</code>.</div>
|
||||||
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label for="edit_embed_src" class="form-label">Embed URL or iframe code</label>
|
<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>
|
<textarea class="form-control font-monospace" id="edit_embed_src" rows="3" placeholder="https://... OR <iframe ...>"></textarea>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ window.onload = async function () {
|
|||||||
const editPlayerModeEl = document.getElementById('edit_player_mode');
|
const editPlayerModeEl = document.getElementById('edit_player_mode');
|
||||||
const editVideoSrcEl = document.getElementById('edit_video_src');
|
const editVideoSrcEl = document.getElementById('edit_video_src');
|
||||||
const editVideoFileEl = document.getElementById('edit_video_file');
|
const editVideoFileEl = document.getElementById('edit_video_file');
|
||||||
|
const editVideoFileLowEl = document.getElementById('edit_video_file_low');
|
||||||
const editEmbedSrcEl = document.getElementById('edit_embed_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');
|
||||||
@@ -1927,6 +1928,7 @@ window.onload = async function () {
|
|||||||
const player_mode = (document.getElementById('player_mode')?.value || 'auto').trim();
|
const player_mode = (document.getElementById('player_mode')?.value || 'auto').trim();
|
||||||
const video_src = document.getElementById('video_src')?.value?.trim() || '';
|
const video_src = document.getElementById('video_src')?.value?.trim() || '';
|
||||||
const video_file = document.getElementById('video_file')?.value?.trim() || '';
|
const video_file = document.getElementById('video_file')?.value?.trim() || '';
|
||||||
|
const video_file_low = document.getElementById('video_file_low')?.value?.trim() || '';
|
||||||
const embed_src = document.getElementById('embed_src')?.value?.trim() || '';
|
const embed_src = document.getElementById('embed_src')?.value?.trim() || '';
|
||||||
|
|
||||||
if (!year) {
|
if (!year) {
|
||||||
@@ -1946,7 +1948,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, player_mode, video_src, video_file, embed_src, cam })
|
body: JSON.stringify({ year, imdb_id, category, vod_link, player_mode, video_src, video_file, video_file_low, embed_src, cam })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -2194,6 +2196,7 @@ window.onload = async function () {
|
|||||||
if (editPlayerModeEl) editPlayerModeEl.value = movie.player_mode || 'auto';
|
if (editPlayerModeEl) editPlayerModeEl.value = movie.player_mode || 'auto';
|
||||||
if (editVideoSrcEl) editVideoSrcEl.value = movie.video_src || '';
|
if (editVideoSrcEl) editVideoSrcEl.value = movie.video_src || '';
|
||||||
if (editVideoFileEl) editVideoFileEl.value = movie.video_file || '';
|
if (editVideoFileEl) editVideoFileEl.value = movie.video_file || '';
|
||||||
|
if (editVideoFileLowEl) editVideoFileLowEl.value = movie.video_file_low || '';
|
||||||
if (editEmbedSrcEl) editEmbedSrcEl.value = movie.embed_src || '';
|
if (editEmbedSrcEl) editEmbedSrcEl.value = movie.embed_src || '';
|
||||||
editRefreshOmdbEl.checked = false;
|
editRefreshOmdbEl.checked = false;
|
||||||
|
|
||||||
@@ -2222,6 +2225,7 @@ window.onload = async function () {
|
|||||||
const player_mode = (editPlayerModeEl?.value || 'auto').trim();
|
const player_mode = (editPlayerModeEl?.value || 'auto').trim();
|
||||||
const video_src = (editVideoSrcEl?.value || '').trim();
|
const video_src = (editVideoSrcEl?.value || '').trim();
|
||||||
const video_file = (editVideoFileEl?.value || '').trim();
|
const video_file = (editVideoFileEl?.value || '').trim();
|
||||||
|
const video_file_low = (editVideoFileLowEl?.value || '').trim();
|
||||||
const embed_src = (editEmbedSrcEl?.value || '').trim();
|
const embed_src = (editEmbedSrcEl?.value || '').trim();
|
||||||
const refreshOmdb = !!editRefreshOmdbEl.checked;
|
const refreshOmdb = !!editRefreshOmdbEl.checked;
|
||||||
const cam = !!editCamFlagEl?.checked;
|
const cam = !!editCamFlagEl?.checked;
|
||||||
@@ -2258,6 +2262,7 @@ window.onload = async function () {
|
|||||||
player_mode,
|
player_mode,
|
||||||
video_src,
|
video_src,
|
||||||
video_file,
|
video_file,
|
||||||
|
video_file_low,
|
||||||
embed_src,
|
embed_src,
|
||||||
refreshOmdb,
|
refreshOmdb,
|
||||||
cam
|
cam
|
||||||
@@ -2333,11 +2338,13 @@ window.onload = async function () {
|
|||||||
const badges = [];
|
const badges = [];
|
||||||
const hasVideo = !!(m && m.video_src);
|
const hasVideo = !!(m && m.video_src);
|
||||||
const hasFile = !!(m && m.video_file);
|
const hasFile = !!(m && m.video_file);
|
||||||
|
const hasLowFile = !!(m && m.video_file_low);
|
||||||
const hasEmbed = !!(m && m.embed_src);
|
const hasEmbed = !!(m && m.embed_src);
|
||||||
const hasLegacy = !!(m && m.vod_link);
|
const hasLegacy = !!(m && m.vod_link);
|
||||||
|
|
||||||
if (hasVideo) badges.push({ text: 'Video', cls: 'bg-primary' });
|
if (hasVideo) badges.push({ text: 'Video', cls: 'bg-primary' });
|
||||||
if (hasFile) badges.push({ text: 'Server file', cls: 'bg-dark' });
|
if (hasFile) badges.push({ text: 'Server file', cls: 'bg-dark' });
|
||||||
|
if (hasLowFile) badges.push({ text: 'Low file', cls: 'bg-secondary' });
|
||||||
if (hasEmbed) badges.push({ text: 'Embed', cls: 'bg-info text-dark' });
|
if (hasEmbed) badges.push({ text: 'Embed', cls: 'bg-info text-dark' });
|
||||||
if (hasLegacy) badges.push({ text: 'Legacy', cls: 'bg-secondary' });
|
if (hasLegacy) badges.push({ text: 'Legacy', cls: 'bg-secondary' });
|
||||||
|
|
||||||
|
|||||||
+121
-1
@@ -753,6 +753,21 @@ async function probeApiVideoReadable(rawUrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function probeApiVideoExists(rawUrl) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(rawUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Range: 'bytes=0-0' },
|
||||||
|
credentials: 'include',
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
if (res.status === 401 || res.status === 403 || res.status === 404) return false;
|
||||||
|
return res.ok;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function ensurePlayableApiVideoUrl(rawUrl, token) {
|
async function ensurePlayableApiVideoUrl(rawUrl, token) {
|
||||||
if (!isApiVideoUrl(rawUrl) || !token) return rawUrl;
|
if (!isApiVideoUrl(rawUrl) || !token) return rawUrl;
|
||||||
// IMPORTANT: Prefer query-token auth for media elements.
|
// IMPORTANT: Prefer query-token auth for media elements.
|
||||||
@@ -883,6 +898,109 @@ function resolveMovieSource(movie) {
|
|||||||
return { src: null, isLegacy: false };
|
return { src: null, isLegacy: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function qualityStorageKey(movieId) {
|
||||||
|
return `player_quality:${String(movieId || '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoredQuality(movieId) {
|
||||||
|
const raw = localStorage.getItem(qualityStorageKey(movieId));
|
||||||
|
return raw === 'low' ? 'low' : 'high';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStoredQuality(movieId, quality) {
|
||||||
|
const value = quality === 'low' ? 'low' : 'high';
|
||||||
|
try { localStorage.setItem(qualityStorageKey(movieId), value); } catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addQualityParam(rawUrl, quality) {
|
||||||
|
if (!rawUrl) return rawUrl;
|
||||||
|
try {
|
||||||
|
const isRelative = String(rawUrl).trim().startsWith('/');
|
||||||
|
const u = new URL(String(rawUrl), window.location.origin);
|
||||||
|
if (quality) u.searchParams.set('quality', quality);
|
||||||
|
else u.searchParams.delete('quality');
|
||||||
|
return isRelative ? `${u.pathname}${u.search}${u.hash}` : u.toString();
|
||||||
|
} catch (_) {
|
||||||
|
return rawUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setQualityToggleUi({ visible, isLow }) {
|
||||||
|
const wrap = document.getElementById('quality-toggle');
|
||||||
|
const toggle = document.getElementById('quality-switch');
|
||||||
|
const label = document.getElementById('quality-switch-label');
|
||||||
|
if (!wrap || !toggle || !label) return;
|
||||||
|
if (visible) {
|
||||||
|
wrap.classList.remove('d-none');
|
||||||
|
toggle.checked = !!isLow;
|
||||||
|
label.textContent = isLow ? 'Basse' : 'Haute';
|
||||||
|
} else {
|
||||||
|
wrap.classList.add('d-none');
|
||||||
|
toggle.checked = false;
|
||||||
|
label.textContent = 'Haute';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchVideoSource({ src, token, preserveTime }) {
|
||||||
|
const videoEl = document.getElementById('video');
|
||||||
|
if (!videoEl || !src) return;
|
||||||
|
|
||||||
|
const shouldPreserve = !!preserveTime && Number.isFinite(videoEl.currentTime) && videoEl.currentTime > 0;
|
||||||
|
const previousTime = shouldPreserve ? Number(videoEl.currentTime) : 0;
|
||||||
|
const wasPlaying = !videoEl.paused && !videoEl.ended;
|
||||||
|
|
||||||
|
const onLoaded = () => {
|
||||||
|
if (previousTime > 1) {
|
||||||
|
try { videoEl.currentTime = previousTime; } catch (_) {}
|
||||||
|
}
|
||||||
|
if (wasPlaying) {
|
||||||
|
const playPromise = videoEl.play();
|
||||||
|
if (playPromise && typeof playPromise.catch === 'function') playPromise.catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
videoEl.addEventListener('loadedmetadata', onLoaded, { once: true });
|
||||||
|
|
||||||
|
await playVodLink(src, { token });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initQualityToggle({ movieId, token, fallbackSrc }) {
|
||||||
|
const wrap = document.getElementById('quality-toggle');
|
||||||
|
const toggle = document.getElementById('quality-switch');
|
||||||
|
if (!wrap || !toggle) return { src: fallbackSrc };
|
||||||
|
|
||||||
|
if (!isApiVideoUrl(fallbackSrc)) {
|
||||||
|
setQualityToggleUi({ visible: false, isLow: false });
|
||||||
|
return { src: fallbackSrc };
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowSrcRaw = addQualityParam(fallbackSrc, 'low');
|
||||||
|
const playableLow = await ensurePlayableApiVideoUrl(lowSrcRaw, token);
|
||||||
|
const hasLow = await probeApiVideoExists(playableLow);
|
||||||
|
if (!hasLow) {
|
||||||
|
setQualityToggleUi({ visible: false, isLow: false });
|
||||||
|
return { src: fallbackSrc };
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferred = getStoredQuality(movieId);
|
||||||
|
const shouldUseLow = preferred === 'low';
|
||||||
|
const pickedSrc = shouldUseLow ? lowSrcRaw : fallbackSrc;
|
||||||
|
|
||||||
|
setQualityToggleUi({ visible: true, isLow: shouldUseLow });
|
||||||
|
|
||||||
|
toggle.addEventListener('change', async () => {
|
||||||
|
const nextIsLow = !!toggle.checked;
|
||||||
|
setStoredQuality(movieId, nextIsLow ? 'low' : 'high');
|
||||||
|
setQualityToggleUi({ visible: true, isLow: nextIsLow });
|
||||||
|
await switchVideoSource({
|
||||||
|
src: nextIsLow ? lowSrcRaw : fallbackSrc,
|
||||||
|
token,
|
||||||
|
preserveTime: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { src: pickedSrc };
|
||||||
|
}
|
||||||
|
|
||||||
window.onload = async function () {
|
window.onload = async function () {
|
||||||
const pageLoader = createPageLoader({ title: 'Chargement du lecteur' });
|
const pageLoader = createPageLoader({ title: 'Chargement du lecteur' });
|
||||||
|
|
||||||
@@ -987,7 +1105,9 @@ window.onload = async function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pageLoader.setProgress(70);
|
pageLoader.setProgress(70);
|
||||||
await playVodLink(resolved.src, { token });
|
const qualityChoice = await initQualityToggle({ movieId: id, token, fallbackSrc: resolved.src });
|
||||||
|
const selectedSrc = qualityChoice?.src || resolved.src;
|
||||||
|
await playVodLink(selectedSrc, { token });
|
||||||
pageLoader.setProgress(82);
|
pageLoader.setProgress(82);
|
||||||
|
|
||||||
// Only the <video> player supports reliable progress tracking (mp4/hls).
|
// Only the <video> player supports reliable progress tracking (mp4/hls).
|
||||||
|
|||||||
@@ -152,6 +152,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column gap-2 align-items-start align-items-lg-end">
|
||||||
|
<div id="quality-toggle" class="d-none d-flex align-items-center gap-2">
|
||||||
|
<span class="text-muted small">Qualité</span>
|
||||||
|
<div class="form-check form-switch m-0">
|
||||||
|
<input class="form-check-input" type="checkbox" id="quality-switch">
|
||||||
|
<label class="form-check-label small" for="quality-switch" id="quality-switch-label">Haute</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Admin-only debug/status (Source + Progress) -->
|
<!-- Admin-only debug/status (Source + Progress) -->
|
||||||
<div id="player-admin-status" class="d-none d-flex flex-column gap-2">
|
<div id="player-admin-status" class="d-none d-flex flex-column gap-2">
|
||||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||||
@@ -170,6 +179,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Player surface -->
|
<!-- Player surface -->
|
||||||
<div class="card shadow-sm mt-3">
|
<div class="card shadow-sm mt-3">
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ def stream_video(
|
|||||||
try:
|
try:
|
||||||
client = _mongo_client_cached()
|
client = _mongo_client_cached()
|
||||||
db = client[db_name]
|
db = client[db_name]
|
||||||
movie = db["movies"].find_one({"_id": oid}, {"video_file": 1})
|
movie = db["movies"].find_one({"_id": oid}, {"video_file": 1, "video_file_low": 1})
|
||||||
except PyMongoError as e:
|
except PyMongoError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=502,
|
status_code=502,
|
||||||
@@ -285,15 +285,23 @@ def stream_video(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
quality = (request.query_params.get("quality") or "").strip().lower()
|
||||||
|
wants_low = quality == "low"
|
||||||
|
|
||||||
video_file = (movie or {}).get("video_file") or ""
|
video_file = (movie or {}).get("video_file") or ""
|
||||||
if not isinstance(video_file, str) or not video_file.strip():
|
video_file_low = (movie or {}).get("video_file_low") or ""
|
||||||
|
chosen = video_file_low if wants_low else video_file
|
||||||
|
|
||||||
|
if not isinstance(chosen, str) or not chosen.strip():
|
||||||
|
if wants_low:
|
||||||
|
raise HTTPException(status_code=404, detail="No low-quality server video file configured for this movie")
|
||||||
raise HTTPException(status_code=404, detail="No server video file configured for this movie")
|
raise HTTPException(status_code=404, detail="No server video file configured for this movie")
|
||||||
video_file = video_file.strip()
|
chosen = chosen.strip()
|
||||||
if not _is_safe_relative_file(video_file):
|
if not _is_safe_relative_file(chosen):
|
||||||
raise HTTPException(status_code=400, detail="Invalid video_file path")
|
raise HTTPException(status_code=400, detail="Invalid video_file path")
|
||||||
|
|
||||||
video_dir = _video_files_dir()
|
video_dir = _video_files_dir()
|
||||||
full_path = (video_dir / video_file).resolve()
|
full_path = (video_dir / chosen).resolve()
|
||||||
if not str(full_path).startswith(str(video_dir) + os.sep) and full_path != video_dir:
|
if not str(full_path).startswith(str(video_dir) + os.sep) and full_path != video_dir:
|
||||||
raise HTTPException(status_code=400, detail="Invalid video_file path")
|
raise HTTPException(status_code=400, detail="Invalid video_file path")
|
||||||
if not full_path.exists() or not full_path.is_file():
|
if not full_path.exists() or not full_path.is_file():
|
||||||
|
|||||||
+9
-2
@@ -148,7 +148,7 @@ router.get("/", async (req, res) => {
|
|||||||
|
|
||||||
const projection = isChecklist
|
const projection = isChecklist
|
||||||
? "imdb_id title"
|
? "imdb_id title"
|
||||||
: "imdb_id title description rating poster cam year category vod_link player_mode video_src embed_src video_file updatedAt createdAt";
|
: "imdb_id title description rating poster cam year category vod_link player_mode video_src embed_src video_file video_file_low updatedAt createdAt";
|
||||||
|
|
||||||
const cacheKey = `movies:list:${year || "all"}:${isChecklist ? "checklist" : "films"}`;
|
const cacheKey = `movies:list:${year || "all"}:${isChecklist ? "checklist" : "films"}`;
|
||||||
const cached = cacheGet(cacheKey);
|
const cached = cacheGet(cacheKey);
|
||||||
@@ -333,7 +333,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, player_mode, video_src, embed_src, video_file, cam } = req.body;
|
const { imdb_id, category, vod_link, year, player_mode, video_src, embed_src, video_file, video_file_low, cam } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the token from the Authorization header
|
// Get the token from the Authorization header
|
||||||
@@ -360,6 +360,7 @@ router.post("/add", async (req, res) => {
|
|||||||
const normalizedVideo = normalizeOptionalString(video_src, 4096);
|
const normalizedVideo = normalizeOptionalString(video_src, 4096);
|
||||||
const normalizedEmbed = normalizeOptionalString(embed_src, 20000);
|
const normalizedEmbed = normalizeOptionalString(embed_src, 20000);
|
||||||
const normalizedFile = normalizeVideoFile(video_file);
|
const normalizedFile = normalizeVideoFile(video_file);
|
||||||
|
const normalizedLowFile = normalizeVideoFile(video_file_low);
|
||||||
const normalizedMode = normalizePlayerMode(player_mode);
|
const normalizedMode = normalizePlayerMode(player_mode);
|
||||||
if (normalizedMode === null) {
|
if (normalizedMode === null) {
|
||||||
return res.status(400).json({ message: "player_mode invalide (auto|video|embed)" });
|
return res.status(400).json({ message: "player_mode invalide (auto|video|embed)" });
|
||||||
@@ -398,6 +399,7 @@ router.post("/add", async (req, res) => {
|
|||||||
video_src: normalizedVideo || undefined,
|
video_src: normalizedVideo || undefined,
|
||||||
embed_src: normalizedEmbed || undefined,
|
embed_src: normalizedEmbed || undefined,
|
||||||
video_file: normalizedFile || undefined,
|
video_file: normalizedFile || undefined,
|
||||||
|
video_file_low: normalizedLowFile || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await movie.save();
|
await movie.save();
|
||||||
@@ -491,6 +493,7 @@ router.put("/:id", async (req, res) => {
|
|||||||
video_src,
|
video_src,
|
||||||
embed_src,
|
embed_src,
|
||||||
video_file,
|
video_file,
|
||||||
|
video_file_low,
|
||||||
refreshOmdb,
|
refreshOmdb,
|
||||||
cam,
|
cam,
|
||||||
} = req.body || {};
|
} = req.body || {};
|
||||||
@@ -550,6 +553,10 @@ router.put("/:id", async (req, res) => {
|
|||||||
movie.video_src = `/api/video/${movie._id}`;
|
movie.video_src = `/api/video/${movie._id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (video_file_low !== undefined) {
|
||||||
|
const v = normalizeVideoFile(video_file_low);
|
||||||
|
movie.video_file_low = v ? v : null;
|
||||||
|
}
|
||||||
|
|
||||||
if (year !== undefined && year !== null && year !== "") {
|
if (year !== undefined && year !== null && year !== "") {
|
||||||
movie.year = parseOscarYear(year);
|
movie.year = parseOscarYear(year);
|
||||||
|
|||||||
+11
-5
@@ -119,17 +119,23 @@ async function streamMovieFile(req, res) {
|
|||||||
return res.status(400).json({ message: "Invalid movie id" });
|
return res.status(400).json({ message: "Invalid movie id" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const movie = await Movie.findById(id).select("video_file").lean();
|
const qualityRaw = typeof req.query?.quality === "string" ? req.query.quality.trim().toLowerCase() : "";
|
||||||
|
const wantsLow = qualityRaw === "low";
|
||||||
|
|
||||||
|
const movie = await Movie.findById(id).select("video_file video_file_low").lean();
|
||||||
const videoFile = typeof movie?.video_file === "string" ? movie.video_file.trim() : "";
|
const videoFile = typeof movie?.video_file === "string" ? movie.video_file.trim() : "";
|
||||||
if (!videoFile) {
|
const videoFileLow = typeof movie?.video_file_low === "string" ? movie.video_file_low.trim() : "";
|
||||||
return res.status(404).json({ message: "No server video file configured for this movie" });
|
|
||||||
|
const chosen = wantsLow ? videoFileLow : videoFile;
|
||||||
|
if (!chosen) {
|
||||||
|
return res.status(404).json({ message: wantsLow ? "No low-quality server video file configured for this movie" : "No server video file configured for this movie" });
|
||||||
}
|
}
|
||||||
if (!isSafeRelativeFile(videoFile)) {
|
if (!isSafeRelativeFile(chosen)) {
|
||||||
return res.status(400).json({ message: "Invalid video_file path" });
|
return res.status(400).json({ message: "Invalid video_file path" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseDir = getVideoBaseDir();
|
const baseDir = getVideoBaseDir();
|
||||||
const fullPath = path.resolve(baseDir, videoFile);
|
const fullPath = path.resolve(baseDir, chosen);
|
||||||
if (!fullPath.startsWith(baseDir + path.sep) && fullPath !== baseDir) {
|
if (!fullPath.startsWith(baseDir + path.sep) && fullPath !== baseDir) {
|
||||||
return res.status(400).json({ message: "Invalid video_file path" });
|
return res.status(400).json({ message: "Invalid video_file path" });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user