diff --git a/models/Movie.js b/models/Movie.js
index 635275d..75f5c67 100644
--- a/models/Movie.js
+++ b/models/Movie.js
@@ -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 },
diff --git a/public/control.html b/public/control.html
index 70036c9..705df9b 100644
--- a/public/control.html
+++ b/public/control.html
@@ -1418,7 +1418,16 @@
diff --git a/public/js/app_control.js b/public/js/app_control.js
index c846b30..11c1eb9 100644
--- a/public/js/app_control.js
+++ b/public/js/app_control.js
@@ -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 = '
—';
+ 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 ? '
Par défaut' : '';
+
+ return `
+
+
+ ${title}
+ ${meta ? `${meta}` : ''}
+ ${isDefault}
+
+
+
+ `;
+ }).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' });
diff --git a/public/js/app_player.js b/public/js/app_player.js
index 78b8523..b982b9c 100644
--- a/public/js/app_player.js
+++ b/public/js/app_player.js
@@ -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;
diff --git a/routes/movies.js b/routes/movies.js
index a0113bb..f60beea 100644
--- a/routes/movies.js
+++ b/routes/movies.js
@@ -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",
+ 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 || {};
@@ -734,19 +872,67 @@ router.put("/:id", async (req, res) => {
movie.video_file_low = v ? v : null;
}
- if (subtitle_lang !== undefined) {
- const v = normalizeSubtitleLang(subtitle_lang);
- movie.subtitle_lang = 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 (_) {}
+ }
+ }
- if (subtitle_label !== undefined) {
- const v = normalizeSubtitleLabel(subtitle_label);
- movie.subtitle_label = v ? v : null;
- }
+ 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 (subtitle_default !== undefined) {
- const v = parseSubtitleDefault(subtitle_default);
- if (v !== undefined) movie.subtitle_default = v;
+ 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;
+ }
+
+ if (subtitle_label !== undefined) {
+ const v = normalizeSubtitleLabel(subtitle_label);
+ movie.subtitle_label = v ? v : null;
+ }
+
+ if (subtitle_default !== undefined) {
+ const v = parseSubtitleDefault(subtitle_default);
+ if (v !== undefined) movie.subtitle_default = v;
+ }
+ }
}
if (year !== undefined && year !== null && 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 });
}
diff --git a/routes/subtitles.js b/routes/subtitles.js
index 3413357..5f9d6db 100644
--- a/routes/subtitles.js
+++ b/routes/subtitles.js
@@ -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" });