feat: Dedup enhancements (streaming, retry, filename mode, scroll, auto-select)

Iterazioni sul feature Dedup (v1.10.0) da subito-post-scaffolding:
- compute_fingerprint espone il vero errore invece del generico None,
  worker propaga l'err_msg via progress_callback esteso
- Retry automatico: -length 30 su "invalid data / decoding frame",
  -length 60 + -algorithm 1 su "fingerprint vuoto"
- Nuovo metodo "filename" (Jaccard sui token nome file) con algoritmo
  incrementale O(N·K) — utile per file corrotti o per anteprima veloce
- Streaming groups: sia fingerprint che filename emettono `dedup:group`
  appena un gruppo raggiunge >=2 file; JS accumula in Map per update
  in-place. Progress bar avanza in tempo reale
- Fix shape bug che dava "NaN duplicati" nella summary line
- .dedup-groups-scroll: max-height 55vh + overflow-y auto per non
  perdere l'header/footer scrollando molti gruppi
- Bottone "Seleziona tutti i consigliati" nel footer: ripristina la
  selezione di default (tutti tranne il TIENI marcati per cancellazione)
- .gitignore: dedup_cache.db (SQLite locale per macchina utente)
This commit is contained in:
luciano committed 2026-08-02 13:59:39 +02:00
1 parent 00ff5a6220
commit e7d11d0235
8 files changed
+315 -47

No files matched your search

+25 -7
View File
@@ -1841,24 +1841,28 @@ class Api:
directory = (payload.get("directory") or "").strip()
recursive = bool(payload.get("recursive", True))
method = (payload.get("method") or "fingerprint").strip()
if method not in ("fingerprint", "filename"):
method = "fingerprint"
if not directory:
return {"ok": False, "error": "Cartella non impostata"}
if not os.path.isdir(directory):
return {"ok": False, "error": "Cartella non trovata"}
# Persist last folder/recursive
# Persist last folder/recursive/method
try:
cfg = load_config()
cfg["dedup_last_folder"] = directory
cfg["dedup_recursive"] = recursive
cfg["dedup_method"] = method
save_config(cfg)
except Exception:
pass
self._dedup_thread = threading.Thread(
target=self._dedup_worker,
args=(directory, recursive),
args=(directory, recursive, method),
daemon=True,
)
self._dedup_thread.start()
@@ -1890,16 +1894,18 @@ class Api:
"failed": result.get("failed", []),
"moved_count": moved_n, "failed_count": failed_n}
def _dedup_worker(self, directory: str, recursive: bool) -> None:
def _dedup_worker(self, directory: str, recursive: bool,
method: str = "fingerprint") -> None:
"""Esegue la scansione in background e emette progress/done."""
view = "dedup"
dedup.reset_stop()
self._log(view, f"[INFO] Scansione: {directory} (recursive={recursive})")
method_label = "audio fingerprint" if method == "fingerprint" else "nome file"
self._log(view, f"[INFO] Scansione: {directory} (metodo: {method_label}, recursive={recursive})")
_last = [0.0]
_THROTTLE = 0.05
def progress_cb(idx: int, total: int, filename: str, status: str) -> None:
def progress_cb(idx: int, total: int, filename: str, status: str, err_msg: str = "") -> None:
# Throttle solo eventi 'computing'/'cached' (che possono essere migliaia)
if status in ("computing", "cached"):
now = time.monotonic()
@@ -1910,6 +1916,8 @@ class Api:
"idx": idx, "total": total,
"filename": filename, "status": status,
}
if err_msg:
payload_evt["error_msg"] = err_msg
if total > 0:
payload_evt["overall"] = min(idx / total, 1.0)
if status == "completed":
@@ -1918,12 +1926,22 @@ class Api:
elif status == "stopped":
self._log(view, "[INFO] Scansione interrotta.")
elif status == "error" and filename:
self._log(view, f"[ERRORE] {filename}: errore lettura")
detail = f": {err_msg}" if err_msg else ""
self._log(view, f"[ERRORE] {filename}{detail}")
self._emit("dedup:progress", payload_evt)
def group_cb(group: dict) -> None:
"""Emit streaming: appena un gruppo raggiunge/aggiorna >=2 file."""
try:
self._emit("dedup:group", group)
except Exception:
pass
try:
groups = dedup.scan_folder(directory, recursive=recursive,
progress_callback=progress_cb)
progress_callback=progress_cb,
method=method,
group_callback=group_cb)
except Exception as e:
self._log(view, f"[ERRORE] {e}")
self._emit("dedup:done", {"ok": False, "error": str(e),