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
@@ -793,6 +793,7 @@ input[type="number"]::-webkit-inner-spin-button {
|
|||||||
.percent.blue { color: var(--blue-text); }
|
.percent.blue { color: var(--blue-text); }
|
||||||
.percent.pink { color: var(--pink-hover); }
|
.percent.pink { color: var(--pink-hover); }
|
||||||
.percent.purple { color: var(--purple-text); }
|
.percent.purple { color: var(--purple-text); }
|
||||||
|
.percent.amber { color: var(--amber-text); }
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -825,6 +826,10 @@ input[type="number"]::-webkit-inner-spin-button {
|
|||||||
background: linear-gradient(90deg, var(--purple), var(--purple-hover));
|
background: linear-gradient(90deg, var(--purple), var(--purple-hover));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-fill.amber {
|
||||||
|
background: linear-gradient(90deg, var(--amber), var(--amber-hover));
|
||||||
|
}
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
LOG BOX
|
LOG BOX
|
||||||
=============================== */
|
=============================== */
|
||||||
@@ -2345,3 +2350,94 @@ input[type="number"]::-webkit-inner-spin-button {
|
|||||||
border: 1px solid var(--border);
|
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);
|
||||||
|
}
|
||||||
@@ -76,6 +76,10 @@
|
|||||||
<span class="nav-icon">⚡</span>
|
<span class="nav-icon">⚡</span>
|
||||||
<span>Upgrade</span>
|
<span>Upgrade</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="nav-item" data-view="royalty" data-feature="audio">
|
||||||
|
<span class="nav-icon">⚖️</span>
|
||||||
|
<span>Diritti</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="nav-section-label">Altro</div>
|
<div class="nav-section-label">Altro</div>
|
||||||
<button class="nav-item" data-view="record" data-feature="record">
|
<button class="nav-item" data-view="record" data-feature="record">
|
||||||
@@ -591,6 +595,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2 class="section-label">Jamendo (tab Diritti)</h2>
|
||||||
|
<div class="card">
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label">Jamendo client_id (opzionale, per check royalty-free)</label>
|
||||||
|
<input type="text" id="jamendoClientIdInput" class="input pill" placeholder="Es: 0e23c33a" />
|
||||||
|
<div class="hint">
|
||||||
|
<span class="hint-ico">ⓘ</span>
|
||||||
|
<div>
|
||||||
|
Registrati gratis su <code>devportal.jamendo.com</code> per avere una chiave e
|
||||||
|
verificare contro il catalogo Jamendo (~500k brani Creative Commons).
|
||||||
|
Senza chiave la tab Diritti usa solo ccMixter.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="section-label">Download</h2>
|
<h2 class="section-label">Download</h2>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -1709,6 +1729,96 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ===== VIEW: DIRITTI (royalty-free check) ===== -->
|
||||||
|
<section class="view" id="view-royalty">
|
||||||
|
<header class="hero hero-amber">
|
||||||
|
<div class="hero-content">
|
||||||
|
<div class="hero-eyebrow amber">DIRITTI</div>
|
||||||
|
<h1 class="hero-title">Royalty-free o da verificare</h1>
|
||||||
|
<p class="hero-subtitle">
|
||||||
|
Confronta i brani locali con i cataloghi Creative Commons
|
||||||
|
(ccMixter, Jamendo). Segnala solo i brani con match certo RF —
|
||||||
|
gli altri richiedono verifica manuale.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="hero-deco">⚖️</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<h2 class="section-label">Cartella da analizzare</h2>
|
||||||
|
<div class="card">
|
||||||
|
<div class="beatport-header">
|
||||||
|
<button id="royalty-pick-folder" class="btn btn-primary pill">📁 Scegli cartella</button>
|
||||||
|
<button id="royalty-start-btn" class="btn btn-amber pill" disabled>
|
||||||
|
<span class="ico">▶</span> Avvia analisi
|
||||||
|
</button>
|
||||||
|
<button id="royalty-stop-btn" class="btn btn-danger pill" hidden>
|
||||||
|
<span class="ico">◼</span> Interrompi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="beatport-header" style="margin-top:12px;">
|
||||||
|
<span id="royalty-path-display" class="beatport-output-info" style="margin:0;">Nessuna cartella selezionata</span>
|
||||||
|
</div>
|
||||||
|
<div class="beatport-header" style="margin-top:12px;gap:16px;">
|
||||||
|
<label style="display:flex;gap:6px;align-items:center;color:var(--text-2);font-size:13px;">
|
||||||
|
<input type="checkbox" id="royalty-recursive" checked />
|
||||||
|
<span>Ricerca ricorsiva (include sottocartelle)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="hint" style="margin-top:10px;">
|
||||||
|
<span class="hint-ico">ⓘ</span>
|
||||||
|
<div>
|
||||||
|
L'app verifica solo contro DB <strong>Creative Commons / royalty-free</strong>.
|
||||||
|
"Non trovato" NON implica automaticamente SIAE: significa "da verificare manualmente".
|
||||||
|
Per abilitare anche Jamendo registra una <code>client_id</code> gratuita su
|
||||||
|
<code>devportal.jamendo.com</code> e inseriscila in Impostazioni.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="royalty-status" class="beatport-status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="section-label">Progresso</h2>
|
||||||
|
<div class="card">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar"><div id="royaltyProgressFill" class="progress-fill amber"></div></div>
|
||||||
|
<div class="progress-meta">
|
||||||
|
<span id="royaltyPercent" class="percent amber">0%</span>
|
||||||
|
<span id="royaltyCounter">In attesa</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="section-label">Risultati</h2>
|
||||||
|
<div class="card" id="royalty-summary-card" hidden>
|
||||||
|
<div class="catalog-summary" id="royalty-summary-line"></div>
|
||||||
|
</div>
|
||||||
|
<div id="royalty-table-wrap" class="royalty-scroll" hidden>
|
||||||
|
<table class="catalog-table royalty-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="royalty-col-file">File</th>
|
||||||
|
<th class="royalty-col-artist">Artista</th>
|
||||||
|
<th class="royalty-col-title">Titolo</th>
|
||||||
|
<th class="royalty-col-status">Stato</th>
|
||||||
|
<th class="royalty-col-match">Match / Licenza</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="royalty-table-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="catalog-footer-bar" id="royalty-footer-bar" hidden>
|
||||||
|
<span class="counter" id="royalty-footer-count">0 file analizzati</span>
|
||||||
|
<button id="royalty-export-csv" class="btn btn-ghost pill" disabled>
|
||||||
|
<span class="ico">📄</span> Esporta CSV
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="section-label">Log</h2>
|
||||||
|
<div class="card log-card">
|
||||||
|
<div class="log" id="royalty-log"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+338
@@ -412,6 +412,7 @@ const logEls = {
|
|||||||
catalog: () => $("#catalog-log"),
|
catalog: () => $("#catalog-log"),
|
||||||
flatten: () => $("#flatten-log"),
|
flatten: () => $("#flatten-log"),
|
||||||
charts: () => $("#charts-log"),
|
charts: () => $("#charts-log"),
|
||||||
|
royalty: () => $("#royalty-log"),
|
||||||
};
|
};
|
||||||
|
|
||||||
function classifyLog(msg) {
|
function classifyLog(msg) {
|
||||||
@@ -489,6 +490,7 @@ async function init() {
|
|||||||
$("#hqThresholdInput").value = state.config.hq_threshold || 310;
|
$("#hqThresholdInput").value = state.config.hq_threshold || 310;
|
||||||
$("#cookiesInput").value = state.config.cookies_path || "";
|
$("#cookiesInput").value = state.config.cookies_path || "";
|
||||||
if ($("#cookiesBrowserSelect")) $("#cookiesBrowserSelect").value = state.config.cookies_browser || "";
|
if ($("#cookiesBrowserSelect")) $("#cookiesBrowserSelect").value = state.config.cookies_browser || "";
|
||||||
|
if ($("#jamendoClientIdInput")) $("#jamendoClientIdInput").value = state.config.jamendo_client_id || "";
|
||||||
$("#outputInput").value = state.config.output_dir || "";
|
$("#outputInput").value = state.config.output_dir || "";
|
||||||
$("#themeSelect").value = state.config.theme || "dark";
|
$("#themeSelect").value = state.config.theme || "dark";
|
||||||
// Popola path del log corrente
|
// Popola path del log corrente
|
||||||
@@ -546,6 +548,9 @@ async function init() {
|
|||||||
|
|
||||||
// Charts tab (iTunes / Spotify / M2O / Last.fm)
|
// Charts tab (iTunes / Spotify / M2O / Last.fm)
|
||||||
await ChartsUI.init();
|
await ChartsUI.init();
|
||||||
|
|
||||||
|
// Diritti tab (royalty-free check ccMixter + Jamendo)
|
||||||
|
await RoyaltyUI.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshRecDevices() {
|
async function refreshRecDevices() {
|
||||||
@@ -1412,6 +1417,7 @@ $("#saveBtn").addEventListener("click", async () => {
|
|||||||
hq_threshold: parseInt($("#hqThresholdInput").value, 10) || 310,
|
hq_threshold: parseInt($("#hqThresholdInput").value, 10) || 310,
|
||||||
cookies_path: $("#cookiesInput").value,
|
cookies_path: $("#cookiesInput").value,
|
||||||
cookies_browser: $("#cookiesBrowserSelect") ? $("#cookiesBrowserSelect").value : "",
|
cookies_browser: $("#cookiesBrowserSelect") ? $("#cookiesBrowserSelect").value : "",
|
||||||
|
jamendo_client_id: $("#jamendoClientIdInput") ? $("#jamendoClientIdInput").value : "",
|
||||||
output_dir: $("#outputInput").value,
|
output_dir: $("#outputInput").value,
|
||||||
theme: $("#themeSelect").value,
|
theme: $("#themeSelect").value,
|
||||||
};
|
};
|
||||||
@@ -4220,6 +4226,338 @@ const ChartsUI = (() => {
|
|||||||
return { init };
|
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
|
// Boot
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
Reference in new issue
Block a user