diff --git a/app/main.py b/app/main.py index f6d7b78..a41cd95 100644 --- a/app/main.py +++ b/app/main.py @@ -116,11 +116,16 @@ def scan_start(body: ScanStartBody): stats={}, ) - thread = threading.Thread( - target=sc.run_scan, - args=(body.folder_path, scan_id, mode), - daemon=True, - ) + def _scan_then_thumbs(): + try: + sc.run_scan(body.folder_path, scan_id, mode) + finally: + # Kick off thumbnail pre-generation immediately when scan ends. + # Limited to files actually in duplicate groups — that's the gallery + # view and the only place thumbs are looked at. + _start_thumb_thread(only_in_groups=True) + + thread = threading.Thread(target=_scan_then_thumbs, daemon=True) thread.start() return {"scan_id": scan_id} @@ -629,6 +634,106 @@ def clear_thumb_cache(): return {"cleared": True} +# ── Bulk thumbnail pre-generation ──────────────────────────────────────────── + +thumb_state: dict = { + "status": "idle", # idle | running | done | error + "total": 0, + "done": 0, + "skipped": 0, # already cached + "failed": 0, + "current": "", + "started_at": None, + "completed_at": None, +} +_thumb_thread_lock = threading.Lock() + + +def _generate_all_thumbs(only_in_groups: bool = False): + """Walk every file and generate any missing thumbnail. + + Runs in a background thread. Idempotent — already-cached files are + counted as skipped, not regenerated. + """ + import time + from datetime import datetime + thumb_state.update( + status="running", total=0, done=0, skipped=0, failed=0, + current="", started_at=datetime.utcnow().isoformat() + "Z", + completed_at=None, + ) + try: + con = get_db() + cur = con.cursor() + if only_in_groups: + cur.execute(""" + SELECT DISTINCT f.id, f.path, f.extension + FROM files f + JOIN duplicate_members dm ON dm.file_id = f.id + """) + else: + cur.execute("SELECT id, path, extension FROM files") + files = cur.fetchall() + con.close() + thumb_state["total"] = len(files) + + for r in files: + fid = r["id"] + path = r["path"] + ext = (r["extension"] or "").lower() + cached = _thumb_cache_path(fid) + + thumb_state["current"] = path or "" + + if os.path.isfile(cached) and os.path.getsize(cached) > 0: + thumb_state["skipped"] += 1 + elif not path or not os.path.isfile(path): + thumb_state["failed"] += 1 + elif _generate_thumb(path, cached, ext): + thumb_state["done"] += 1 + else: + thumb_state["failed"] += 1 + + # Yield occasionally so the API stays responsive + if (thumb_state["done"] + thumb_state["skipped"] + thumb_state["failed"]) % 50 == 0: + time.sleep(0) + + from datetime import datetime as _dt + thumb_state["status"] = "done" + thumb_state["completed_at"] = _dt.utcnow().isoformat() + "Z" + thumb_state["current"] = "" + except Exception as e: + thumb_state["status"] = "error" + thumb_state["current"] = f"error: {e}" + + +def _start_thumb_thread(only_in_groups: bool = False) -> bool: + """Start the background generator if not already running. Returns True if started.""" + with _thumb_thread_lock: + if thumb_state["status"] == "running": + return False + t = threading.Thread( + target=_generate_all_thumbs, + args=(only_in_groups,), + daemon=True, + ) + t.start() + return True + + +@app.post("/api/thumbs/generate") +def generate_thumbs(only_in_groups: bool = Query(False)): + """Pre-generate thumbnails for every file (or only files in a duplicate group).""" + if not _start_thumb_thread(only_in_groups): + raise HTTPException(409, "Thumbnail generation already in progress") + return {"status": "started"} + + +@app.get("/api/thumbs/status") +def thumbs_status(): + return dict(thumb_state) + + @app.get("/api/files/{file_id}") def get_file_meta(file_id: int): con = get_db() diff --git a/debian/build-deb.sh b/debian/build-deb.sh index 6039870..56ea70e 100644 --- a/debian/build-deb.sh +++ b/debian/build-deb.sh @@ -13,7 +13,7 @@ BUILD_DIR="$REPO_ROOT/build/deb" # ── Config ──────────────────────────────────────────────────────────────────── PKG_NAME="dupfinder" -PKG_VERSION="1.0.8" +PKG_VERSION="1.0.9" PKG_ARCH="amd64" DEB_FILE="${PKG_NAME}_${PKG_VERSION}_${PKG_ARCH}.deb"