Compare commits
4
Commits
83dc8c08ae
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00b841fa67 | ||
|
|
3fb2b34587 | ||
|
|
de0d360a3c | ||
|
|
3f418bb2a3 |
@@ -913,8 +913,18 @@ def scan_one(path):
|
||||
p = Path(path)
|
||||
if not p.exists(): data = {"error": f"Datei nicht gefunden: {p}"}
|
||||
else:
|
||||
try: data = run_ffprobe(str(p)); log.info("Gescannt: %s", p)
|
||||
except Exception as exc: data = {"error": str(exc)}; log.error("Scanfehler für %s: %s", p, error_summary(exc))
|
||||
try:
|
||||
data = run_ffprobe(str(p)); log.info("Gescannt: %s", p)
|
||||
except Exception as exc:
|
||||
# Cache failed probes as completed attempts. A changed mtime will
|
||||
# invalidate this entry automatically and trigger a retry later.
|
||||
data = {
|
||||
"scanner_version": SCANNER_VERSION,
|
||||
"audio_languages": [], "subtitle_languages": [],
|
||||
"external_subtitle_files": [str(file) for file in external_subtitle_files(str(p))],
|
||||
"video_codec": "—", "size": 0, "error": error_summary(exc),
|
||||
}
|
||||
log.error("Scanfehler für %s: %s", p, error_summary(exc))
|
||||
mtime = p.stat().st_mtime
|
||||
con = db(); con.execute("INSERT INTO media_cache(path,mtime,data,scanned_at) VALUES(?,?,?,?) ON CONFLICT(path) DO UPDATE SET mtime=excluded.mtime,data=excluded.data,scanned_at=excluded.scanned_at", (str(p), mtime, json.dumps(data), datetime.utcnow().isoformat())); con.commit(); con.close()
|
||||
return data
|
||||
@@ -996,7 +1006,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, rows_snapshot=None, series_snapshot=None):
|
||||
def start_scan(source, force=False, rows_snapshot=None, series_snapshot=None, pending_only=False):
|
||||
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}
|
||||
@@ -1005,22 +1015,24 @@ def start_scan(source, force=False, rows_snapshot=None, series_snapshot=None):
|
||||
paths = []
|
||||
if source == "radarr":
|
||||
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"])
|
||||
if row["local_path"] != "—" and row["pending"] and not row.get("downloading"): paths.append(row["local_path"])
|
||||
elif source == "sonarr":
|
||||
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"])
|
||||
paths.extend(r["local_path"] for r in group["episodes"] if r["local_path"] != "—" and r["pending"] and not r.get("downloading"))
|
||||
else:
|
||||
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 row["local_path"] != "—" and is_german_review_candidate(row) and not row.get("downloading") and not has_german_track(row) and (not pending_only or row.get("pending")): paths.append(row["local_path"])
|
||||
if SONARR_URL and SONARR_API_KEY:
|
||||
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))
|
||||
paths.extend(r["local_path"] for r in group["episodes"] if r["local_path"] != "—" and is_german_review_candidate(r) and not r.get("downloading") and not has_german_track(r) and (not pending_only or r.get("pending")))
|
||||
with job_lock: jobs[source]["total"] = len(paths)
|
||||
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"
|
||||
# Publish the newly scanned metadata immediately to the cache.
|
||||
refresh_library_cache()
|
||||
except Exception as exc:
|
||||
log.error("%s-Scan fehlgeschlagen: %s", source, error_summary(exc)); jobs[source]["error"] = str(exc); jobs[source]["state"] = "error"
|
||||
executor.submit(work)
|
||||
@@ -1054,8 +1066,15 @@ def refresh_library_cache():
|
||||
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)
|
||||
needs_missing_scan = any(
|
||||
row.get("local_path") != "—" and is_german_review_candidate(row) and row.get("pending") and not has_german_track(row)
|
||||
for row in rows
|
||||
) or any(
|
||||
episode.get("local_path") != "—" and is_german_review_candidate(episode) and episode.get("pending") and not has_german_track(episode)
|
||||
for group in series for episode in group["episodes"]
|
||||
)
|
||||
if needs_missing_scan:
|
||||
start_scan("missing", force=True, rows_snapshot=rows, series_snapshot=series, pending_only=True)
|
||||
log.info("Bibliotheks-Sync abgeschlossen: %s Filme, %s Serien", len(rows), len(series))
|
||||
|
||||
def background_library_sync():
|
||||
@@ -1079,10 +1098,12 @@ def index():
|
||||
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)]
|
||||
# Unscanned files have no language information yet and must not be shown
|
||||
# as missing German while the background scanner is still processing them.
|
||||
missing_german_movies = [row for row in rows if is_german_review_candidate(row) and not row.get("pending") and not has_german_track(row)]
|
||||
missing_german_series = []
|
||||
for group in series:
|
||||
episodes = [episode for episode in group["episodes"] if is_german_review_candidate(episode) and not has_german_track(episode)]
|
||||
episodes = [episode for episode in group["episodes"] if is_german_review_candidate(episode) and not episode.get("pending") and not has_german_track(episode)]
|
||||
if episodes:
|
||||
missing_german_series.append({**group, "episodes": episodes})
|
||||
series_categories = [
|
||||
@@ -1129,6 +1150,11 @@ def api_rescan():
|
||||
def scan_status():
|
||||
with job_lock: return jsonify(jobs)
|
||||
|
||||
@app.get("/api/library-status")
|
||||
def library_status():
|
||||
with library_cache_lock:
|
||||
return jsonify({"ready": library_cache["ready"], "updated_at": library_cache["updated_at"], "error": library_cache["error"]})
|
||||
|
||||
@app.get("/api/downloads")
|
||||
def downloads():
|
||||
if not SAB_URL or not SAB_API_KEY:
|
||||
|
||||
@@ -100,6 +100,9 @@ const seenRunning={radarr:false,sonarr:false,missing:false};async function poll(
|
||||
document.querySelector('#rescan').onclick=async()=>{const b=document.querySelector('#rescan'),source=document.querySelector('#scanSource').value;sessionStorage.setItem('dashboard-active-view',document.querySelector('.nav button.active')?.dataset.view||'dashboard');b.disabled=true;b.textContent='Scan läuft …';await fetch('/api/rescan?source='+source,{method:'POST'});b.disabled=false;b.textContent='↻ Neu scannen';poll()};
|
||||
</script>
|
||||
<script>
|
||||
let lastLibrarySync=0;async function pollLibrarySync(){try{const response=await fetch('/api/library-status',{cache:'no-store'}),data=await response.json();if(!data.ready)return;if(!lastLibrarySync){lastLibrarySync=Number(data.updated_at)||0;return}if(Number(data.updated_at)>lastLibrarySync){lastLibrarySync=Number(data.updated_at);sessionStorage.setItem('dashboard-active-view',document.querySelector('.nav button.active')?.dataset.view||'dashboard');location.reload()}}catch(error){}}pollLibrarySync();setInterval(pollLibrarySync,5000);
|
||||
</script>
|
||||
<script>
|
||||
const scanLabel=document.createElement('label');scanLabel.htmlFor='autoScan';scanLabel.textContent='Automatisches Scanning';const scanHelp=document.createElement('span');scanHelp.className='setting-help';scanHelp.textContent='Bibliotheken werden automatisch geprüft, solange das Dashboard geöffnet ist.';const scanSelect=document.createElement('select');scanSelect.id='autoScan';scanSelect.className='setting-select';[['0','Deaktiviert'],['1800000','Alle 30 Minuten'],['3600000','Jede Stunde'],['7200000','Alle 2 Stunden'],['10800000','Alle 3 Stunden'],['14400000','Alle 4 Stunden'],['18000000','Alle 5 Stunden']].forEach(([value,label])=>{const option=document.createElement('option');option.value=value;option.textContent=label;scanSelect.appendChild(option)});const settingsActions=document.querySelector('.settings-actions');if(settingsActions){settingsActions.before(scanLabel,scanHelp,scanSelect)}
|
||||
function configureAutoScan(){}function saveAutoScan(){localStorage.setItem('auto-scan-interval',scanSelect.value)}scanSelect.value=localStorage.getItem('auto-scan-interval')||'0';document.querySelector('#settingsSave').addEventListener('click',saveAutoScan);
|
||||
const seasonZeroLabel=document.createElement('label');seasonZeroLabel.className='setting-toggle';seasonZeroLabel.htmlFor='showSeasonZero';seasonZeroLabel.innerHTML='<input type="checkbox" id="showSeasonZero"><span>Staffel 0 anzeigen</span>';const seasonZeroHelp=document.createElement('span');seasonZeroHelp.className='setting-help';seasonZeroHelp.textContent='Gilt für Sonarr und die Liste Ohne Deutsch.';if(settingsActions){settingsActions.before(seasonZeroLabel,seasonZeroHelp)}
|
||||
|
||||
Reference in New Issue
Block a user