diff --git a/webui/css/style.css b/webui/css/style.css index 0979113..c050418 100644 --- a/webui/css/style.css +++ b/webui/css/style.css @@ -38,6 +38,10 @@ --rec-red-hover: #F87171; --rec-red-dim: #7F1D1D; + --indigo: #6366F1; + --indigo-hover: #818CF8; + --indigo-dim: #312E81; + --red: #E22134; --red-hover: #FF4055; @@ -249,6 +253,10 @@ button, input, select, textarea { font-family: inherit; font-size: inherit; } background: linear-gradient(135deg, #DC2626 0%, #7F1D1D 60%, #3F0A0A 100%); } +.hero-indigo { + background: linear-gradient(135deg, #4F46E5 0%, #312E81 60%, #1E1B4B 100%); +} + .hero-content { position: relative; z-index: 2; max-width: 70%; } .hero-eyebrow { @@ -264,6 +272,7 @@ button, input, select, textarea { font-family: inherit; font-size: inherit; } .hero-eyebrow.pink { color: var(--pink-hover); } .hero-eyebrow.amber { color: var(--amber-hover); } .hero-eyebrow.red { color: var(--rec-red-hover); } +.hero-eyebrow.indigo { color: var(--indigo-hover); } .hero-title { font-size: 36px; @@ -1038,3 +1047,212 @@ input[type="number"]::-webkit-inner-spin-button { font-weight: 700; margin-right: 10px; } + +/* =============================== + GUIDE + =============================== */ +.guide-layout { + display: grid; + grid-template-columns: 200px 1fr; + gap: 32px; + align-items: start; +} + +.guide-toc { + position: sticky; + top: 0; + display: flex; + flex-direction: column; + gap: 2px; + padding: 16px 10px; + background: var(--bg-card); + border-radius: var(--r-md); + max-height: calc(100vh - 80px); + overflow-y: auto; +} + +.toc-link { + color: var(--text-2); + text-decoration: none; + padding: 8px 12px; + border-radius: var(--r-sm); + font-size: 13px; + font-weight: 600; + transition: background-color 0.15s, color 0.15s; +} + +.toc-link:hover { + background: var(--bg-input); + color: var(--text); +} + +.toc-link.active { + background: var(--indigo-dim); + color: var(--indigo-hover); +} + +.guide-content { + min-width: 0; /* evita overflow horizontal */ + color: var(--text-2); + font-size: 14px; + line-height: 1.65; +} + +.g-section { + background: var(--bg-card); + border-radius: var(--r-md); + padding: 24px 28px; + margin-bottom: 18px; + scroll-margin-top: 12px; +} + +.g-section h2 { + font-size: 22px; + font-weight: 800; + color: var(--text); + margin-bottom: 10px; + letter-spacing: -0.3px; +} + +.g-section h3 { + font-size: 15px; + font-weight: 800; + color: var(--text); + margin-top: 22px; + margin-bottom: 8px; + letter-spacing: 0.2px; +} + +.g-section h4 { + font-size: 13px; + font-weight: 700; + color: var(--indigo-hover); + margin-top: 16px; + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.6px; +} + +.g-section p { + margin-bottom: 10px; +} + +.g-section ul, +.g-section ol { + padding-left: 22px; + margin-bottom: 12px; +} + +.g-section li { + margin-bottom: 6px; +} + +.g-section strong { color: var(--text); } + +.g-section code { + background: var(--bg-input); + padding: 2px 7px; + border-radius: 4px; + font-family: "SF Mono", Menlo, Consolas, monospace; + font-size: 12.5px; + color: var(--green-hover); +} + +.g-section pre { + background: var(--bg-card-2); + border: 1px solid var(--border); + border-radius: var(--r-sm); + padding: 12px 16px; + margin: 10px 0 14px; + overflow-x: auto; +} + +.g-section pre code { + background: transparent; + padding: 0; + color: var(--text); + font-size: 13px; +} + +.g-section a { + color: var(--indigo-hover); + text-decoration: none; + font-weight: 600; + border-bottom: 1px solid transparent; +} + +.g-section a:hover { + border-bottom-color: var(--indigo-hover); +} + +.kbd { + background: var(--bg-input); + border: 1px solid var(--bg-input-hover); + color: var(--text); + padding: 1px 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.g-box { + padding: 14px 18px; + border-radius: var(--r-sm); + margin: 14px 0; + font-size: 13.5px; + line-height: 1.55; +} + +.g-box.info { + background: rgba(99, 102, 241, 0.10); + border-left: 3px solid var(--indigo); +} + +.g-box.success { + background: rgba(29, 185, 84, 0.10); + border-left: 3px solid var(--green); +} + +.g-box.warn { + background: rgba(253, 203, 110, 0.10); + border-left: 3px solid #FDCB6E; +} + +.g-table { + width: 100%; + border-collapse: collapse; + margin: 12px 0; + font-size: 13px; +} + +.g-table th, +.g-table td { + padding: 10px 14px; + text-align: left; + border-bottom: 1px solid var(--divider); + vertical-align: top; +} + +.g-table th { + color: var(--text); + font-weight: 700; + font-size: 11.5px; + letter-spacing: 0.5px; + text-transform: uppercase; + background: var(--bg-card-2); +} + +.g-table tr:last-child td { + border-bottom: none; +} + +@media (max-width: 900px) { + .guide-layout { grid-template-columns: 1fr; } + .guide-toc { + position: static; + flex-direction: row; + flex-wrap: wrap; + max-height: none; + } +} diff --git a/webui/index.html b/webui/index.html index 136ebdf..6ce2cb8 100644 --- a/webui/index.html +++ b/webui/index.html @@ -45,6 +45,10 @@ ⚙ Impostazioni + + +
+
+
+
GUIDA
+

Manuale d'uso

+

Come usare MusicTools al meglio · setup · troubleshooting

+
+
📖
+
+ +
+ + + + +
+ +
+

Introduzione

+

MusicTools è una app desktop tutto-in-uno per chi lavora con la musica: scarica brani e video da decine di piattaforme, migliora la qualità dei file esistenti, modifica i tag/metadati e registra l'audio da qualsiasi ingresso del computer.

+

L'interfaccia è organizzata in 6 sezioni accessibili dalla barra laterale: ⬇ Scarica, 🎬 Video, ⚡ Upgrade, 🏷 Metadati, ● Registra, ⚙ Impostazioni.

+
+ +
+

Setup iniziale

+

Prima di usare l'app, conviene fare 3 cose nelle Impostazioni:

+
    +
  1. Cartella output predefinita — dove vengono salvati tutti i download.
  2. +
  3. Credenziali Spotify (solo se vuoi scaricare da link Spotify) — vedi il bottone "Come ottenere le credenziali?" nella tab Impostazioni.
  4. +
  5. cookies.txt (opzionale) — necessario solo per scaricare contenuti privati Instagram/Facebook o aggirare rate-limit YouTube. Esportalo dal browser con l'estensione "Get cookies.txt LOCALLY".
  6. +
+

Tutti i campi vengono salvati automaticamente quando premi Salva Impostazioni.

+
+ +
+

Tab ⬇ Scarica

+

Scarica musica in MP3 al bitrate impostato in Impostazioni (default 320 kbps).

+ +

1 · Da Spotify

+

Incolla un link a una playlist, album o brano (es. open.spotify.com/playlist/...). L'app risolve i metadati via API Spotify e scarica i brani da YouTube. Richiede le credenziali API impostate.

+ +

2 · Da YouTube, SoundCloud e altri

+

Incolla qualsiasi URL supportato da yt-dlp (singolo video o playlist). Funziona anche con Bandcamp, Mixcloud, ecc.

+ +

3 · Lista da file .txt

+

Clicca 📂 Carica lista. Il file può contenere:

+
    +
  • Una URL per riga — l'app le scarica una dopo l'altra.
  • +
  • Tracklist con titoli (es. Beatport Top 100): formato 1. Artista – Titolo (variant) (durata). L'app riconosce automaticamente il formato e cerca ogni brano su YouTube.
  • +
+

Nel caso tracklist, viene creata automaticamente una sottocartella col nome del file (es. Beatport Top 100.txt → Beatport Top 100/).

+ +

4 · Skip duplicati

+

Se riesegui la stessa playlist, i brani già scaricati vengono saltati in automatico. Il tracking funziona in due modi:

+
    +
  • File .downloaded_tracks dentro la cartella output.
  • +
  • Scan dei file presenti, con match dei nomi token-per-token (ignora parole come "Official Video", "Lyrics", "HD"...).
  • +
+
+ +
+

Tab 🎬 Video

+

Scarica video da YouTube, TikTok, Instagram, Facebook e oltre 1000 altri siti supportati da yt-dlp.

+ +

Modalità

+
    +
  • 🎬 Video (MP4) — output video+audio in MP4, qualità a scelta (best / 1080p / 720p / 480p).
  • +
  • 🎵 Solo audio (MP3) — estrae solo l'audio in MP3 al bitrate impostato in Impostazioni, con thumbnail embedded come copertina.
  • +
+ +

Contenuti privati

+

Per scaricare reel Instagram privati, post Facebook privati o aggirare rate-limit YouTube serve un file cookies.txt valido (formato Netscape). Esportalo dal browser e impostalo in Impostazioni → Percorsi.

+ +
+ Esempio: per scaricare la Storia di un utente Instagram seguito, fai login al browser, esporta i cookies, riavvia l'app. +
+
+ +
+

Tab ⚡ Upgrade qualità

+

Scansiona una cartella e riscarica i file sotto la soglia kbps impostata, sostituendoli con la versione a qualità maggiore.

+
    +
  1. Seleziona la cartella con i file audio.
  2. +
  3. Imposta la Soglia HQ (kbps) — i file con bitrate inferiore verranno riprocessati. Default 310 kbps.
  4. +
  5. (Opzionale) Spunta Includi sottocartelle per la scansione ricorsiva.
  6. +
  7. Premi Avvia Upgrade.
  8. +
+

Per ogni file l'app cerca la versione migliore su YouTube e la sostituisce mantenendo nome originale. I file già processati vengono saltati grazie al tracking .upgraded_tracks.

+
+ +
+

Tab 🏷 Metadati

+

Editor completo dei tag audio. Apri un file con 📂 Apri file audio.

+ +

Formati supportati

+
    +
  • MP3 — ID3v2.3
  • +
  • M4A / AAC / MP4 — Apple atoms
  • +
  • FLAC — Vorbis comments
  • +
  • WAV — ID3 embedded + fallback al chunk RIFF LIST/INFO (per file taggati da ffmpeg/Audacity)
  • +
+ +

Campi editabili

+

Titolo, artista, album, album artist, anno, traccia (formato 1/12), genere, BPM, key musicale, commento, copertina (JPG/PNG), e su macOS anche kMDItemWhereFroms (l'extended attribute che traccia l'origine del file scaricato).

+ +

Copertina

+

Anteprima 180×180 della copertina corrente. Cambia copertina ne carica una nuova; Rimuovi la marca per la rimozione al prossimo salvataggio.

+ +

Annulla modifiche

+

Premi ↺ Ricarica per rileggere i tag dal file e scartare le modifiche non salvate.

+
+ +
+

Tab ● Registra

+

Cattura audio in MP3 da qualsiasi ingresso del computer: mixer, scheda audio esterna, microfono, driver loopback (BlackHole/Soundflower/VB-Cable). Bitrate selezionabile 128–320 kbps.

+ +

Setup standard (mixer/microfono)

+
    +
  1. Dropdown Dispositivo: seleziona la sorgente.
  2. +
  3. Imposta una cartella output e (opzionale) un nome file.
  4. +
  5. Premi ● REC → registrazione, timer in tempo reale.
  6. +
  7. ◼ Stop → file MP3 finalizzato e linkato sotto.
  8. +
+ +

Registrare l'audio di sistema su macOS (Spotify, YouTube...)

+

macOS non espone nativamente l'uscita audio. Serve un driver loopback gratuito, il più diffuso è BlackHole.

+ +

Installazione

+

Apri Terminale e lancia:

+
brew install blackhole-2ch
+

Se subito dopo l'install BlackHole non compare nel dropdown, riavvia il servizio audio:

+
sudo killall coreaudiod
+

(rilancia da solo in pochi secondi, nessun rischio)

+ +

Permesso microfono

+

macOS richiede il permesso "Microfono" per leggere da BlackHole via AVFoundation. Vai su Preferenze di Sistema → Privacy e sicurezza → Microfono e abilita Terminal/MusicTools. Se la voce non c'è, comparirà un popup la prima volta che premi REC.

+ +

Setup A — minimo (non senti l'audio durante)

+
    +
  1. Click icona altoparlante nella barra menu → Output → BlackHole 2ch
  2. +
  3. Fai partire Spotify (non sentirai nulla — l'audio va a BlackHole)
  4. +
  5. Tab Registra → dispositivo 🔄 BlackHole 2ch (loopback) → REC
  6. +
+ +

Setup B — completo (registri E senti l'audio)

+
    +
  1. Apri Configurazione MIDI Audio (Applicazioni → Utility)
  2. +
  3. In basso a sinistra: + → Crea dispositivo aggregato a uscita multipla
  4. +
  5. Spunta sia BlackHole 2ch sia i tuoi Altoparlanti integrati
  6. +
  7. Imposta BlackHole 2ch come Master Device (menu in alto)
  8. +
  9. Abilita Drift Correction sulla riga degli altoparlanti
  10. +
  11. (Opzionale) Rinomina in Speakers + BlackHole
  12. +
  13. Preferenze di Sistema → Audio → Uscita → seleziona Speakers + BlackHole
  14. +
  15. Nella tab Registra: scegli 🔄 BlackHole 2ch (loopback), non il dispositivo aggregato
  16. +
+
+ Adesso ogni audio del sistema viene riprodotto sugli speaker (lo senti) e contemporaneamente catturato da BlackHole (viene registrato). +
+
+ +
+

Impostazioni

+ +

Spotify API

+

Le credenziali API Spotify sono gratuite e non richiedono Premium. Vai su Spotify Developer Dashboard, "Create app", compila i campi (Redirect URI http://localhost:8888/callback, seleziona "Web API"), e copia Client ID + Client Secret nei campi corrispondenti.

+ +

Download

+
    +
  • Bitrate — qualità target per gli MP3 (128/192/256/320 kbps).
  • +
  • Soglia HQ — soglia kbps per la tab Upgrade. I file sotto questa soglia vengono riscaricati.
  • +
+ +

Percorsi

+
    +
  • cookies.txt — per contenuti privati/protetti.
  • +
  • Cartella output predefinita — viene proposta in tutte le tab.
  • +
+
+ +
+

Aggiornamenti

+

In Impostazioni → Aggiornamenti trovi il bottone Controlla aggiornamenti. L'app interroga la pagina release di GitHub e ti mostra se c'è una versione nuova.

+

Se è disponibile un aggiornamento, comparirà un secondo bottone Scarica aggiornamento che apre direttamente il browser sull'asset corretto (macOS o Windows) della release più recente.

+
+ +
+

Troubleshooting

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SintomoCausa probabileFix
BlackHole non compare nel dropdown della tab RegistraCoreAudio non l'ha ancora caricato dopo l'installsudo killall coreaudiod + premi ↻ Aggiorna
"Permesso microfono mancante" o errore ffmpeg -6macOS nega l'accesso al devicePrivacy e sicurezza → Microfono → abilita Terminal/MusicTools
File MP3 della registrazione è mutoL'output di sistema non passa da BlackHoleImposta output su BlackHole o Multi-Output Device (Setup A/B)
Spotify dice "Autenticazione fallita"Client ID/Secret mancanti o erratiImpostazioni → Spotify API → ripeti la procedura
Instagram/Facebook non scaricaContenuto privato senza cookies validiEsporta cookies.txt dal browser dopo login
Brano scaricato è sbagliato (versione cover invece dell'originale)Ricerca YouTube ha preso il primo risultatoModifica la query nel file di tracklist (più dettagli artista/titolo)
Tag non vengono letti su un WAVFile usa solo chunk INFO non ID3 (raro)L'app ora gestisce entrambi — controlla di avere v1.2.1+
Stop button non risponde durante il downloadyt-dlp sta finalizzando il fileAttendi qualche secondo, il processo si chiude dopo l'I/O
+ +

Reset configurazione

+

Se vuoi azzerare tutte le impostazioni, cancella il file config.json. Posizione:

+
    +
  • macOS: ~/Library/Application Support/MusicTools/config.json
  • +
  • Windows: %APPDATA%\MusicTools\config.json
  • +
+

Verrà ricreato con valori di default al prossimo avvio.

+
+ +
+

Risorse

+ +

MusicTools — by LuZa

+
+ +
+
+
+ diff --git a/webui/js/app.js b/webui/js/app.js index 03cf5a5..d4b6cc1 100644 --- a/webui/js/app.js +++ b/webui/js/app.js @@ -488,11 +488,55 @@ $("#recRefreshBtn").addEventListener("click", refreshRecDevices); $("#recOpenGuide").addEventListener("click", (e) => { e.preventDefault(); - window.pywebview.api.open_external_url( - "https://github.com/luzadev/musicdownload#guida-rapida-registrazione-audio-di-sistema-macos" - ); + showView("guide"); + setTimeout(() => { + const el = document.getElementById("g-record"); + if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); + }, 80); }); +// ============================================================ +// GUIDE — link esterni + TOC active highlight +// ============================================================ +document.querySelectorAll("#view-guide a[data-ext]").forEach((a) => { + a.addEventListener("click", (e) => { + e.preventDefault(); + window.pywebview.api.open_external_url(a.dataset.ext); + }); +}); + +document.querySelectorAll(".toc-link").forEach((a) => { + a.addEventListener("click", (e) => { + e.preventDefault(); + const id = a.getAttribute("href").slice(1); + const el = document.getElementById(id); + if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); + }); +}); + +// Aggiorna TOC active in base allo scroll della view +function setupGuideObserver() { + const sections = document.querySelectorAll(".g-section[id]"); + const tocLinks = document.querySelectorAll(".toc-link"); + if (!sections.length || !tocLinks.length) return; + + const main = document.querySelector(".main"); + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (!entry.isIntersecting) return; + const id = entry.target.id; + tocLinks.forEach((l) => { + l.classList.toggle("active", l.getAttribute("href") === "#" + id); + }); + }); + }, + { root: main, rootMargin: "-20% 0px -65% 0px", threshold: 0 } + ); + sections.forEach((s) => observer.observe(s)); +} +setupGuideObserver(); + $("#recBrowseBtn").addEventListener("click", async () => { const path = await window.pywebview.api.browse_directory(); if (path) {