feat: add parallel file scanning with dedicated executor and support for scanning all sources simultaneously

Add scan_executor ThreadPoolExecutor with 4 workers for parallel file scanning. Import as_completed from concurrent.futures to process scan results. Replace sequential scan_one() loop with parallel futures submission and result collection using as_completed().

Update /api/rescan endpoint to accept "all" source parameter for scanning both Radarr and Sonarr simultaneously. Fix cache deletion
This commit is contained in:
2026-08-14 13:22:15 +02:00
parent 644c54a6f6
commit b5365170fb
2 changed files with 19 additions and 11 deletions
+15 -7
View File
@@ -7,7 +7,7 @@ import threading
import time
import shutil
import re
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from pathlib import Path
@@ -31,6 +31,7 @@ logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO").upper(), format="%
log = logging.getLogger("radarr-language-dashboard")
app = Flask(__name__)
executor = ThreadPoolExecutor(max_workers=int(os.environ.get("SCAN_WORKERS", "2")))
scan_executor = ThreadPoolExecutor(max_workers=int(os.environ.get("SCAN_WORKERS", "4")))
jobs = {"radarr": {"state": "idle", "total": 0, "done": 0, "error": None}, "sonarr": {"state": "idle", "total": 0, "done": 0, "error": None}}
job_lock = threading.Lock()
system_lock = threading.Lock()
@@ -283,8 +284,9 @@ def start_scan(source, force=False):
for group in sonarr_groups():
paths.extend(r["local_path"] for r in group["episodes"] if r["local_path"] != "" and r["pending"])
with job_lock: jobs[source]["total"] = len(paths)
for path in paths:
scan_one(path)
futures = [scan_executor.submit(scan_one, path) for path in paths]
for future in as_completed(futures):
future.result()
with job_lock: jobs[source]["done"] += 1
with job_lock: jobs[source]["state"] = "done"
except Exception as exc:
@@ -329,11 +331,17 @@ def index():
@app.post("/api/rescan")
def api_rescan():
source = request.args.get("source", "radarr")
if source not in jobs: return jsonify({"ok":False, "error":"Unbekannte Quelle"}), 400
if source not in (*jobs.keys(), "all"): return jsonify({"ok":False, "error":"Unbekannte Quelle"}), 400
con = db()
if source == "radarr": con.execute("DELETE FROM media_cache")
else: con.execute("DELETE FROM media_cache WHERE path LIKE ?", (LOCAL_SERIES_PATH.rstrip("/") + "/%",))
con.commit(); con.close(); start_scan(source, force=True)
if source in ("radarr", "all"): con.execute("DELETE FROM media_cache WHERE path NOT LIKE ?", (LOCAL_SERIES_PATH.rstrip("/") + "/%",))
if source in ("sonarr", "all"): con.execute("DELETE FROM media_cache WHERE path LIKE ?", (LOCAL_SERIES_PATH.rstrip("/") + "/%",))
con.commit(); con.close()
if source == "all":
start_scan("radarr", force=True)
if SONARR_URL and SONARR_API_KEY:
start_scan("sonarr", force=True)
else:
start_scan(source, force=True)
return jsonify({"ok":True})
@app.get("/api/scan-status")