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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user