Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f52fdf747c | ||
|
|
f78944795c |
No files matched your search
@@ -0,0 +1,19 @@
|
|||||||
|
RADARR_URL=http://radarr:7878
|
||||||
|
RADARR_API_KEY=CHANGE_ME
|
||||||
|
RADARR_MEDIA_PATH=/data/filme
|
||||||
|
LOCAL_MEDIA_PATH=/media/filme
|
||||||
|
FILM_MEDIA_VOLUME=/nesflix/filme
|
||||||
|
|
||||||
|
# Optional: Sonarr aktivieren
|
||||||
|
# SONARR_URL=http://sonarr:8989
|
||||||
|
# SONARR_API_KEY=CHANGE_ME
|
||||||
|
# SONARR_MEDIA_PATH=/data/serien
|
||||||
|
# LOCAL_SERIES_PATH=/media/serien
|
||||||
|
# SERIES_MEDIA_VOLUME=/nesflix/serien
|
||||||
|
|
||||||
|
# Optional: SABnzbd aktivieren
|
||||||
|
# SAB_URL=http://sabnzbd:8080
|
||||||
|
# SAB_API_KEY=CHANGE_ME
|
||||||
|
|
||||||
|
DB_PATH=/data/cache.db
|
||||||
|
LOG_LEVEL=INFO
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
data/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -22,9 +22,21 @@ wird dadurch für ffprobe zu:
|
|||||||
|
|
||||||
`/media/filme/Avatar (2009)/Avatar.mkv`
|
`/media/filme/Avatar (2009)/Avatar.mkv`
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
Die persönlichen URLs, Pfade und API-Keys liegen in `.env`. Diese Datei ist in `.gitignore` eingetragen und bleibt bei `git pull` erhalten. Als Vorlage dient `.env.example`.
|
||||||
|
|
||||||
|
Beim ersten Start:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach die Werte in `.env` eintragen. Sonarr und SABnzbd werden dort durch Auskommentieren der jeweiligen Variablen aktiviert.
|
||||||
|
|
||||||
## Start
|
## Start
|
||||||
|
|
||||||
API-Key und ggf. Docker-Netz in `docker-compose.yml` anpassen:
|
API-Keys und Pfade in `.env` anpassen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose down
|
docker compose down
|
||||||
@@ -43,6 +55,15 @@ SONARR_MEDIA_PATH: "/data/serien"
|
|||||||
LOCAL_SERIES_PATH: "/media/serien"
|
LOCAL_SERIES_PATH: "/media/serien"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
SABnzbd benötigt für den Download-Tab:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SAB_URL: "http://sabnzbd:8080"
|
||||||
|
SAB_API_KEY: "..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Der API-Key steht in SABnzbd unter `Config > General`. Die laufenden Jobs kommen aus der SABnzbd-Queue, abgeschlossene und fehlgeschlagene Jobs aus der History. Die Sprache eines laufenden Releases wird aus dessen Namen abgeleitet; nach dem Import ist die Sprache im Radarr-/Sonarr-Scan verlässlich.
|
||||||
|
|
||||||
## Bei weiterem Scanfehler
|
## Bei weiterem Scanfehler
|
||||||
|
|
||||||
Prüfe einen Pfad im Container:
|
Prüfe einen Pfad im Container:
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ SONARR_URL = os.environ.get("SONARR_URL", "").rstrip("/")
|
|||||||
SONARR_API_KEY = os.environ.get("SONARR_API_KEY", "")
|
SONARR_API_KEY = os.environ.get("SONARR_API_KEY", "")
|
||||||
SONARR_MEDIA_PATH = os.environ.get("SONARR_MEDIA_PATH", "/data/serien").rstrip("/")
|
SONARR_MEDIA_PATH = os.environ.get("SONARR_MEDIA_PATH", "/data/serien").rstrip("/")
|
||||||
LOCAL_SERIES_PATH = os.environ.get("LOCAL_SERIES_PATH", "/media/serien").rstrip("/")
|
LOCAL_SERIES_PATH = os.environ.get("LOCAL_SERIES_PATH", "/media/serien").rstrip("/")
|
||||||
|
SAB_URL = os.environ.get("SAB_URL", "").rstrip("/")
|
||||||
|
SAB_API_KEY = os.environ.get("SAB_API_KEY", "")
|
||||||
DB_PATH = os.environ.get("DB_PATH", "/data/cache.db")
|
DB_PATH = os.environ.get("DB_PATH", "/data/cache.db")
|
||||||
REQUEST_TIMEOUT = int(os.environ.get("REQUEST_TIMEOUT", "20"))
|
REQUEST_TIMEOUT = int(os.environ.get("REQUEST_TIMEOUT", "20"))
|
||||||
|
|
||||||
@@ -46,6 +48,50 @@ def api_get(base, key, endpoint):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
def sab_get(mode, **params):
|
||||||
|
if not SAB_URL or not SAB_API_KEY:
|
||||||
|
raise RuntimeError("SAB_URL oder SAB_API_KEY ist nicht gesetzt.")
|
||||||
|
query = {"output": "json", "apikey": SAB_API_KEY, "mode": mode, **params}
|
||||||
|
r = requests.get(f"{SAB_URL}/api", params=query, timeout=REQUEST_TIMEOUT)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
if data.get("error"):
|
||||||
|
raise RuntimeError(data["error"])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def release_languages(name):
|
||||||
|
value = f" {name.lower().replace('.', ' ').replace('-', ' ')} "
|
||||||
|
found = []
|
||||||
|
patterns = [("Deutsch", (" german ", " deutsch ", " german dubbed ", " ger dub ", " german dl ")), ("Englisch", (" english ", " eng ")), ("Japanisch", (" japanese ", " japan ", " jpn ")), ("Französisch", (" french ", " francais ", " fra ")), ("Spanisch", (" spanish ", " esp "))]
|
||||||
|
codes = {"Deutsch": "de", "Englisch": "en", "Japanisch": "ja", "Französisch": "fr", "Spanisch": "es"}
|
||||||
|
for language, tokens in patterns:
|
||||||
|
if any(token in value for token in tokens): found.append(norm_lang(codes[language]))
|
||||||
|
return unique_langs(found)
|
||||||
|
|
||||||
|
def sab_downloads():
|
||||||
|
queue = sab_get("queue", start=0, limit=100).get("queue") or {}
|
||||||
|
history = sab_get("history", start=0, limit=30, failed_only=0).get("history") or {}
|
||||||
|
wanted = []
|
||||||
|
try:
|
||||||
|
if RADARR_API_KEY: wanted.extend({"title": x.get("movie", {}).get("title") or x.get("title", ""), "source": "Radarr"} for x in api_get(RADARR_URL, RADARR_API_KEY, "queue?includeUnknownMovieItems=true").get("records", []))
|
||||||
|
if SONARR_URL and SONARR_API_KEY: wanted.extend({"title": x.get("title", ""), "source": "Sonarr"} for x in api_get(SONARR_URL, SONARR_API_KEY, "queue?includeUnknownSeriesItems=true").get("records", []))
|
||||||
|
except Exception:
|
||||||
|
log.warning("Radarr/Sonarr-Queue konnte für SAB-Zuordnung nicht geladen werden", exc_info=True)
|
||||||
|
|
||||||
|
def make_item(item, completed=False):
|
||||||
|
title = item.get("name") or item.get("filename") or item.get("nzb_name") or "—"
|
||||||
|
title_lower = title.lower()
|
||||||
|
source = "SABnzbd"
|
||||||
|
for candidate in wanted:
|
||||||
|
candidate_title = candidate["title"].lower()
|
||||||
|
if candidate_title and (candidate_title in title_lower or title_lower in candidate_title): source = candidate["source"]; break
|
||||||
|
percentage = 100.0 if completed else float(item.get("percentage") or 0)
|
||||||
|
return {"title": title, "status": item.get("status") or ("Completed" if completed else "Queued"), "percentage": percentage, "size": item.get("size") or "—", "remaining": item.get("sizeleft") or ("0 B" if completed else "—"), "timeleft": item.get("timeleft") or "—", "speed": item.get("speed") or "—", "category": item.get("cat") or item.get("category") or "—", "source": source, "languages": release_languages(title), "completed": completed}
|
||||||
|
|
||||||
|
items = [make_item(item) for item in queue.get("slots", [])]
|
||||||
|
items.extend(make_item(item, completed=True) for item in history.get("slots", [])[:15])
|
||||||
|
return {"items": items, "queue_status": queue.get("status", "Idle"), "speed": queue.get("speed", "0 B/s"), "paused": bool(queue.get("paused_all")), "error": None}
|
||||||
|
|
||||||
def norm_lang(code):
|
def norm_lang(code):
|
||||||
code = (code or "").strip().lower()
|
code = (code or "").strip().lower()
|
||||||
name = LANG_NAMES.get(code, code.upper() if code else "Unbekannt")
|
name = LANG_NAMES.get(code, code.upper() if code else "Unbekannt")
|
||||||
@@ -173,7 +219,7 @@ def index():
|
|||||||
series = sonarr_groups()
|
series = sonarr_groups()
|
||||||
if any(row["pending"] for group in series for row in group["episodes"]): start_scan("sonarr")
|
if any(row["pending"] for group in series for row in group["episodes"]): start_scan("sonarr")
|
||||||
except Exception as exc: log.exception("Sonarr nicht erreichbar"); error = f"{error + ' | ' if error else ''}Sonarr: {exc}"
|
except Exception as exc: log.exception("Sonarr nicht erreichbar"); error = f"{error + ' | ' if error else ''}Sonarr: {exc}"
|
||||||
return render_template("index.html", rows=rows, series=series, error=error, sonarr_enabled=bool(SONARR_URL and SONARR_API_KEY), radarr_media_path=RADARR_MEDIA_PATH, local_media_path=LOCAL_MEDIA_PATH, local_series_path=LOCAL_SERIES_PATH)
|
return render_template("index.html", rows=rows, series=series, error=error, sonarr_enabled=bool(SONARR_URL and SONARR_API_KEY), sab_enabled=bool(SAB_URL and SAB_API_KEY), radarr_media_path=RADARR_MEDIA_PATH, local_media_path=LOCAL_MEDIA_PATH, local_series_path=LOCAL_SERIES_PATH)
|
||||||
|
|
||||||
@app.post("/api/rescan")
|
@app.post("/api/rescan")
|
||||||
def api_rescan():
|
def api_rescan():
|
||||||
@@ -189,7 +235,17 @@ def api_rescan():
|
|||||||
def scan_status():
|
def scan_status():
|
||||||
with job_lock: return jsonify(jobs)
|
with job_lock: return jsonify(jobs)
|
||||||
|
|
||||||
|
@app.get("/api/downloads")
|
||||||
|
def downloads():
|
||||||
|
if not SAB_URL or not SAB_API_KEY:
|
||||||
|
return jsonify({"items": [], "queue_status": "Nicht konfiguriert", "speed": "—", "paused": False, "error": "SAB_URL oder SAB_API_KEY ist nicht gesetzt."})
|
||||||
|
try:
|
||||||
|
return jsonify(sab_downloads())
|
||||||
|
except Exception as exc:
|
||||||
|
log.exception("SABnzbd konnte nicht abgefragt werden")
|
||||||
|
return jsonify({"items": [], "queue_status": "Fehler", "speed": "—", "paused": False, "error": str(exc)}), 502
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health(): return jsonify({"ok":True, "radarr_url":RADARR_URL, "sonarr_enabled":bool(SONARR_URL and SONARR_API_KEY)})
|
def health(): return jsonify({"ok":True, "radarr_url":RADARR_URL, "sonarr_enabled":bool(SONARR_URL and SONARR_API_KEY), "sab_enabled":bool(SAB_URL and SAB_API_KEY)})
|
||||||
|
|
||||||
if __name__ == "__main__": app.run(host="0.0.0.0", port=8099)
|
if __name__ == "__main__": app.run(host="0.0.0.0", port=8099)
|
||||||
+5
-20
@@ -5,28 +5,13 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8099:8099"
|
- "8099:8099"
|
||||||
environment:
|
env_file:
|
||||||
RADARR_URL: "http://radarr:7878"
|
- .env
|
||||||
RADARR_API_KEY: "CHANGE_ME"
|
|
||||||
|
|
||||||
# Optional: Sonarr aktivieren (auskommentieren und Werte anpassen)
|
|
||||||
# SONARR_URL: "http://sonarr:8989"
|
|
||||||
# SONARR_API_KEY: "CHANGE_ME"
|
|
||||||
|
|
||||||
# Pfad, den Radarr in seiner API meldet:
|
|
||||||
RADARR_MEDIA_PATH: "/data/filme"
|
|
||||||
|
|
||||||
# Pfad derselben Dateien INNERHALB dieses Containers:
|
|
||||||
LOCAL_MEDIA_PATH: "/media/filme"
|
|
||||||
# SONARR_MEDIA_PATH: "/data/serien"
|
|
||||||
# LOCAL_SERIES_PATH: "/media/serien"
|
|
||||||
|
|
||||||
DB_PATH: "/data/cache.db"
|
|
||||||
LOG_LEVEL: "INFO"
|
|
||||||
volumes:
|
volumes:
|
||||||
# Host-Pfad deiner Film-Library -> interner Dashboard-Pfad
|
# Host-Pfad deiner Film-Library -> interner Dashboard-Pfad
|
||||||
- /nesflix/filme:/media/filme:ro
|
- ${FILM_MEDIA_VOLUME}:/media/filme:ro
|
||||||
# - /nesflix/serien:/media/serien:ro
|
# Sonarr aktivieren, wenn SERIES_MEDIA_VOLUME gesetzt ist:
|
||||||
|
# - ${SERIES_MEDIA_VOLUME}:/media/serien:ro
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
networks:
|
networks:
|
||||||
- media
|
- media
|
||||||
|
|||||||
+15
-1
@@ -6,7 +6,7 @@
|
|||||||
<header><div class="logo">MEDIA</div><div><div class="head-title">Language Dashboard</div><div class="head-sub">Audio- und Untertitelspuren aus den Mediendateien</div></div><div class="actions"><button id="rescan">↻ Neu scannen</button></div></header>
|
<header><div class="logo">MEDIA</div><div><div class="head-title">Language Dashboard</div><div class="head-sub">Audio- und Untertitelspuren aus den Mediendateien</div></div><div class="actions"><button id="rescan">↻ Neu scannen</button></div></header>
|
||||||
<main>
|
<main>
|
||||||
{% if error %}<div class="banner"><strong>Fehler:</strong> {{ error }}</div>{% endif %}
|
{% if error %}<div class="banner"><strong>Fehler:</strong> {{ error }}</div>{% endif %}
|
||||||
<nav class="nav"><button class="active" data-view="radarr">RADARR · Filme</button>{% if sonarr_enabled %}<button data-view="sonarr">SONARR · Serien</button>{% endif %}</nav>
|
<nav class="nav"><button class="active" data-view="radarr">RADARR · Filme</button>{% if sonarr_enabled %}<button data-view="sonarr">SONARR · Serien</button>{% endif %}{% if sab_enabled %}<button data-view="downloads">SABNZBD · Downloads</button>{% endif %}</nav>
|
||||||
<section id="radarr" class="view active" data-source="radarr">
|
<section id="radarr" class="view active" 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="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="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>
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
{% 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">
|
{% 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 %}
|
{% 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 %}
|
</div></section>{% endif %}
|
||||||
|
{% if sab_enabled %}<section id="downloads" class="view" data-source="downloads"><div class="stats"><span class="stat" id="downloadCount">Downloads werden geladen …</span><span class="stat" id="sabQueueStatus">SABnzbd …</span><span class="stat" id="sabSpeed">Geschwindigkeit: —</span></div><div id="downloadError" class="banner hidden"></div><div class="tablewrap"><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>
|
</main>
|
||||||
<script>
|
<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');
|
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');
|
||||||
@@ -30,3 +31,16 @@ details.series{margin-bottom:8px;border:1px solid var(--line);border-radius:6px;
|
|||||||
details.series:last-child{margin-bottom:0}
|
details.series:last-child{margin-bottom:0}
|
||||||
</style>
|
</style>
|
||||||
<script>document.querySelectorAll('details.series').forEach(series => { series.open = false; });</script>
|
<script>document.querySelectorAll('details.series').forEach(series => { series.open = false; });</script>
|
||||||
|
<style>
|
||||||
|
.subtitle-de{background:#126f39!important;border-color:#19894b!important;color:#d9ffe8!important}
|
||||||
|
.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}
|
||||||
|
</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')});
|
||||||
|
const downloadBody=document.querySelector('#downloadsBody');
|
||||||
|
function html(value){return String(value??'—').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
|
||||||
|
function renderDownloads(data){if(!downloadBody)return;const items=data.items||[];document.querySelector('#downloadCount').textContent=`${items.length} Downloads`;document.querySelector('#sabQueueStatus').textContent=`SABnzbd: ${data.paused?'Pausiert':data.queue_status||'Idle'}`;document.querySelector('#sabSpeed').textContent=`Geschwindigkeit: ${data.speed||'—'}`;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)}catch(error){renderDownloads({error:'Dashboard konnte SABnzbd nicht erreichen.',items:[]})}};
|
||||||
|
if(downloadBody){fetchDownloads();setInterval(fetchDownloads,4000)}
|
||||||
|
</script>
|
||||||
Reference in new issue
Block a user