Support multiple subtitle tracks
Adds multi-language subtitle storage, upload/removal, and player/admin UI support, plus fixes subtitle metadata updates during upload.
This commit is contained in:
+99
-11
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user