From 05e32fb5af436f8a074f3e9f58fcb4972a5b5205 Mon Sep 17 00:00:00 2001 From: nessi Date: Thu, 13 Aug 2026 16:28:10 +0200 Subject: [PATCH] feat: add enhanced SABnzbd download dashboard with remaining data calculation and visual redesign Add parse_size_bytes() and format_bytes() helper functions to convert and format file sizes. Calculate total remaining bytes from queue and include active_count, history_count, remaining_bytes, and formatted remaining in downloads API response. Update error responses to include new fields. Add media-mark.svg favicon with gradient background, golden lines, and green checkmark. Replace text logo with inline --- app.py | 28 +++++++++++++++++++++++++--- static/media-mark.svg | 12 ++++++++++++ templates/index.html | 12 +++++++----- 3 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 static/media-mark.svg diff --git a/app.py b/app.py index e7112db..0412620 100644 --- a/app.py +++ b/app.py @@ -4,6 +4,7 @@ import os import sqlite3 import subprocess import threading +import re from concurrent.futures import ThreadPoolExecutor from datetime import datetime from pathlib import Path @@ -59,6 +60,23 @@ def sab_get(mode, **params): raise RuntimeError(data["error"]) return data +def parse_size_bytes(value): + if isinstance(value, (int, float)): + return int(value) + match = re.search(r"([\d.,]+)\s*(B|KB|MB|GB|TB)", str(value or ""), re.IGNORECASE) + if not match: + return 0 + number = float(match.group(1).replace(',', '.')) + multiplier = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3, "TB": 1024**4}[match.group(2).upper()] + return int(number * multiplier) + +def format_bytes(size): + value = float(size or 0) + for unit in ("B", "KB", "MB", "GB", "TB"): + if value < 1024 or unit == "TB": + return f"{value:.1f} {unit}" + value /= 1024 + def release_languages(name): value = f" {name.lower().replace('.', ' ').replace('-', ' ')} " found = [] @@ -90,7 +108,11 @@ def sab_downloads(): 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} + remaining_bytes = sum(parse_size_bytes(item.get("sizeleft")) for item in queue.get("slots", [])) + return {"items": items, "queue_status": queue.get("status", "Idle"), "speed": queue.get("speed", "0 B/s"), + "paused": bool(queue.get("paused_all")), "active_count": len(queue.get("slots", [])), + "history_count": len(history.get("slots", [])[:15]), "remaining_bytes": remaining_bytes, + "remaining": format_bytes(remaining_bytes), "error": None} def norm_lang(code): code = (code or "").strip().lower() @@ -257,12 +279,12 @@ def scan_status(): @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."}) + return jsonify({"items": [], "queue_status": "Nicht konfiguriert", "speed": "—", "paused": False, "active_count": 0, "history_count": 0, "remaining": "—", "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 + return jsonify({"items": [], "queue_status": "Fehler", "speed": "—", "paused": False, "active_count": 0, "history_count": 0, "remaining": "—", "error": str(exc)}), 502 @app.get("/health") 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)}) diff --git a/static/media-mark.svg b/static/media-mark.svg new file mode 100644 index 0000000..ae34839 --- /dev/null +++ b/static/media-mark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/templates/index.html b/templates/index.html index 538028a..ea40361 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,9 +1,9 @@ -Language Dashboard +Language Dashboard -
Language Dashboard
Audio- und Untertitelspuren aus den Mediendateien
+
MEDIA
Language Dashboard
Audio- und Untertitelspuren aus den Mediendateien
{% if error %}{% endif %} @@ -20,7 +20,7 @@ {% for r in missing_german_movies %}{% endfor %}
FilmQualitätStatusAudioUntertitelVideoGröße
{{ r.title }}{{ r.year or '' }}{{ r.quality }}Ohne Deutsch
{% for l in r.audio_languages %}{{ l.flag }} {{ l.name }}{% else %}{% endfor %}
{% for l in r.subtitle_languages %}{{ l.flag }} {{ l.name }}{% else %}{% endfor %}
{{ r.video_codec }}{{ r.size }}
{% for group in missing_german_series %}
{{ group.title }} ({{ group.year or '—' }}) · {{ group.episodes|length }} Episoden ohne Deutsch{% for r in group.episodes %}{% endfor %}
EpisodeQualitätStatusAudioUntertitelVideoGröße
{{ r.title }}{{ r.quality }}Ohne Deutsch
{% for l in r.audio_languages %}{{ l.flag }} {{ l.name }}{% else %}{% endfor %}
{% for l in r.subtitle_languages %}{{ l.flag }} {{ l.name }}{% else %}{% endfor %}
{{ r.video_codec }}{{ r.size }}
{% endfor %} {% if not missing_german_movies and not missing_german_series %}
Alle vorhandenen Filme und Episoden haben deutsches Audio oder deutsche Untertitel.
{% endif %} -{% if sab_enabled %}
Downloads werden geladen …SABnzbd …Geschwindigkeit: —
DownloadStatusFortschrittGröße / RestRestzeitQuelleSprache
Downloads werden geladen …
{% endif %} +{% if sab_enabled %}
SABNZBD · DOWNLOAD CENTER

Download-Übersicht

Queue, Fortschritt und verbleibender Speicherplatz auf einen Blick.

Verbinde mit SABnzbd …
Noch zu ladenWird berechnet …
Aktive DownloadsVerlauf: —
GeschwindigkeitAktuelle Queue
Status
DownloadStatusFortschrittGröße / RestRestzeitQuelleSprache
Downloads werden geladen …
{% endif %}