feat: add background library sync thread with configurable interval to eliminate browser dependency for automatic scanning

Add SYNC_INTERVAL_SECONDS environment variable with default 1800 (30 minutes). Add library_cache global dict with rows/series/updated_at/error/ready fields and library_cache_lock for thread-safe access. Extract refresh_library_cache() function to update Radarr/Sonarr data with error handling that preserves last good snapshot during outages. Add background_library_sync() daemon
This commit is contained in:
2026-08-25 20:11:38 +02:00
parent 7313eb96c3
commit 952797005f
4 changed files with 62 additions and 14 deletions
+56 -12
View File
@@ -48,6 +48,7 @@ STORAGE_CACHE_SECONDS = int(os.environ.get("STORAGE_CACHE_SECONDS", "900"))
SMART_ENABLED = os.environ.get("SMART_ENABLED", "false").lower() in {"1", "true", "yes", "on"}
LOG_FILE = os.environ.get("LOG_FILE", "/data/media-max.log")
LOG_RETENTION_HOURS = max(1, int(os.environ.get("LOG_RETENTION_HOURS", "6")))
SYNC_INTERVAL_SECONDS = max(0, int(os.environ.get("SYNC_INTERVAL_SECONDS", "1800")))
logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO").upper(), format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("media-max")
@@ -90,6 +91,8 @@ raid_log_state = None
graphics_log_state = None
graphics_process_log_state = None
hardware_log_once = set()
library_cache_lock = threading.Lock()
library_cache = {"rows": [], "series": [], "updated_at": 0.0, "error": None, "ready": False}
LANG_NAMES = {"de":"Deutsch", "deu":"Deutsch", "ger":"Deutsch", "en":"Englisch", "eng":"Englisch", "ja":"Japanisch", "jpn":"Japanisch", "ko":"Koreanisch", "kor":"Koreanisch", "fr":"Französisch", "fra":"Französisch", "fre":"Französisch", "es":"Spanisch", "spa":"Spanisch", "it":"Italienisch", "ita":"Italienisch", "ru":"Russisch", "rus":"Russisch", "zh":"Chinesisch", "zho":"Chinesisch", "chi":"Chinesisch", "und":"Unbekannt", "":"Unbekannt"}
FLAGS = {"Deutsch":"🇩🇪", "Englisch":"🇬🇧", "Japanisch":"🇯🇵", "Koreanisch":"🇰🇷", "Französisch":"🇫🇷", "Spanisch":"🇪🇸", "Italienisch":"🇮🇹", "Russisch":"🇷🇺", "Chinesisch":"🇨🇳", "Unbekannt":""}
@@ -960,7 +963,7 @@ def sonarr_groups():
"category":category_key, "category_label":category_label})
return sorted(groups, key=lambda x: x["title"].lower())
def start_scan(source, force=False):
def start_scan(source, force=False, rows_snapshot=None, series_snapshot=None):
with job_lock:
if jobs[source]["state"] == "running" or (jobs[source]["state"] in ("done", "error") and not force): return
jobs[source] = {"state":"running", "total":0, "done":0, "error":None}
@@ -968,16 +971,16 @@ def start_scan(source, force=False):
try:
paths = []
if source == "radarr":
for row in radarr_rows():
for row in rows_snapshot if rows_snapshot is not None else radarr_rows():
if row["local_path"] != "" and row["pending"]: paths.append(row["local_path"])
elif source == "sonarr":
for group in sonarr_groups():
for group in series_snapshot if series_snapshot is not None else sonarr_groups():
paths.extend(r["local_path"] for r in group["episodes"] if r["local_path"] != "" and r["pending"])
else:
for row in radarr_rows():
for row in rows_snapshot if rows_snapshot is not None else radarr_rows():
if row["local_path"] != "" and is_german_review_candidate(row) and not has_german_track(row): paths.append(row["local_path"])
if SONARR_URL and SONARR_API_KEY:
for group in sonarr_groups():
for group in series_snapshot if series_snapshot is not None else sonarr_groups():
paths.extend(r["local_path"] for r in group["episodes"] if r["local_path"] != "" and is_german_review_candidate(r) and not has_german_track(r))
with job_lock: jobs[source]["total"] = len(paths)
futures = [scan_executor.submit(scan_one, path) for path in paths]
@@ -989,18 +992,59 @@ def start_scan(source, force=False):
log.error("%s-Scan fehlgeschlagen: %s", source, error_summary(exc)); jobs[source]["error"] = str(exc); jobs[source]["state"] = "error"
executor.submit(work)
@app.route("/")
def index():
error = None; rows = []; series = []
def refresh_library_cache():
"""Refresh Radarr/Sonarr data outside the request thread."""
rows = []
series = []
errors = []
radarr_ok = False
sonarr_ok = not (SONARR_URL and SONARR_API_KEY)
try:
rows = radarr_rows()
if any(row["pending"] for row in rows): start_scan("radarr")
except Exception as exc: error = str(exc)
radarr_ok = True
except Exception as exc:
errors.append(f"Radarr nicht erreichbar: {error_summary(exc)}")
if SONARR_URL and SONARR_API_KEY:
try:
series = sonarr_groups()
if any(row["pending"] for group in series for row in group["episodes"]): start_scan("sonarr")
except Exception as exc: log.error("Sonarr nicht erreichbar: %s", error_summary(exc)); error = f"{error + ' | ' if error else ''}Sonarr nicht erreichbar"
sonarr_ok = True
except Exception as exc:
errors.append(f"Sonarr nicht erreichbar: {error_summary(exc)}")
with library_cache_lock:
# Keep the last good snapshot during a short service/DNS outage.
if not radarr_ok:
rows = library_cache["rows"]
if not sonarr_ok:
series = library_cache["series"]
library_cache.update({"rows": rows, "series": series, "updated_at": time.time(), "error": " | ".join(errors) or None, "ready": True})
if rows and any(row.get("pending") for row in rows):
start_scan("radarr", force=True, rows_snapshot=rows)
if series and any(row.get("pending") for group in series for row in group["episodes"]):
start_scan("sonarr", force=True, series_snapshot=series)
if rows or series:
start_scan("missing", force=True, rows_snapshot=rows, series_snapshot=series)
log.info("Bibliotheks-Sync abgeschlossen: %s Filme, %s Serien", len(rows), len(series))
def background_library_sync():
"""Keep library data and media metadata current without a browser tab."""
log.info("Hintergrund-Sync gestartet (Intervall: %ss)", SYNC_INTERVAL_SECONDS or "deaktiviert")
while True:
try:
refresh_library_cache()
except Exception as exc:
log.error("Hintergrund-Sync fehlgeschlagen: %s", error_summary(exc))
if not SYNC_INTERVAL_SECONDS:
return
time.sleep(SYNC_INTERVAL_SECONDS)
threading.Thread(target=background_library_sync, name="library-sync", daemon=True).start()
@app.route("/")
def index():
with library_cache_lock:
rows = list(library_cache["rows"])
series = list(library_cache["series"])
error = library_cache["error"]
missing_german_movies = [row for row in rows if is_german_review_candidate(row) and not has_german_track(row)]
missing_german_series = []
for group in series: