feat: fallback Spotify + layout selector + tab Appiattisci (v1.11.0)
Cataloga: - Fallback Spotify quando AcoustID non trova (copertura ottima per dance/electronic mashup/bootleg/remix) - Dropdown "Struttura cartelle" per scegliere tra <Anno>/<Genere>/ e <Genere>/<Anno>/ - Chiave AcoustID default sostituita da placeholder invalido a quella pubblica di MusicBrainz Picard (valida) Nuova tab Appiattisci: - Sposta tutti i file audio delle sotto-cartelle nella cartella padre - Utile come undo della catalogazione o merge di archivi frammentati - Rimuove sotto-cartelle vuote dopo il move (opzionale) - Suffisso _1, _2 su nomi duplicati (no overwrite)
This commit is contained in:
1 parent
401bed967a
commit
cdbb1d85d5
7 files changed
+546
-9
No files matched your search
+73
-2
@@ -57,6 +57,10 @@
|
||||
<span class="nav-icon">📅</span>
|
||||
<span>Cataloga</span>
|
||||
</button>
|
||||
<button class="nav-item" data-view="flatten" data-feature="audio">
|
||||
<span class="nav-icon">📤</span>
|
||||
<span>Appiattisci</span>
|
||||
</button>
|
||||
<button class="nav-item" data-view="upgrade" data-feature="upgrade">
|
||||
<span class="nav-icon">⚡</span>
|
||||
<span>Upgrade</span>
|
||||
@@ -1350,12 +1354,21 @@
|
||||
<div class="beatport-header" style="margin-top:12px;gap:16px;">
|
||||
<span id="catalog-target-display" class="beatport-output-info" style="margin:0;">Uguale alla sorgente (in-place)</span>
|
||||
</div>
|
||||
<div class="beatport-header" style="margin-top:12px;gap:12px;">
|
||||
<label style="display:flex;gap:8px;align-items:center;color:var(--text-2);font-size:13px;">
|
||||
<span>Struttura cartelle:</span>
|
||||
<select id="catalog-layout" class="input" style="padding:4px 8px;font-size:13px;">
|
||||
<option value="year_genre"><Anno>/<Genere>/</option>
|
||||
<option value="genre_year"><Genere>/<Anno>/</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hint" style="margin-top:10px;">
|
||||
<span class="hint-ico">ⓘ</span>
|
||||
<div>
|
||||
Se lasci vuota la destinazione i file vengono organizzati <strong>in-place</strong> nella cartella sorgente.
|
||||
Le sottocartelle <code><Anno>/<Genere>/</code> vengono create automaticamente.
|
||||
File senza match AcoustID finiscono in <code>Unknown Year/Unknown Genre/</code>.
|
||||
Le sottocartelle vengono create automaticamente in base allo schema scelto.
|
||||
File senza match finiscono in <code>Unknown Year</code> / <code>Unknown Genre</code>.
|
||||
</div>
|
||||
</div>
|
||||
<div id="catalog-status" class="beatport-status"></div>
|
||||
@@ -1408,6 +1421,64 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== VIEW: FLATTEN ===== -->
|
||||
<section class="view" id="view-flatten">
|
||||
<header class="hero hero-blue">
|
||||
<div class="hero-content">
|
||||
<div class="hero-eyebrow blue">APPIATTISCI</div>
|
||||
<h1 class="hero-title">Sposta tutti i brani nella cartella padre</h1>
|
||||
<p class="hero-subtitle">
|
||||
Scansiona ricorsivamente ogni sotto-cartella e sposta i file audio nella root.
|
||||
Utile come "undo" della catalogazione o per unire archivi frammentati.
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-deco">📤</div>
|
||||
</header>
|
||||
|
||||
<h2 class="section-label">Cartella</h2>
|
||||
<div class="card">
|
||||
<div class="beatport-header">
|
||||
<button id="flatten-pick-folder" class="btn btn-primary pill">📁 Scegli cartella</button>
|
||||
<button id="flatten-start-btn" class="btn btn-blue pill" disabled>▶ Sposta tutto nella root</button>
|
||||
<button id="flatten-stop-btn" class="btn btn-danger pill" hidden>■ Interrompi</button>
|
||||
</div>
|
||||
<div class="beatport-header" style="margin-top:12px;">
|
||||
<span id="flatten-path-display" class="beatport-output-info" style="margin:0;">Nessuna cartella selezionata</span>
|
||||
</div>
|
||||
<div class="beatport-header" style="margin-top:12px;">
|
||||
<label style="display:flex;gap:6px;align-items:center;color:var(--text-2);font-size:13px;">
|
||||
<input type="checkbox" id="flatten-remove-empty" checked />
|
||||
<span>Elimina le sotto-cartelle rimaste vuote dopo lo spostamento</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hint" style="margin-top:10px;">
|
||||
<span class="hint-ico">ⓘ</span>
|
||||
<div>
|
||||
Ogni file con estensione audio (mp3, m4a, wav, flac...) nelle sotto-cartelle
|
||||
verrà spostato nella cartella padre. In caso di nomi duplicati viene aggiunto
|
||||
suffisso <code>_1</code>, <code>_2</code>... (nessun overwrite).
|
||||
</div>
|
||||
</div>
|
||||
<div id="flatten-status" class="beatport-status"></div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-label">Progresso</h2>
|
||||
<div class="card">
|
||||
<div class="progress">
|
||||
<div class="progress-bar"><div id="flattenProgressFill" class="progress-fill blue"></div></div>
|
||||
<div class="progress-meta">
|
||||
<span id="flattenPercent" class="percent blue">0%</span>
|
||||
<span id="flattenCounter">In attesa</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-label">Log</h2>
|
||||
<div class="card log-card">
|
||||
<div class="log" id="flatten-log"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
+113
-1
@@ -410,6 +410,7 @@ const logEls = {
|
||||
traxsource: () => $("#traxsource-log"),
|
||||
dedup: () => $("#dedup-log"),
|
||||
catalog: () => $("#catalog-log"),
|
||||
flatten: () => $("#flatten-log"),
|
||||
};
|
||||
|
||||
function classifyLog(msg) {
|
||||
@@ -534,6 +535,7 @@ async function init() {
|
||||
|
||||
// Cataloga tab (AcoustID -> <Anno>/<Genere>/)
|
||||
await CatalogUI.init();
|
||||
await FlattenUI.init();
|
||||
}
|
||||
|
||||
async function refreshRecDevices() {
|
||||
@@ -3512,7 +3514,8 @@ const CatalogUI = (() => {
|
||||
btn.disabled = true;
|
||||
let res;
|
||||
try {
|
||||
res = await window.pywebview.api.catalog_move_files(selectedEntries, target);
|
||||
const layout = $("#catalog-layout")?.value || "year_genre";
|
||||
res = await window.pywebview.api.catalog_move_files(selectedEntries, target, layout);
|
||||
} catch (e) {
|
||||
toast("Errore: " + ((e && e.message) || e), "error");
|
||||
btn.disabled = false;
|
||||
@@ -3643,6 +3646,115 @@ const CatalogUI = (() => {
|
||||
return { init };
|
||||
})();
|
||||
|
||||
// ============================================================
|
||||
// FlattenUI — appiattisci sotto-cartelle nella root
|
||||
// ============================================================
|
||||
const FlattenUI = (() => {
|
||||
const mstate = { folder: "", removeEmpty: true, running: false };
|
||||
|
||||
function setStatus(text, kind) {
|
||||
const el = $("#flatten-status");
|
||||
if (!el) return;
|
||||
el.textContent = text || "";
|
||||
el.className = "beatport-status" + (kind ? " " + kind : "");
|
||||
}
|
||||
|
||||
async function pickFolder() {
|
||||
const p = await window.pywebview.api.flatten_pick_folder();
|
||||
if (p && typeof p === "string") {
|
||||
mstate.folder = p;
|
||||
$("#flatten-path-display").textContent = p;
|
||||
$("#flatten-start-btn").disabled = false;
|
||||
setStatus("", "");
|
||||
}
|
||||
}
|
||||
|
||||
async function startFlatten() {
|
||||
if (!mstate.folder) return;
|
||||
mstate.removeEmpty = $("#flatten-remove-empty").checked;
|
||||
if (!window.confirm(`Vuoi spostare TUTTI i file audio delle sotto-cartelle nella cartella:\n\n${mstate.folder}\n\n${mstate.removeEmpty ? "Le sotto-cartelle vuote verranno eliminate." : "Le sotto-cartelle vuote verranno lasciate."}\n\nProseguire?`)) return;
|
||||
|
||||
mstate.running = true;
|
||||
$("#flatten-start-btn").disabled = true;
|
||||
$("#flatten-pick-folder").disabled = true;
|
||||
$("#flatten-stop-btn").hidden = false;
|
||||
$("#flatten-log").innerHTML = "";
|
||||
$("#flattenProgressFill").style.width = "0%";
|
||||
$("#flattenPercent").textContent = "0%";
|
||||
$("#flattenCounter").textContent = "In corso…";
|
||||
setStatus("Operazione in corso...", "loading");
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await window.pywebview.api.flatten_start({
|
||||
directory: mstate.folder,
|
||||
remove_empty: mstate.removeEmpty,
|
||||
});
|
||||
} catch (e) {
|
||||
setStatus("Errore: " + e, "error");
|
||||
finishFlatten();
|
||||
return;
|
||||
}
|
||||
if (!res || !res.ok) {
|
||||
setStatus(res && res.error || "Errore avvio", "error");
|
||||
finishFlatten();
|
||||
}
|
||||
}
|
||||
|
||||
async function stopFlatten() {
|
||||
try { await window.pywebview.api.flatten_stop(); } catch {}
|
||||
}
|
||||
|
||||
function finishFlatten() {
|
||||
mstate.running = false;
|
||||
$("#flatten-start-btn").disabled = !mstate.folder;
|
||||
$("#flatten-pick-folder").disabled = false;
|
||||
$("#flatten-stop-btn").hidden = true;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
$("#flatten-pick-folder").addEventListener("click", pickFolder);
|
||||
$("#flatten-start-btn").addEventListener("click", startFlatten);
|
||||
$("#flatten-stop-btn").addEventListener("click", stopFlatten);
|
||||
|
||||
bridgeHandlers["flatten:progress"] = (p) => {
|
||||
if (!p) return;
|
||||
if (typeof p.overall === "number") {
|
||||
const pct = Math.round(p.overall * 100);
|
||||
$("#flattenProgressFill").style.width = pct + "%";
|
||||
$("#flattenPercent").textContent = pct + "%";
|
||||
}
|
||||
if (typeof p.idx === "number" && typeof p.total === "number" && p.total > 0) {
|
||||
const s = p.status || "";
|
||||
let label;
|
||||
if (s === "moving") label = `${p.idx}/${p.total} · ${p.filename || ""}`;
|
||||
else if (s === "completed") label = `Completato: ${p.total} file`;
|
||||
else if (s === "stopped") label = "Interrotta";
|
||||
else if (s === "error") label = `${p.idx}/${p.total} · errore`;
|
||||
else label = `${p.idx}/${p.total}`;
|
||||
$("#flattenCounter").textContent = label;
|
||||
}
|
||||
};
|
||||
|
||||
bridgeHandlers["flatten:done"] = (p) => {
|
||||
if (p && p.ok) {
|
||||
const moved = p.moved || 0;
|
||||
const dirs = p.dirs_removed || 0;
|
||||
const failed = p.failed_count || 0;
|
||||
const parts = [`${moved} file spostati`];
|
||||
if (dirs) parts.push(`${dirs} cartelle rimosse`);
|
||||
if (failed) parts.push(`${failed} errori`);
|
||||
setStatus(parts.join(" · "), failed ? "error" : "ok");
|
||||
} else if (p) {
|
||||
setStatus(p.error || "Errore", "error");
|
||||
}
|
||||
finishFlatten();
|
||||
};
|
||||
}
|
||||
|
||||
return { init };
|
||||
})();
|
||||
|
||||
// ============================================================
|
||||
// Boot
|
||||
// ============================================================
|
||||
|
||||
Reference in new issue
Block a user