Pre-generate all thumbnails up-front, not on scroll

After every scan, automatically kick off a background thread that
generates a JPEG thumbnail for every file in a duplicate group and
caches it locally at /data/thumbs/. Idempotent — already-cached files
are skipped.

New endpoints:
  POST /api/thumbs/generate            — start pre-gen for all files
  POST /api/thumbs/generate?only_in_groups=true  — only dup-group files
  GET  /api/thumbs/status              — progress (total/done/skipped/failed)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Carlos
2026-04-26 16:33:19 -04:00
parent 4c21e9fa1c
commit 759288b37e
2 changed files with 111 additions and 6 deletions

View File

@@ -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()

2
debian/build-deb.sh vendored
View File

@@ -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"