From 82667226547a9644d958536dbb01fabaeca446ee Mon Sep 17 00:00:00 2001 From: luciano Date: Fri, 9 Oct 2026 14:29:45 +0200 Subject: [PATCH] ui: tab Diritti con tabella streaming + export CSV + settings Jamendo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nav item "⚖️ Diritti" nel gruppo Organizza (dopo Upgrade), feature=audio - View #view-royalty: hero amber, folder picker, checkbox recursive, progress bar, tabella streaming (file · artista · titolo · stato · match+licenza) con badge colorati - Hint esplicito: "Non trovato" NON implica SIAE - Modulo RoyaltyUI (pattern CatalogUI/DedupUI): streaming via royalty:progress / royalty:entry / royalty:done, link ai match aperti via open_external_url, export CSV client-side (Blob + download) - Settings: nuovo campo jamendo_client_id (opzionale) + hint link devportal.jamendo.com; save/load via save_settings - CSS: .royalty-scroll, .royalty-table, .royalty-badge-*, .royalty-row-rf/error, .progress-fill.amber, .percent.amber node --check webui/js/app.js OK · 184 test verdi. Co-Authored-By: Claude Opus 4.7 --- webui/css/style.css | 96 +++++++++++++ webui/index.html | 110 ++++++++++++++ webui/js/app.js | 338 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 544 insertions(+) diff --git a/webui/css/style.css b/webui/css/style.css index a870beb..091a8d8 100644 --- a/webui/css/style.css +++ b/webui/css/style.css @@ -793,6 +793,7 @@ input[type="number"]::-webkit-inner-spin-button { .percent.blue { color: var(--blue-text); } .percent.pink { color: var(--pink-hover); } .percent.purple { color: var(--purple-text); } +.percent.amber { color: var(--amber-text); } .progress { width: 100%; @@ -825,6 +826,10 @@ input[type="number"]::-webkit-inner-spin-button { background: linear-gradient(90deg, var(--purple), var(--purple-hover)); } +.progress-fill.amber { + background: linear-gradient(90deg, var(--amber), var(--amber-hover)); +} + /* =============================== LOG BOX =============================== */ @@ -2345,3 +2350,94 @@ input[type="number"]::-webkit-inner-spin-button { border: 1px solid var(--border); } + +/* ================================================================== + * Royalty (tab Diritti) — verifica royalty-free status. + * Riusa .catalog-table per lo stile base; override specifici qui. + * ================================================================== */ +.royalty-scroll { + max-height: 55vh; + overflow-y: auto; + overflow-x: auto; + padding-right: 6px; + border: 1px solid var(--border); + border-radius: var(--r-md); + background: var(--bg-card); +} + +.royalty-table tbody td { + padding: 8px; + border-bottom: 1px solid var(--border); + vertical-align: middle; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 320px; +} + +.royalty-col-file { max-width: 260px; } +.royalty-col-artist { max-width: 160px; } +.royalty-col-title { max-width: 220px; } +.royalty-col-status { width: 120px; } +.royalty-col-match { max-width: 320px; } + +/* Badge stato royalty */ +.royalty-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 10px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.3px; + text-transform: uppercase; + white-space: nowrap; +} +.royalty-badge-rf { + background: var(--green-dim, rgba(16, 185, 129, 0.15)); + color: var(--green-text); +} +.royalty-badge-unknown { + background: var(--bg-input); + color: var(--text-3); +} +.royalty-badge-error { + background: rgba(220, 38, 38, 0.15); + color: var(--red); +} + +/* Dettaglio match (sorgente + licenza + link) */ +.royalty-match-cell { + display: block; + white-space: normal; + line-height: 1.4; + color: var(--text-2); + font-size: 12px; +} +.royalty-match-cell a { + color: var(--amber-text); + text-decoration: none; +} +.royalty-match-cell a:hover { + text-decoration: underline; +} +.royalty-match-source { + font-weight: 700; + color: var(--text-1); + margin-right: 6px; +} +.royalty-match-score { + color: var(--text-3); + font-size: 11px; + margin-left: 6px; +} + +/* Riga con stato royalty_free: tinta verde tenue di sfondo */ +.royalty-table tbody tr.royalty-row-rf { + background: rgba(16, 185, 129, 0.06); +} +.royalty-table tbody tr.royalty-row-rf:hover { + background: rgba(16, 185, 129, 0.12); +} +.royalty-table tbody tr.royalty-row-error { + background: rgba(220, 38, 38, 0.05); +} diff --git a/webui/index.html b/webui/index.html index 4536c78..d675305 100644 --- a/webui/index.html +++ b/webui/index.html @@ -76,6 +76,10 @@ ⚡ Upgrade + + + + +
+ Nessuna cartella selezionata +
+
+ +
+
+ ⓘ +
+ L'app verifica solo contro DB Creative Commons / royalty-free. + "Non trovato" NON implica automaticamente SIAE: significa "da verificare manualmente". + Per abilitare anche Jamendo registra una client_id gratuita su + devportal.jamendo.com e inseriscila in Impostazioni. +
+
+
+ + + +
+
+
+
+ 0% + In attesa +
+
+
+ + + + + + + + +
+
+
+ + diff --git a/webui/js/app.js b/webui/js/app.js index 7426afe..9738d89 100644 --- a/webui/js/app.js +++ b/webui/js/app.js @@ -412,6 +412,7 @@ const logEls = { catalog: () => $("#catalog-log"), flatten: () => $("#flatten-log"), charts: () => $("#charts-log"), + royalty: () => $("#royalty-log"), }; function classifyLog(msg) { @@ -489,6 +490,7 @@ async function init() { $("#hqThresholdInput").value = state.config.hq_threshold || 310; $("#cookiesInput").value = state.config.cookies_path || ""; if ($("#cookiesBrowserSelect")) $("#cookiesBrowserSelect").value = state.config.cookies_browser || ""; + if ($("#jamendoClientIdInput")) $("#jamendoClientIdInput").value = state.config.jamendo_client_id || ""; $("#outputInput").value = state.config.output_dir || ""; $("#themeSelect").value = state.config.theme || "dark"; // Popola path del log corrente @@ -546,6 +548,9 @@ async function init() { // Charts tab (iTunes / Spotify / M2O / Last.fm) await ChartsUI.init(); + + // Diritti tab (royalty-free check ccMixter + Jamendo) + await RoyaltyUI.init(); } async function refreshRecDevices() { @@ -1412,6 +1417,7 @@ $("#saveBtn").addEventListener("click", async () => { hq_threshold: parseInt($("#hqThresholdInput").value, 10) || 310, cookies_path: $("#cookiesInput").value, cookies_browser: $("#cookiesBrowserSelect") ? $("#cookiesBrowserSelect").value : "", + jamendo_client_id: $("#jamendoClientIdInput") ? $("#jamendoClientIdInput").value : "", output_dir: $("#outputInput").value, theme: $("#themeSelect").value, }; @@ -4220,6 +4226,338 @@ const ChartsUI = (() => { return { init }; })(); +// ============================================================ +// RoyaltyUI — verifica royalty-free (ccMixter + Jamendo) +// ============================================================ +const RoyaltyUI = (() => { + const rstate = { + folder: "", + recursive: true, + scanning: false, + // Map path -> entry + entries: new Map(), + }; + + function setStatus(text, kind) { + const el = $("#royalty-status"); + if (!el) return; + el.textContent = text || ""; + el.className = "beatport-status" + (kind ? " " + kind : ""); + } + + function _statusBadge(entry) { + const s = entry.status || ""; + if (s === "royalty_free") { + return `🟢 Royalty-free`; + } + if (s === "error") { + const err = entry.error ? ` title="${_escapeHtml(entry.error)}"` : ""; + return `🔴 Errore`; + } + return `⚪ Non trovato`; + } + + function _matchCell(entry) { + const matches = entry.matches || []; + if (!matches.length) { + if (entry.status === "error") { + return `${_escapeHtml(entry.error || "errore")}`; + } + return `—`; + } + const m = matches[0]; + const src = _escapeHtml(m.source || "?"); + const lic = _escapeHtml(m.license || "Creative Commons"); + const url = m.url || ""; + const licUrl = m.license_url || ""; + const score = (typeof m.score === "number") + ? `score ${m.score}` + : ""; + const linkTrack = url + ? `traccia` + : ""; + const linkLic = licUrl + ? `${lic}` + : lic; + const links = [linkTrack, linkLic].filter(Boolean).join(" · "); + const extra = matches.length > 1 + ? `
+${matches.length - 1} altri match
` + : ""; + return `${src}${links}${score}${extra}`; + } + + function _renderRow(entry) { + const path = entry.path || ""; + const tr = document.createElement("tr"); + tr.dataset.path = path; + if (entry.status === "royalty_free") tr.classList.add("royalty-row-rf"); + else if (entry.status === "error") tr.classList.add("royalty-row-error"); + + // File + const tdFile = document.createElement("td"); + tdFile.className = "royalty-col-file"; + tdFile.innerHTML = `${_escapeHtml(_basename(path))}`; + tr.appendChild(tdFile); + + // Artista + const tdArtist = document.createElement("td"); + tdArtist.className = "royalty-col-artist"; + tdArtist.textContent = entry.artist || "—"; + tdArtist.title = entry.artist || ""; + tr.appendChild(tdArtist); + + // Titolo + const tdTitle = document.createElement("td"); + tdTitle.className = "royalty-col-title"; + tdTitle.textContent = entry.title || "—"; + tdTitle.title = entry.title || ""; + tr.appendChild(tdTitle); + + // Stato + const tdStatus = document.createElement("td"); + tdStatus.className = "royalty-col-status"; + tdStatus.innerHTML = _statusBadge(entry); + tr.appendChild(tdStatus); + + // Match / licenza + const tdMatch = document.createElement("td"); + tdMatch.className = "royalty-col-match"; + tdMatch.innerHTML = _matchCell(entry); + tr.appendChild(tdMatch); + + return tr; + } + + function _updateSummary() { + const line = $("#royalty-summary-line"); + const card = $("#royalty-summary-card"); + if (!line || !card) return; + const n = rstate.entries.size; + if (n === 0) { card.hidden = true; return; } + let rf = 0, unk = 0, errs = 0; + rstate.entries.forEach((e) => { + if (e.status === "royalty_free") rf++; + else if (e.status === "error") errs++; + else unk++; + }); + line.textContent = + `${n} file · ${rf} royalty-free · ${unk} da verificare · ${errs} errori`; + card.hidden = false; + + const foot = $("#royalty-footer-bar"); + const cnt = $("#royalty-footer-count"); + const exp = $("#royalty-export-csv"); + if (foot) foot.hidden = false; + if (cnt) cnt.textContent = `${n} file analizzati`; + if (exp) exp.disabled = n === 0; + } + + function _upsertEntry(entry) { + if (!entry || !entry.path) return; + const body = $("#royalty-table-body"); + if (!body) return; + const existed = rstate.entries.has(entry.path); + rstate.entries.set(entry.path, entry); + const row = _renderRow(entry); + if (existed) { + const old = body.querySelector(`tr[data-path="${CSS.escape(entry.path)}"]`); + if (old) body.replaceChild(row, old); + else body.appendChild(row); + } else { + body.appendChild(row); + } + const wrap = $("#royalty-table-wrap"); + if (wrap) wrap.hidden = false; + _updateSummary(); + } + + function _clearTable() { + const body = $("#royalty-table-body"); + if (body) body.innerHTML = ""; + const wrap = $("#royalty-table-wrap"); + if (wrap) wrap.hidden = true; + const card = $("#royalty-summary-card"); + if (card) card.hidden = true; + const foot = $("#royalty-footer-bar"); + if (foot) foot.hidden = true; + const exp = $("#royalty-export-csv"); + if (exp) exp.disabled = true; + rstate.entries.clear(); + } + + async function pickFolder() { + const p = await window.pywebview.api.royalty_pick_folder(); + if (p && typeof p === "string") { + rstate.folder = p; + $("#royalty-path-display").textContent = p; + $("#royalty-start-btn").disabled = false; + setStatus("", ""); + } + } + + async function startScan() { + if (!rstate.folder) return; + rstate.recursive = $("#royalty-recursive").checked; + rstate.scanning = true; + _clearTable(); + $("#royalty-start-btn").disabled = true; + $("#royalty-pick-folder").disabled = true; + $("#royalty-stop-btn").hidden = false; + $("#royalty-log").innerHTML = ""; + $("#royaltyProgressFill").style.width = "0%"; + $("#royaltyPercent").textContent = "0%"; + $("#royaltyCounter").textContent = "Analisi in corso…"; + setStatus("Analisi in corso...", "loading"); + + let res; + try { + res = await window.pywebview.api.royalty_start_scan({ + directory: rstate.folder, + recursive: rstate.recursive, + }); + } catch (e) { + setStatus("Errore avvio: " + ((e && e.message) || e), "error"); + finishScan(); + return; + } + if (!res || !res.ok) { + const msg = (res && res.error) || "Impossibile avviare l'analisi"; + setStatus(msg, "error"); + if (!handleGateBlock(res || {})) toast(msg, "error"); + finishScan(); + } + } + + async function stopScan() { + try { await window.pywebview.api.royalty_stop_scan(); } catch (e) { console.error(e); } + } + + function finishScan() { + rstate.scanning = false; + $("#royalty-start-btn").disabled = !rstate.folder; + $("#royalty-pick-folder").disabled = false; + $("#royalty-stop-btn").hidden = true; + } + + function _csvEscape(s) { + const str = (s == null) ? "" : String(s); + if (/[",\n;]/.test(str)) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; + } + + function exportCsv() { + if (rstate.entries.size === 0) return; + const rows = [["file", "artista", "titolo", "stato", "sorgente", "licenza", "licenza_url", "track_url", "score"]]; + rstate.entries.forEach((e) => { + const m = (e.matches && e.matches[0]) || {}; + rows.push([ + _basename(e.path || ""), + e.artist || "", + e.title || "", + e.status || "", + m.source || "", + m.license || "", + m.license_url || "", + m.url || "", + (typeof m.score === "number") ? m.score : "", + ]); + }); + const csv = rows.map((r) => r.map(_csvEscape).join(",")).join("\n"); + const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `royalty_report_${Date.now()}.csv`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => window.URL.revokeObjectURL(url), 1500); + toast("CSV esportato", "success"); + } + + async function init() { + $("#royalty-pick-folder").addEventListener("click", pickFolder); + $("#royalty-start-btn").addEventListener("click", startScan); + $("#royalty-stop-btn").addEventListener("click", stopScan); + $("#royalty-export-csv").addEventListener("click", exportCsv); + $("#royalty-recursive").addEventListener("change", (e) => { + rstate.recursive = e.target.checked; + }); + + // Delegazione: link "traccia" / licenza aprono in browser esterno + const body = $("#royalty-table-body"); + if (body) { + body.addEventListener("click", (ev) => { + const a = ev.target && ev.target.closest("a[data-ext-url]"); + if (!a) return; + ev.preventDefault(); + const u = a.getAttribute("data-ext-url"); + if (u && window.pywebview && window.pywebview.api + && window.pywebview.api.open_external_url) { + window.pywebview.api.open_external_url(u); + } + }); + } + + // Ripristina ultimo state da config + const cfg = state.config || {}; + if (cfg.royalty_last_folder) { + rstate.folder = cfg.royalty_last_folder; + $("#royalty-path-display").textContent = cfg.royalty_last_folder; + $("#royalty-start-btn").disabled = false; + } + if (typeof cfg.royalty_recursive === "boolean") { + $("#royalty-recursive").checked = cfg.royalty_recursive; + rstate.recursive = cfg.royalty_recursive; + } + + // Bridge handlers + bridgeHandlers["royalty:progress"] = (p) => { + if (!p) return; + if (typeof p.overall === "number") { + const pct = Math.round(p.overall * 100); + $("#royaltyProgressFill").style.width = pct + "%"; + $("#royaltyPercent").textContent = pct + "%"; + } + if (typeof p.idx === "number" && typeof p.total === "number" && p.total > 0) { + const s = p.status || ""; + let label; + if (s === "checking") label = `File ${p.idx}/${p.total} · verifica`; + else if (s === "completed") label = `Completato: ${p.total} file`; + else if (s === "stopped") label = "Interrotta"; + else if (s === "error") label = `File ${p.idx}/${p.total} · errore`; + else label = `File ${p.idx}/${p.total}`; + $("#royaltyCounter").textContent = label; + } + }; + + bridgeHandlers["royalty:entry"] = (entry) => { + _upsertEntry(entry); + setStatus(`Elaborati ${rstate.entries.size} file...`, "loading"); + }; + + bridgeHandlers["royalty:done"] = (p) => { + if (rstate.entries.size === 0 && p && Array.isArray(p.entries)) { + p.entries.forEach(_upsertEntry); + } + if (p && p.ok === false) { + setStatus(p.error || "Errore analisi", "error"); + } else if (rstate.entries.size === 0) { + setStatus("Nessun file audio trovato", "ok"); + } else { + const rf = (p && p.royalty_free_count) || 0; + const tot = (p && p.total) || rstate.entries.size; + setStatus(`Analisi completata · ${rf}/${tot} royalty-free`, "ok"); + } + finishScan(); + }; + } + + return { init }; +})(); + // ============================================================ // Boot // ============================================================