ui: tab Diritti con tabella streaming + export CSV + settings Jamendo
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
ddacd050d8
commit
8266722654
3 files changed
+544
No files matched your search
+338
@@ -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 `<span class="royalty-badge royalty-badge-rf">🟢 Royalty-free</span>`;
|
||||
}
|
||||
if (s === "error") {
|
||||
const err = entry.error ? ` title="${_escapeHtml(entry.error)}"` : "";
|
||||
return `<span class="royalty-badge royalty-badge-error"${err}>🔴 Errore</span>`;
|
||||
}
|
||||
return `<span class="royalty-badge royalty-badge-unknown">⚪ Non trovato</span>`;
|
||||
}
|
||||
|
||||
function _matchCell(entry) {
|
||||
const matches = entry.matches || [];
|
||||
if (!matches.length) {
|
||||
if (entry.status === "error") {
|
||||
return `<span class="royalty-match-cell" style="color:var(--red);">${_escapeHtml(entry.error || "errore")}</span>`;
|
||||
}
|
||||
return `<span class="royalty-match-cell" style="color:var(--text-3);">—</span>`;
|
||||
}
|
||||
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")
|
||||
? `<span class="royalty-match-score">score ${m.score}</span>`
|
||||
: "";
|
||||
const linkTrack = url
|
||||
? `<a href="#" data-ext-url="${_escapeHtml(url)}">traccia</a>`
|
||||
: "";
|
||||
const linkLic = licUrl
|
||||
? `<a href="#" data-ext-url="${_escapeHtml(licUrl)}">${lic}</a>`
|
||||
: lic;
|
||||
const links = [linkTrack, linkLic].filter(Boolean).join(" · ");
|
||||
const extra = matches.length > 1
|
||||
? `<div style="color:var(--text-3);font-size:11px;margin-top:2px;">+${matches.length - 1} altri match</div>`
|
||||
: "";
|
||||
return `<span class="royalty-match-cell"><span class="royalty-match-source">${src}</span>${links}${score}</span>${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 = `<span class="catalog-file-cell" title="${_escapeHtml(path)}">${_escapeHtml(_basename(path))}</span>`;
|
||||
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
|
||||
// ============================================================
|
||||
|
||||
Reference in new issue
Block a user