/** * 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 += ``; for (const lp of enabled) { const icon = lp.media_type === 'series' ? '🎬' : '🎦'; const isActive = activePathId === lp.id; html += ``; } 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 += `
`; html += `

${escapeHtml(lp.name)}

`; html += `${escapeHtml(lp.path)}`; html += `
`; html += ``; html += `
`; // Tabs - Serien-Pfad: Videos+Serien+Ordner / Film-Pfad: Videos+Filme+Ordner html += `
`; html += ``; if (isSeriesLib) { html += ``; } else { html += ``; } html += ``; 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' : ``; 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 += ''; html += ''; 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 += ``; } html += '
DateinameAufl.CodecAudioUntertitelGroesseDauerContainerAktion
${escapeHtml(v.file_name || "-")} ${res}${is10bit} ${v.video_codec || "-"} ${audioInfo || "-"} ${subInfo || "-"} ${formatSize(v.file_size || 0)} ${formatDuration(v.duration_sec || 0)} ${(v.container || "-").toUpperCase()}
'; return html; } function renderPagination(total, page, pages, pathId, tabType) { let html = '
'; html += ''; html += `
`; 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` : ``; 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 = '
'; html += `Basis`; for (const c of crumbs) { html += ' / '; html += `${escapeHtml(c.name)}`; } 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 = '
'; if (series.poster_url) { html += ``; } html += '
'; if (series.overview) html += `

${escapeHtml(series.overview)}

`; html += '
'; if (series.first_aired) html += `${series.first_aired}`; if (series.status) html += `${series.status}`; html += `${series.local_episodes || 0} lokal`; if (series.total_episodes) html += `${series.total_episodes} gesamt`; if (series.missing_episodes > 0) html += `${series.missing_episodes} fehlend`; 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 += ''; 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 += ``; } 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 += ``; } } html += '
NrTitelAufl.CodecAudioAktion
${ep.episode_number || "-"} ${escapeHtml(ep.episode_name || "-")} Nicht vorhanden FEHLT
${ep.episode_number || "-"} ${escapeHtml(epTitle)} ${res} ${ep.video_codec || "-"} ${audioInfo || "-"}
'; } 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 += `

${escapeHtml(type.charAt(0).toUpperCase() + type.slice(1))} (${items.length})

`; 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 = '
'; if (data.poster_url) { html += ``; } html += '
'; if (data.overview) html += `

${escapeHtml(data.overview)}

`; html += '
'; if (data.year) html += `${data.year}`; if (data.runtime) html += `${data.runtime} min`; if (data.status) html += `${data.status}`; html += `${data.video_count || 0} Dateien`; if (data.total_size) html += `${formatSize(data.total_size)}`; html += '
'; // Video-Dateien des Films const videos = data.videos || []; if (videos.length) { html += '

Video-Dateien

'; html += ''; html += ''; 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 += ``; } html += '
DateiAufl.CodecAudioGroesseDauerAktion
${escapeHtml(v.file_name || "-")} ${res}${v.is_10bit ? ' 10bit' : ''} ${v.video_codec || "-"} ${audioInfo || "-"} ${formatSize(v.file_size || 0)} ${formatDuration(v.duration_sec || 0)}
'; } 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 += `
${series.poster_url ? `` : ''}

${series.series_title}

${series.episodes.length} fehlend
`; 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 += '
'; if (page > 1) { html += ``; } html += ` Seite ${page} von ${data.pages} `; if (page < data.pages) { html += ``; } 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 += `
`; html += `${typeLabel}`; html += `${escapeHtml(item.local_name)}`; if (item.year) html += `(${item.year})`; if (statusHtml) html += statusHtml; if (!item._confirmed && !item._skipped) { html += ``; 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 += `
`; } 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` : ""}
`).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 = ''; for (const ext of [...exts].sort()) { select.innerHTML += ``; } 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 = ''; html += ''; html += ''; html += ''; for (let i = 0; i < files.length; i++) { const f = files[i]; html += ``; } html += '
DateinameSerie/OrdnerExtensionGroesse
${escapeHtml(f.name)} ${escapeHtml(f.parent_series || "-")} ${escapeHtml(f.extension)} ${formatSize(f.size)}
'; 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 => `` ).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 ` ${j.status !== 'importing' ? `` : ''} `; }).join(""); }) .catch(() => { document.getElementById("import-existing").style.display = "none"; }); } function deleteImportJob(jobId, ev) { if (ev) ev.stopPropagation(); if (!confirm("Import-Job wirklich loeschen?")) return; fetch(`/api/library/import/${jobId}`, { method: "DELETE" }) .then(r => r.json()) .then(data => { if (data.error) { alert("Fehler: " + data.error); return; } loadExistingImportJobs(); }) .catch(() => alert("Loeschen fehlgeschlagen")); } function deleteCurrentImportJob() { if (!currentImportJobId) return; if (!confirm("Import-Job wirklich loeschen?")) return; fetch(`/api/library/import/${currentImportJobId}`, { method: "DELETE" }) .then(r => r.json()) .then(data => { if (data.error) { alert("Fehler: " + data.error); return; } resetImport(); }) .catch(() => alert("Loeschen fehlgeschlagen")); } 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" ); const btn = document.getElementById("btn-start-import"); btn.disabled = hasUnresolved; btn.title = hasUnresolved ? `${pending} Dateien muessen erst zugeordnet werden` : "Import starten"; if (!items.length) { list.innerHTML = '
Keine Dateien gefunden
'; return; } let html = ''; html += ''; 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 = ` `; } else if (item.status === "pending") { actionHtml = ` `; } else if (item.user_action) { actionHtml = `${item.user_action}`; } const rowClass = item.status === "conflict" ? "row-conflict" : item.status === "pending" ? "row-pending" : ""; html += ``; } html += '
QuelldateiSerieS/ETitelZielStatusAktion
${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}
'; 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")); }