Tab Metadati: editor dei tag audio (MP3, M4A, FLAC)
Nuova sezione 'Metadati' nella sidebar con editor completo dei tag: titolo, artista, album, album artist, anno, traccia, genere, BPM, key, commento, copertina. - core/metadata.py: read/write metadati via mutagen, supporto ID3 (MP3), MP4 atoms (M4A/AAC) e Vorbis comments (FLAC) - api/bridge.py: pick_audio_file, pick_image_file, read_metadata, save_metadata. Cover passata come base64 al frontend per preview - webui: hero ambra, form a griglia 2 colonne, anteprima cover 180x180, bottoni cambia/rimuovi copertina, ricarica file - requirements.txt: aggiunto mutagen>=1.47.0 - build_macos.py / build_windows.py: --collect-all mutagen Modifiche correlate (in attesa di decisione utente sulla distribuzione degli aggiornamenti): - VERSION bumpato a v1.2.2 in core/config.py - check_update ora interroga l'API GitHub Releases invece di djluza.com/version.json (funzionera quando il repo sara pubblico) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
d8423ec215
commit
946452ef03
9 files changed
+712
-10
No files matched your search
@@ -30,6 +30,10 @@
|
||||
--pink-hover: #F472B6;
|
||||
--pink-dim: #831843;
|
||||
|
||||
--amber: #F59E0B;
|
||||
--amber-hover: #FBBF24;
|
||||
--amber-dim: #78350F;
|
||||
|
||||
--red: #E22134;
|
||||
--red-hover: #FF4055;
|
||||
|
||||
@@ -233,6 +237,10 @@ button, input, select, textarea { font-family: inherit; font-size: inherit; }
|
||||
background: linear-gradient(135deg, #EC4899 0%, #831843 60%, #3F0A23 100%);
|
||||
}
|
||||
|
||||
.hero-amber {
|
||||
background: linear-gradient(135deg, #F59E0B 0%, #78350F 60%, #3B1E07 100%);
|
||||
}
|
||||
|
||||
.hero-content { position: relative; z-index: 2; max-width: 70%; }
|
||||
|
||||
.hero-eyebrow {
|
||||
@@ -246,6 +254,7 @@ button, input, select, textarea { font-family: inherit; font-size: inherit; }
|
||||
.hero-eyebrow.blue { color: var(--blue); }
|
||||
.hero-eyebrow.purple { color: var(--purple); }
|
||||
.hero-eyebrow.pink { color: var(--pink-hover); }
|
||||
.hero-eyebrow.amber { color: var(--amber-hover); }
|
||||
|
||||
.hero-title {
|
||||
font-size: 36px;
|
||||
@@ -463,6 +472,11 @@ input[type="number"]::-webkit-inner-spin-button {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-amber {
|
||||
background: var(--amber);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #00CEC9;
|
||||
color: #000;
|
||||
@@ -819,3 +833,96 @@ input[type="number"]::-webkit-inner-spin-button {
|
||||
|
||||
.log::-webkit-scrollbar-thumb { border-color: var(--bg-card-2); }
|
||||
.main::-webkit-scrollbar-thumb { border-color: var(--bg); }
|
||||
|
||||
/* ===============================
|
||||
METADATA EDITOR
|
||||
=============================== */
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px 18px;
|
||||
}
|
||||
|
||||
.form-grid .field { margin-bottom: 0; }
|
||||
.form-grid .field.full { grid-column: 1 / -1; }
|
||||
|
||||
.textarea {
|
||||
border-radius: var(--r-md);
|
||||
padding: 12px 16px;
|
||||
resize: vertical;
|
||||
min-height: 70px;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px 22px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--divider);
|
||||
color: var(--text-2);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.meta-info span strong {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.cover-card {
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.cover-preview {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: var(--r-md);
|
||||
background: var(--bg-card-2);
|
||||
border: 1px dashed var(--bg-input-hover);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cover-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cover-empty {
|
||||
color: var(--text-3);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.cover-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.cover-hint {
|
||||
color: var(--text-3);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.cover-hint.set { color: var(--amber-hover); font-weight: 700; }
|
||||
.cover-hint.remove { color: var(--red); font-weight: 700; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.form-grid { grid-template-columns: 1fr; }
|
||||
.cover-card { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -33,6 +33,10 @@
|
||||
<span class="nav-icon">⚡</span>
|
||||
<span>Upgrade</span>
|
||||
</button>
|
||||
<button class="nav-item" data-view="meta">
|
||||
<span class="nav-icon">🏷</span>
|
||||
<span>Metadati</span>
|
||||
</button>
|
||||
<button class="nav-item" data-view="settings">
|
||||
<span class="nav-icon">⚙</span>
|
||||
<span>Impostazioni</span>
|
||||
@@ -237,6 +241,93 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== VIEW: METADATI ===== -->
|
||||
<section class="view" id="view-meta">
|
||||
<header class="hero hero-amber">
|
||||
<div class="hero-content">
|
||||
<div class="hero-eyebrow amber">METADATI</div>
|
||||
<h1 class="hero-title">Modifica tag audio</h1>
|
||||
<p class="hero-subtitle">Titolo · Artista · Album · Anno · Copertina · BPM · Key · MP3/M4A/FLAC</p>
|
||||
</div>
|
||||
<div class="hero-deco">🏷</div>
|
||||
</header>
|
||||
|
||||
<h2 class="section-label">File</h2>
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<button class="btn btn-amber pill" id="metaPickBtn">📂 Apri file audio</button>
|
||||
<div class="path-display" id="metaPathDisplay">Nessun file selezionato</div>
|
||||
</div>
|
||||
<div class="meta-info" id="metaInfo" hidden></div>
|
||||
</div>
|
||||
|
||||
<div id="metaForm" hidden>
|
||||
<h2 class="section-label">Tag</h2>
|
||||
<div class="card">
|
||||
<div class="form-grid">
|
||||
<div class="field full">
|
||||
<label class="field-label">Titolo</label>
|
||||
<input type="text" id="metaTitle" class="input pill">
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label class="field-label">Artista</label>
|
||||
<input type="text" id="metaArtist" class="input pill">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">Album</label>
|
||||
<input type="text" id="metaAlbum" class="input pill">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">Album Artist</label>
|
||||
<input type="text" id="metaAlbumArtist" class="input pill">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">Anno</label>
|
||||
<input type="text" id="metaYear" class="input pill">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">Traccia</label>
|
||||
<input type="text" id="metaTrack" class="input pill" placeholder="1/12">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">Genere</label>
|
||||
<input type="text" id="metaGenre" class="input pill">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">BPM</label>
|
||||
<input type="text" id="metaBpm" class="input pill">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">Key</label>
|
||||
<input type="text" id="metaKey" class="input pill">
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label class="field-label">Commento</label>
|
||||
<textarea id="metaComment" class="input textarea" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-label">Copertina</h2>
|
||||
<div class="card cover-card">
|
||||
<div class="cover-preview" id="metaCoverPreview">
|
||||
<span class="cover-empty">Nessuna copertina</span>
|
||||
</div>
|
||||
<div class="cover-actions">
|
||||
<button class="btn btn-ghost pill" id="metaCoverBtn">📂 Cambia copertina</button>
|
||||
<button class="btn btn-ghost pill" id="metaCoverRemoveBtn">✕ Rimuovi</button>
|
||||
<div class="cover-hint" id="metaCoverHint"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card actions">
|
||||
<button class="btn btn-amber pill btn-lg" id="metaSaveBtn">💾 Salva tag</button>
|
||||
<button class="btn btn-ghost pill btn-lg" id="metaReloadBtn">↺ Ricarica</button>
|
||||
<div class="counter" id="metaStatus"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== VIEW: SETTINGS ===== -->
|
||||
<section class="view" id="view-settings">
|
||||
<header class="hero hero-purple">
|
||||
|
||||
+139
@@ -15,6 +15,7 @@ const state = {
|
||||
downloading: false,
|
||||
upgrading: false,
|
||||
videoDownloading: false,
|
||||
meta: { path: "", newCoverPath: "", removeCover: false },
|
||||
};
|
||||
|
||||
// Wait for pywebview ready
|
||||
@@ -391,6 +392,144 @@ $("#stopUpgradeBtn").addEventListener("click", async () => {
|
||||
$("#stopUpgradeBtn").disabled = true;
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// METADATA editor
|
||||
// ============================================================
|
||||
function formatDuration(sec) {
|
||||
if (!sec) return "—";
|
||||
const m = Math.floor(sec / 60);
|
||||
const s = Math.floor(sec % 60);
|
||||
return `${m}:${s.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function populateMetaForm(d) {
|
||||
$("#metaTitle").value = d.title || "";
|
||||
$("#metaArtist").value = d.artist || "";
|
||||
$("#metaAlbum").value = d.album || "";
|
||||
$("#metaAlbumArtist").value = d.album_artist || "";
|
||||
$("#metaYear").value = d.year || "";
|
||||
$("#metaTrack").value = d.track || "";
|
||||
$("#metaGenre").value = d.genre || "";
|
||||
$("#metaBpm").value = d.bpm || "";
|
||||
$("#metaKey").value = d.key || "";
|
||||
$("#metaComment").value = d.comment || "";
|
||||
|
||||
// Info riga sotto il path
|
||||
const info = $("#metaInfo");
|
||||
info.hidden = false;
|
||||
info.innerHTML = `
|
||||
<span><strong>Formato:</strong>${d.format || "—"}</span>
|
||||
<span><strong>Bitrate:</strong>${d.bitrate ? d.bitrate + " kbps" : "—"}</span>
|
||||
<span><strong>Durata:</strong>${formatDuration(d.duration)}</span>
|
||||
`;
|
||||
|
||||
// Cover
|
||||
const preview = $("#metaCoverPreview");
|
||||
preview.innerHTML = "";
|
||||
if (d.cover_base64) {
|
||||
const img = document.createElement("img");
|
||||
img.src = `data:${d.cover_mime || "image/jpeg"};base64,${d.cover_base64}`;
|
||||
preview.appendChild(img);
|
||||
} else {
|
||||
const span = document.createElement("span");
|
||||
span.className = "cover-empty";
|
||||
span.textContent = "Nessuna copertina";
|
||||
preview.appendChild(span);
|
||||
}
|
||||
$("#metaCoverHint").textContent = "";
|
||||
$("#metaCoverHint").className = "cover-hint";
|
||||
|
||||
state.meta.newCoverPath = "";
|
||||
state.meta.removeCover = false;
|
||||
}
|
||||
|
||||
async function loadMetaFile(path) {
|
||||
const res = await window.pywebview.api.read_metadata(path);
|
||||
if (!res.ok) {
|
||||
toast("Errore lettura: " + (res.error || ""), "error");
|
||||
return;
|
||||
}
|
||||
state.meta.path = path;
|
||||
$("#metaPathDisplay").textContent = path;
|
||||
$("#metaPathDisplay").classList.remove("empty");
|
||||
$("#metaForm").hidden = false;
|
||||
populateMetaForm(res.data);
|
||||
}
|
||||
|
||||
$("#metaPickBtn").addEventListener("click", async () => {
|
||||
const path = await window.pywebview.api.pick_audio_file();
|
||||
if (path) await loadMetaFile(path);
|
||||
});
|
||||
|
||||
$("#metaCoverBtn").addEventListener("click", async () => {
|
||||
const path = await window.pywebview.api.pick_image_file();
|
||||
if (!path) return;
|
||||
state.meta.newCoverPath = path;
|
||||
state.meta.removeCover = false;
|
||||
|
||||
// Mostra anteprima leggendo il file via fetch (file:// non funziona da webview;
|
||||
// usiamo solo il nome come hint, l'utente vedra il risultato dopo Save)
|
||||
const hint = $("#metaCoverHint");
|
||||
hint.textContent = "Nuova copertina: " + path.split("/").pop();
|
||||
hint.className = "cover-hint set";
|
||||
});
|
||||
|
||||
$("#metaCoverRemoveBtn").addEventListener("click", () => {
|
||||
state.meta.newCoverPath = "";
|
||||
state.meta.removeCover = true;
|
||||
const preview = $("#metaCoverPreview");
|
||||
preview.innerHTML = '<span class="cover-empty">Verra rimossa al salvataggio</span>';
|
||||
const hint = $("#metaCoverHint");
|
||||
hint.textContent = "Copertina marcata per rimozione";
|
||||
hint.className = "cover-hint remove";
|
||||
});
|
||||
|
||||
$("#metaReloadBtn").addEventListener("click", async () => {
|
||||
if (state.meta.path) await loadMetaFile(state.meta.path);
|
||||
});
|
||||
|
||||
$("#metaSaveBtn").addEventListener("click", async () => {
|
||||
if (!state.meta.path) {
|
||||
toast("Nessun file caricato", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
$("#metaSaveBtn").disabled = true;
|
||||
const status = $("#metaStatus");
|
||||
status.textContent = "Salvataggio…";
|
||||
|
||||
const payload = {
|
||||
path: state.meta.path,
|
||||
data: {
|
||||
title: $("#metaTitle").value,
|
||||
artist: $("#metaArtist").value,
|
||||
album: $("#metaAlbum").value,
|
||||
album_artist: $("#metaAlbumArtist").value,
|
||||
year: $("#metaYear").value,
|
||||
track: $("#metaTrack").value,
|
||||
genre: $("#metaGenre").value,
|
||||
bpm: $("#metaBpm").value,
|
||||
key: $("#metaKey").value,
|
||||
comment: $("#metaComment").value,
|
||||
},
|
||||
cover_path: state.meta.newCoverPath,
|
||||
remove_cover: state.meta.removeCover,
|
||||
};
|
||||
|
||||
const res = await window.pywebview.api.save_metadata(payload);
|
||||
$("#metaSaveBtn").disabled = false;
|
||||
|
||||
if (res.ok) {
|
||||
status.textContent = "✓ Tag salvati";
|
||||
toast("Tag salvati", "success");
|
||||
// Ricarica per mostrare lo stato aggiornato (es. nuova cover)
|
||||
await loadMetaFile(state.meta.path);
|
||||
} else {
|
||||
status.textContent = "";
|
||||
toast("Errore: " + (res.error || ""), "error");
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// SETTINGS tab
|
||||
// ============================================================
|
||||
|
||||
Reference in new issue
Block a user