/**
* Video-Bibliothek Frontend
* Bereiche pro Scan-Pfad, CRUD, Clean, Import, TVDB-Metadaten
*/
let libraryPaths = [];
let sectionStates = {}; // pathId -> {tab, page, limit}
let activePathId = null; // null = alle anzeigen
let filterTimeout = null;
let currentSeriesId = null;
let currentMovieId = null;
let currentDetailTab = "episodes";
let currentImportJobId = null;
let cleanData = [];
// === Initialisierung ===
document.addEventListener("DOMContentLoaded", function () {
loadStats();
loadFilterPresets();
loadLibraryPaths();
});
// === Statistiken ===
function loadStats() {
fetch("/api/library/stats")
.then(r => r.json())
.then(data => {
document.getElementById("stat-videos").textContent = data.total_videos || 0;
document.getElementById("stat-series").textContent = data.total_series || 0;
document.getElementById("stat-size").textContent = formatSize(data.total_size || 0);
document.getElementById("stat-duration").textContent = formatDuration(data.total_duration || 0);
})
.catch(() => {});
}
// === Library-Bereiche laden ===
function loadLibraryPaths() {
fetch("/api/library/paths")
.then(r => r.json())
.then(data => {
libraryPaths = data.paths || [];
renderPathNav();
renderLibrarySections();
})
.catch(() => {
document.getElementById("library-content").innerHTML =
'
Fehler beim Laden der Bibliothek
';
});
}
// === Pfad-Navigation (links) ===
function renderPathNav() {
const nav = document.getElementById("nav-paths-list");
if (!nav) return;
const enabled = libraryPaths.filter(p => p.enabled);
if (!enabled.length) {
nav.innerHTML = 'Keine Pfade
';
return;
}
let html = '';
// "Alle" Eintrag
html += `
📚
Alle
`;
for (const lp of enabled) {
const icon = lp.media_type === 'series' ? '🎬' : '🎦';
const isActive = activePathId === lp.id;
html += `
${icon}
${escapeHtml(lp.name)}
`;
}
nav.innerHTML = html;
}
function selectLibraryPath(pathId) {
activePathId = pathId;
renderPathNav();
renderLibrarySections();
}
function renderLibrarySections() {
const container = document.getElementById("library-content");
if (!libraryPaths.length) {
container.innerHTML = 'Keine Scan-Pfade konfiguriert. Klicke "Pfade verwalten" um zu starten.
';
return;
}
// Welche Pfade anzeigen?
const visiblePaths = libraryPaths.filter(lp => {
if (!lp.enabled) return false;
if (activePathId !== null && lp.id !== activePathId) return false;
return true;
});
if (!visiblePaths.length) {
container.innerHTML = 'Kein aktiver Pfad ausgewaehlt
';
return;
}
let html = "";
for (const lp of visiblePaths) {
const pid = lp.id;
const isSeriesLib = lp.media_type === "series";
if (!sectionStates[pid]) {
sectionStates[pid] = {
tab: "videos",
page: 1,
limit: 50,
};
}
const st = sectionStates[pid];
html += ``;
html += ``;
// Tabs - Serien-Pfad: Videos+Serien+Ordner / Film-Pfad: Videos+Filme+Ordner
html += `
`;
html += `Videos `;
if (isSeriesLib) {
html += `Serien `;
} else {
html += `Filme `;
}
html += `Ordner `;
html += `
`;
// Tab-Content
html += `
`;
html += `
Lade...
`;
html += `
`;
html += `
`;
}
container.innerHTML = html;
// Daten laden fuer sichtbare Bereiche
for (const lp of visiblePaths) {
loadSectionData(lp.id);
}
}
function switchSectionTab(pathId, tab) {
sectionStates[pathId].tab = tab;
sectionStates[pathId].page = 1;
// Tab-Buttons aktualisieren
const tabBar = document.getElementById("tabs-" + pathId);
if (tabBar) {
tabBar.querySelectorAll(".tab-btn").forEach(b => {
b.classList.toggle("active", b.getAttribute("data-tab") === tab);
});
}
loadSectionData(pathId);
}
function loadSectionData(pathId) {
const st = sectionStates[pathId];
switch (st.tab) {
case "videos": loadSectionVideos(pathId); break;
case "series": loadSectionSeries(pathId); break;
case "movies": loadSectionMovies(pathId); break;
case "browser": loadSectionBrowser(pathId); break;
}
}
// === Videos pro Bereich ===
function loadSectionVideos(pathId, page) {
const st = sectionStates[pathId];
if (page) st.page = page;
const params = buildFilterParams();
params.set("library_path_id", pathId);
params.set("page", st.page);
params.set("limit", st.limit);
const content = document.getElementById("content-" + pathId);
fetch("/api/library/videos?" + params.toString())
.then(r => r.json())
.then(data => {
let html = '';
html += renderVideoTable(data.items || []);
html += '
';
html += renderPagination(data.total || 0, data.page || 1, data.pages || 1, pathId, "videos");
content.innerHTML = html;
})
.catch(() => { content.innerHTML = 'Fehler
'; });
}
// === Filme pro Bereich ===
function loadSectionMovies(pathId) {
const content = document.getElementById("content-" + pathId);
fetch("/api/library/movies-list?path_id=" + pathId)
.then(r => r.json())
.then(data => {
content.innerHTML = renderMovieGrid(data.movies || []);
})
.catch(() => { content.innerHTML = 'Fehler
'; });
}
function renderMovieGrid(movies) {
if (!movies.length) return 'Keine Filme gefunden
';
let html = '';
for (const m of movies) {
const poster = m.poster_url
? `
`
: '
Kein Poster
';
const year = m.year ? `
${m.year} ` : "";
const genres = m.genres ? `
${escapeHtml(m.genres)}
` : "";
const duration = m.duration_sec ? formatDuration(m.duration_sec) : "";
const size = m.total_size ? formatSize(m.total_size) : "";
const tvdbBtn = m.tvdb_id
? '
TVDB '
: `
TVDB zuordnen `;
const overview = m.overview
? `
${escapeHtml(m.overview.substring(0, 120))}${m.overview.length > 120 ? '...' : ''}
`
: "";
html += `
${poster}
${escapeHtml(m.title || m.folder_name)}
${genres}
${overview}
${year}
${duration ? `${duration} ` : ""}
${size ? `${size} ` : ""}
${m.video_count || 0} Dateien
${tvdbBtn}
`;
}
html += '
';
return html;
}
// === Serien pro Bereich ===
function loadSectionSeries(pathId) {
const content = document.getElementById("content-" + pathId);
fetch("/api/library/series?path_id=" + pathId)
.then(r => r.json())
.then(data => {
content.innerHTML = renderSeriesGrid(data.series || []);
})
.catch(() => { content.innerHTML = 'Fehler
'; });
}
// === Ordner pro Bereich ===
let _browserLoading = false;
function loadSectionBrowser(pathId, subPath) {
// Doppelklick-Schutz: Zweiten Aufruf ignorieren solange geladen wird
if (_browserLoading) return;
_browserLoading = true;
const content = document.getElementById("content-" + pathId);
content.innerHTML = 'Lade Ordner...
';
const params = new URLSearchParams();
if (subPath) params.set("path", subPath);
else {
// Basispfad der Library verwenden
const lp = libraryPaths.find(p => p.id === pathId);
if (lp) params.set("path", lp.path);
}
fetch("/api/library/browse?" + params.toString())
.then(r => r.json())
.then(data => {
let html = renderBreadcrumb(data.breadcrumb || [], pathId);
html += renderBrowser(data.folders || [], data.videos || [], pathId);
content.innerHTML = html;
})
.catch(() => { content.innerHTML = 'Fehler
'; })
.finally(() => { _browserLoading = false; });
}
// === Video-Tabelle (gemeinsam genutzt) ===
function renderVideoTable(items) {
if (!items.length) return 'Keine Videos gefunden
';
let html = '';
html += 'Dateiname Aufl. Codec Audio ';
html += 'Untertitel Groesse Dauer Container Aktion ';
html += ' ';
for (const v of items) {
const audioInfo = (v.audio_tracks || []).map(a => {
const lang = (a.lang || "?").toUpperCase().substring(0, 3);
const ch = channelLayout(a.channels);
return `${lang} ${ch} `;
}).join(" ");
const subInfo = (v.subtitle_tracks || []).map(s =>
`${(s.lang || "?").toUpperCase().substring(0, 3)} `
).join(" ");
const res = v.width && v.height ? resolutionLabel(v.width, v.height) : "-";
const is10bit = v.is_10bit ? ' 10bit ' : "";
const vidTitle = v.file_name || "Video";
html += `
${escapeHtml(v.file_name || "-")}
${res}${is10bit}
${v.video_codec || "-"}
${audioInfo || "-"}
${subInfo || "-"}
${formatSize(v.file_size || 0)}
${formatDuration(v.duration_sec || 0)}
${(v.container || "-").toUpperCase()}
▶
Conv
✕
`;
}
html += '
';
return html;
}
function renderPagination(total, page, pages, pathId, tabType) {
let html = '';
return html;
}
function changeSectionLimit(pathId, val) {
sectionStates[pathId].limit = parseInt(val) || 50;
sectionStates[pathId].page = 1;
loadSectionData(pathId);
}
// === Serien-Grid ===
function renderSeriesGrid(series) {
if (!series.length) return 'Keine Serien gefunden
';
let html = '';
for (const s of series) {
const poster = s.poster_url
? `
`
: '
Kein Poster
';
const missing = s.missing_episodes > 0
? `
${s.missing_episodes} fehlend ` : "";
const genres = s.genres ? `
${escapeHtml(s.genres)}
` : "";
const tvdbBtn = s.tvdb_id
? `
TVDB `
: `
TVDB zuordnen `;
html += `
${poster}
${escapeHtml(s.title || s.folder_name)}
${genres}
${s.local_episodes || 0} Episoden
${missing}
${tvdbBtn}
${s.status ? `
${s.status} ` : ""}
`;
}
html += '
';
return html;
}
// === Ordner-Ansicht ===
function renderBreadcrumb(crumbs, pathId) {
let html = '';
return html;
}
function renderBrowser(folders, videos, pathId) {
if (!folders.length && !videos.length) return 'Leerer Ordner
';
let html = "";
if (folders.length) {
html += '';
for (const f of folders) {
const size = formatSize(f.total_size || 0);
const pathEsc = f.path.replace(/'/g, "\\'");
html += `
📁
${escapeHtml(f.name)}
${f.video_count} Videos, ${size}
`;
}
html += '
';
}
if (videos.length) {
html += '';
html += renderVideoTable(videos);
html += '
';
}
return html;
}
// === Serien-Detail ===
function openSeriesDetail(seriesId) {
if (event) event.stopPropagation();
currentSeriesId = seriesId;
currentDetailTab = "episodes";
document.getElementById("series-modal").style.display = "flex";
// Tabs zuruecksetzen
document.querySelectorAll(".detail-tab").forEach(b => b.classList.remove("active"));
document.querySelector('.detail-tab[onclick*="episodes"]').classList.add("active");
fetch(`/api/library/series/${seriesId}`)
.then(r => r.json())
.then(data => {
document.getElementById("series-modal-title").textContent = data.title || data.folder_name;
document.getElementById("series-modal-genres").textContent = data.genres || "";
// Aktions-Buttons anzeigen
document.getElementById("btn-tvdb-refresh").style.display = data.tvdb_id ? "" : "none";
document.getElementById("btn-tvdb-unlink").style.display = data.tvdb_id ? "" : "none";
document.getElementById("btn-metadata-dl").style.display = data.tvdb_id ? "" : "none";
renderEpisodesTab(data);
})
.catch(() => {
document.getElementById("series-modal-body").innerHTML =
'Fehler beim Laden
';
});
}
function switchDetailTab(tab) {
currentDetailTab = tab;
document.querySelectorAll(".detail-tab").forEach(b => b.classList.remove("active"));
document.querySelector(`.detail-tab[onclick*="${tab}"]`).classList.add("active");
if (tab === "episodes") {
fetch(`/api/library/series/${currentSeriesId}`)
.then(r => r.json())
.then(data => renderEpisodesTab(data))
.catch(() => {});
} else if (tab === "cast") {
loadCast();
} else if (tab === "artworks") {
loadArtworks();
}
}
function renderEpisodesTab(series) {
const body = document.getElementById("series-modal-body");
let html = '';
// Episoden nach Staffeln
const episodes = series.episodes || [];
const tvdbEpisodes = series.tvdb_episodes || [];
const seasons = {};
for (const ep of episodes) {
const s = ep.season_number || 0;
if (!seasons[s]) seasons[s] = {local: [], missing: []};
seasons[s].local.push(ep);
}
if (tvdbEpisodes.length) {
const localSet = new Set(episodes.map(e => `${e.season_number}-${e.episode_number}`));
for (const ep of tvdbEpisodes) {
const key = `${ep.season_number}-${ep.episode_number}`;
if (!localSet.has(key) && ep.season_number > 0) {
const s = ep.season_number;
if (!seasons[s]) seasons[s] = {local: [], missing: []};
seasons[s].missing.push(ep);
}
}
}
const sortedSeasons = Object.keys(seasons).map(Number).sort((a, b) => a - b);
for (const sNum of sortedSeasons) {
const sData = seasons[sNum];
html += ``;
html += `Staffel ${sNum || "Unbekannt"} (${sData.local.length} vorhanden`;
if (sData.missing.length) html += `, ${sData.missing.length} fehlend `;
html += ') ';
html += '';
html += 'Nr Titel Aufl. Codec Audio Aktion ';
html += ' ';
const allEps = [];
for (const ep of sData.local) allEps.push({...ep, _type: "local"});
for (const ep of sData.missing) allEps.push({...ep, _type: "missing"});
allEps.sort((a, b) => (a.episode_number || 0) - (b.episode_number || 0));
for (const ep of allEps) {
if (ep._type === "missing") {
html += `
${ep.episode_number || "-"}
${escapeHtml(ep.episode_name || "-")}
Nicht vorhanden
FEHLT
`;
} else {
const audioInfo = (ep.audio_tracks || []).map(a => {
const lang = (a.lang || "?").toUpperCase().substring(0, 3);
return `${lang} ${channelLayout(a.channels)} `;
}).join(" ");
const res = ep.width && ep.height ? resolutionLabel(ep.width, ep.height) : "-";
const epTitle = ep.episode_title || ep.file_name || "Episode";
html += `
${ep.episode_number || "-"}
${escapeHtml(epTitle)}
${res}
${ep.video_codec || "-"}
${audioInfo || "-"}
▶
Conv
✕
`;
}
}
html += '
';
}
if (!sortedSeasons.length) html += 'Keine Episoden gefunden
';
body.innerHTML = html;
}
function loadCast() {
const body = document.getElementById("series-modal-body");
body.innerHTML = 'Lade Darsteller...
';
fetch(`/api/library/series/${currentSeriesId}/cast`)
.then(r => r.json())
.then(data => {
const cast = data.cast || [];
if (!cast.length) {
body.innerHTML = 'Keine Darsteller-Daten vorhanden
';
return;
}
let html = '';
for (const c of cast) {
const img = c.person_image_url || c.image_url;
const imgTag = img
? `
`
: '
?
';
html += `
${imgTag}
${escapeHtml(c.person_name)}
${escapeHtml(c.character_name || "")}
`;
}
html += '
';
body.innerHTML = html;
})
.catch(() => { body.innerHTML = 'Fehler
'; });
}
function loadArtworks() {
const body = document.getElementById("series-modal-body");
body.innerHTML = 'Lade Bilder...
';
fetch(`/api/library/series/${currentSeriesId}/artworks`)
.then(r => r.json())
.then(data => {
const artworks = data.artworks || [];
if (!artworks.length) {
body.innerHTML = 'Keine Bilder vorhanden
';
return;
}
// Nach Typ gruppieren
const groups = {};
for (const a of artworks) {
const t = a.artwork_type || "sonstige";
if (!groups[t]) groups[t] = [];
groups[t].push(a);
}
let html = '';
for (const [type, items] of Object.entries(groups)) {
html += ``;
html += '';
}
body.innerHTML = html;
})
.catch(() => { body.innerHTML = 'Fehler
'; });
}
function closeSeriesModal() {
document.getElementById("series-modal").style.display = "none";
currentSeriesId = null;
}
// === Serien-Aktionen ===
function tvdbRefresh() {
if (!currentSeriesId) return;
fetch(`/api/library/series/${currentSeriesId}/tvdb-refresh`, {method: "POST"})
.then(r => r.json())
.then(data => {
if (data.error) alert("Fehler: " + data.error);
else { alert("TVDB aktualisiert: " + (data.name || "")); openSeriesDetail(currentSeriesId); }
})
.catch(e => alert("Fehler: " + e));
}
function tvdbUnlink() {
if (!currentSeriesId || !confirm("TVDB-Zuordnung wirklich loesen?")) return;
fetch(`/api/library/series/${currentSeriesId}/tvdb`, {method: "DELETE"})
.then(r => r.json())
.then(data => {
if (data.error) alert("Fehler: " + data.error);
else { closeSeriesModal(); reloadAllSections(); }
})
.catch(e => alert("Fehler: " + e));
}
function downloadMetadata() {
if (!currentSeriesId) return;
const btn = document.getElementById("btn-metadata-dl");
btn.textContent = "Laden...";
btn.disabled = true;
fetch(`/api/library/series/${currentSeriesId}/metadata-download`, {method: "POST"})
.then(r => r.json())
.then(data => {
btn.textContent = "Metadaten laden";
btn.disabled = false;
if (data.error) alert("Fehler: " + data.error);
else alert(`${data.downloaded || 0} Dateien heruntergeladen, ${data.errors || 0} Fehler`);
})
.catch(e => { btn.textContent = "Metadaten laden"; btn.disabled = false; alert("Fehler: " + e); });
}
function deleteSeries(withFiles) {
if (!currentSeriesId) return;
if (withFiles) {
if (!confirm("ACHTUNG: Serie komplett loeschen?\n\nAlle Dateien und Ordner werden UNWIDERRUFLICH geloescht!")) return;
if (!confirm("Wirklich sicher? Dieser Vorgang kann NICHT rueckgaengig gemacht werden!")) return;
} else {
if (!confirm("Serie aus der Datenbank loeschen?\n(Dateien bleiben erhalten)")) return;
}
const url = withFiles
? `/api/library/series/${currentSeriesId}?delete_files=1`
: `/api/library/series/${currentSeriesId}`;
fetch(url, {method: "DELETE"})
.then(r => r.json())
.then(data => {
if (data.error) { alert("Fehler: " + data.error); return; }
let msg = "Serie aus DB geloescht.";
if (data.deleted_folder) msg += "\nOrdner geloescht: " + data.deleted_folder;
if (data.folder_error) msg += "\nOrdner-Fehler: " + data.folder_error;
alert(msg);
closeSeriesModal();
reloadAllSections();
loadStats();
})
.catch(e => alert("Fehler: " + e));
}
// === Bestaetigungs-Dialog ===
let pendingConfirmAction = null;
function showDeleteFolderDialog(folderPath, pathId, videoCount) {
const folderName = folderPath.split('/').pop();
document.getElementById("confirm-title").textContent = "Ordner loeschen";
document.getElementById("confirm-icon").innerHTML = `
`;
document.getElementById("confirm-message").innerHTML = `
${escapeHtml(folderName)}
wirklich loeschen?`;
document.getElementById("confirm-detail").innerHTML = `
${videoCount} Video${videoCount !== 1 ? 's' : ''} werden unwiderruflich geloescht.
Dieser Vorgang kann nicht rueckgaengig gemacht werden! `;
document.getElementById("confirm-btn-ok").textContent = "Endgueltig loeschen";
document.getElementById("confirm-modal").style.display = "flex";
pendingConfirmAction = () => executeDeleteFolder(folderPath, pathId);
}
function closeConfirmModal() {
document.getElementById("confirm-modal").style.display = "none";
pendingConfirmAction = null;
}
function confirmAction() {
if (pendingConfirmAction) {
pendingConfirmAction();
}
closeConfirmModal();
}
function executeDeleteFolder(folderPath, pathId) {
fetch("/api/library/delete-folder", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({folder_path: folderPath})
})
.then(r => r.json())
.then(data => {
if (data.error) {
showToast("Fehler: " + data.error, "error");
return;
}
const msg = `${data.deleted_files || 0} Dateien geloescht`;
showToast(msg, "success");
if (pathId) loadSectionData(pathId);
loadStats();
})
.catch(e => showToast("Fehler: " + e, "error"));
}
function showToast(message, type = "info") {
const container = document.getElementById("toast-container");
if (!container) return;
const toast = document.createElement("div");
toast.className = `toast toast-${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => toast.classList.add("show"), 10);
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => toast.remove(), 300);
}, 4000);
}
// === Film-Detail ===
function openMovieDetail(movieId) {
if (event) event.stopPropagation();
currentMovieId = movieId;
document.getElementById("movie-modal").style.display = "flex";
fetch(`/api/library/movies/${movieId}`)
.then(r => r.json())
.then(data => {
document.getElementById("movie-modal-title").textContent =
data.title || data.folder_name;
document.getElementById("movie-modal-genres").textContent =
data.genres || "";
// Aktions-Buttons
document.getElementById("btn-movie-tvdb-unlink").style.display =
data.tvdb_id ? "" : "none";
let html = '';
// Video-Dateien des Films
const videos = data.videos || [];
if (videos.length) {
html += 'Video-Dateien ';
html += '';
html += 'Datei Aufl. Codec Audio Groesse Dauer Aktion ';
html += ' ';
for (const v of videos) {
const audioInfo = (v.audio_tracks || []).map(a => {
const lang = (a.lang || "?").toUpperCase().substring(0, 3);
return `${lang} ${channelLayout(a.channels)} `;
}).join(" ");
const res = v.width && v.height ? resolutionLabel(v.width, v.height) : "-";
const movieTitle = v.file_name || "Video";
html += `
${escapeHtml(v.file_name || "-")}
${res}${v.is_10bit ? ' 10bit ' : ''}
${v.video_codec || "-"}
${audioInfo || "-"}
${formatSize(v.file_size || 0)}
${formatDuration(v.duration_sec || 0)}
▶
Conv
✕
`;
}
html += '
';
}
document.getElementById("movie-modal-body").innerHTML = html;
})
.catch(() => {
document.getElementById("movie-modal-body").innerHTML =
'Fehler beim Laden
';
});
}
function closeMovieModal() {
document.getElementById("movie-modal").style.display = "none";
currentMovieId = null;
}
function movieTvdbUnlink() {
if (!currentMovieId || !confirm("TVDB-Zuordnung wirklich loesen?")) return;
fetch(`/api/library/movies/${currentMovieId}/tvdb`, {method: "DELETE"})
.then(r => r.json())
.then(data => {
if (data.error) alert("Fehler: " + data.error);
else { closeMovieModal(); reloadAllSections(); }
})
.catch(e => alert("Fehler: " + e));
}
function deleteMovie(withFiles) {
if (!currentMovieId) return;
if (withFiles) {
if (!confirm("ACHTUNG: Film komplett loeschen?\n\nAlle Dateien werden UNWIDERRUFLICH geloescht!")) return;
if (!confirm("Wirklich sicher?")) return;
} else {
if (!confirm("Film aus der Datenbank loeschen?\n(Dateien bleiben erhalten)")) return;
}
const url = withFiles
? `/api/library/movies/${currentMovieId}?delete_files=1`
: `/api/library/movies/${currentMovieId}`;
fetch(url, {method: "DELETE"})
.then(r => r.json())
.then(data => {
if (data.error) { alert("Fehler: " + data.error); return; }
alert("Film aus DB geloescht." + (data.deleted_folder ? "\nOrdner geloescht." : ""));
closeMovieModal();
reloadAllSections();
loadStats();
})
.catch(e => alert("Fehler: " + e));
}
// === Film-TVDB-Zuordnung ===
function openMovieTvdbModal(movieId, title) {
document.getElementById("movie-tvdb-modal").style.display = "flex";
document.getElementById("movie-tvdb-id").value = movieId;
document.getElementById("movie-tvdb-search-input").value = cleanSearchTitle(title);
document.getElementById("movie-tvdb-results").innerHTML = "";
searchMovieTvdb();
}
function closeMovieTvdbModal() {
document.getElementById("movie-tvdb-modal").style.display = "none";
}
function searchMovieTvdb() {
const query = document.getElementById("movie-tvdb-search-input").value.trim();
if (!query) return;
const results = document.getElementById("movie-tvdb-results");
results.innerHTML = 'Suche...
';
fetch(`/api/tvdb/search-movies?q=${encodeURIComponent(query)}`)
.then(r => r.json())
.then(data => {
if (data.error) { results.innerHTML = `${escapeHtml(data.error)}
`; return; }
if (!data.results || !data.results.length) { results.innerHTML = 'Keine Ergebnisse
'; return; }
results.innerHTML = data.results.map(r => `
${r.poster ? `
` : ""}
${escapeHtml(r.name)}
${r.year || ""}
${escapeHtml((r.overview || "").substring(0, 150))}
`).join("");
})
.catch(e => { results.innerHTML = `Fehler: ${e}
`; });
}
function matchMovieTvdb(tvdbId) {
const movieId = document.getElementById("movie-tvdb-id").value;
const results = document.getElementById("movie-tvdb-results");
results.innerHTML = 'Verknuepfe...
';
fetch(`/api/library/movies/${movieId}/tvdb-match`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({tvdb_id: tvdbId}),
})
.then(r => r.json())
.then(data => {
if (data.error) { results.innerHTML = `${escapeHtml(data.error)}
`; }
else { closeMovieTvdbModal(); reloadAllSections(); }
})
.catch(e => { results.innerHTML = `Fehler: ${e}
`; });
}
let movieTvdbSearchTimer = null;
function debounceMovieTvdbSearch() {
if (movieTvdbSearchTimer) clearTimeout(movieTvdbSearchTimer);
movieTvdbSearchTimer = setTimeout(searchMovieTvdb, 500);
}
// === Filter ===
let filterPresets = {};
let defaultView = "all";
function buildFilterParams() {
const params = new URLSearchParams();
const search = document.getElementById("filter-search").value.trim();
if (search) params.set("search", search);
const codec = document.getElementById("filter-codec").value;
if (codec) params.set("video_codec", codec);
const container = document.getElementById("filter-container").value;
if (container) params.set("container", container);
const audioLang = document.getElementById("filter-audio-lang").value;
if (audioLang) params.set("audio_lang", audioLang);
const audioCh = document.getElementById("filter-audio-ch").value;
if (audioCh) params.set("audio_channels", audioCh);
if (document.getElementById("filter-10bit").checked) params.set("is_10bit", "1");
if (document.getElementById("filter-not-converted").checked) params.set("not_converted", "1");
const resolution = document.getElementById("filter-resolution").value;
if (resolution) params.set("min_width", resolution);
const sort = document.getElementById("filter-sort").value;
if (sort) params.set("sort", sort);
const order = document.getElementById("filter-order").value;
if (order) params.set("order", order);
return params;
}
function applyFilters() {
// Preset-Auswahl zuruecksetzen wenn manuell gefiltert wird
document.getElementById("filter-preset").value = "";
for (const pid of Object.keys(sectionStates)) {
sectionStates[pid].page = 1;
loadSectionData(parseInt(pid));
}
}
function debounceFilter() {
if (filterTimeout) clearTimeout(filterTimeout);
filterTimeout = setTimeout(applyFilters, 400);
}
// Filter-Presets laden
async function loadFilterPresets() {
try {
const resp = await fetch("/api/library/filter-presets");
const data = await resp.json();
filterPresets = data.presets || {};
defaultView = data.default_view || "all";
// Preset-Dropdown befuellen
const select = document.getElementById("filter-preset");
// Bestehende Custom-Presets entfernen (4 feste Optionen behalten: Alle, Nicht konvertiert, Alte Formate, Fehlende Episoden)
while (select.options.length > 4) {
select.remove(4);
}
// Gespeicherte Presets hinzufuegen
for (const [id, preset] of Object.entries(filterPresets)) {
const opt = document.createElement("option");
opt.value = id;
opt.textContent = preset.name || id;
select.appendChild(opt);
}
// Standard-Ansicht anwenden
if (defaultView && defaultView !== "all") {
applyPreset(defaultView);
}
} catch (e) {
console.error("Filter-Presets laden fehlgeschlagen:", e);
}
}
// Preset anwenden
let showMissingMode = false;
function applyPreset(presetId) {
const id = presetId || document.getElementById("filter-preset").value;
// Alle Filter zuruecksetzen
resetFiltersQuiet();
showMissingMode = false;
if (!id) {
// "Alle anzeigen"
applyFilters();
return;
}
// Eingebaute Presets
if (id === "not_converted") {
document.getElementById("filter-not-converted").checked = true;
} else if (id === "old_formats") {
// Alte Formate = alles ausser AV1
document.getElementById("filter-codec").value = "";
document.getElementById("filter-not-converted").checked = true;
} else if (id === "missing_episodes") {
// Fehlende Episoden - spezieller Modus
showMissingMode = true;
loadMissingEpisodes();
document.getElementById("filter-preset").value = id;
return;
} else if (filterPresets[id]) {
// Custom Preset
const p = filterPresets[id];
if (p.video_codec) document.getElementById("filter-codec").value = p.video_codec;
if (p.container) document.getElementById("filter-container").value = p.container;
if (p.min_width) document.getElementById("filter-resolution").value = p.min_width;
if (p.audio_lang) document.getElementById("filter-audio-lang").value = p.audio_lang;
if (p.is_10bit) document.getElementById("filter-10bit").checked = true;
if (p.not_converted) document.getElementById("filter-not-converted").checked = true;
if (p.show_missing) {
showMissingMode = true;
loadMissingEpisodes();
document.getElementById("filter-preset").value = id;
return;
}
}
document.getElementById("filter-preset").value = id;
for (const pid of Object.keys(sectionStates)) {
sectionStates[pid].page = 1;
loadSectionData(parseInt(pid));
}
}
// Fehlende Episoden laden und anzeigen
let missingPage = 1;
async function loadMissingEpisodes(page = 1) {
missingPage = page;
const container = document.getElementById("library-content");
container.innerHTML = 'Lade fehlende Episoden...
';
try {
const resp = await fetch(`/api/library/missing-episodes?page=${page}&limit=50`);
const data = await resp.json();
if (!data.items || data.items.length === 0) {
container.innerHTML = 'Keine fehlenden Episoden gefunden. Alle Serien sind vollstaendig!
';
return;
}
// Gruppiere nach Serie
const bySeries = {};
for (const ep of data.items) {
const key = ep.series_id;
if (!bySeries[key]) {
bySeries[key] = {
series_id: ep.series_id,
series_title: ep.series_title,
poster_url: ep.poster_url,
episodes: []
};
}
bySeries[key].episodes.push(ep);
}
let html = `
Fehlende Episoden (${data.total} insgesamt) `;
for (const series of Object.values(bySeries)) {
html += `
`;
for (const ep of series.episodes) {
const aired = ep.aired ? ` (${ep.aired})` : '';
html += `
S${String(ep.season_number).padStart(2,'0')}E${String(ep.episode_number).padStart(2,'0')}
${ep.episode_name || 'Unbekannt'}${aired}
`;
}
html += `
`;
}
// Pagination
if (data.pages > 1) {
html += '';
}
html += '
';
container.innerHTML = html;
} catch (e) {
container.innerHTML = `Fehler: ${e}
`;
}
}
// Filter zuruecksetzen (ohne Reload)
function resetFiltersQuiet() {
document.getElementById("filter-search").value = "";
document.getElementById("filter-codec").value = "";
document.getElementById("filter-container").value = "";
document.getElementById("filter-resolution").value = "";
document.getElementById("filter-audio-lang").value = "";
document.getElementById("filter-audio-ch").value = "";
document.getElementById("filter-10bit").checked = false;
document.getElementById("filter-not-converted").checked = false;
document.getElementById("filter-sort").value = "file_name";
document.getElementById("filter-order").value = "asc";
}
// Filter zuruecksetzen (mit Reload)
function resetFilters() {
resetFiltersQuiet();
document.getElementById("filter-preset").value = "";
applyFilters();
}
// Aktuellen Filter als Preset speichern
async function saveCurrentFilter() {
const name = prompt("Name fuer diesen Filter:");
if (!name) return;
const id = name.toLowerCase().replace(/[^a-z0-9]/g, "_");
const filters = {};
const codec = document.getElementById("filter-codec").value;
if (codec) filters.video_codec = codec;
const container = document.getElementById("filter-container").value;
if (container) filters.container = container;
const resolution = document.getElementById("filter-resolution").value;
if (resolution) filters.min_width = resolution;
const audioLang = document.getElementById("filter-audio-lang").value;
if (audioLang) filters.audio_lang = audioLang;
if (document.getElementById("filter-10bit").checked) filters.is_10bit = true;
if (document.getElementById("filter-not-converted").checked) filters.not_converted = true;
try {
const resp = await fetch("/api/library/filter-presets", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({id, name, filters}),
});
if (resp.ok) {
showToast(`Filter "${name}" gespeichert`, "success");
loadFilterPresets();
} else {
showToast("Fehler beim Speichern", "error");
}
} catch (e) {
showToast("Fehler: " + e, "error");
}
}
// Aktuellen Filter als Standard setzen
async function setAsDefault() {
const presetId = document.getElementById("filter-preset").value || "all";
try {
const resp = await fetch("/api/library/default-view", {
method: "PUT",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({default_view: presetId}),
});
if (resp.ok) {
defaultView = presetId;
showToast("Standard-Ansicht gespeichert", "success");
} else {
showToast("Fehler beim Speichern", "error");
}
} catch (e) {
showToast("Fehler: " + e, "error");
}
}
// === Scan ===
function startScan() {
const progress = document.getElementById("scan-progress");
progress.style.display = "block";
document.getElementById("scan-status").textContent = "Scan wird gestartet...";
document.getElementById("scan-bar").style.width = "0%";
fetch("/api/library/scan", {method: "POST"})
.then(() => pollScanStatus())
.catch(e => { document.getElementById("scan-status").textContent = "Fehler: " + e; });
}
function scanSinglePath(pathId) {
const progress = document.getElementById("scan-progress");
progress.style.display = "block";
document.getElementById("scan-status").textContent = "Scan wird gestartet...";
document.getElementById("scan-bar").style.width = "0%";
fetch(`/api/library/scan/${pathId}`, {method: "POST"})
.then(() => pollScanStatus())
.catch(e => { document.getElementById("scan-status").textContent = "Fehler: " + e; });
}
function pollScanStatus() {
const interval = setInterval(() => {
fetch("/api/library/scan-status")
.then(r => r.json())
.then(data => {
if (data.status === "idle") {
clearInterval(interval);
document.getElementById("scan-progress").style.display = "none";
loadStats();
reloadAllSections();
} else {
const pct = data.total > 0 ? Math.round((data.done / data.total) * 100) : 0;
document.getElementById("scan-bar").style.width = pct + "%";
document.getElementById("scan-status").textContent =
`Scanne: ${data.current || ""} (${data.done || 0}/${data.total || 0})`;
}
})
.catch(() => clearInterval(interval));
}, 1000);
}
function reloadAllSections() {
for (const pid of Object.keys(sectionStates)) {
loadSectionData(parseInt(pid));
}
}
// === TVDB Auto-Match (Review-Modus) ===
let tvdbReviewData = []; // Vorschlaege die noch geprueft werden muessen
function startAutoMatch() {
if (!confirm("TVDB-Vorschlaege fuer alle nicht-zugeordneten Serien und Filme sammeln?\n\nDas kann einige Minuten dauern. Du kannst danach jeden Vorschlag pruefen und bestaetigen.")) return;
const progress = document.getElementById("auto-match-progress");
progress.style.display = "block";
document.getElementById("auto-match-status").textContent = "Suche TVDB-Vorschlaege...";
document.getElementById("auto-match-bar").style.width = "0%";
fetch("/api/library/tvdb-auto-match?type=all", {method: "POST"})
.then(r => r.json())
.then(data => {
if (data.error) {
document.getElementById("auto-match-status").textContent = "Fehler: " + data.error;
setTimeout(() => { progress.style.display = "none"; }, 3000);
return;
}
pollAutoMatchStatus();
})
.catch(e => {
document.getElementById("auto-match-status").textContent = "Fehler: " + e;
});
}
function pollAutoMatchStatus() {
const progress = document.getElementById("auto-match-progress");
const interval = setInterval(() => {
fetch("/api/library/tvdb-auto-match-status")
.then(r => r.json())
.then(data => {
const bar = document.getElementById("auto-match-bar");
const status = document.getElementById("auto-match-status");
if (data.phase === "done") {
clearInterval(interval);
bar.style.width = "100%";
const suggestions = data.suggestions || [];
// Nur Items mit mindestens einem Vorschlag anzeigen
const withSuggestions = suggestions.filter(s => s.suggestions && s.suggestions.length > 0);
const noResults = suggestions.length - withSuggestions.length;
status.textContent = `${withSuggestions.length} Vorschlaege gefunden, ${noResults} ohne Ergebnis`;
setTimeout(() => {
progress.style.display = "none";
}, 2000);
if (withSuggestions.length > 0) {
openTvdbReviewModal(withSuggestions);
}
} else if (data.phase === "error") {
clearInterval(interval);
status.textContent = "Fehler beim Sammeln der Vorschlaege";
setTimeout(() => { progress.style.display = "none"; }, 3000);
} else if (!data.active && data.phase !== "done") {
clearInterval(interval);
progress.style.display = "none";
} else {
const pct = data.total > 0 ? Math.round((data.done / data.total) * 100) : 0;
bar.style.width = pct + "%";
const phase = data.phase === "series" ? "Serien" : "Filme";
status.textContent = `${phase}: ${data.current || ""} (${data.done}/${data.total})`;
}
})
.catch(() => clearInterval(interval));
}, 1000);
}
// === TVDB Review-Modal ===
function openTvdbReviewModal(suggestions) {
tvdbReviewData = suggestions;
document.getElementById("tvdb-review-modal").style.display = "flex";
renderTvdbReviewList();
}
function closeTvdbReviewModal() {
document.getElementById("tvdb-review-modal").style.display = "none";
tvdbReviewData = [];
reloadAllSections();
loadStats();
}
function renderTvdbReviewList() {
const list = document.getElementById("tvdb-review-list");
const remaining = tvdbReviewData.filter(item => !item._confirmed && !item._skipped);
const confirmed = tvdbReviewData.filter(item => item._confirmed);
const skipped = tvdbReviewData.filter(item => item._skipped);
document.getElementById("tvdb-review-info").textContent =
`${remaining.length} offen, ${confirmed.length} zugeordnet, ${skipped.length} uebersprungen`;
if (!tvdbReviewData.length) {
list.innerHTML = 'Keine Vorschlaege vorhanden
';
return;
}
let html = '';
for (let i = 0; i < tvdbReviewData.length; i++) {
const item = tvdbReviewData[i];
const typeLabel = item.type === "series" ? "Serie" : "Film";
const typeClass = item.type === "series" ? "tag-series" : "tag-movie";
// Status-Klasse
let statusClass = "";
let statusHtml = "";
if (item._confirmed) {
statusClass = "review-item-done";
statusHtml = `Zugeordnet: ${escapeHtml(item._confirmedName || "")} `;
} else if (item._skipped) {
statusClass = "review-item-skipped";
statusHtml = 'Uebersprungen ';
}
html += ``;
html += ``;
// Vorschlaege anzeigen (nur wenn noch nicht bestaetigt/uebersprungen)
if (!item._confirmed && !item._skipped) {
html += `
`;
if (!item.suggestions || !item.suggestions.length) {
html += '
Keine Vorschlaege gefunden ';
} else {
for (const s of item.suggestions) {
const poster = s.poster
? `
`
: '
?
';
html += `
`;
html += poster;
html += `
`;
html += `
${escapeHtml(s.name)} `;
if (s.year) html += `
(${s.year}) `;
if (s.overview) html += `
${escapeHtml(s.overview)}
`;
html += `
`;
html += `
`;
}
}
// Manuelles Suchfeld (versteckt, wird bei Klick auf "Manuell suchen" angezeigt)
html += `
`;
html += `
`;
html += `
Suchen `;
html += `
`;
html += `
`;
html += `
`;
}
html += `
`;
}
list.innerHTML = html;
}
function confirmReviewItem(index, tvdbId, name) {
const item = tvdbReviewData[index];
if (item._confirmed || item._skipped) return;
// Visuelles Feedback
const el = document.getElementById("review-item-" + index);
if (el) el.classList.add("review-item-loading");
fetch("/api/library/tvdb-confirm", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({id: item.id, type: item.type, tvdb_id: tvdbId}),
})
.then(r => r.json())
.then(data => {
if (data.error) {
alert("Fehler: " + data.error);
if (el) el.classList.remove("review-item-loading");
return;
}
item._confirmed = true;
item._confirmedName = data.name || name;
renderTvdbReviewList();
})
.catch(e => {
alert("Fehler: " + e);
if (el) el.classList.remove("review-item-loading");
});
}
function skipReviewItem(index) {
tvdbReviewData[index]._skipped = true;
renderTvdbReviewList();
}
function skipAllReviewItems() {
for (const item of tvdbReviewData) {
if (!item._confirmed) item._skipped = true;
}
renderTvdbReviewList();
}
function manualTvdbSearchReview(index) {
const el = document.getElementById("review-manual-" + index);
if (el) {
el.style.display = el.style.display === "none" ? "flex" : "none";
}
}
function executeManualReviewSearch(index) {
const item = tvdbReviewData[index];
const input = document.getElementById("review-search-input-" + index);
const results = document.getElementById("review-search-results-" + index);
const query = input ? input.value.trim() : "";
if (!query) return;
results.innerHTML = 'Suche...
';
const endpoint = item.type === "series"
? `/api/tvdb/search?q=${encodeURIComponent(query)}`
: `/api/tvdb/search-movies?q=${encodeURIComponent(query)}`;
fetch(endpoint)
.then(r => r.json())
.then(data => {
if (data.error) {
results.innerHTML = `${escapeHtml(data.error)}
`;
return;
}
const list = data.results || [];
if (!list.length) {
results.innerHTML = 'Keine Ergebnisse
';
return;
}
results.innerHTML = list.map(r => `
${r.poster ? `
` : '
?
'}
${escapeHtml(r.name)}
${r.year ? `
(${r.year}) ` : ""}
${escapeHtml((r.overview || "").substring(0, 120))}
`).join("");
})
.catch(e => { results.innerHTML = `Fehler: ${e}
`; });
}
// === TVDB Modal ===
function openTvdbModal(seriesId, folderName) {
document.getElementById("tvdb-modal").style.display = "flex";
document.getElementById("tvdb-series-id").value = seriesId;
document.getElementById("tvdb-search-input").value = cleanSearchTitle(folderName);
document.getElementById("tvdb-results").innerHTML = "";
// Checkbox zuruecksetzen
const engCheckbox = document.getElementById("tvdb-search-english");
if (engCheckbox) engCheckbox.checked = false;
searchTvdb();
}
function closeTvdbModal() {
document.getElementById("tvdb-modal").style.display = "none";
}
function searchTvdb() {
const query = document.getElementById("tvdb-search-input").value.trim();
if (!query) return;
const results = document.getElementById("tvdb-results");
results.innerHTML = 'Suche...
';
// Sprache: eng wenn Checkbox aktiv, sonst Standard (deu)
const useEnglish = document.getElementById("tvdb-search-english")?.checked;
const langParam = useEnglish ? "&lang=eng" : "";
fetch(`/api/tvdb/search?q=${encodeURIComponent(query)}${langParam}`)
.then(r => r.json())
.then(data => {
if (data.error) { results.innerHTML = `${escapeHtml(data.error)}
`; return; }
if (!data.results || !data.results.length) { results.innerHTML = 'Keine Ergebnisse
'; return; }
results.innerHTML = data.results.map(r => {
// Zeige Original-Namen wenn vorhanden und unterschiedlich
const origName = r.original_name && r.original_name !== r.name
? `(${escapeHtml(r.original_name)}) `
: "";
return `
${r.poster ? `
` : ""}
${escapeHtml(r.name)} ${origName}
${r.year || ""}
${escapeHtml((r.overview || "").substring(0, 150))}
`}).join("");
})
.catch(e => { results.innerHTML = `Fehler: ${e}
`; });
}
function matchTvdb(tvdbId) {
const seriesId = document.getElementById("tvdb-series-id").value;
const results = document.getElementById("tvdb-results");
results.innerHTML = 'Verknuepfe...
';
fetch(`/api/library/series/${seriesId}/tvdb-match`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({tvdb_id: tvdbId}),
})
.then(r => r.json())
.then(data => {
if (data.error) { results.innerHTML = `${escapeHtml(data.error)}
`; }
else { closeTvdbModal(); reloadAllSections(); }
})
.catch(e => { results.innerHTML = `Fehler: ${e}
`; });
}
let tvdbSearchTimer = null;
function debounceTvdbSearch() {
if (tvdbSearchTimer) clearTimeout(tvdbSearchTimer);
tvdbSearchTimer = setTimeout(searchTvdb, 500);
}
// === Konvertierung ===
function convertVideo(videoId) {
if (event) event.stopPropagation();
if (!confirm("Video zur Konvertierung senden?")) return;
fetch(`/api/library/videos/${videoId}/convert`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({}),
})
.then(r => r.json())
.then(data => {
if (data.error) alert("Fehler: " + data.error);
else alert("Job erstellt: " + (data.message || "OK"));
})
.catch(e => alert("Fehler: " + e));
}
// === Serie komplett konvertieren ===
function openConvertSeriesModal() {
if (!currentSeriesId) return;
document.getElementById("convert-series-modal").style.display = "flex";
document.getElementById("convert-series-status").innerHTML =
'Lade Codec-Status...
';
// Codec-Status laden
fetch(`/api/library/series/${currentSeriesId}/convert-status`)
.then(r => r.json())
.then(data => {
if (data.error) {
document.getElementById("convert-series-status").innerHTML =
`${escapeHtml(data.error)}
`;
return;
}
let html = `${data.total} Episoden
`;
html += '';
for (const [codec, count] of Object.entries(data.codec_counts || {})) {
const isTarget = codec.includes("av1") || codec.includes("hevc");
const cls = isTarget ? "tag ok" : "tag";
html += `${codec}: ${count} `;
}
html += '
';
document.getElementById("convert-series-status").innerHTML = html;
})
.catch(e => {
document.getElementById("convert-series-status").innerHTML =
`Fehler: ${e}
`;
});
}
function closeConvertSeriesModal() {
document.getElementById("convert-series-modal").style.display = "none";
}
function executeConvertSeries() {
if (!currentSeriesId) return;
const targetCodec = document.getElementById("convert-target-codec").value;
const forceAll = document.getElementById("convert-force-all").checked;
const deleteOld = document.getElementById("convert-delete-old").checked;
const btn = document.querySelector("#convert-series-modal .btn-primary");
btn.textContent = "Wird gestartet...";
btn.disabled = true;
fetch(`/api/library/series/${currentSeriesId}/convert`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
target_codec: targetCodec,
force_all: forceAll,
delete_old: deleteOld,
}),
})
.then(r => r.json())
.then(data => {
btn.textContent = "Konvertierung starten";
btn.disabled = false;
if (data.error) {
alert("Fehler: " + data.error);
return;
}
let msg = data.message || "Konvertierung gestartet";
if (data.already_done > 0) {
msg += `\n${data.already_done} Episoden sind bereits im Zielformat.`;
}
alert(msg);
closeConvertSeriesModal();
})
.catch(e => {
btn.textContent = "Konvertierung starten";
btn.disabled = false;
alert("Fehler: " + e);
});
}
// === Serien-Ordner aufraeumen ===
function cleanupSeriesFolder() {
if (!currentSeriesId) return;
if (!confirm("Alle Dateien im Serien-Ordner loeschen, die NICHT in der Bibliothek sind?\n\n" +
"Behalten werden:\n" +
"- Alle Videos in der Bibliothek\n" +
"- .metadata Ordner\n" +
"- .nfo, .jpg, .png Dateien\n\n" +
"ACHTUNG: Dies kann nicht rueckgaengig gemacht werden!")) {
return;
}
fetch(`/api/library/series/${currentSeriesId}/cleanup`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({}),
})
.then(r => r.json())
.then(data => {
if (data.error) {
alert("Fehler: " + data.error);
return;
}
let msg = `${data.deleted} Dateien geloescht.`;
if (data.errors > 0) {
msg += `\n${data.errors} Fehler.`;
}
alert(msg);
})
.catch(e => alert("Fehler: " + e));
}
// === Duplikate ===
function showDuplicates() {
document.getElementById("duplicates-modal").style.display = "flex";
const list = document.getElementById("duplicates-list");
list.innerHTML = 'Suche Duplikate...
';
fetch("/api/library/duplicates")
.then(r => r.json())
.then(data => {
const dupes = data.duplicates || [];
if (!dupes.length) { list.innerHTML = 'Keine Duplikate gefunden
'; return; }
list.innerHTML = dupes.map(d => `
${escapeHtml(d.name1)}
${d.codec1}
${d.width1}x${d.height1}
${formatSize(d.size1)}
vs
${escapeHtml(d.name2)}
${d.codec2}
${d.width2}x${d.height2}
${formatSize(d.size2)}
`).join("");
})
.catch(e => { list.innerHTML = `Fehler: ${e}
`; });
}
function closeDuplicatesModal() {
document.getElementById("duplicates-modal").style.display = "none";
}
// === Pfade-Verwaltung ===
function openPathsModal() {
document.getElementById("paths-modal").style.display = "flex";
loadPathsList();
}
function closePathsModal() {
document.getElementById("paths-modal").style.display = "none";
}
function loadPathsList() {
fetch("/api/library/paths")
.then(r => r.json())
.then(data => {
const paths = data.paths || [];
const list = document.getElementById("paths-list");
if (!paths.length) {
list.innerHTML = 'Keine Pfade konfiguriert
';
return;
}
list.innerHTML = paths.map(p => `
${escapeHtml(p.name)}
${escapeHtml(p.path)}
${p.media_type === 'series' ? 'Serien' : 'Filme'}
${p.enabled ? 'Aktiv ' : 'Deaktiviert '}
${p.video_count !== undefined ? `${p.video_count} Videos ` : ""}
${p.enabled ? 'Deaktivieren' : 'Aktivieren'}
Loeschen
`).join("");
})
.catch(() => {});
}
function addPath() {
const name = document.getElementById("new-path-name").value.trim();
const path = document.getElementById("new-path-path").value.trim();
const media_type = document.getElementById("new-path-type").value;
if (!name || !path) { alert("Name und Pfad erforderlich"); return; }
fetch("/api/library/paths", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({name, path, media_type}),
})
.then(r => r.json())
.then(data => {
if (data.error) alert("Fehler: " + data.error);
else {
document.getElementById("new-path-name").value = "";
document.getElementById("new-path-path").value = "";
loadPathsList();
loadLibraryPaths();
}
})
.catch(e => alert("Fehler: " + e));
}
function togglePath(pathId, enabled) {
fetch(`/api/library/paths/${pathId}`, {
method: "PUT",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({enabled}),
})
.then(r => r.json())
.then(data => {
if (data.error) alert("Fehler: " + data.error);
else { loadPathsList(); loadLibraryPaths(); }
})
.catch(e => alert("Fehler: " + e));
}
function deletePath(pathId) {
if (!confirm("Pfad wirklich loeschen? (Videos bleiben erhalten, aber werden aus DB entfernt)")) return;
fetch(`/api/library/paths/${pathId}`, {method: "DELETE"})
.then(r => r.json())
.then(data => {
if (data.error) alert("Fehler: " + data.error);
else { loadPathsList(); loadLibraryPaths(); }
})
.catch(e => alert("Fehler: " + e));
}
// === Clean-Modal ===
function openCleanModal() {
document.getElementById("clean-modal").style.display = "flex";
document.getElementById("clean-list").innerHTML =
'Klicke "Junk scannen" um zu starten
';
document.getElementById("clean-info").textContent = "";
cleanData = [];
}
function closeCleanModal() {
document.getElementById("clean-modal").style.display = "none";
}
function scanForJunk() {
const list = document.getElementById("clean-list");
list.innerHTML = 'Scanne...
';
fetch("/api/library/clean/scan")
.then(r => r.json())
.then(data => {
cleanData = data.files || [];
document.getElementById("clean-info").textContent =
`${data.total_count || 0} Dateien, ${formatSize(data.total_size || 0)}`;
// Extensions-Filter fuellen
const exts = new Set(cleanData.map(f => f.extension));
const select = document.getElementById("clean-ext-filter");
select.innerHTML = 'Alle ';
for (const ext of [...exts].sort()) {
select.innerHTML += `${ext} `;
}
renderCleanList(cleanData);
})
.catch(e => { list.innerHTML = `Fehler: ${e}
`; });
}
function renderCleanList(files) {
const list = document.getElementById("clean-list");
if (!files.length) {
list.innerHTML = 'Keine Junk-Dateien gefunden
';
return;
}
let html = '';
list.innerHTML = html;
}
function filterCleanList() {
const ext = document.getElementById("clean-ext-filter").value;
if (!ext) { renderCleanList(cleanData); return; }
renderCleanList(cleanData.filter(f => f.extension === ext));
}
function toggleCleanSelectAll() {
const checked = document.getElementById("clean-select-all")?.checked
|| document.getElementById("clean-select-all-header")?.checked || false;
document.querySelectorAll(".clean-check").forEach(cb => cb.checked = checked);
}
function deleteSelectedJunk() {
const checked = document.querySelectorAll(".clean-check:checked");
if (!checked.length) { alert("Keine Dateien ausgewaehlt"); return; }
if (!confirm(`${checked.length} Dateien wirklich loeschen?`)) return;
const files = [];
checked.forEach(cb => {
const idx = parseInt(cb.getAttribute("data-idx"));
if (cleanData[idx]) files.push(cleanData[idx].path);
});
fetch("/api/library/clean/delete", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({files}),
})
.then(r => r.json())
.then(data => {
alert(`${data.deleted || 0} geloescht, ${data.failed || 0} fehlgeschlagen`);
if (data.errors && data.errors.length) console.warn("Clean-Fehler:", data.errors);
scanForJunk();
})
.catch(e => alert("Fehler: " + e));
}
function deleteEmptyDirs() {
fetch("/api/library/clean/empty-dirs", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({}),
})
.then(r => r.json())
.then(data => alert(`${data.deleted_dirs || 0} leere Ordner geloescht`))
.catch(e => alert("Fehler: " + e));
}
// === Import-Modal ===
let importBrowseTimer = null;
function openImportModal() {
document.getElementById("import-modal").style.display = "flex";
document.getElementById("import-setup").style.display = "";
document.getElementById("import-preview").style.display = "none";
document.getElementById("import-progress").style.display = "none";
document.getElementById("import-source").value = "";
document.getElementById("import-folder-info").textContent = "";
document.getElementById("btn-analyze-import").disabled = true;
currentImportJobId = null;
// Ziel-Libraries laden
fetch("/api/library/paths")
.then(r => r.json())
.then(data => {
const select = document.getElementById("import-target");
select.innerHTML = (data.paths || []).map(p =>
`${escapeHtml(p.name)} (${p.media_type === 'series' ? 'Serien' : 'Filme'}) `
).join("");
})
.catch(() => {});
// Bestehende Import-Jobs laden
loadExistingImportJobs();
// Standard-Pfad im Filebrowser oeffnen
importBrowse("/mnt");
}
function loadExistingImportJobs() {
fetch("/api/library/import")
.then(r => r.json())
.then(data => {
const jobs = (data.jobs || []).filter(j => j.status !== 'done');
const container = document.getElementById("import-existing");
const list = document.getElementById("import-jobs-list");
if (!jobs.length) {
container.style.display = "none";
return;
}
container.style.display = "";
list.innerHTML = jobs.map(j => {
const statusClass = j.status === 'ready' ? 'tag-success' :
j.status === 'error' ? 'tag-error' :
j.status === 'importing' ? 'tag-warning' : '';
const statusText = j.status === 'ready' ? 'Bereit' :
j.status === 'analyzing' ? 'Analyse...' :
j.status === 'importing' ? 'Laeuft' :
j.status === 'error' ? 'Fehler' : j.status;
const sourceName = j.source_path.split('/').pop();
return `
${escapeHtml(sourceName)} (${j.processed_files}/${j.total_files})
${statusText}
`;
}).join("");
})
.catch(() => {
document.getElementById("import-existing").style.display = "none";
});
}
function loadImportJob(jobId) {
currentImportJobId = jobId;
document.getElementById("import-setup").style.display = "none";
document.getElementById("import-existing").style.display = "none";
document.getElementById("import-preview").style.display = "";
fetch(`/api/library/import/${jobId}`)
.then(r => r.json())
.then(data => {
if (data.error) {
alert("Fehler: " + data.error);
resetImport();
return;
}
renderImportItems(data);
// Falls Job bereits laeuft, Polling starten
if (data.job && data.job.status === 'importing') {
document.getElementById("import-preview").style.display = "none";
document.getElementById("import-progress").style.display = "";
startImportPolling();
}
})
.catch(e => {
alert("Fehler beim Laden: " + e);
resetImport();
});
}
function closeImportModal() {
document.getElementById("import-modal").style.display = "none";
}
function resetImport() {
document.getElementById("import-setup").style.display = "";
document.getElementById("import-preview").style.display = "none";
document.getElementById("import-progress").style.display = "none";
currentImportJobId = null;
}
function debounceImportPath() {
if (importBrowseTimer) clearTimeout(importBrowseTimer);
importBrowseTimer = setTimeout(() => {
const val = document.getElementById("import-source").value.trim();
if (val && val.startsWith("/")) importBrowse(val);
}, 600);
}
function importBrowse(path) {
const browser = document.getElementById("import-browser");
browser.innerHTML = 'Lade...
';
fetch("/api/library/browse-fs?path=" + encodeURIComponent(path))
.then(r => r.json())
.then(data => {
if (data.error) {
browser.innerHTML = `${escapeHtml(data.error)}
`;
return;
}
let html = '';
// Breadcrumb
html += '';
html += `
/mnt `;
for (const c of (data.breadcrumb || [])) {
if (c.path === "/mnt" || c.path.length < 5) continue;
html += `
/ `;
html += `
${escapeHtml(c.name)} `;
}
html += '
';
// Eltern-Ordner
const parts = data.current_path.split("/");
if (parts.length > 2) {
const parentPath = parts.slice(0, -1).join("/") || "/mnt";
html += `
🔙
..
`;
}
// Unterordner: Einfachklick = auswaehlen, Doppelklick = navigieren
for (const f of (data.folders || [])) {
const meta = f.video_count > 0 ? `${f.video_count} Videos` : "";
html += `
📁
${escapeHtml(f.name)}
${meta}
`;
}
if (!data.folders?.length && !data.video_count) {
html += 'Leerer Ordner
';
}
browser.innerHTML = html;
// Pfad-Input aktualisieren und Video-Info anzeigen
updateImportFolderInfo(data);
})
.catch(e => {
browser.innerHTML = `Fehler: ${e}
`;
});
}
// Klick-Handler: Einfachklick = auswaehlen, Doppelklick = navigieren
let _importClickTimer = null;
function importFolderClick(path, el) {
if (_importClickTimer) {
// Zweiter Klick innerhalb 300ms -> Doppelklick -> navigieren
clearTimeout(_importClickTimer);
_importClickTimer = null;
importBrowse(path);
} else {
// Erster Klick -> kurz warten ob Doppelklick kommt
_importClickTimer = setTimeout(() => {
_importClickTimer = null;
importSelectFolder(path, el);
}, 250);
}
}
function importSelectFolder(path, el) {
// Vorherige Auswahl entfernen
document.querySelectorAll(".import-browser-folder.selected").forEach(
f => f.classList.remove("selected")
);
el.classList.add("selected");
document.getElementById("import-source").value = path;
// Video-Info fuer den ausgewaehlten Ordner laden
fetch("/api/library/browse-fs?path=" + encodeURIComponent(path))
.then(r => r.json())
.then(data => {
if (!data.error) updateImportFolderInfo(data);
})
.catch(() => {});
}
function updateImportFolderInfo(data) {
const info = document.getElementById("import-folder-info");
const btn = document.getElementById("btn-analyze-import");
const totalVids = (data.video_count || 0);
// Auch Unterordner zaehlen
let subVids = 0;
for (const f of (data.folders || [])) subVids += (f.video_count || 0);
const allVids = totalVids + subVids;
if (allVids > 0) {
info.textContent = `${allVids} Videos gefunden`;
btn.disabled = false;
} else {
info.textContent = "Keine Videos im Ordner";
btn.disabled = true;
}
}
function createImportJob() {
const source = document.getElementById("import-source").value.trim();
const target = document.getElementById("import-target").value;
const mode = document.getElementById("import-mode").value;
if (!source || !target) { alert("Quellordner und Ziel erforderlich"); return; }
const btn = document.getElementById("btn-analyze-import");
btn.textContent = "Analysiere...";
btn.disabled = true;
fetch("/api/library/import", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({source_path: source, target_library_id: parseInt(target), mode}),
})
.then(r => r.json())
.then(data => {
if (data.error) { alert("Fehler: " + data.error); btn.textContent = "Analysieren"; btn.disabled = false; return; }
currentImportJobId = data.job_id;
return fetch(`/api/library/import/${data.job_id}/analyze`, {method: "POST"});
})
.then(r => r ? r.json() : null)
.then(data => {
btn.textContent = "Analysieren";
btn.disabled = false;
if (!data) return;
if (data.error) { alert("Analyse-Fehler: " + data.error); return; }
document.getElementById("import-setup").style.display = "none";
document.getElementById("import-preview").style.display = "";
renderImportItems(data);
})
.catch(e => { btn.textContent = "Analysieren"; btn.disabled = false; alert("Fehler: " + e); });
}
function renderImportItems(data) {
const items = data.items || [];
const list = document.getElementById("import-items-list");
const matched = items.filter(i => i.status === "matched").length;
const conflicts = items.filter(i => i.status === "conflict").length;
const pending = items.filter(i => i.status === "pending").length;
document.getElementById("import-info").textContent =
`${items.length} Dateien: ${matched} erkannt, ${conflicts} Konflikte, ${pending} offen`;
// Start-Button nur wenn keine ungeloesten Konflikte UND keine pending Items
const hasUnresolved = items.some(i =>
(i.status === "conflict" && !i.user_action) || i.status === "pending"
);
document.getElementById("btn-start-import").disabled = hasUnresolved;
if (!items.length) {
list.innerHTML = 'Keine Dateien gefunden
';
return;
}
let html = '';
html += 'Quelldatei Serie S/E Titel Ziel Status Aktion ';
html += ' ';
for (const item of items) {
const statusClass = item.status === "conflict" ? "status-badge warn"
: item.status === "matched" ? "status-badge ok"
: item.status === "done" ? "status-badge ok"
: item.status === "pending" ? "status-badge error"
: "status-badge";
const statusText = item.status === "conflict" ? "Konflikt"
: item.status === "matched" ? "OK"
: item.status === "done" ? "Fertig"
: item.status === "skipped" ? "Uebersprungen"
: item.status === "pending" ? "Nicht erkannt"
: item.status;
const sourceName = item.source_file ? item.source_file.split("/").pop() : "-";
const se = (item.detected_season && item.detected_episode)
? `S${String(item.detected_season).padStart(2, "0")}E${String(item.detected_episode).padStart(2, "0")}`
: "-";
let actionHtml = "";
if (item.status === "conflict" && !item.user_action) {
actionHtml = `
Ueberschr.
Skip
Umbenennen
`;
} else if (item.status === "pending") {
actionHtml = `
Zuordnen
Skip
`;
} else if (item.user_action) {
actionHtml = `${item.user_action} `;
}
const rowClass = item.status === "conflict" ? "row-conflict"
: item.status === "pending" ? "row-pending" : "";
html += `
${escapeHtml(sourceName)}
${escapeHtml(item.tvdb_series_name || item.detected_series || "-")}
${se}
${escapeHtml(item.tvdb_episode_title || "-")}
${escapeHtml(item.target_filename || "-")}
${statusText}
${item.conflict_reason ? `${escapeHtml(item.conflict_reason)}
` : ""}
${actionHtml}
`;
}
html += '
';
list.innerHTML = html;
}
function resolveImportConflict(itemId, action) {
fetch(`/api/library/import/items/${itemId}/resolve`, {
method: "PUT",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({action}),
})
.then(r => r.json())
.then(() => refreshImportPreview())
.catch(e => alert("Fehler: " + e));
}
// === Import-Zuordnungs-Modal ===
let _assignItemId = null;
let _assignTvdbId = null;
let _assignSeriesName = "";
let _assignSearchTimer = null;
function openImportAssignModal(itemId, filename) {
_assignItemId = itemId;
_assignTvdbId = null;
_assignSeriesName = "";
const modal = document.getElementById("import-assign-modal");
modal.style.display = "flex";
document.getElementById("import-assign-filename").textContent = filename;
document.getElementById("import-assign-search").value = "";
document.getElementById("import-assign-results").innerHTML = "";
document.getElementById("import-assign-selected").style.display = "none";
document.getElementById("import-assign-season").value = "";
document.getElementById("import-assign-episode").value = "";
document.getElementById("import-assign-search").focus();
}
function closeImportAssignModal() {
document.getElementById("import-assign-modal").style.display = "none";
_assignItemId = null;
}
function debounceAssignSearch() {
if (_assignSearchTimer) clearTimeout(_assignSearchTimer);
_assignSearchTimer = setTimeout(searchAssignTvdb, 500);
}
function searchAssignTvdb() {
const query = document.getElementById("import-assign-search").value.trim();
if (!query) return;
const results = document.getElementById("import-assign-results");
results.innerHTML = 'Suche...
';
fetch(`/api/tvdb/search?q=${encodeURIComponent(query)}`)
.then(r => r.json())
.then(data => {
if (data.error) { results.innerHTML = `${escapeHtml(data.error)}
`; return; }
if (!data.results || !data.results.length) { results.innerHTML = 'Keine Ergebnisse
'; return; }
results.innerHTML = data.results.slice(0, 8).map(r => `
${r.poster ? `
` : ""}
${escapeHtml(r.name)}
${r.year || ""}
${escapeHtml((r.overview || "").substring(0, 120))}
`).join("");
})
.catch(e => { results.innerHTML = `Fehler: ${e}
`; });
}
function selectAssignSeries(tvdbId, name) {
_assignTvdbId = tvdbId;
_assignSeriesName = name;
document.getElementById("import-assign-results").innerHTML = "";
document.getElementById("import-assign-selected").style.display = "";
document.getElementById("import-assign-selected-name").textContent = name;
document.getElementById("import-assign-search").value = "";
}
function submitImportAssign() {
if (!_assignItemId) return;
const season = parseInt(document.getElementById("import-assign-season").value);
const episode = parseInt(document.getElementById("import-assign-episode").value);
const manualName = document.getElementById("import-assign-search").value.trim();
const seriesName = _assignSeriesName || manualName;
if (!seriesName) { alert("Serie auswaehlen oder Namen eingeben"); return; }
if (isNaN(season) || isNaN(episode)) { alert("Staffel und Episode eingeben"); return; }
const btn = document.querySelector("#import-assign-modal .btn-primary");
btn.disabled = true;
btn.textContent = "Zuordne...";
fetch(`/api/library/import/items/${_assignItemId}/reassign`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
series_name: seriesName,
season: season,
episode: episode,
tvdb_id: _assignTvdbId || null,
}),
})
.then(r => r.json())
.then(data => {
btn.disabled = false;
btn.textContent = "Zuordnen";
if (data.error) { alert("Fehler: " + data.error); return; }
closeImportAssignModal();
refreshImportPreview();
})
.catch(e => { btn.disabled = false; btn.textContent = "Zuordnen"; alert("Fehler: " + e); });
}
function skipImportItem(itemId) {
fetch(`/api/library/import/items/${itemId}/skip`, {method: "POST"})
.then(r => r.json())
.then(() => refreshImportPreview())
.catch(e => alert("Fehler: " + e));
}
function refreshImportPreview() {
if (!currentImportJobId) return;
fetch(`/api/library/import/${currentImportJobId}`)
.then(r => r.json())
.then(data => renderImportItems(data))
.catch(() => {});
}
let importPollingId = null;
function executeImport() {
if (!currentImportJobId || !confirm("Import jetzt starten?")) return;
document.getElementById("import-preview").style.display = "none";
document.getElementById("import-progress").style.display = "";
document.getElementById("import-status-text").textContent = "Importiere...";
document.getElementById("import-bar").style.width = "0%";
// Starte Import (non-blocking - Server antwortet sofort)
fetch(`/api/library/import/${currentImportJobId}/execute`, {method: "POST"});
// Polling fuer Fortschritt starten
startImportPolling();
}
function startImportPolling() {
if (importPollingId) clearInterval(importPollingId);
importPollingId = setInterval(async () => {
try {
const r = await fetch(`/api/library/import/${currentImportJobId}`);
const data = await r.json();
if (data.error) {
stopImportPolling();
document.getElementById("import-status-text").textContent = "Fehler: " + data.error;
return;
}
const job = data.job;
if (!job) return;
const total = job.total_files || 1;
const done = job.processed_files || 0;
// Byte-Fortschritt der aktuellen Datei
const curFile = job.current_file_name || "";
const curBytes = job.current_file_bytes || 0;
const curTotal = job.current_file_total || 0;
// Prozent: fertige Dateien + anteilig aktuelle Datei
let pct = (done / total) * 100;
if (curTotal > 0 && done < total) {
pct += (curBytes / curTotal) * (100 / total);
}
pct = Math.min(Math.round(pct), 100);
document.getElementById("import-bar").style.width = pct + "%";
// Status-Text mit Byte-Fortschritt
let statusText = `Importiere: ${done} / ${total} Dateien`;
if (curFile && curTotal > 0 && done < total) {
const curPct = Math.round((curBytes / curTotal) * 100);
statusText += ` - ${curFile.substring(0, 40)}... (${formatSize(curBytes)} / ${formatSize(curTotal)}, ${curPct}%)`;
} else {
statusText += ` (${pct}%)`;
}
document.getElementById("import-status-text").textContent = statusText;
// Fertig?
if (job.status === "done" || job.status === "error") {
stopImportPolling();
document.getElementById("import-bar").style.width = "100%";
// Zaehle Ergebnisse
const items = data.items || [];
const imported = items.filter(i => i.status === "done").length;
const errors = items.filter(i => i.status === "error").length;
const skipped = items.filter(i => i.status === "skipped").length;
document.getElementById("import-status-text").textContent =
`Fertig: ${imported} importiert, ${skipped} uebersprungen, ${errors} Fehler`;
// Nur Ziel-Pfad scannen und neu laden (statt alles)
const targetPathId = job.target_library_id;
if (targetPathId && imported > 0) {
// Gezielten Scan starten
fetch(`/api/library/scan/${targetPathId}`, {method: "POST"})
.then(() => {
// Warte kurz, dann nur diese Sektion neu laden
setTimeout(() => {
loadSectionData(targetPathId);
loadStats();
}, 2000);
})
.catch(() => {
// Fallback: Alles neu laden
reloadAllSections();
loadStats();
});
} else {
// Kein Import oder unbekannter Pfad: Alles neu laden
reloadAllSections();
loadStats();
}
}
} catch (e) {
console.error("Import-Polling Fehler:", e);
}
}, 500);
}
function stopImportPolling() {
if (importPollingId) {
clearInterval(importPollingId);
importPollingId = null;
}
}
// === Hilfsfunktionen ===
function formatSize(bytes) {
if (!bytes) return "0 B";
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
let i = 0;
let size = bytes;
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; }
return size.toFixed(i > 1 ? 1 : 0) + " " + units[i];
}
function formatDuration(sec) {
if (!sec || sec <= 0) return "-";
const d = Math.floor(sec / 86400);
const h = Math.floor((sec % 86400) / 3600);
const m = Math.floor((sec % 3600) / 60);
const parts = [];
if (d) parts.push(d + "d");
if (h) parts.push(h + "h");
if (m || !parts.length) parts.push(m + "m");
return parts.join(" ");
}
function resolutionLabel(w, h) {
if (w >= 3840) return "4K";
if (w >= 1920) return "1080p";
if (w >= 1280) return "720p";
if (w >= 720) return "576p";
return w + "x" + h;
}
function channelLayout(ch) {
const layouts = {1: "Mono", 2: "2.0", 3: "2.1", 6: "5.1", 8: "7.1"};
return layouts[ch] || ch + "ch";
}
function escapeHtml(str) {
if (!str) return "";
return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """)
.replace(/'/g, "'");
}
function escapeAttr(str) {
// JS-String-Literal sicher fuer HTML-onclick-Attribute erzeugen
// JSON.stringify erzeugt "...", die " muessen fuer HTML-Attribute escaped werden
return JSON.stringify(str || "").replace(/&/g, "&").replace(/"/g, """).replace(/ {
// Autoplay blockiert - User muss manuell starten
});
}
function closePlayer() {
const video = document.getElementById("player-video");
video.pause();
video.removeAttribute("src");
video.load();
_playerVideoId = null;
document.getElementById("player-modal").style.display = "none";
}
// ESC schliesst den Player
document.addEventListener("keydown", function(e) {
if (e.key === "Escape") {
const player = document.getElementById("player-modal");
if (player && player.style.display === "flex") {
closePlayer();
e.stopPropagation();
}
}
});
// === Video loeschen ===
function deleteVideo(videoId, title, context) {
if (!confirm(`"${title}" wirklich loeschen?\n\nDatei wird unwiderruflich entfernt!`)) return;
fetch(`/api/library/videos/${videoId}?delete_file=1`, {method: "DELETE"})
.then(r => r.json())
.then(data => {
if (data.error) { showToast("Fehler: " + data.error, "error"); return; }
showToast("Video geloescht", "success");
// Ansicht aktualisieren
if (context === "series" && currentSeriesId) {
openSeriesDetail(currentSeriesId);
} else if (context === "movie" && currentMovieId) {
openMovieDetail(currentMovieId);
} else {
reloadAllSections();
}
loadStats();
})
.catch(e => showToast("Fehler: " + e, "error"));
}