Fix strutturale ricorrente errore "yt-dlp exit 1" nel download.
YouTube rompe API ogni 1-2 settimane; yt-dlp stable esce ogni ~4
settimane, quindi la build imbarcava binari gia' obsoleti al rilascio.
- build_macos.py + build_windows.py: URL switch da
yt-dlp/yt-dlp/releases/latest → yt-dlp/yt-dlp-nightly-builds
- Sempre re-download (niente skip se bundle_bin/yt-dlp esiste) —
garantisce binario 24-48h, non 3-4 settimane
- Sidebar diventata troppo lunga (14+ tab): overflow-y auto sulla nav,
brand e footer restano sticky. Scrollbar 6px tema-coerente
- Nuovo core/applog.py: TimedRotatingFileHandler daily con 7 giorni
di retention. NON tocca sys.stdout/stderr (causa segfault WebKit
su macOS); cattura tracebacks via sys.excepthook + threading.excepthook
- bridge._log mirror su logger persistente (ui.<view>) → tutto quello
che vedi nei log delle tab e' anche su file
- Bridge: get_log_file_path() + open_log_folder() per la nuova sezione
Impostazioni con path visibile e bottone "Apri cartella"
Alcuni video YouTube restituiscono 403 anche con yt-dlp aggiornato.
Fix: nuova opzione Impostazioni "Cookies da browser" con dropdown
(chrome/safari/firefox/edge/brave). Priorita': file cookies.txt
esistente -> --cookies-from-browser <name> -> nessuno.
Bonus:
- core/paths.py::_bundle_dirs() ora include <project>/bundle_bin/
anche in dev, cosi' python main.py usa lo stesso yt-dlp del build
invece di Homebrew (spesso obsoleto)
- Helper _cookie_args() in downloader.py per non ripetere la logica
su tutti i punti dove serve --cookies
Iterazioni sul feature Dedup (v1.10.0) da subito-post-scaffolding:
- compute_fingerprint espone il vero errore invece del generico None,
worker propaga l'err_msg via progress_callback esteso
- Retry automatico: -length 30 su "invalid data / decoding frame",
-length 60 + -algorithm 1 su "fingerprint vuoto"
- Nuovo metodo "filename" (Jaccard sui token nome file) con algoritmo
incrementale O(N·K) — utile per file corrotti o per anteprima veloce
- Streaming groups: sia fingerprint che filename emettono `dedup:group`
appena un gruppo raggiunge >=2 file; JS accumula in Map per update
in-place. Progress bar avanza in tempo reale
- Fix shape bug che dava "NaN duplicati" nella summary line
- .dedup-groups-scroll: max-height 55vh + overflow-y auto per non
perdere l'header/footer scrollando molti gruppi
- Bottone "Seleziona tutti i consigliati" nel footer: ripristina la
selezione di default (tutti tranne il TIENI marcati per cancellazione)
- .gitignore: dedup_cache.db (SQLite locale per macchina utente)
Il typo (underscore prefix) causava NameError nel thread upgrade
quando un file era già HQ (soglia superata) e serviva solo aggiornare
la copertina. Il thread moriva silenziosamente e l'utente vedeva
l'app 'bloccata' senza feedback.
Tab Upgrade:
- Checkbox "Scelta automatica" per skippare il modal quando ci sono
multi-match (usa bitrate DESC)
- Anteprima audio nel picker: play button sul source + ogni candidato
via bridge read_audio_data_url (WKWebView blocca file://)
- Bottone "Annulla tutto" nel picker → interrompe l'intera coda
- Fix mtime: shutil.copy (non copy2) così il file upgradato ha
data odierna nel Finder
- Fix stop audio → non triggera più toast di errore
- Modal picker più largo (620-780px) con footer wrap
Extra: campo release_date estratto anche in Beatport (publish_date)
e YouTube (upload_date) — completato scaffold sort colonna Data
Frontend Upgrade:
- state.upArchiveDir + handler Sfoglia/Rimuovi per la seconda path-display
- start_upgrade payload include archive_dir
- bridge handler 'upgrade:candidates_needed' apre openUpgradePicker() con
lista candidati (basename + bitrate + size + similarity + path)
- 3 azioni utente: Usa selezionato / Cerca YouTube / Salta; ESC e click
su backdrop = Salta. Chiusura implicita quando upgrade:done.
- CSS minimale (.upgrade-picker-*) accodato dopo le sezioni esistenti.
Version bump v1.9.2 -> v1.9.3.
Il worker Python era bloccato in `for line in proc.stdout` che non
ritornava mai quando il subprocess yt-dlp veniva killed ma lasciava
figli ffmpeg orfani con pipe stdout ancora aperto. Effetti:
1) Il bottone Stop non fermava l'upgrade
2) Anche il watchdog kill non liberava il thread
Fix:
- Lettura stdout in thread separato + queue.Queue
- Main loop polla ogni 500ms con timeout: controlla is_stopped() e
proc.poll() -> Stop risponde entro 1s
- Watchdog usa kill() (SIGKILL) invece di terminate() (SIGTERM) per
garantire cleanup child orfani
Prima: se yt-dlp si bloccava nel download di un video (geo-restricted,
YouTube throttle, rete lenta), il for loop su stdout attendeva
indefinitamente e la tab Upgrade si fermava dopo 2-3 brani.
Ora: threading.Timer da 5 min uccide il subprocess se in scaduta.
Al kill il for esce -> codice trova temp_dir vuota -> marca il brano
"download_error" e prosegue col successivo.
- requirements.txt: certifi come dep esplicita
- main.py: setta SSL_CERT_FILE/REQUESTS_CA_BUNDLE su certifi.where() al boot
- build_windows.py + build_macos.py: --collect-data certifi nel PyInstaller
Bugfix: senza certifi bundlato, le chiamate HTTPS dal build Windows
falliscono con CERTIFICATE_VERIFY_FAILED (attivazione licenza impossibile).
Crash report v1.7.14 ha rivelato che NSMicrophoneUsageDescription
NON era stato scritto nell'Info.plist distribuito. Causa: il valore
contiene l'apostrofo "l'audio" che rompe PlistBuddy con quote
shell. PlistBuddy falliva ma restituiva exit code != 0 con un
messaggio diverso da "already exists", quindi anche il fallback
Set non veniva chiamato -> chiave mai aggiunta.
Riscritto patch_info_plist con plistlib di Python: nessun shell
escaping, robusto a qualsiasi carattere. Aggiunto check di verifica
finale che logga errore critico se la chiave manca dopo write.
Dopo che codesign --info-plist e wrap in sub-bundle .app si sono
rivelati strade morte (macOS 14+ TCC e' troppo severo per ad-hoc
signed binaries), riscritta la registrazione macOS per usare
PyObjC + AVCaptureSession nel processo Python principale.
Il main MusicTools ha il TCC del bundle (NSMicrophoneUsageDescription
nell'Info.plist), e usando AVFoundation IN-PROCESS il permesso
viene applicato correttamente. ffmpeg subprocess viene usato SOLO
per la conversione CAF -> MP3 dopo lo stop (operazione su file,
niente accesso microfono = niente TCC).
Aggiunto pyobjc-framework-AVFoundation a requirements.
core/paths.py: ripristinato lookup semplice in Contents/Frameworks/
(non serve piu' cercare in sub-bundle).
build_macos.py: wrap_subprocess_in_bundle disabilitato.
Diagnosi crash report v1.7.13 + investigazione TCC behavior
confermano: PyObjC AVCaptureSession in-process e' l'unica
soluzione affidabile senza Developer ID Apple.
Vedi: https://www.qt.io/blog/the-curious-case-of-the-responsible-process
Crash report ha dato la spiegazione precisa:
"Namespace TCC, Code 0 - This app has crashed because it attempted
to access privacy-sensitive data without a usage description.
The app's Info.plist must contain an NSMicrophoneUsageDescription key."
macOS 14+ richiede che ogni binario che apre device privacy-sensitive
abbia il suo PROPRIO Info.plist con NSMicrophoneUsageDescription, NON
basta che il main MusicTools lo abbia. Per binari standalone come
ffmpeg, non c'e' Info.plist associato -> SIGABRT immediato.
Fix: incapsulare ffmpeg e ffprobe in mini-bundle .app dentro
MusicTools.app/Contents/Frameworks/ffmpeg.app/Contents/MacOS/ffmpeg
con il proprio Info.plist contenente NSMicrophoneUsageDescription.
Aggiunto rpath @executable_path/../../../ per puntare alle dylib
di Contents/Frameworks/. Codesign di ogni sub-bundle con identifier
com.djluza.musictools.{ffmpeg,ffprobe}.
core/paths.py aggiornato per cercare i binari anche dentro i
sub-bundle .app oltre che in Contents/Frameworks/ legacy.
L'utente al primo lancio della registrazione vedra' un prompt
microfono per "ffmpeg" (in aggiunta a quello del main MusicTools).
Bug: codesign --deep ad-hoc dava a ffmpeg un identifier auto-generato
(ffmpeg-<hash>) diverso da com.djluza.musictools del bundle, quindi
macOS trattava ffmpeg come app distinta per TCC -> permesso microfono
del main NON ereditato dal subprocess -> registrazione muta o errore.
Fix: in adhoc_codesign() ora firmiamo OGNI Mach-O nested con
--identifier com.djluza.musictools (stesso del bundle), poi firmiamo
il bundle .app. Cosi' TCC vede un unico "team" coerente e il
permesso microfono si propaga a ffmpeg/ffprobe/yt-dlp. Verificato
in dev mode: registrazione BlackHole funziona quando ffmpeg eredita
correttamente il TCC.
Anche fix collaterale: bridge._gate() salta la chiamata consume_quota
al server quando il piano corrente ha daily_limit=None (Annual). Per
i piani unlimited non c'e' motivo di chiamare il server -> niente
piu' errori online se il server e' temporaneamente irraggiungibile
durante una registrazione su un piano Annual.
Causa root: MusicTools.app ha il permesso TCC microfono, ma quando
lancia ffmpeg come subprocess macOS tratta ffmpeg come binario non
firmato distinto -> AVFoundation apre il device senza errore ma
restituisce solo silenzio.
Fix in build_macos.py: dopo PyInstaller + Info.plist, applico
`codesign --force --deep --sign -` ad-hoc su tutto il bundle.
La firma e' "free" (no Developer ID) ma rende l'app un singolo
team coerente per TCC, quindi il permesso del main si propaga
anche a ffmpeg/ffprobe/yt-dlp.
QuickTime registrava da BlackHole, MusicTools no: il bug era che
list_input_devices passava al record l'index AVFoundation, ma
l'index puo' rinumerarsi tra l'elenco e l'apertura (iPhone in
continuita', Multi-Output Device, hot-plug). Quando ffmpeg apriva
:<index> col numero sbagliato, registrava silenzio dal device
"vero" mappato a quel numero.
Fix: in _list_macos uso il NOME del device come id (es.
"BlackHole 2ch"). ffmpeg AVFoundation accetta sia :<index> sia
:<name> ed il nome e' stabile. Aggiunto campo 'index' solo per
log/diagnostica. Nessun cambio Windows (gia' faceva by-name).
Senza NSMicrophoneUsageDescription nell'Info.plist macOS NEGA
silenziosamente l'accesso al microfono (incluso BlackHole come
dispositivo input) e l'app non compare nemmeno in Impostazioni di
Sistema > Privacy e sicurezza > Microfono, quindi l'utente non puo
nemmeno autorizzarla manualmente.
Aggiunto patch_info_plist() in build_macos.py che dopo PyInstaller
aggiunge via PlistBuddy:
- NSMicrophoneUsageDescription (causa primaria)
- CFBundleIdentifier stabile (com.djluza.musictools)
- LSApplicationCategoryType = music
Impostazioni: il bottone "Salva impostazioni" e' ora sticky in fondo
al viewport, sempre visibile senza dover scrollare in coda alla pagina.
Spotify: aggiornata guida in-app e landing per riflettere il nuovo
requisito Premium per creare le API keys (fino a oggi era gratis con
account Spotify Free; Spotify ha cambiato policy). Singoli brani e
tutte le altre fonti continuano a funzionare senza Premium.
Inclusi anche file gia deployati lato server in sessioni precedenti
(webhook LS, landing icone PNG, download.html Gatekeeper instructions).
La causa root del "Failed to resolve Python.Runtime.Loader.Initialize"
era il Mark of the Web (Zone.Identifier ADS) che Windows aggiunge ai
file estratti da uno zip scaricato dal browser. .NET Framework
rifiuta SILENZIOSAMENTE di risolvere i symbol export degli assembly
managed con MOTW.
Fix multilivello:
- main.py: al primo avvio cancella :Zone.Identifier da ogni
.dll/.exe/.pyd nel bundle (via ctypes kernel32.DeleteFileW)
- CI: Unblock-File su dist/ pre-zip
- requirements: pythonnet==3.0.5 + clr-loader==0.2.7.post0 pinnati
Vedi: https://github.com/r0x0r/pywebview/issues/1215https://github.com/pyinstaller/pyinstaller/issues/7412
Preload esplicito di pythonnet.load("netfx") in main.py PRIMA di
importare webview: il lazy-import del 'clr' fallisce sotto PyInstaller
perche' Python.Runtime.dll viene bundlata ma il loader netfx non sa
risolverla.
Aggiunti:
- main.py: bootstrap pythonnet con PYTHONNET_RUNTIME=netfx + load()
- build_windows.py: --clean rebuild, --collect-data pythonnet/clr_loader
(per Python.Runtime.runtimeconfig.json e altri non-Python files),
hidden-import clr_loader.netfx
PyInstaller non bundlava i file di supporto di pythonnet/clr_loader
(Python.Runtime.dll, nethost.dll, runtimeconfig.json), per cui al
caricamento di webview.platforms.winforms.py l'import 'clr' moriva.
Aggiunti --collect-all pythonnet/clr_loader + copy-metadata.
Pinnata pythonnet a 3.0.3 (l'ultima compatibile con PyInstaller
senza patch aggiuntive).
Macos ora viene distribuito come .dmg invece di .zip:
- contiene MusicTools.app + alias Applicazioni + "Fix Gatekeeper.command"
- doppio click sullo script rimuove com.apple.quarantine senza
Terminal manuale -> niente piu' "App non verificata"
Backend: /api/download serve .dmg con Content-Type
application/x-apple-diskimage. Landing /download aggiornata
con il nuovo flusso (3 step).
Icona: rigenerati icon.icns (macOS multi-size) e icon.ico (Windows
multi-size 16-256) dalla sorgente assets/icon_1024.png.
Landing: nuovo box "Per sfruttare al massimo MusicTools serve un
account Spotify" + FAQ esplicita su cosa funziona senza setup
e dove servono le chiavi.
Spotify: per i singoli brani ora basta incollare il link, l'app
estrae name+artist dai meta og:title/og:description pubblici e
risolve via YouTube. Album/playlist continuano a richiedere
le credenziali (gratis, 2 min).
Landing /download: bottoni Mac/Windows ora navigano nella stessa
tab via JS handler + feedback "avvio in corso" invece del fragile
target="_blank".
Il selettore Impostazioni > Tema salvava il valore ma nessuno applicava
data-theme al <html>, e mancavano gli override CSS per il tema chiaro.
Aggiunte custom properties light + applyTheme() su init/change/save.
Pricing model: subscription mensili (Basic 2.99 / Pro 5.99 / Premium 9.99)
piu' Annual 49.90 one-time. Feature gating per piano (Basic = solo audio,
Pro+ = tutto). Quota giornaliera (10/30/150) con reset mezzanotte Europe/Rome.
Server: tabella daily_usage, endpoint /api/usage/consume e /status,
webhook LS gestisce subscription_* events + mappatura variant_id -> plan
via .env (LS_VARIANT_BASIC|PRO|PREMIUM|ANNUAL).
Client: gating tab in sidebar in base ai features del piano, contatore
"X/Y oggi" + bottone Upgrade, modal blocco con CTA verso landing.
L'input URL ora accetta anche query libere: se non sembra un link
(http/https/spotify:/dominio noto), il testo viene parsato come
{artist, name} e scaricato via download_playlist con una traccia.
Riusa lo stesso flusso ytsearch1: gia in uso per le tracklist da file.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Aggiunto helper subprocess_flags() in core/paths.py che ritorna
CREATE_NO_WINDOW su Windows. Applicato a tutte le subprocess.run/Popen
in downloader, upgrader e recorder per evitare il flash di console nera
ad ogni traccia scaricata.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- main.py: titolo finestra pywebview -> 'MusicTools'
- core/config.py: APP_NAME='MusicTools', cartella config bundled
ora ~/Library/Application Support/MusicTools/ e %APPDATA%/MusicTools/.
Migrazione one-shot: se non esiste la nuova ma esiste la legacy
'MusicDownload', il config.json viene copiato cosi' le impostazioni
non vanno perse dopo l'aggiornamento
- core/recorder.py: messaggio errore permesso microfono
- api/bridge.py: User-Agent update check + guida Spotify menzionano
MusicTools
- build_macos.py / build_windows.py / .github/workflows/build.yml:
prodotti rinominati (MusicTools.app, MusicTools.exe,
MusicTools-macOS.zip, MusicTools-Windows.zip, Release 'Build MusicTools')
- webui: <title>, brand-name sidebar, intestazioni commenti
- README: tutti i riferimenti aggiornati + nota che il repo Git
conserva il nome storico 'musicdownload' (URL stabili per
release gia' pubblicate)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>