we subbing now bois #73

Merged
quoije merged 5 commits from 2026 into prod 2026-01-25 02:02:20 -05:00
6 changed files with 390 additions and 45 deletions
Showing only changes of commit 406a4f0bb3 - Show all commits
+8 -1
View File
@@ -24,7 +24,14 @@ const movieSchema = new mongoose.Schema({
// 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 },
// Optional subtitles (server-hosted .vtt, streamed via /api/subtitles/:movieId)
// Optional subtitles (server-hosted .vtt, streamed via /api/subtitles/:movieId/:subtitleId)
subtitles: [{
file: { type: String, required: true },
lang: { type: String, required: false },
label: { type: String, required: false },
default: { type: Boolean, required: false, default: false },
}],
// Legacy single-subtitle fields (kept for backward compatibility)
subtitle_file: { type: String, required: false },
subtitle_lang: { type: String, required: false },
subtitle_label: { type: String, required: false },
+10 -1
View File
@@ -1418,7 +1418,16 @@
<div class="col-12">
<label for="edit_subtitle_file" class="form-label">Sous-titres (SRT, optionnel)</label>
<input type="file" class="form-control" id="edit_subtitle_file" accept=".srt,.vtt,text/plain,text/vtt">
<div class="form-text">Fichier actuel: <span id="edit_subtitle_current">—</span></div>
<div class="form-text">Sous-titres existants:</div>
<div id="edit_subtitle_list" class="d-flex flex-column gap-2 mt-1"></div>
<div class="form-text">Ajoute un fichier par langue; répète l’upload pour plusieurs sous-titres.</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="edit_subtitle_remove">
<label class="form-check-label" for="edit_subtitle_remove">Supprimer tous les sous-titres</label>
</div>
<div class="form-text">Efface les fichiers VTT/SRT et réinitialise les champs de langue/label.</div>
</div>
<div class="col-12 col-lg-4">
<label for="edit_subtitle_lang" class="form-label">Langue (ex: fr, en)</label>
+99 -11
View File
@@ -114,10 +114,11 @@ window.onload = async function () {
const editVideoFileEl = document.getElementById('edit_video_file');
const editVideoFileLowEl = document.getElementById('edit_video_file_low');
const editSubtitleFileEl = document.getElementById('edit_subtitle_file');
const editSubtitleRemoveEl = document.getElementById('edit_subtitle_remove');
const editSubtitleLangEl = document.getElementById('edit_subtitle_lang');
const editSubtitleLabelEl = document.getElementById('edit_subtitle_label');
const editSubtitleDefaultEl = document.getElementById('edit_subtitle_default');
const editSubtitleCurrentEl = document.getElementById('edit_subtitle_current');
const editSubtitleListEl = document.getElementById('edit_subtitle_list');
const editEmbedSrcEl = document.getElementById('edit_embed_src');
const editRefreshOmdbEl = document.getElementById('edit_refresh_omdb');
const editTitleEl = document.getElementById('edit_title');
@@ -171,6 +172,20 @@ window.onload = async function () {
return s.slice(0, maxLen);
}
function getMovieSubtitles(movie) {
const subs = Array.isArray(movie?.subtitles) ? movie.subtitles.filter(Boolean) : [];
if (subs.length > 0) return subs;
const legacyFile = typeof movie?.subtitle_file === 'string' ? movie.subtitle_file.trim() : '';
if (!legacyFile) return [];
return [{
_id: 'legacy',
file: legacyFile,
lang: movie?.subtitle_lang || '',
label: movie?.subtitle_label || '',
default: !!movie?.subtitle_default,
}];
}
async function uploadMovieSubtitle({ movieId, file, lang, label, isDefault }) {
if (!movieId || !file) return null;
const formData = new FormData();
@@ -195,6 +210,22 @@ window.onload = async function () {
return data;
}
async function removeMovieSubtitle({ movieId, subtitleId }) {
if (!movieId || !subtitleId) return null;
const res = await fetch(`/api/movies/${encodeURIComponent(movieId)}/subtitles/${encodeURIComponent(subtitleId)}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
},
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const msg = data.message || data.error || `Erreur (${res.status})`;
throw new Error(msg);
}
return data;
}
function normalizePlayerUi(value) {
const v = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
const showSource = v.showSource === undefined ? DEFAULT_PLAYER_UI.showSource : !!v.showSource;
@@ -2255,6 +2286,59 @@ window.onload = async function () {
selectAllBox.checked = allBoxes.length > 0 && checked === allBoxes.length;
}
function renderSubtitleList(movie) {
if (!editSubtitleListEl) return;
const subs = getMovieSubtitles(movie);
if (!subs.length) {
editSubtitleListEl.innerHTML = '<span class="text-muted">—</span>';
return;
}
editSubtitleListEl.innerHTML = subs.map((s) => {
const id = escapeHtml(String(s?._id || ''));
const lang = escapeHtml(String(s?.lang || ''));
const label = escapeHtml(String(s?.label || ''));
const file = escapeHtml(String(s?.file || ''));
const title = label || lang || file || 'Sous-titres';
const meta = [lang, label].filter(Boolean).join(' · ');
const isDefault = s?.default ? '<span class="badge bg-success ms-2">Par défaut</span>' : '';
return `
<div class="d-flex align-items-center justify-content-between gap-2">
<div class="small">
<span class="fw-semibold">${title}</span>
${meta ? `<span class="text-muted ms-2">${meta}</span>` : ''}
${isDefault}
</div>
<button type="button" class="btn btn-sm btn-outline-danger" data-remove-subtitle-id="${id}">
Supprimer
</button>
</div>
`;
}).join('');
editSubtitleListEl.querySelectorAll('[data-remove-subtitle-id]').forEach((btn) => {
btn.addEventListener('click', async () => {
const subtitleId = btn.getAttribute('data-remove-subtitle-id');
const movieId = String(editMovieIdEl?.value || '');
if (!movieId || !subtitleId) return;
if (!confirm('Supprimer ce sous-titre ?')) return;
try {
btn.disabled = true;
const updated = await removeMovieSubtitle({ movieId, subtitleId });
moviesById.set(updated._id, updated);
renderSubtitleList(updated);
showResponse('success', 'Sous-titre supprimé.');
await loadMoviesForManagement();
} catch (err) {
showResponse('danger', err.message || 'Erreur réseau');
} finally {
btn.disabled = false;
}
});
});
}
function openEditModal(movieId) {
if (!editMovieModal) return;
const movie = moviesById.get(movieId);
@@ -2273,10 +2357,8 @@ window.onload = async function () {
if (editSubtitleLangEl) editSubtitleLangEl.value = movie.subtitle_lang || '';
if (editSubtitleLabelEl) editSubtitleLabelEl.value = movie.subtitle_label || '';
if (editSubtitleDefaultEl) editSubtitleDefaultEl.checked = !!movie.subtitle_default;
if (editSubtitleCurrentEl) {
const current = movie.subtitle_file ? String(movie.subtitle_file) : '';
editSubtitleCurrentEl.textContent = current || '—';
}
renderSubtitleList(movie);
if (editSubtitleRemoveEl) editSubtitleRemoveEl.checked = false;
if (editSubtitleFileEl) editSubtitleFileEl.value = '';
if (editEmbedSrcEl) editEmbedSrcEl.value = movie.embed_src || '';
editRefreshOmdbEl.checked = false;
@@ -2311,7 +2393,8 @@ window.onload = async function () {
const subtitle_lang = normalizeSubtitleText(editSubtitleLangEl?.value, 24);
const subtitle_label = normalizeSubtitleText(editSubtitleLabelEl?.value, 80);
const subtitle_default = !!editSubtitleDefaultEl?.checked;
const subtitleFile = editSubtitleFileEl?.files?.[0] || null;
const subtitle_remove = !!editSubtitleRemoveEl?.checked;
const subtitleFile = subtitle_remove ? null : (editSubtitleFileEl?.files?.[0] || null);
const refreshOmdb = !!editRefreshOmdbEl.checked;
const cam = !!editCamFlagEl?.checked;
@@ -2349,13 +2432,17 @@ window.onload = async function () {
video_file,
video_file_low,
embed_src,
subtitle_lang,
subtitle_label,
subtitle_default,
subtitle_remove,
refreshOmdb,
cam
};
if (!subtitle_remove && !subtitleFile) {
body.subtitle_lang = subtitle_lang;
body.subtitle_label = subtitle_label;
body.subtitle_default = subtitle_default;
}
// Only send manual fields if we're not refreshing from OMDb
if (!refreshOmdb) {
body.title = title;
@@ -2390,13 +2477,14 @@ window.onload = async function () {
const updated = await res.json();
if (subtitleFile) {
try {
await uploadMovieSubtitle({
const uploaded = await uploadMovieSubtitle({
movieId,
file: subtitleFile,
lang: subtitle_lang,
label: subtitle_label,
isDefault: subtitle_default,
});
if (uploaded) moviesById.set(uploaded._id, uploaded);
} catch (err) {
showResponse('warning', `Film mis à jour, mais sous-titres non uploadés: ${err.message || 'Erreur'}`);
}
@@ -2442,7 +2530,7 @@ window.onload = async function () {
const hasLowFile = !!(m && m.video_file_low);
const hasEmbed = !!(m && m.embed_src);
const hasLegacy = !!(m && m.vod_link);
const hasSubtitle = !!(m && m.subtitle_file);
const hasSubtitle = Array.isArray(m?.subtitles) ? m.subtitles.length > 0 : !!(m && m.subtitle_file);
if (hasVideo) badges.push({ text: 'Video', cls: 'bg-primary' });
if (hasFile) badges.push({ text: 'Server file', cls: 'bg-dark' });
+27 -2
View File
@@ -804,9 +804,34 @@ function applySubtitleTrack({ videoEl, movie, token }) {
if (!videoEl) return;
clearVideoTracks(videoEl);
const movieId = String(movie?._id || '');
const subtitleFile = String(movie?.subtitle_file || '').trim();
if (!movieId || !subtitleFile) return;
if (!movieId) return;
const subtitles = Array.isArray(movie?.subtitles) ? movie.subtitles : [];
if (subtitles.length > 0) {
const hasDefault = subtitles.some((s) => s?.default);
subtitles.forEach((s, idx) => {
const lang = String(s?.lang || 'en').trim() || 'en';
const label = String(s?.label || 'Subtitles').trim() || 'Subtitles';
const isDefault = s?.default === undefined ? (!hasDefault && idx === 0) : !!s.default;
const subId = String(s?._id || '').trim();
const rawUrl = subId && subId !== 'legacy'
? `/api/subtitles/${encodeURIComponent(movieId)}/${encodeURIComponent(subId)}`
: `/api/subtitles/${encodeURIComponent(movieId)}`;
const src = addTokenToApiSubtitleUrl(rawUrl, token);
const track = document.createElement('track');
track.kind = 'subtitles';
track.label = label;
track.srclang = lang;
track.src = src;
track.default = isDefault;
videoEl.appendChild(track);
});
return;
}
const subtitleFile = String(movie?.subtitle_file || '').trim();
if (!subtitleFile) return;
const lang = String(movie?.subtitle_lang || 'en').trim() || 'en';
const label = String(movie?.subtitle_label || 'Subtitles').trim() || 'Subtitles';
const isDefault = movie?.subtitle_default === undefined ? true : !!movie.subtitle_default;
+203 -17
View File
@@ -85,6 +85,62 @@ function parseSubtitleDefault(v) {
return undefined;
}
function parseBooleanFlag(v) {
if (v === undefined) return undefined;
if (typeof v === "boolean") return v;
const s = String(v).trim().toLowerCase();
if (!s) return undefined;
if (["true", "1", "yes", "on"].includes(s)) return true;
if (["false", "0", "no", "off"].includes(s)) return false;
return undefined;
}
function normalizeSubtitleEntry({ file, lang, label, isDefault } = {}) {
const safeFile = typeof file === "string" ? file.trim() : "";
if (!safeFile) return null;
return {
file: safeFile,
lang: normalizeSubtitleLang(lang) || null,
label: normalizeSubtitleLabel(label) || null,
default: !!isDefault,
};
}
function getLegacySubtitle(movie) {
const file = typeof movie?.subtitle_file === "string" ? movie.subtitle_file.trim() : "";
if (!file) return null;
return {
_id: "legacy",
cursor[bot] commented 2026-01-25 02:10:04 -05:00 (Migrated from github.com)
Review

Duplicate identical functions for boolean parsing

Low Severity

The parseSubtitleDefault function (lines 78-86) and parseBooleanFlag function (lines 88-96) are completely identical implementations. Both parse boolean values from string inputs using the exact same logic (handling "true", "1", "yes", "on" for true, etc.). One function should be removed and the other reused in both places where they're currently called.

Fix in Cursor Fix in Web

### Duplicate identical functions for boolean parsing **Low Severity** <!-- DESCRIPTION START --> The `parseSubtitleDefault` function (lines 78-86) and `parseBooleanFlag` function (lines 88-96) are completely identical implementations. Both parse boolean values from string inputs using the exact same logic (handling `"true"`, `"1"`, `"yes"`, `"on"` for true, etc.). One function should be removed and the other reused in both places where they're currently called. <!-- DESCRIPTION END --> <!-- BUGBOT_BUG_ID: e8232935-88e0-46aa-a11d-f87144aa4467 --> <!-- LOCATIONS START routes/movies.js#L77-L96 LOCATIONS END --> <a href="https://cursor.com/open?data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImJ1Z2JvdC12MiJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjZkNTQ2MzE1LTRlMjEtNDc2OS05NTQ1LTc5YTNlMWM3MTY4NiIsImVuY3J5cHRpb25LZXkiOiJScDBtWV83TVA0cGhHaExqTk1qcTMxMFRMNjRkT1ptTXA1STlJbzJhYTU0IiwiYnJhbmNoIjoiMjAyNiJ9LCJpYXQiOjE3NjkzMjUwMDMsImV4cCI6MTc3MTkxNzAwM30.Tj54I4d2hItocuhS5JBzM2-O2aW-J410ubBLfiKqPZr-0bxe47_Bio6DKJWXXjSPSHRgrUNInPX1YpqOspKmGX1vfLjblvVJZnQsMP1wr8pOW6r-nFnb9yWbOyHFdQDC9xxYqbbL_mW78DA38rof9b7dzleH0sRPWVtuS7zL7_Lx6-yLpa7aW9EMcm8w9L_-tnNVzCCZp2-dUpBmmvliS52uNPkvY-w7UoUkTb95WbKSTNCcTuHi9pcrOH1DdIIdr4yVaqdFqOwlndFp2GeWzvOrGbz5EqFjxL3E_OOUt0gxQPYHRIug8KSoofBYehEkrm3lSTOeIfUyLBX3eFp_Mw" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/fix-in-cursor-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/fix-in-cursor-light.svg"><img alt="Fix in Cursor" src="https://cursor.com/fix-in-cursor.svg"></picture></a>&nbsp;<a href="https://cursor.com/agents?data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImJ1Z2JvdC12MiJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjZkNTQ2MzE1LTRlMjEtNDc2OS05NTQ1LTc5YTNlMWM3MTY4NiIsImVuY3J5cHRpb25LZXkiOiJScDBtWV83TVA0cGhHaExqTk1qcTMxMFRMNjRkT1ptTXA1STlJbzJhYTU0IiwiYnJhbmNoIjoiMjAyNiIsInJlcG9Pd25lciI6InF1b2lqZSIsInJlcG9OYW1lIjoib3NjYXJzLXBvb2wiLCJwck51bWJlciI6NzMsImNvbW1pdFNoYSI6IjQwNmE0ZjBiYjNhMTdlMGU3MDFmNmQ4YmJhMWNiMmVhZmUyMWY0MWUiLCJwcm92aWRlciI6ImdpdGh1YiJ9LCJpYXQiOjE3NjkzMjUwMDMsImV4cCI6MTc3MTkxNzAwM30.bwaj7A2IDeyIJhlMwBvMDTlSe6utkA_s6CP2TFd-oI260RkzQ4swy6QFu0PN-XWzyUE2JiLOtbvH92C3hdxuzxgDkNUSPeu0qvLASkd4gzY4w0GKxVm7Ss0_b_UiCf52SOMOWoqImNunYo8hhdfETCGTaq-S9jmNvLHRWKjX8AAmRAct8p6aI1wC0Hc8zituGKge5fCBr2lLs46wKIu6-Xpfsei5lXsqL4d3MG0E5_Aaquogm760B-TWsOIy8lE5raelJmto4HULL5Cu0AxrmML_Gkq4KIZWI2QHpYWr9V5KShkUEE5jHMI8ijb5C0Qhi37WWsOP8Ot4czcHrUWm3A" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/fix-in-web-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/fix-in-web-light.svg"><img alt="Fix in Web" src="https://cursor.com/fix-in-web.svg"></picture></a>
file,
lang: typeof movie?.subtitle_lang === "string" ? movie.subtitle_lang : null,
label: typeof movie?.subtitle_label === "string" ? movie.subtitle_label : null,
default: !!movie?.subtitle_default,
};
}
function getSubtitlesForResponse(movie) {
const subs = Array.isArray(movie?.subtitles) ? movie.subtitles.filter(Boolean) : [];
if (subs.length > 0) return subs;
const legacy = getLegacySubtitle(movie);
return legacy ? [legacy] : [];
}
function syncLegacyFromSubtitles(movie) {
const subs = Array.isArray(movie?.subtitles) ? movie.subtitles : [];
if (!subs.length) {
movie.subtitle_file = null;
movie.subtitle_lang = null;
movie.subtitle_label = null;
movie.subtitle_default = false;
return;
}
const preferred = subs.find((s) => s?.default) || subs[0];
movie.subtitle_file = preferred?.file || null;
movie.subtitle_lang = preferred?.lang || null;
movie.subtitle_label = preferred?.label || null;
movie.subtitle_default = !!preferred?.default;
}
async function ensureSubtitleDir() {
await fs.promises.mkdir(SUBTITLE_FILES_DIR, { recursive: true });
}
@@ -223,7 +279,7 @@ router.get("/", async (req, res) => {
const projection = isChecklist
? "imdb_id title"
: "imdb_id title description rating poster cam year category vod_link player_mode video_src embed_src video_file video_file_low subtitle_file subtitle_lang subtitle_label subtitle_default updatedAt createdAt";
: "imdb_id title description rating poster cam year category vod_link player_mode video_src embed_src video_file video_file_low subtitles subtitle_file subtitle_lang subtitle_label subtitle_default updatedAt createdAt";
const cacheKey = `movies:list:${year || "all"}:${isChecklist ? "checklist" : "films"}`;
const cached = cacheGet(cacheKey);
@@ -237,9 +293,10 @@ router.get("/", async (req, res) => {
.sort({ title: 1 })
.lean();
cacheSet(cacheKey, movies);
const payload = movies.map((m) => ({ ...m, subtitles: getSubtitlesForResponse(m) }));
cacheSet(cacheKey, payload);
res.set("Cache-Control", "public, max-age=30");
res.status(200).json(movies);
res.status(200).json(payload);
} catch (err) {
res.status(500).json({ error: err.message });
}
@@ -567,32 +624,112 @@ router.post("/:id/subtitles", (req, res) => {
}
}
const existing = typeof movie.subtitle_file === "string" ? movie.subtitle_file.trim() : "";
if (existing) {
const oldPath = path.resolve(SUBTITLE_FILES_DIR, existing);
if (oldPath.startsWith(SUBTITLE_FILES_DIR + path.sep)) {
try { await fs.promises.unlink(oldPath); } catch (_) {}
}
if (!Array.isArray(movie.subtitles)) movie.subtitles = [];
if (movie.subtitles.length === 0) {
const legacy = normalizeSubtitleEntry({
file: movie.subtitle_file,
lang: movie.subtitle_lang,
label: movie.subtitle_label,
isDefault: movie.subtitle_default,
});
if (legacy) movie.subtitles.push(legacy);
}
const rel = path.relative(SUBTITLE_FILES_DIR, storedPath).replace(/\\/g, "/");
movie.subtitle_file = rel;
const incomingLang = normalizeSubtitleLang(req.body?.subtitle_lang);
if (incomingLang !== undefined) movie.subtitle_lang = incomingLang || null;
const incomingLabel = normalizeSubtitleLabel(req.body?.subtitle_label);
if (incomingLabel !== undefined) movie.subtitle_label = incomingLabel || null;
const incomingDefault = parseSubtitleDefault(req.body?.subtitle_default);
if (incomingDefault !== undefined) movie.subtitle_default = incomingDefault;
const shouldDefault = incomingDefault !== undefined
? incomingDefault
: movie.subtitles.length === 0;
if (shouldDefault) {
movie.subtitles.forEach((s) => { s.default = false; });
}
const entry = normalizeSubtitleEntry({
file: rel,
lang: incomingLang,
label: incomingLabel,
isDefault: shouldDefault,
});
if (entry) movie.subtitles.push(entry);
syncLegacyFromSubtitles(movie);
await movie.save();
cacheClear();
return res.status(200).json(movie);
});
});
// Admin: delete one subtitle track from a movie
router.delete("/:id/subtitles/:subtitleId", async (req, res) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ message: "Authentication token is required" });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
if (!decoded.admin) {
return res.status(403).json({ message: "You do not have admin privileges" });
}
} catch (err) {
if (err instanceof jwt.JsonWebTokenError) {
return res.status(401).json({ message: "Invalid or expired token" });
}
return res.status(500).json({ error: err.message });
}
const { id, subtitleId } = req.params;
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ message: "Invalid movie id" });
}
const movie = await Movie.findById(id);
if (!movie) return res.status(404).json({ message: "Movie not found" });
if (subtitleId === "legacy") {
const existing = typeof movie.subtitle_file === "string" ? movie.subtitle_file.trim() : "";
if (!existing) return res.status(404).json({ message: "Subtitle not found" });
const oldPath = path.resolve(SUBTITLE_FILES_DIR, existing);
if (oldPath.startsWith(SUBTITLE_FILES_DIR + path.sep)) {
try { await fs.promises.unlink(oldPath); } catch (_) {}
}
movie.subtitle_file = null;
movie.subtitle_lang = null;
movie.subtitle_label = null;
movie.subtitle_default = false;
await movie.save();
cacheClear();
return res.status(200).json(movie);
}
const subs = Array.isArray(movie.subtitles) ? movie.subtitles : [];
const idx = subs.findIndex((s) => String(s?._id || "") === String(subtitleId));
if (idx < 0) {
return res.status(404).json({ message: "Subtitle not found" });
}
const removed = subs[idx];
if (removed?.file) {
const full = path.resolve(SUBTITLE_FILES_DIR, String(removed.file));
if (full.startsWith(SUBTITLE_FILES_DIR + path.sep)) {
try { await fs.promises.unlink(full); } catch (_) {}
}
}
subs.splice(idx, 1);
if (subs.length > 0 && !subs.some((s) => s?.default)) {
subs[0].default = true;
}
movie.subtitles = subs;
syncLegacyFromSubtitles(movie);
await movie.save();
cacheClear();
return res.status(200).json(movie);
});
// Admin: delete one or more movies (by Mongo _id or imdb_id)
router.delete("/delete", async (req, res) => {
const token = req.headers.authorization?.split(" ")[1];
@@ -670,6 +807,7 @@ router.put("/:id", async (req, res) => {
subtitle_lang,
subtitle_label,
subtitle_default,
subtitle_remove,
refreshOmdb,
cam,
} = req.body || {};
1
@@ -734,6 +872,52 @@ router.put("/:id", async (req, res) => {
movie.video_file_low = v ? v : null;
}
const removeSubtitles = parseBooleanFlag(subtitle_remove);
if (removeSubtitles) {
const entries = Array.isArray(movie.subtitles) ? movie.subtitles : [];
for (const entry of entries) {
const rel = typeof entry?.file === "string" ? entry.file.trim() : "";
if (!rel) continue;
const full = path.resolve(SUBTITLE_FILES_DIR, rel);
if (full.startsWith(SUBTITLE_FILES_DIR + path.sep)) {
try { await fs.promises.unlink(full); } catch (_) {}
}
}
const existing = typeof movie.subtitle_file === "string" ? movie.subtitle_file.trim() : "";
if (existing) {
const oldPath = path.resolve(SUBTITLE_FILES_DIR, existing);
if (oldPath.startsWith(SUBTITLE_FILES_DIR + path.sep)) {
try { await fs.promises.unlink(oldPath); } catch (_) {}
}
}
movie.subtitles = [];
movie.subtitle_file = null;
movie.subtitle_lang = null;
movie.subtitle_label = null;
movie.subtitle_default = false;
} else {
const subs = Array.isArray(movie.subtitles) ? movie.subtitles : [];
if (subs.length > 0) {
const target = subs.find((s) => s?.default) || subs[0];
if (subtitle_lang !== undefined) {
const v = normalizeSubtitleLang(subtitle_lang);
target.lang = v ? v : null;
}
if (subtitle_label !== undefined) {
const v = normalizeSubtitleLabel(subtitle_label);
target.label = v ? v : null;
}
if (subtitle_default !== undefined) {
const v = parseSubtitleDefault(subtitle_default);
if (v !== undefined) {
subs.forEach((s) => { s.default = false; });
target.default = !!v;
}
}
syncLegacyFromSubtitles(movie);
} else {
if (subtitle_lang !== undefined) {
const v = normalizeSubtitleLang(subtitle_lang);
movie.subtitle_lang = v ? v : null;
@@ -748,6 +932,8 @@ router.put("/:id", async (req, res) => {
const v = parseSubtitleDefault(subtitle_default);
if (v !== undefined) movie.subtitle_default = v;
}
}
}
if (year !== undefined && year !== null && year !== "") {
movie.year = parseOscarYear(year);
@@ -972,7 +1158,7 @@ router.get("/:id", async (req, res) => {
const movie = await Movie.findById(id).lean();
if (!movie) return res.status(404).json({ message: "Movie not found" });
return res.status(200).json(movie);
return res.status(200).json({ ...movie, subtitles: getSubtitlesForResponse(movie) });
} catch (err) {
return res.status(500).json({ error: err.message });
}
+32 -2
View File
@@ -65,8 +65,26 @@ async function streamSubtitle(req, res) {
return res.status(400).json({ message: "Invalid movie id" });
}
const movie = await Movie.findById(id).select("subtitle_file").lean();
const subtitleFile = typeof movie?.subtitle_file === "string" ? movie.subtitle_file.trim() : "";
const movie = await Movie.findById(id).select("subtitles subtitle_file subtitle_lang subtitle_label subtitle_default").lean();
if (!movie) return res.status(404).json({ message: "Movie not found" });
let subtitleFile = "";
const subtitleId = req.params?.subtitleId;
const subs = Array.isArray(movie?.subtitles) ? movie.subtitles : [];
if (subtitleId) {
if (subtitleId === "legacy") {
subtitleFile = typeof movie?.subtitle_file === "string" ? movie.subtitle_file.trim() : "";
} else {
const match = subs.find((s) => String(s?._id || "") === String(subtitleId));
subtitleFile = typeof match?.file === "string" ? match.file.trim() : "";
}
} else if (subs.length > 0) {
const preferred = subs.find((s) => s?.default) || subs[0];
subtitleFile = typeof preferred?.file === "string" ? preferred.file.trim() : "";
} else {
subtitleFile = typeof movie?.subtitle_file === "string" ? movie.subtitle_file.trim() : "";
}
if (!subtitleFile) return res.status(404).json({ message: "No subtitles configured for this movie" });
if (!isSafeRelativeFile(subtitleFile)) return res.status(400).json({ message: "Invalid subtitle_file path" });
@@ -94,6 +112,18 @@ async function streamSubtitle(req, res) {
return fs.createReadStream(fullPath).pipe(res);
}
router.get("/:id/:subtitleId", (req, res) => {
streamSubtitle(req, res).catch((err) => {
return res.status(500).json({ error: err?.message || "Subtitle streaming error" });
});
});
router.head("/:id/:subtitleId", (req, res) => {
streamSubtitle(req, res).catch((err) => {
return res.status(500).json({ error: err?.message || "Subtitle streaming error" });
});
});
router.get("/:id", (req, res) => {
streamSubtitle(req, res).catch((err) => {
return res.status(500).json({ error: err?.message || "Subtitle streaming error" });