Files
language-dashboard/templates/index.html
T
nessi 5912b7078c feat: add system metrics dashboard with live CPU, RAM, and disk usage monitoring plus service status overview
Add system_metrics() function to read CPU usage from /proc/stat with delta calculation, memory stats from /proc/meminfo, and disk usage for System/Daten/Filme/Serien mounts with device deduplication. Add /api/system endpoint returning metrics JSON with error handling.

Add dashboard view as default tab with hero section, 4-card overview grid showing movie/series/missing/download counts, system
2026-08-13 16:38:19 +02:00

150 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='media-mark.svg') }}"><title>Language Dashboard</title>
<style>
:root{color-scheme:dark;--bg:#181818;--panel:#222;--line:#3a3a3a;--text:#dedede;--muted:#929292;--yellow:#f4c430;--blue:#60a9e6}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:13px Inter,system-ui,sans-serif}header{height:58px;display:flex;align-items:center;gap:15px;padding:0 20px;background:#242424;border-bottom:1px solid #343434}.logo{color:var(--yellow);font-size:22px;font-weight:900;letter-spacing:1px}.head-title{font-weight:700}.head-sub{font-size:12px;color:var(--muted);margin-top:2px}.actions{margin-left:auto}button,input,select{font:inherit;color:var(--text);background:#292929;border:1px solid #444;border-radius:5px}button{padding:7px 10px;cursor:pointer}main{padding:14px 20px 30px}.nav{display:flex;gap:4px;margin-bottom:12px;border-bottom:1px solid var(--line)}.nav button{border:0;border-radius:5px 5px 0 0;background:transparent;color:#aaa}.nav button.active{background:#303030;color:#fff}.view{display:none}.view.active{display:block}.toolbar{display:grid;grid-template-columns:minmax(280px,1fr) 190px 190px 170px;gap:8px;margin-bottom:10px}input,select{padding:7px 10px;height:34px}.stats{display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap}.stat{background:#242424;border:1px solid #363636;padding:5px 9px;border-radius:5px;color:#bbb}.scan{color:#f4c430}.banner{padding:9px 11px;border:1px solid #693b3b;background:#372424;border-radius:5px;margin-bottom:10px;color:#ffd1d1}.tablewrap{border:1px solid #353535;border-radius:6px;overflow:hidden;background:var(--panel)}table{width:100%;border-collapse:collapse;table-layout:fixed}th{padding:8px 10px;background:#282828;border-bottom:1px solid #444;text-align:left;font-size:12px}td{padding:7px 10px;border-bottom:1px solid #343434;vertical-align:middle}.col-title{width:28%}.col-quality{width:12%}.col-state{width:10%}.col-audio{width:17%}.col-subs{width:17%}.col-video{width:8%}.col-size{width:8%}.title{color:var(--blue);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:inline-block;max-width:90%;vertical-align:bottom}.year{color:var(--muted);font-size:11px;margin-left:5px}.badges{display:flex;gap:4px;flex-wrap:wrap}.badge{display:inline-flex;gap:4px;align-items:center;padding:2px 6px;border-radius:999px;border:1px solid #484848;background:#303030;white-space:nowrap;font-size:11px}.quality{background:#0e7138;border-color:#19894b}.state{font-size:11px;font-weight:700;padding:3px 7px;border-radius:4px;display:inline-block}.state.ok{background:#126f39;color:#d9ffe8}.state.warn{background:#77561e;color:#ffebc4}.state.bad{background:#763030;color:#ffd9d9}.state.neutral{background:#3b3b3b;color:#ccc}.muted{color:var(--muted)}.hidden{display:none}.series{border-bottom:1px solid var(--line)}.series:last-child{border:0}.series summary{padding:12px;cursor:pointer;background:#282828;font-weight:700}.series summary:hover{background:#303030}.series table{border-radius:0}.series td:first-child{padding-left:28px}@media(max-width:1100px){.toolbar{grid-template-columns:1fr 1fr}.col-video,.col-size{display:none}}
.brand-mark{width:34px;height:34px;display:grid;place-items:center;filter:drop-shadow(0 0 10px rgba(96,169,230,.24))}.brand-mark svg{width:34px;height:34px}.brand-name{color:var(--yellow);font-size:16px;font-weight:900;letter-spacing:1.5px}.brand{display:flex;align-items:center;gap:9px}
.dashboard-hero{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:14px;padding:26px;border:1px solid rgba(96,169,230,.2);border-radius:14px;background:radial-gradient(circle at 85% 15%,rgba(96,169,230,.2),transparent 34%),linear-gradient(135deg,rgba(39,48,61,.96),rgba(27,30,36,.96));box-shadow:0 16px 40px rgba(0,0,0,.22)}.dashboard-hero h1{margin:7px 0 4px;font-size:25px}.dashboard-hero p{margin:0;color:#9da5b2}.live-pill{display:flex;align-items:center;gap:8px;padding:9px 14px;border-radius:999px;background:rgba(21,148,71,.17);border:1px solid rgba(106,226,154,.25);color:#aaf2c5;font-weight:700}.live-dot{width:8px;height:8px;border-radius:50%;background:#6ae29a;box-shadow:0 0 12px #6ae29a}.overview-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:14px}.overview-card{display:flex;align-items:center;gap:12px;min-height:104px;padding:17px;border:1px solid rgba(255,255,255,.08);border-radius:11px;background:linear-gradient(145deg,rgba(43,47,55,.92),rgba(29,31,36,.92));box-shadow:0 8px 22px rgba(0,0,0,.16)}.overview-icon{display:grid;place-items:center;width:40px;height:40px;border-radius:11px;background:rgba(96,169,230,.17);color:#8bc9f2;font-size:23px;font-weight:700}.overview-icon.series-icon{background:rgba(146,87,194,.2);color:#d7adf3}.overview-icon.warning{background:rgba(188,70,70,.2);color:#ff9e9e}.overview-icon.download-icon{background:rgba(21,148,71,.2);color:#6ae29a}.overview-card div{min-width:0}.overview-card span:not(.overview-icon){display:block;color:#9da5b2;font-size:11px;text-transform:uppercase;letter-spacing:.06em}.overview-card strong{display:block;margin:3px 0;font-size:22px;color:#f1f5fa}.overview-card small{display:block;color:#7f8996;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dashboard-columns{display:grid;grid-template-columns:1.45fr 1fr;gap:14px}.system-panel,.service-panel{padding:19px;border:1px solid rgba(255,255,255,.08);border-radius:12px;background:rgba(31,33,38,.84);box-shadow:0 12px 30px rgba(0,0,0,.18)}.panel-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:15px;margin-bottom:17px}.panel-heading h2{margin:0 0 4px;font-size:16px}.panel-heading p{margin:0;color:#7f8996;font-size:12px}.system-meters{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-bottom:20px}.meter-top{display:flex;justify-content:space-between;margin-bottom:7px;color:#cbd1db}.meter-top strong{color:#fff}.meter-track{height:9px;background:#363b44;border-radius:99px;overflow:hidden}.meter-track span{display:block;width:0;height:100%;border-radius:99px;background:linear-gradient(90deg,#60a9e6,#159447);transition:width .45s ease}.disk-list{display:grid;gap:12px}.disk-row{display:grid;grid-template-columns:92px 1fr auto;align-items:center;gap:10px}.disk-name{color:#cbd1db;font-weight:700}.disk-path{display:block;color:#7f8996;font-size:11px;font-weight:400}.disk-bar{height:7px;background:#363b44;border-radius:99px;overflow:hidden}.disk-bar span{display:block;height:100%;border-radius:99px;background:linear-gradient(90deg,#60a9e6,#f4c430);transition:width .45s ease}.disk-values{text-align:right;color:#cbd1db;font-size:11px}.service-row{display:flex;align-items:center;gap:11px;padding:12px 0;border-bottom:1px solid rgba(255,255,255,.07)}.service-row:last-child{border-bottom:0}.service-logo{display:grid;place-items:center;width:34px;height:34px;border-radius:9px;color:#fff;font-weight:800}.radarr-dot{background:#1d7e45}.sonarr-dot{background:#4d65b1}.sab-dot{background:#9a6830}.service-row div{flex:1}.service-row strong,.service-row small{display:block}.service-row small{margin-top:2px;color:#7f8996;font-size:11px}.service-status{padding:4px 8px;border-radius:999px;font-size:11px;font-weight:700}.service-status.online{background:#126f39;color:#d9ffe8}.service-status.offline{background:#3b3b3b;color:#aaa}
@media(max-width:1000px){.overview-grid{grid-template-columns:repeat(2,1fr)}.dashboard-columns{grid-template-columns:1fr}}@media(max-width:600px){.overview-grid,.system-meters{grid-template-columns:1fr}.dashboard-hero{align-items:flex-start;flex-direction:column;padding:20px}.live-pill{align-self:stretch;justify-content:center}.disk-row{grid-template-columns:78px 1fr auto}}
</style></head><body>
<header><div class="brand"><div class="brand-mark" aria-label="Media Dashboard"><svg viewBox="0 0 48 48" role="img"><path d="M10 8h28a4 4 0 0 1 4 4v24a4 4 0 0 1-4 4H10a4 4 0 0 1-4-4V12a4 4 0 0 1 4-4Z" fill="#20252d" stroke="#60a9e6" stroke-width="2"/><path d="M14 17h20M14 24h13M14 31h20" stroke="#f4c430" stroke-width="3" stroke-linecap="round"/><circle cx="34" cy="24" r="5" fill="#159447" stroke="#c9ffe0" stroke-width="1.5"/></svg></div><div class="brand-name">MEDIA</div></div><div><div class="head-title" data-de="Language Dashboard" data-en="Language Dashboard">Language Dashboard</div><div class="head-sub" data-de="Audio- und Untertitelspuren aus den Mediendateien" data-en="Audio and subtitle tracks from media files">Audio- und Untertitelspuren aus den Mediendateien</div></div><div class="actions"><button class="lang-switch" id="languageToggle" title="Sprache / Language">DE</button><button class="settings-button" id="settingsOpen" title="Einstellungen"></button><button id="rescan" data-de="↻ Neu scannen" data-en="↻ Rescan">↻ Neu scannen</button></div></header>
<main>
{% if error %}<div class="banner"><strong>Fehler:</strong> {{ error }}</div>{% endif %}
<nav class="nav"><button class="active" data-view="dashboard">▦ Dashboard</button><button data-view="radarr">RADARR · Filme</button>{% if sonarr_enabled %}<button data-view="sonarr">SONARR · Serien</button>{% endif %}<button data-view="missing">⚠ Ohne Deutsch <span class="nav-count">{{ missing_german_movies|length + (missing_german_series|map(attribute='episodes')|map('length')|sum) }}</span></button>{% if sab_enabled %}<button data-view="downloads">SABNZBD · Downloads</button>{% endif %}</nav>
<section id="dashboard" class="view active" data-source="dashboard"><div class="dashboard-hero"><div><div class="eyebrow">MEDIA CONTROL CENTER</div><h1>Dashboard</h1><p>Deine gesamte Medienbibliothek und Systemauslastung auf einen Blick.</p></div><div class="live-pill"><span class="live-dot"></span> Live</div></div><div class="overview-grid"><div class="overview-card"><span class="overview-icon movie"></span><div><span>Filme</span><strong>{{ rows|length }}</strong><small>Radarr Library</small></div></div><div class="overview-card"><span class="overview-icon series-icon"></span><div><span>Serien</span><strong>{{ series|length }}</strong><small>Sonarr Shows</small></div></div><div class="overview-card"><span class="overview-icon warning"></span><div><span>Ohne Deutsch</span><strong id="dashboardMissing">{{ missing_german_movies|length + (missing_german_series|map(attribute='episodes')|map('length')|sum) }}</strong><small>Audio oder Untertitel prüfen</small></div></div><div class="overview-card"><span class="overview-icon download-icon"></span><div><span>Downloads</span><strong id="dashboardDownloads"></strong><small id="dashboardRemaining">SABnzbd wird geladen …</small></div></div></div><div class="dashboard-columns"><section class="system-panel"><div class="panel-heading"><div><h2>Systemauslastung</h2><p>Aktuelle Werte aus dem Dashboard-Container</p></div><span id="systemUpdated" class="muted">Wird geladen …</span></div><div class="system-meters"><div class="meter"><div class="meter-top"><span>CPU</span><strong id="cpuValue"></strong></div><div class="meter-track"><span id="cpuBar"></span></div></div><div class="meter"><div class="meter-top"><span>RAM</span><strong id="ramValue"></strong></div><div class="meter-track"><span id="ramBar"></span></div></div></div><div id="diskList" class="disk-list"><div class="disk-row muted">Laufwerke werden geladen …</div></div></section><section class="service-panel"><div class="panel-heading"><div><h2>Dienste</h2><p>Verbindung und Bibliotheksstatus</p></div></div><div class="service-row"><span class="service-logo radarr-dot">R</span><div><strong>Radarr</strong><small>Filme & Qualität</small></div><span class="service-status online">Aktiv</span></div><div class="service-row"><span class="service-logo sonarr-dot">S</span><div><strong>Sonarr</strong><small>Serien & Episoden</small></div><span class="service-status {% if sonarr_enabled %}online{% else %}offline{% endif %}">{% if sonarr_enabled %}Aktiv{% else %}Nicht konfiguriert{% endif %}</span></div><div class="service-row"><span class="service-logo sab-dot"></span><div><strong>SABnzbd</strong><small>Downloads & Queue</small></div><span id="dashboardSabStatus" class="service-status {% if sab_enabled %}online{% else %}offline{% endif %}">{% if sab_enabled %}Wird geprüft …{% else %}Nicht konfiguriert{% endif %}</span></div></section></div></section>
<section id="radarr" class="view" data-source="radarr">
<div class="toolbar"><input id="search" type="search" placeholder="Film suchen …"><select id="audioFilter"><option value="">Alle Audio-Sprachen</option><option>Deutsch</option><option>Englisch</option><option>Japanisch</option><option>Koreanisch</option></select><select id="subFilter"><option value="">Alle Untertitel</option><option>Deutsch</option><option>Englisch</option><option>Japanisch</option><option>Koreanisch</option></select><select id="stateFilter"><option value="">Alle Status</option><option>DE + EN</option><option>Deutsch</option><option>English only</option></select></div>
<div class="stats"><span class="stat" id="visibleCount"></span><span class="stat scan" id="radarrStatus">Scan wird vorbereitet …</span><span class="stat">Radarr: {{ radarr_media_path }}</span></div>
<div class="tablewrap"><table id="movies"><thead><tr><th class="col-title">Film</th><th class="col-quality">Quality</th><th class="col-state">Status</th><th class="col-audio">Audio</th><th class="col-subs">Untertitel</th><th class="col-video">Video</th><th class="col-size">Größe</th></tr></thead><tbody>
{% for r in rows %}<tr data-title="{{ (r.title ~ ' ' ~ (r.year or ''))|lower }}" data-audio="{{ r.audio_languages|map(attribute='name')|join(',') }}" data-subs="{{ r.subtitle_languages|map(attribute='name')|join(',') }}" data-state="{{ r.state.label }}"><td><span class="title">{{ r.title }}</span><span class="year">{{ r.year or '' }}</span>{% if r.error %} ⚠{% endif %}</td><td><span class="badge {% if not r.missing %}quality{% endif %}">{{ r.quality }}</span></td><td><span class="state {{ r.state.class }}">{{ r.state.label }}</span></td><td><div class="badges">{% for l in r.audio_languages %}<span class="badge">{{ l.flag }} {{ l.name }}</span>{% else %}<span class="muted"></span>{% endfor %}</div></td><td><div class="badges">{% for l in r.subtitle_languages %}<span class="badge">{{ l.flag }} {{ l.name }}</span>{% else %}<span class="muted"></span>{% endfor %}</div></td><td>{{ r.video_codec }}</td><td>{{ r.size }}</td></tr>{% endfor %}
</tbody></table></div></section>
{% if sonarr_enabled %}<section id="sonarr" class="view" data-source="sonarr"><div class="stats"><span class="stat" id="seriesCount">{{ series|length }} Serien</span><span class="stat scan" id="sonarrStatus">Scan wird vorbereitet …</span><span class="stat">Dashboard: {{ local_series_path }}</span></div><div class="tablewrap" id="seriesList">
{% for group in series %}<details class="series" open><summary>{{ group.title }} <span class="muted">({{ group.year or '—' }}) · {{ group.episodes|length }} Episoden</span></summary><table><thead><tr><th class="col-title">Episode</th><th class="col-quality">Quality</th><th class="col-state">Status</th><th class="col-audio">Audio</th><th class="col-subs">Untertitel</th><th class="col-video">Video</th><th class="col-size">Größe</th></tr></thead><tbody>{% for r in group.episodes %}<tr><td><span class="title">{{ r.title }}</span></td><td><span class="badge {% if not r.missing %}quality{% endif %}">{{ r.quality }}</span></td><td><span class="state {{ r.state.class }}">{{ r.state.label }}</span></td><td><div class="badges">{% for l in r.audio_languages %}<span class="badge">{{ l.flag }} {{ l.name }}</span>{% else %}<span class="muted"></span>{% endfor %}</div></td><td><div class="badges">{% for l in r.subtitle_languages %}<span class="badge">{{ l.flag }} {{ l.name }}</span>{% else %}<span class="muted"></span>{% endfor %}</div></td><td>{{ r.video_codec }}</td><td>{{ r.size }}</td></tr>{% endfor %}</tbody></table></details>{% endfor %}
</div></section>{% endif %}
<section id="missing" class="view" data-source="radarr"><div class="stats"><span class="stat" id="missingCount">{{ missing_german_movies|length + (missing_german_series|map(attribute='episodes')|map('length')|sum) }} Einträge ohne Deutsch</span><span class="stat scan">Vorhanden oder wird gerade geladen · Staffel 0 ausgeblendet</span></div><div class="tablewrap missing-table">
<table id="missingMovies"><thead><tr><th class="col-title">Film</th><th class="col-quality">Qualität</th><th class="col-state">Status</th><th class="col-audio">Audio</th><th class="col-subs">Untertitel</th><th class="col-video">Video</th><th class="col-size">Größe</th></tr></thead><tbody>{% for r in missing_german_movies %}<tr><td><span class="title">{{ r.title }}</span><span class="year">{{ r.year or '' }}</span></td><td><span class="badge {% if not r.missing %}quality{% endif %}">{{ r.quality }}</span></td><td><span class="state bad">Ohne Deutsch</span></td><td><div class="badges">{% for l in r.audio_languages %}<span class="badge">{{ l.flag }} {{ l.name }}</span>{% else %}<span class="muted"></span>{% endfor %}</div></td><td><div class="badges">{% for l in r.subtitle_languages %}<span class="badge">{{ l.flag }} {{ l.name }}</span>{% else %}<span class="muted"></span>{% endfor %}</div></td><td>{{ r.video_codec }}</td><td>{{ r.size }}</td></tr>{% endfor %}</tbody></table>
{% for group in missing_german_series %}<details class="series missing-series"><summary>{{ group.title }} <span class="muted">({{ group.year or '—' }}) · {{ group.episodes|length }} Episoden ohne Deutsch</span></summary><table><thead><tr><th class="col-title">Episode</th><th class="col-quality">Qualität</th><th class="col-state">Status</th><th class="col-audio">Audio</th><th class="col-subs">Untertitel</th><th class="col-video">Video</th><th class="col-size">Größe</th></tr></thead><tbody>{% for r in group.episodes %}<tr><td><span class="title">{{ r.title }}</span></td><td><span class="badge {% if not r.missing %}quality{% endif %}">{{ r.quality }}</span></td><td><span class="state bad">Ohne Deutsch</span></td><td><div class="badges">{% for l in r.audio_languages %}<span class="badge">{{ l.flag }} {{ l.name }}</span>{% else %}<span class="muted"></span>{% endfor %}</div></td><td><div class="badges">{% for l in r.subtitle_languages %}<span class="badge">{{ l.flag }} {{ l.name }}</span>{% else %}<span class="muted"></span>{% endfor %}</div></td><td>{{ r.video_codec }}</td><td>{{ r.size }}</td></tr>{% endfor %}</tbody></table></details>{% endfor %}
{% if not missing_german_movies and not missing_german_series %}<div class="empty-state">Alle vorhandenen Filme und Episoden haben deutsches Audio oder deutsche Untertitel.</div>{% endif %}</div></section>
{% if sab_enabled %}<section id="downloads" class="view" data-source="downloads"><div class="download-hero"><div><div class="eyebrow">SABNZBD · DOWNLOAD CENTER</div><h2>Download-Übersicht</h2><p>Queue, Fortschritt und verbleibender Speicherplatz auf einen Blick.</p></div><div class="queue-pill" id="sabQueueCard"><span class="queue-dot"></span><span>Verbinde mit SABnzbd …</span></div></div><div class="download-stats"><div class="download-card remaining-card"><span class="card-icon"></span><div><span class="card-label">Noch zu laden</span><strong id="sabRemaining"></strong><small id="sabRemainingHint">Wird berechnet …</small></div></div><div class="download-card"><span class="card-icon blue"></span><div><span class="card-label">Aktive Downloads</span><strong id="sabActive"></strong><small id="sabHistory">Verlauf: —</small></div></div><div class="download-card"><span class="card-icon purple"></span><div><span class="card-label">Geschwindigkeit</span><strong id="sabSpeedCard"></strong><small>Aktuelle Queue</small></div></div><div class="download-card"><span class="card-icon orange"></span><div><span class="card-label">Status</span><strong id="sabStatusCard"></strong><small id="downloadCount"></small></div></div></div><div id="downloadError" class="banner hidden"></div><div class="tablewrap download-table"><table id="downloadsTable"><thead><tr><th class="col-title">Download</th><th class="col-state">Status</th><th class="col-progress">Fortschritt</th><th class="col-size">Größe / Rest</th><th class="col-time">Restzeit</th><th class="col-source">Quelle</th><th class="col-language">Sprache</th></tr></thead><tbody id="downloadsBody"><tr><td colspan="7" class="muted">Downloads werden geladen …</td></tr></tbody></table></div></section>{% endif %}
</main>
<div class="settings-backdrop hidden" id="settingsBackdrop"><section class="settings-modal" role="dialog" aria-modal="true" aria-labelledby="settingsTitle"><div class="settings-head"><div><h2 id="settingsTitle" data-de="Einstellungen" data-en="Settings">Einstellungen</h2><p data-de="Deine Anzeige- und Sprachpräferenzen" data-en="Your display and language preferences">Deine Anzeige- und Sprachpräferenzen</p></div><button class="settings-close" id="settingsClose" aria-label="Schließen">×</button></div><label for="preferredAudio" data-de="Bevorzugte Audio-Sprache" data-en="Preferred audio language">Bevorzugte Audio-Sprache</label><select id="preferredAudio"><option value="" data-de="Keine Präferenz" data-en="No preference">Keine Präferenz</option><option value="Deutsch" data-de="Deutsch" data-en="German">Deutsch</option><option value="Englisch" data-de="Englisch" data-en="English">Englisch</option><option value="Japanisch" data-de="Japanisch" data-en="Japanese">Japanisch</option><option value="Koreanisch" data-de="Koreanisch" data-en="Korean">Koreanisch</option></select><label for="preferredSubs" data-de="Bevorzugte Untertitel-Sprache" data-en="Preferred subtitle language">Bevorzugte Untertitel-Sprache</label><select id="preferredSubs"><option value="" data-de="Keine Präferenz" data-en="No preference">Keine Präferenz</option><option value="Deutsch" data-de="Deutsch" data-en="German">Deutsch</option><option value="Englisch" data-de="Englisch" data-en="English">Englisch</option><option value="Japanisch" data-de="Japanisch" data-en="Japanese">Japanisch</option><option value="Koreanisch" data-de="Koreanisch" data-en="Korean">Koreanisch</option></select><div class="settings-actions"><button id="settingsCancel" data-de="Abbrechen" data-en="Cancel">Abbrechen</button><button class="settings-save" id="settingsSave" data-de="Speichern" data-en="Save">Speichern</button></div></section></div>
<script>
const rows=[...document.querySelectorAll('#movies tbody tr')], search=document.querySelector('#search'), audio=document.querySelector('#audioFilter'), subs=document.querySelector('#subFilter'), state=document.querySelector('#stateFilter'), count=document.querySelector('#visibleCount');
function apply(){const q=search.value.trim().toLowerCase();let n=0;rows.forEach(r=>{const ok=(!q||r.dataset.title.includes(q))&&(!audio.value||r.dataset.audio.includes(audio.value))&&(!subs.value||r.dataset.subs.includes(subs.value))&&(!state.value||r.dataset.state===state.value);r.classList.toggle('hidden',!ok);if(ok)n++});count.textContent=`${n} Filme`};[search,audio,subs,state].forEach(x=>x.addEventListener(x===search?'input':'change',apply));apply();
document.querySelectorAll('.nav button').forEach(b=>b.onclick=()=>{document.querySelectorAll('.nav button').forEach(x=>x.classList.remove('active'));document.querySelectorAll('.view').forEach(x=>x.classList.remove('active'));b.classList.add('active');document.querySelector('#'+b.dataset.view).classList.add('active');const rescan=document.querySelector('#rescan'),canRescan=['radarr','sonarr'].includes(b.dataset.view);rescan.dataset.source=canRescan?b.dataset.view:'radarr';rescan.disabled=!canRescan;rescan.title=canRescan?'Bibliothek neu scannen':'Für diesen Tab nicht verfügbar'});
function statusText(s){if(s.state==='running')return `Scan: ${s.done}/${s.total||'?'} Dateien`;if(s.state==='done')return 'Scan abgeschlossen';if(s.state==='error')return 'Scanfehler: '+s.error;return 'Scan wird vorbereitet …'}
const seenRunning={radarr:false,sonarr:false};async function poll(){try{const data=await (await fetch('/api/scan-status')).json();document.querySelector('#radarrStatus').textContent=statusText(data.radarr);const sonarr=document.querySelector('#sonarrStatus');if(sonarr)sonarr.textContent=statusText(data.sonarr);['radarr','sonarr'].forEach(source=>{if(data[source].state==='running')seenRunning[source]=true;if(seenRunning[source]&&['done','error'].includes(data[source].state)){seenRunning[source]=false;setTimeout(()=>location.reload(),700)}})}catch(e){} };setInterval(poll,1200);poll();
document.querySelector('#rescan').onclick=async()=>{const b=document.querySelector('#rescan');b.disabled=true;b.textContent='Scanne …';await fetch('/api/rescan?source='+(b.dataset.source||'radarr'),{method:'POST'});b.disabled=false;b.textContent='↻ Neu scannen';poll()};
</script></body></html>
<style>
details.series{margin-bottom:8px;border:1px solid var(--line);border-radius:6px;overflow:hidden;background:#222}
details.series:last-child{margin-bottom:0}
.nav-count{display:inline-block;min-width:18px;padding:1px 5px;margin-left:4px;border-radius:999px;background:#713030;color:#ffd9d9;font-size:10px}.empty-state{padding:28px;text-align:center;color:var(--muted)}.missing-table>table{border-bottom:1px solid var(--line)}.missing-series{border-radius:0!important;border-left:0!important;border-right:0!important;border-bottom:1px solid var(--line)!important}.missing-series:last-child{border-bottom:0!important}
</style>
<script>document.querySelectorAll('details.series').forEach(series => { series.open = false; });</script>
<style>
header{z-index:100!important}
#seriesList{overflow:visible!important}
#seriesList details.series{overflow:visible!important}
#seriesList details.series>summary{position:sticky!important;top:64px!important;z-index:30!important}
#seriesList .season-divider td{top:110px!important;z-index:29!important}
</style>
<style>
.subtitle-de{background:#126f39!important;border-color:#19894b!important;color:#d9ffe8!important}
.audio-preferred{background:#2673a8!important;border-color:#64b5ed!important;color:#eef8ff!important;box-shadow:0 0 12px rgba(96,169,230,.3)}
.col-progress{width:16%}.col-time{width:10%}.col-source{width:10%}.col-language{width:12%}
.progress-wrap{display:flex;align-items:center;gap:8px}.progress-bar{height:7px;min-width:70px;flex:1;background:#3a3a3a;border-radius:99px;overflow:hidden}.progress-bar span{display:block;height:100%;background:#159447;border-radius:99px}.download-title{color:var(--blue);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}
.download-hero{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:14px;padding:24px 26px;border:1px solid rgba(96,169,230,.2);border-radius:14px;background:radial-gradient(circle at 88% 20%,rgba(21,148,71,.18),transparent 32%),linear-gradient(135deg,rgba(39,48,61,.96),rgba(27,30,36,.96));box-shadow:0 16px 40px rgba(0,0,0,.22)}.eyebrow{color:#79b9e9;font-size:10px;font-weight:800;letter-spacing:.16em}.download-hero h2{margin:7px 0 4px;font-size:22px}.download-hero p{margin:0;color:#9da5b2}.queue-pill{display:flex;align-items:center;gap:9px;padding:10px 14px;border:1px solid rgba(255,255,255,.12);border-radius:999px;background:rgba(13,16,20,.35);white-space:nowrap;color:#d9dee7}.queue-dot{width:9px;height:9px;border-radius:50%;background:#f4c430;box-shadow:0 0 12px currentColor}.download-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:14px}.download-card{display:flex;align-items:center;gap:12px;min-height:92px;padding:16px;border:1px solid rgba(255,255,255,.08);border-radius:11px;background:linear-gradient(145deg,rgba(43,47,55,.92),rgba(29,31,36,.92));box-shadow:0 8px 22px rgba(0,0,0,.16)}.card-icon{display:grid;place-items:center;width:38px;height:38px;border-radius:10px;background:rgba(21,148,71,.2);color:#6ae29a;font-size:21px;font-weight:700}.card-icon.blue{background:rgba(96,169,230,.18);color:#8bc9f2}.card-icon.purple{background:rgba(146,87,194,.2);color:#d7adf3}.card-icon.orange{background:rgba(244,166,48,.18);color:#ffd383}.download-card div{min-width:0}.card-label{display:block;color:#9da5b2;font-size:11px;text-transform:uppercase;letter-spacing:.06em}.download-card strong{display:block;margin:3px 0;font-size:20px;color:#f1f5fa}.download-card small{display:block;color:#7f8996;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.download-table{box-shadow:0 16px 34px rgba(0,0,0,.24)}
@media(max-width:900px){.download-stats{grid-template-columns:repeat(2,1fr)}.download-hero{align-items:flex-start;flex-direction:column}.queue-pill{align-self:stretch;justify-content:center}}@media(max-width:520px){.download-stats{grid-template-columns:1fr}.download-hero{padding:18px}.download-hero h2{font-size:19px}}
</style>
<script>
document.querySelectorAll('#movies tbody tr td:nth-child(5) .badge,#sonarr tbody tr td:nth-child(5) .badge').forEach(badge=>{if(badge.textContent.includes('Deutsch'))badge.classList.add('subtitle-de')});
document.querySelectorAll('#movies tbody tr,#sonarr tbody tr').forEach(row=>{if(row.querySelector('.state.downloading'))row.children[1].querySelector('.badge')?.classList.add('downloading-badge')});
const downloadBody=document.querySelector('#downloadsBody');
function html(value){return String(value??'—').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))}
function renderDownloads(data){if(!downloadBody)return;const items=data.items||[], active=Number(data.active_count)||0, history=Number(data.history_count)||0, paused=!!data.paused, status=data.error?'Fehler':(paused?'Pausiert':(data.queue_status||'Idle'));document.querySelector('#downloadCount').textContent=`${items.length} Einträge`;const dashboardDownloads=document.querySelector('#dashboardDownloads'),dashboardRemaining=document.querySelector('#dashboardRemaining'),dashboardSabStatus=document.querySelector('#dashboardSabStatus');if(dashboardDownloads)dashboardDownloads.textContent=active;if(dashboardRemaining)dashboardRemaining.textContent=data.remaining?`Noch ${data.remaining}`:'Queue ist leer';if(dashboardSabStatus)dashboardSabStatus.textContent=data.error?'Fehler':(paused?'Pausiert':'Aktiv');document.querySelector('#sabRemaining').textContent=data.remaining||'—';document.querySelector('#sabRemainingHint').textContent=active?`${active} Einträge in der Queue`:'Queue ist leer';document.querySelector('#sabActive').textContent=active;document.querySelector('#sabHistory').textContent=`Verlauf: ${history} Einträge`;document.querySelector('#sabSpeedCard').textContent=data.speed||'—';document.querySelector('#sabStatusCard').textContent=status;document.querySelector('#sabQueueCard').innerHTML=`<span class="queue-dot ${paused?'paused':''}"></span><span>${html(paused?'Pausiert':(data.error?'Nicht erreichbar':'SABnzbd '+status))}</span>`;const error=document.querySelector('#downloadError');error.classList.toggle('hidden',!data.error);error.innerHTML=data.error?`<strong>SABnzbd:</strong> ${html(data.error)}`:'';downloadBody.innerHTML=items.length?items.map(item=>{const pct=Math.max(0,Math.min(100,Number(item.percentage)||0));const languages=(item.languages||[]).map(l=>`<span class="badge">${html(l.flag)} ${html(l.name)}</span>`).join('')||'<span class="muted">Nicht erkannt</span>';return `<tr><td><span class="download-title" title="${html(item.title)}">${html(item.title)}</span><span class="muted">${html(item.category)}</span></td><td><span class="state ${item.completed?'ok':'neutral'}">${html(item.status)}</span></td><td><div class="progress-wrap"><div class="progress-bar"><span style="width:${pct}%"></span></div><span>${pct.toFixed(1)}%</span></div></td><td>${html(item.size)}<br><span class="muted">Rest: ${html(item.remaining)}</span></td><td>${html(item.completed?'—':item.timeleft)}</td><td>${html(item.source)}</td><td><div class="badges">${languages}</div></td></tr>`}).join(''):'<tr><td colspan="7" class="muted">Keine aktuellen Downloads</td></tr>'}
async function fetchDownloads(){if(!downloadBody)return;try{const response=await fetch('/api/downloads');const data=await response.json();renderDownloads(data);if(typeof markDownloading==='function')markDownloading(data.items)}catch(error){renderDownloads({error:'Dashboard konnte SABnzbd nicht erreichen.',items:[]})}};
if(downloadBody){fetchDownloads();setInterval(fetchDownloads,4000)}
const diskList=document.querySelector('#diskList');
function renderSystem(data){const cpu=Number(data.cpu_percent)||0,ram=data.memory||{};const cpuValue=document.querySelector('#cpuValue'),ramValue=document.querySelector('#ramValue'),cpuBar=document.querySelector('#cpuBar'),ramBar=document.querySelector('#ramBar'),updated=document.querySelector('#systemUpdated');if(cpuValue)cpuValue.textContent=`${cpu.toFixed(1)}%`;if(ramValue)ramValue.textContent=`${ram.used||'—'} / ${ram.total||'—'} (${Number(ram.percent||0).toFixed(1)}%)`;if(cpuBar)cpuBar.style.width=`${Math.min(100,cpu)}%`;if(ramBar)ramBar.style.width=`${Math.min(100,Number(ram.percent)||0)}%`;if(updated)updated.textContent='Gerade aktualisiert';if(!diskList)return;diskList.innerHTML=(data.disks||[]).map(d=>`<div class="disk-row"><div class="disk-name">${html(d.label)}<span class="disk-path">${html(d.path)}</span></div><div class="disk-bar"><span style="width:${Math.min(100,Number(d.percent)||0)}%"></span></div><div class="disk-values">${html(d.used)} / ${html(d.total)}<br><span class="muted">${Number(d.percent||0).toFixed(1)}% belegt</span></div></div>`).join('')||'<div class="disk-row muted">Keine Laufwerke gefunden</div>'}
async function fetchSystem(){try{const response=await fetch('/api/system');renderSystem(await response.json())}catch(error){const updated=document.querySelector('#systemUpdated');if(updated)updated.textContent='Nicht verfügbar'}}
if(document.querySelector('#dashboard')){fetchSystem();setInterval(fetchSystem,5000)}
</script>
<style>
.season-divider td{padding:6px 10px;background:#1b1b1b;border-top:1px solid #444;border-bottom:1px solid #303030;color:#aaa;font-size:11px;font-weight:700;letter-spacing:.3px}
#seriesList{overflow:visible}.series{overflow:visible!important}.series summary{position:sticky!important;top:64px!important;z-index:30!important}.season-divider td{top:110px!important;z-index:29!important}
.series[open] table{animation:seriesExpand .38s cubic-bezier(.2,.75,.25,1) both;transform-origin:top center}.series summary::after{content:'⌄';margin-left:auto;color:#929292;font-size:16px;line-height:1;transition:transform .3s ease,color .3s ease}.series[open] summary::after{transform:rotate(180deg);color:#60a9e6}@keyframes seriesExpand{from{opacity:0;transform:translateY(-8px) scaleY(.98)}to{opacity:1;transform:translateY(0) scaleY(1)}}
.series{position:relative;margin-bottom:14px;border:1px solid #353b46;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.14);transition:border-color .25s ease,box-shadow .25s ease,transform .25s ease}.series:hover{border-color:#4a5565;box-shadow:0 12px 30px rgba(0,0,0,.24);transform:translateY(-1px)}.series-current{border-color:#557ba5;box-shadow:0 0 0 1px rgba(96,169,230,.18),0 14px 34px rgba(0,0,0,.25)}.series summary{position:sticky;top:0;z-index:5;display:flex;align-items:center;min-height:46px;padding:12px 14px;background:rgba(40,40,40,.96);backdrop-filter:blur(12px);border-bottom:1px solid transparent;transition:background .25s ease,border-color .25s ease,color .25s ease}.series-current summary{background:rgba(47,61,78,.98)}.series summary::marker{color:#60a9e6}.series[open] summary{border-bottom-color:#3d444f;box-shadow:0 4px 14px rgba(0,0,0,.16)}.series summary:hover{background:rgba(54,60,70,.97);color:#fff}.series summary .muted{margin-left:7px}.series table tbody tr:not(.season-divider){animation:rowIn .35s ease both;transition:background .2s ease,transform .2s ease}.series table tbody tr:not(.season-divider):hover{background:#292e36;transform:translateX(2px)}.season-divider td{position:sticky;top:46px;z-index:4;background:rgba(27,27,27,.96);backdrop-filter:blur(8px)}
.state.downloading,.downloading-badge{animation:downloadPulse 2s ease-in-out infinite}.progress-bar span{position:relative;overflow:hidden}.progress-bar span::after{content:"";position:absolute;inset:0;background:linear-gradient(110deg,transparent 20%,rgba(255,255,255,.25) 48%,transparent 72%);animation:progressShimmer 1.8s linear infinite}.nav button{transition:color .2s ease,background .2s ease,transform .2s ease}.nav button:hover{transform:translateY(-1px)}.badge{transition:transform .18s ease,filter .18s ease}.badge:hover{transform:translateY(-2px);filter:brightness(1.15)}
@keyframes rowIn{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}@keyframes downloadPulse{0%,100%{box-shadow:0 0 0 rgba(113,60,158,0)}50%{box-shadow:0 0 14px rgba(146,87,194,.35)}}@keyframes progressShimmer{from{transform:translateX(-100%)}to{transform:translateX(100%)}}
@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}
</style>
<style>
:root{--accent:#f4c430;--accent-blue:#60a9e6;--surface:rgba(35,37,42,.82);--surface-strong:#282b31;--border-soft:rgba(255,255,255,.09)}
html,body{min-height:100%;background:#151619}body{min-height:100vh;background:radial-gradient(circle at 10% -15%,rgba(96,169,230,.12),transparent 33%),radial-gradient(circle at 90% 0%,rgba(244,196,48,.07),transparent 28%),#151619;background-attachment:fixed;letter-spacing:.01em}
header{height:64px;background:rgba(31,33,38,.9);backdrop-filter:blur(18px);border-bottom:1px solid var(--border-soft);box-shadow:0 8px 30px rgba(0,0,0,.2);position:sticky;top:0;z-index:20}.logo{font-size:23px;text-shadow:0 0 18px rgba(244,196,48,.24)}.head-title{font-size:14px;letter-spacing:.01em}.head-sub{color:#9da5b2}
main{max-width:1800px;margin:0 auto;padding:18px 20px 42px}.nav{gap:6px;margin:0 0 16px;padding:4px;border:1px solid var(--border-soft);border-radius:9px;background:rgba(31,33,38,.72);box-shadow:0 8px 24px rgba(0,0,0,.12)}.nav button{padding:9px 14px;border-radius:6px;color:#aeb5c1}.nav button.active{background:linear-gradient(135deg,#3a3f48,#2d3139);box-shadow:inset 0 1px rgba(255,255,255,.08),0 4px 12px rgba(0,0,0,.18);color:#fff}.nav button.active::after{content:"";display:block;height:2px;margin:6px 2px -4px;border-radius:2px;background:var(--accent-blue)}
.toolbar{padding:10px;border:1px solid var(--border-soft);border-radius:10px;background:rgba(35,37,42,.72);box-shadow:0 10px 28px rgba(0,0,0,.14);gap:9px}.toolbar input,.toolbar select{background:#2b2e34;border-color:#474d57;transition:border-color .2s,box-shadow .2s,background .2s}.toolbar input:focus,.toolbar select:focus{outline:none;background:#30343b;border-color:var(--accent-blue);box-shadow:0 0 0 3px rgba(96,169,230,.14)}
.stats{gap:9px;margin:12px 0}.stat{background:linear-gradient(135deg,rgba(48,52,60,.9),rgba(34,36,41,.9));border:1px solid var(--border-soft);border-radius:8px;padding:7px 11px;box-shadow:0 5px 15px rgba(0,0,0,.12)}.stat:first-child{color:#fff;font-weight:700}.scan{color:#f4c430!important}
.tablewrap{border:1px solid var(--border-soft);border-radius:10px;background:rgba(31,33,38,.84);box-shadow:0 14px 34px rgba(0,0,0,.2)}table{border-spacing:0}thead th{height:38px;background:linear-gradient(180deg,#30343b,#292c32);color:#bfc6d1;text-transform:uppercase;letter-spacing:.07em;font-size:10px}tbody td{height:42px;border-bottom-color:rgba(255,255,255,.075)}tbody tr{transition:background .2s,transform .2s}tbody tr:hover{background:rgba(96,169,230,.065);transform:translateX(1px)}.title{font-size:13px}.quality{box-shadow:0 0 12px rgba(21,148,71,.18)}
#rescan{padding:9px 13px;background:linear-gradient(135deg,#343943,#292c32);border-color:#515865;box-shadow:0 5px 16px rgba(0,0,0,.18);transition:transform .2s,box-shadow .2s,border-color .2s}#rescan:hover{transform:translateY(-2px);border-color:var(--accent-blue);box-shadow:0 8px 20px rgba(0,0,0,.28)}
.actions{display:flex;align-items:center;gap:7px}.lang-switch,.settings-button{height:36px;min-width:36px;padding:0 9px;background:rgba(48,52,60,.9);border-color:#515865;color:#d9dee7;font-weight:700}.settings-button{font-size:17px;padding:0}.lang-switch:hover,.settings-button:hover{border-color:var(--accent-blue);background:#363c47}.settings-backdrop{position:fixed;inset:0;z-index:200;display:grid;place-items:center;background:rgba(5,7,10,.68);backdrop-filter:blur(8px);animation:fadeIn .2s ease}.settings-backdrop.hidden{display:none}.settings-modal{width:min(440px,calc(100vw - 28px));padding:22px;border:1px solid #4a5362;border-radius:14px;background:linear-gradient(145deg,#292e36,#1f2228);box-shadow:0 24px 80px rgba(0,0,0,.55);animation:modalIn .28s cubic-bezier(.2,.8,.2,1)}.settings-head{display:flex;justify-content:space-between;gap:20px;margin-bottom:22px}.settings-head h2{margin:0 0 5px;font-size:19px}.settings-head p{margin:0;color:var(--muted);font-size:12px}.settings-close{padding:0;width:30px;height:30px;border:0;background:transparent;font-size:25px;color:#aeb5c1}.settings-modal label{display:block;margin:15px 0 7px;color:#cbd1db;font-size:12px;font-weight:700}.settings-modal select{width:100%;height:38px;background:#20242a;border-color:#505967}.settings-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:24px}.settings-actions button{padding:9px 14px}.settings-save{background:#176f40;border-color:#23965b;color:#e4ffef}.settings-save:hover{background:#1c8850}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes modalIn{from{opacity:0;transform:translateY(12px) scale(.97)}to{opacity:1;transform:none}}
@media(max-width:760px){header{padding:0 12px}.head-sub{display:none}main{padding:12px}.toolbar{grid-template-columns:1fr}.actions{margin-left:auto}.nav{overflow-x:auto}.nav button{white-space:nowrap}.tablewrap{overflow-x:auto}table{min-width:900px}}
main{min-height:calc(100vh - 64px)}
</style>
<script>
document.querySelectorAll('details.series tbody').forEach(tbody=>{let currentSeason=null;[...tbody.querySelectorAll('tr')].forEach(row=>{if(row.classList.contains('season-divider'))return;const match=row.querySelector('.title')?.textContent.match(/^S(\d{2})E\d{2}/i);if(!match||match[1]===currentSeason)return;currentSeason=match[1];const divider=document.createElement('tr');divider.className='season-divider';divider.innerHTML=`<td colspan="7">Staffel ${Number(currentSeason)}</td>`;tbody.insertBefore(divider,row)})});
document.querySelectorAll('details.series').forEach(series=>{const summary=series.querySelector('summary');if(!summary)return;summary.title='Klicken zum Auf- oder Zuklappen';const observer=new IntersectionObserver(([entry])=>series.classList.toggle('series-current',!entry.isIntersecting&&series.open),{threshold:0,rootMargin:'-58px 0px 0px 0px'});observer.observe(summary)});
</script>
<style>
.downloading-badge{background:#713c9e!important;border-color:#9257c2!important;color:#f2e5ff!important}.state.downloading{background:#713c9e;color:#f2e5ff}
header{z-index:100!important}
.downloading-state{background:#713c9e!important;color:#f2e5ff!important}
</style>
<script>
function normalizeDownloadText(value){return String(value||'').toLowerCase().replace(/[._()[\]{}'\-·]/g,' ').replace(/\s+/g,' ').trim()}
function markDownloading(items){
const downloads=(items||[]).filter(item=>!item.completed).map(item=>normalizeDownloadText(item.title));
document.querySelectorAll('#movies tbody tr,#sonarr tbody tr').forEach(row=>{
if(row.dataset.downloadMarked==='1'){
row.children[1].innerHTML='<span class="badge">Keine Datei</span>';
row.children[2].innerHTML='<span class="state bad">Fehler</span>';
row.dataset.downloadMarked='0';
}
const details=row.closest('details.series');
const seriesName=details?normalizeDownloadText(details.querySelector('summary')?.textContent.split('(')[0]):'';
const rowName=normalizeDownloadText(row.querySelector('.title')?.textContent);
const rowTokens=rowName.split(' ').filter(token=>token.length>2);
const isMatch=downloads.some(download=>download.includes(rowName)||(seriesName&&download.includes(seriesName)&&download.includes(rowName.match(/s\d{2}e\d{2}/)?.[0]||''))||(rowTokens.length>=3&&rowTokens.filter(token=>download.includes(token)).length>=Math.ceil(rowTokens.length*.75)));
if(!rowName||!isMatch)return;
const quality=row.children[1], state=row.children[2];
if(quality.textContent.includes('Keine Datei')||row.dataset.downloadMarked==='1'){
quality.innerHTML='<span class="badge downloading-badge">Downloading</span>';
state.innerHTML='<span class="state downloading-state">Downloading</span>';
row.dataset.downloadMarked='1';
}
});
}
const originalRenderDownloads=renderDownloads;
renderDownloads=function(data){originalRenderDownloads(data);markDownloading(data.items)};
</script>
<script>
const uiTranslations={de:{film:'Film',quality:'Qualität',status:'Status',audio:'Audio',subs:'Untertitel',video:'Video',size:'Größe',episode:'Episode',allAudio:'Alle Audio-Sprachen',allSubs:'Alle Untertitel',allStatus:'Alle Status',searchMovie:'Film suchen …',searchSeries:'Serie suchen …'},en:{film:'Movie',quality:'Quality',status:'Status',audio:'Audio',subs:'Subtitles',video:'Video',size:'Size',episode:'Episode',allAudio:'All audio languages',allSubs:'All subtitles',allStatus:'All statuses',searchMovie:'Search movies …',searchSeries:'Search series …'}};
let currentUiLanguage=localStorage.getItem('dashboard-language')||'de';
function translateLayout(language){
currentUiLanguage=language;localStorage.setItem('dashboard-language',language);document.documentElement.lang=language;const t=uiTranslations[language];
document.querySelectorAll('[data-de][data-en]').forEach(node=>{node.textContent=node.dataset[language]});
document.querySelector('#languageToggle').textContent=language==='de'?'EN':'DE';
const navLabels={dashboard:['▦ Dashboard','▦ Dashboard'],radarr:['RADARR · Filme','RADARR · Movies'],sonarr:['SONARR · Serien','SONARR · Series'],missing:['⚠ Ohne Deutsch','⚠ Without German'],downloads:['SABNZBD · Downloads','SABNZBD · Downloads']};document.querySelectorAll('.nav button').forEach(button=>{const labels=navLabels[button.dataset.view];if(labels)button.childNodes[0].textContent=labels[language==='de'?0:1]});
const searchInput=document.querySelector('#search');if(searchInput)searchInput.placeholder=t.searchMovie;
const filterOptions=[['#audioFilter',t.allAudio],['#subFilter',t.allSubs],['#stateFilter',t.allStatus]];filterOptions.forEach(([selector,label])=>{const first=document.querySelector(`${selector} option:first-child`);if(first)first.textContent=label});
document.querySelectorAll('#movies thead th').forEach((th,i)=>{th.textContent=[t.film,t.quality,t.status,t.audio,t.subs,t.video,t.size][i]||th.textContent});
document.querySelectorAll('#seriesList thead th').forEach((th,i)=>{th.textContent=[t.episode,t.quality,t.status,t.audio,t.subs,t.video,t.size][i]||th.textContent});
document.querySelectorAll('#downloadsTable thead th').forEach((th,i)=>{th.textContent=(language==='de'?['Download','Status','Fortschritt','Größe / Rest','Restzeit','Quelle','Sprache']:['Download','Status','Progress','Size / Remaining','Time left','Source','Language'])[i]||th.textContent});
}
function highlightPreferences(){const audioPreference=localStorage.getItem('preferred-audio')||'';document.querySelectorAll('#movies tbody tr td:nth-child(4) .badge,#sonarr tbody tr td:nth-child(4) .badge').forEach(badge=>badge.classList.toggle('audio-preferred',!!audioPreference&&badge.textContent.includes(audioPreference)))}
function loadPreferences(){const audioPreference=localStorage.getItem('preferred-audio')||'';const subsPreference=localStorage.getItem('preferred-subs')||'';const audioSelect=document.querySelector('#preferredAudio'),subsSelect=document.querySelector('#preferredSubs');if(audioSelect)audioSelect.value=audioPreference;if(subsSelect)subsSelect.value=subsPreference;const audioFilter=document.querySelector('#audioFilter'),subFilter=document.querySelector('#subFilter');if(audioFilter)audioFilter.value=audioPreference;if(subFilter)subFilter.value=subsPreference;if(typeof apply==='function')apply();highlightPreferences()}
translateLayout(currentUiLanguage);loadPreferences();
document.querySelector('#languageToggle').addEventListener('click',()=>translateLayout(currentUiLanguage==='de'?'en':'de'));
const settingsBackdrop=document.querySelector('#settingsBackdrop');const closeSettings=()=>settingsBackdrop.classList.add('hidden');document.querySelector('#settingsOpen').addEventListener('click',()=>{loadPreferences();settingsBackdrop.classList.remove('hidden')});document.querySelector('#settingsClose').addEventListener('click',closeSettings);document.querySelector('#settingsCancel').addEventListener('click',closeSettings);settingsBackdrop.addEventListener('click',event=>{if(event.target===settingsBackdrop)closeSettings()});document.querySelector('#settingsSave').addEventListener('click',()=>{localStorage.setItem('preferred-audio',document.querySelector('#preferredAudio').value);localStorage.setItem('preferred-subs',document.querySelector('#preferredSubs').value);loadPreferences();closeSettings()});
</script>