bridge: metodi Api per tab Cataloga + bump v1.11.0
This commit is contained in:
1 parent
9f9e4281b7
commit
9b711fd545
2 files changed
+175
-1
No files matched your search
+169
@@ -47,6 +47,7 @@ from core.upgrader import (
|
||||
count_files_info,
|
||||
)
|
||||
from core import dedup
|
||||
from core import catalog
|
||||
|
||||
|
||||
SPOTIFY_GUIDE_TEXT = """\
|
||||
@@ -113,6 +114,7 @@ class Api:
|
||||
self._upgrade_thread: Optional[threading.Thread] = None
|
||||
self._video_thread: Optional[threading.Thread] = None
|
||||
self._dedup_thread: Optional[threading.Thread] = None
|
||||
self._catalog_thread: Optional[threading.Thread] = None
|
||||
# Coda usata dal resolve_callback per attendere la scelta utente
|
||||
# sul modal "match locali multipli" della tab Upgrade.
|
||||
self._upgrade_resolve_q: "queue.Queue[dict]" = queue.Queue(1)
|
||||
@@ -1963,3 +1965,170 @@ class Api:
|
||||
"n_dupes": n_dupes,
|
||||
"reclaimable_bytes": total_bytes,
|
||||
})
|
||||
|
||||
# ================================================================
|
||||
# CATALOGA — sposta file audio in <output>/<Anno>/<Genere>/
|
||||
# via Chromaprint + AcoustID.
|
||||
# ================================================================
|
||||
def catalog_pick_source_folder(self) -> str:
|
||||
"""Folder picker per la cartella sorgente da catalogare."""
|
||||
return self.browse_directory()
|
||||
|
||||
def catalog_pick_target_folder(self) -> str:
|
||||
"""Folder picker per la cartella target (dove finiscono i file
|
||||
organizzati). Se l'utente lascia vuoto → si usa la source."""
|
||||
return self.browse_directory()
|
||||
|
||||
def catalog_start_scan(self, payload: dict) -> dict:
|
||||
"""Avvia worker di scansione + lookup AcoustID. Payload:
|
||||
{source, target?, recursive}. Persiste last source/target/recursive.
|
||||
"""
|
||||
if self._catalog_thread and self._catalog_thread.is_alive():
|
||||
return {"ok": False, "error": "Scansione catalogo gia in corso"}
|
||||
|
||||
source = (payload.get("source") or "").strip()
|
||||
target = (payload.get("target") or "").strip()
|
||||
recursive = bool(payload.get("recursive", True))
|
||||
|
||||
if not source:
|
||||
return {"ok": False, "error": "Cartella input non impostata"}
|
||||
if not os.path.isdir(source):
|
||||
return {"ok": False, "error": "Cartella input non trovata"}
|
||||
# target vuoto → sposta in-place nella source
|
||||
if not target:
|
||||
target = source
|
||||
|
||||
# Persist last source/target/recursive
|
||||
try:
|
||||
cfg = load_config()
|
||||
cfg["catalog_last_source"] = source
|
||||
cfg["catalog_last_target"] = target
|
||||
cfg["catalog_recursive"] = recursive
|
||||
save_config(cfg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._catalog_thread = threading.Thread(
|
||||
target=self._catalog_worker,
|
||||
args=(source, recursive),
|
||||
daemon=True,
|
||||
)
|
||||
self._catalog_thread.start()
|
||||
return {"ok": True}
|
||||
|
||||
def catalog_stop_scan(self) -> dict:
|
||||
catalog.request_stop()
|
||||
self._log("catalog", "[INFO] Interruzione richiesta...")
|
||||
return {"ok": True}
|
||||
|
||||
def catalog_move_files(self, entries: list, target_root: str) -> dict:
|
||||
"""Sposta le entry selezionate in <target_root>/<year>/<genre>/.
|
||||
|
||||
Non consuma quota (e' una riorganizzazione di file locali,
|
||||
non un download). Ritorna il summary con moved/failed/operations.
|
||||
"""
|
||||
if not isinstance(entries, list):
|
||||
return {"ok": False, "error": "entries deve essere una lista"}
|
||||
target = (target_root or "").strip()
|
||||
if not target:
|
||||
return {"ok": False, "error": "Cartella destinazione mancante"}
|
||||
|
||||
def log_cb(op: dict) -> None:
|
||||
src = op.get("src", "")
|
||||
dst = op.get("dst", "")
|
||||
self._log("catalog", f"[MOVE] {src} → {dst}")
|
||||
try:
|
||||
self._emit("catalog:log", op)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
result = catalog.move_files(entries, target, log_callback=log_cb)
|
||||
except Exception as e:
|
||||
self._log("catalog", f"[ERRORE] move: {e}")
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
moved_n = result.get("moved", 0)
|
||||
failed = result.get("failed", []) or []
|
||||
if moved_n:
|
||||
self._log("catalog", f"[OK] {moved_n} file organizzati")
|
||||
for f in failed:
|
||||
self._log("catalog",
|
||||
f"[ERRORE] {f.get('path')}: {f.get('error')}")
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"moved": moved_n,
|
||||
"failed_count": len(failed),
|
||||
"failed": failed,
|
||||
"operations": result.get("operations", []),
|
||||
}
|
||||
|
||||
def _catalog_worker(self, directory: str, recursive: bool) -> None:
|
||||
"""Esegue scan + lookup AcoustID in background, emette streaming."""
|
||||
view = "catalog"
|
||||
catalog.reset_stop()
|
||||
self._log(view,
|
||||
f"[INFO] Scansione: {directory} (recursive={recursive})")
|
||||
|
||||
_last = [0.0]
|
||||
_THROTTLE = 0.05
|
||||
|
||||
def progress_cb(idx: int, total: int, filename: str,
|
||||
status: str, err_msg: str = "") -> None:
|
||||
# Throttle solo eventi "computing"/"lookup" (possono essere tanti)
|
||||
if status in ("computing", "lookup"):
|
||||
now = time.monotonic()
|
||||
if now - _last[0] < _THROTTLE and idx != total:
|
||||
return
|
||||
_last[0] = now
|
||||
payload_evt = {
|
||||
"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":
|
||||
payload_evt["overall"] = 1.0
|
||||
self._log(view, f"[INFO] Scansione completata ({total} file).")
|
||||
elif status == "stopped":
|
||||
self._log(view, "[INFO] Scansione interrotta.")
|
||||
elif status == "error" and filename:
|
||||
detail = f": {err_msg}" if err_msg else ""
|
||||
self._log(view, f"[ERRORE] {filename}{detail}")
|
||||
self._emit("catalog:progress", payload_evt)
|
||||
|
||||
def entry_cb(entry: dict) -> None:
|
||||
"""Streaming: appena un file è processato, emit alla UI."""
|
||||
try:
|
||||
self._emit("catalog:entry", entry)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
entries = catalog.scan_folder(
|
||||
directory,
|
||||
recursive=recursive,
|
||||
progress_callback=progress_cb,
|
||||
entry_callback=entry_cb,
|
||||
)
|
||||
except Exception as e:
|
||||
self._log(view, f"[ERRORE] {e}")
|
||||
self._emit("catalog:done", {"ok": False, "error": str(e),
|
||||
"entries": []})
|
||||
return
|
||||
|
||||
n_matched = sum(1 for e in entries if e.get("matched"))
|
||||
n_unmatched = len(entries) - n_matched
|
||||
self._log(view,
|
||||
f"[INFO] Match AcoustID: {n_matched} · non identificati: "
|
||||
f"{n_unmatched} (totale {len(entries)})")
|
||||
self._emit("catalog:done", {
|
||||
"ok": True,
|
||||
"entries": entries,
|
||||
"n_total": len(entries),
|
||||
"n_matched": n_matched,
|
||||
"n_unmatched": n_unmatched,
|
||||
})
|
||||
+6
-1
@@ -5,7 +5,7 @@ import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
VERSION = "v1.10.1"
|
||||
VERSION = "v1.11.0"
|
||||
|
||||
|
||||
APP_NAME = "MusicTools"
|
||||
@@ -83,6 +83,11 @@ DEFAULTS = {
|
||||
"dedup_last_folder": "",
|
||||
"dedup_recursive": True,
|
||||
"dedup_method": "fingerprint", # "fingerprint" | "filename"
|
||||
# ---- Cataloga (AcoustID -> <Anno>/<Genere>/) ----
|
||||
"catalog_last_source": "",
|
||||
"catalog_last_target": "",
|
||||
"catalog_recursive": True,
|
||||
"acoustid_app_key": "", # opzionale: se vuoto usa default in core/acoustid.py
|
||||
# ---- Licenza ----
|
||||
"license_key": "", # chiave fornita all'utente via email
|
||||
"license_email": "", # email associata all'acquisto
|
||||
|
||||
Reference in new issue
Block a user