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:
115
app/main.py
115
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()
|
||||
|
||||
2
debian/build-deb.sh
vendored
2
debian/build-deb.sh
vendored
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user