bridge: metodi Api per tab Diritti + bump v1.14.0 + config jamendo_client_id

- VERSION v1.13.0 -> v1.14.0
- DEFAULTS: royalty_last_folder, royalty_recursive, jamendo_client_id
- save_settings: persiste jamendo_client_id
- Nuovi metodi Api:
    - royalty_pick_folder
    - royalty_start_scan {directory, recursive}
    - royalty_stop_scan
  piu' worker _royalty_worker che emette:
    - royalty:progress (per file: idx/total/filename/status + overall%)
    - royalty:entry (streaming per ogni risultato)
    - royalty:done (summary {ok, total, royalty_free_count,
                              unknown_count, error_count})

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Opus 4.7 committed 2026-10-09 14:26:22 +02:00
1 parent 018d310f53
commit ddacd050d8
2 files changed
+135 -1

No files matched your search

+130
View File
@@ -50,6 +50,7 @@ from core import dedup
from core import catalog from core import catalog
from core import flatten from core import flatten
from core import charts from core import charts
from core import royalty_check as royalty
SPOTIFY_GUIDE_TEXT = """\ SPOTIFY_GUIDE_TEXT = """\
@@ -118,6 +119,7 @@ class Api:
self._dedup_thread: Optional[threading.Thread] = None self._dedup_thread: Optional[threading.Thread] = None
self._catalog_thread: Optional[threading.Thread] = None self._catalog_thread: Optional[threading.Thread] = None
self._flatten_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 # Coda usata dal resolve_callback per attendere la scelta utente
# sul modal "match locali multipli" della tab Upgrade. # sul modal "match locali multipli" della tab Upgrade.
self._upgrade_resolve_q: "queue.Queue[dict]" = queue.Queue(1) 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(), "cookies_browser": (payload.get("cookies_browser") or "").strip().lower(),
"output_dir": (payload.get("output_dir") or "").strip(), "output_dir": (payload.get("output_dir") or "").strip(),
"theme": payload.get("theme", "dark"), "theme": payload.get("theme", "dark"),
"jamendo_client_id": (payload.get("jamendo_client_id") or "").strip(),
}) })
save_config(config) save_config(config)
return {"ok": True} return {"ok": True}
@@ -2384,3 +2387,130 @@ class Api:
self._emit("charts:add_to_queue", payload) self._emit("charts:add_to_queue", payload)
self._log("charts", f"[INFO] {len(tracks)} brani inviati alla coda Scarica") self._log("charts", f"[INFO] {len(tracks)} brani inviati alla coda Scarica")
return {"ok": True, "count": len(tracks)} 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,
})
+5 -1
View File
@@ -5,7 +5,7 @@ import os
import sys import sys
from pathlib import Path from pathlib import Path
VERSION = "v1.13.0" VERSION = "v1.14.0"
APP_NAME = "MusicTools" APP_NAME = "MusicTools"
@@ -95,6 +95,10 @@ DEFAULTS = {
"charts_lastfm_genre": "dance", # "" | pop, rock, dance, ... "charts_lastfm_genre": "dance", # "" | pop, rock, dance, ...
"charts_spotify_playlist": "top50_global", # chiave in charts._SPOTIFY_CHARTS "charts_spotify_playlist": "top50_global", # chiave in charts._SPOTIFY_CHARTS
"lastfm_api_key": "", # opzionale: override della chiave demo "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 ---- # ---- Licenza ----
"license_key": "", # chiave fornita all'utente via email "license_key": "", # chiave fornita all'utente via email
"license_email": "", # email associata all'acquisto "license_email": "", # email associata all'acquisto