diff --git a/api/bridge.py b/api/bridge.py index 82618ad..3b87f7e 100644 --- a/api/bridge.py +++ b/api/bridge.py @@ -50,6 +50,7 @@ from core import dedup from core import catalog from core import flatten from core import charts +from core import royalty_check as royalty SPOTIFY_GUIDE_TEXT = """\ @@ -118,6 +119,7 @@ class Api: self._dedup_thread: Optional[threading.Thread] = None self._catalog_thread: Optional[threading.Thread] = None self._flatten_thread: Optional[threading.Thread] = None + self._royalty_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) @@ -211,6 +213,7 @@ class Api: "cookies_browser": (payload.get("cookies_browser") or "").strip().lower(), "output_dir": (payload.get("output_dir") or "").strip(), "theme": payload.get("theme", "dark"), + "jamendo_client_id": (payload.get("jamendo_client_id") or "").strip(), }) save_config(config) return {"ok": True} @@ -2384,3 +2387,130 @@ class Api: self._emit("charts:add_to_queue", payload) self._log("charts", f"[INFO] {len(tracks)} brani inviati alla coda Scarica") return {"ok": True, "count": len(tracks)} + + # ================================================================ + # DIRITTI — verifica royalty-free status (ccMixter + Jamendo) + # ================================================================ + def royalty_pick_folder(self) -> str: + """Folder picker per la cartella da analizzare.""" + return self.browse_directory() + + def royalty_start_scan(self, payload: dict) -> dict: + """Avvia worker di scansione royalty-free. + + Payload: {directory, recursive}. + Persiste `royalty_last_folder` e `royalty_recursive` in config. + """ + if self._royalty_thread and self._royalty_thread.is_alive(): + return {"ok": False, "error": "Analisi diritti gia in corso"} + + directory = (payload.get("directory") or "").strip() + recursive = bool(payload.get("recursive", True)) + + if not directory: + return {"ok": False, "error": "Cartella non impostata"} + if not os.path.isdir(directory): + return {"ok": False, "error": "Cartella non trovata"} + + try: + cfg = load_config() + cfg["royalty_last_folder"] = directory + cfg["royalty_recursive"] = recursive + save_config(cfg) + except Exception: + pass + + self._royalty_thread = threading.Thread( + target=self._royalty_worker, + args=(directory, recursive), + daemon=True, + ) + self._royalty_thread.start() + return {"ok": True} + + def royalty_stop_scan(self) -> dict: + royalty.request_stop() + self._log("royalty", "[INFO] Interruzione richiesta...") + return {"ok": True} + + def _royalty_worker(self, directory: str, recursive: bool) -> None: + """Esegue scan royalty-free in background, emette streaming.""" + view = "royalty" + royalty.reset_stop() + self._log(view, + f"[INFO] Analisi diritti: {directory} (recursive={recursive})") + + _last = [0.0] + _THROTTLE = 0.05 + + def progress_cb(idx: int, total: int, filename: str, + status: str, err_msg: str = "") -> None: + if status == "checking": + 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] Analisi completata ({total} file).") + elif status == "stopped": + self._log(view, "[INFO] Analisi interrotta.") + elif status == "error" and filename: + detail = f": {err_msg}" if err_msg else "" + self._log(view, f"[ERRORE] {filename}{detail}") + self._emit("royalty:progress", payload_evt) + + def entry_cb(entry: dict) -> None: + """Streaming: appena un file e' processato, emit alla UI.""" + try: + self._emit("royalty:entry", entry) + except Exception: + pass + # Log per file: solo quando trova un match RF + try: + st = entry.get("status", "") + if st == "royalty_free": + matches = entry.get("matches") or [] + src = matches[0].get("source", "?") if matches else "?" + lic = matches[0].get("license", "") if matches else "" + from pathlib import Path as _P + name = _P(entry.get("path", "")).name + self._log(view, f"[OK] {name} → {src} ({lic})") + except Exception: + pass + + try: + entries = royalty.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("royalty:done", {"ok": False, "error": str(e), + "entries": []}) + return + + n_rf = sum(1 for e in entries if e.get("status") == "royalty_free") + n_unk = sum(1 for e in entries if e.get("status") == "unknown") + n_err = sum(1 for e in entries if e.get("status") == "error") + self._log(view, + f"[INFO] Royalty-free: {n_rf} · sconosciuti: {n_unk} · " + f"errori: {n_err} (totale {len(entries)})") + self._emit("royalty:done", { + "ok": True, + "entries": entries, + "total": len(entries), + "royalty_free_count": n_rf, + "unknown_count": n_unk, + "error_count": n_err, + }) diff --git a/core/config.py b/core/config.py index b78cbac..efb61f5 100644 --- a/core/config.py +++ b/core/config.py @@ -5,7 +5,7 @@ import os import sys from pathlib import Path -VERSION = "v1.13.0" +VERSION = "v1.14.0" APP_NAME = "MusicTools" @@ -95,6 +95,10 @@ DEFAULTS = { "charts_lastfm_genre": "dance", # "" | pop, rock, dance, ... "charts_spotify_playlist": "top50_global", # chiave in charts._SPOTIFY_CHARTS "lastfm_api_key": "", # opzionale: override della chiave demo + # ---- Diritti / Royalty-free check ---- + "royalty_last_folder": "", + "royalty_recursive": True, + "jamendo_client_id": "", # opzionale: registrati su devportal.jamendo.com # ---- Licenza ---- "license_key": "", # chiave fornita all'utente via email "license_email": "", # email associata all'acquisto