Nuovo modulo core.dedup per rilevare brani audio duplicati usando
Chromaprint (fpcalc): scan cartella (opzionalmente ricorsivo),
fingerprint acustico via fpcalc -json, cache SQLite in
_get_config_dir()/dedup_cache.db per non ricalcolare al re-scan,
raggruppamento per fingerprint identico + ordering per bitrate DESC.
move_to_trash() usa send2trash (reversibile via Finder/Explorer).
- core/paths.py: find_fpcalc() con fallback dev su project bundle_bin
- core/dedup.py: scan_folder, compute_fingerprint, move_to_trash
- tests/test_dedup.py: 9 unit test (mock fpcalc + send2trash)
- requirements.txt: send2trash>=1.8.0
- build_macos.py: download fpcalc universal binary da GitHub releases
- build_windows.py: download fpcalc.exe da GitHub releases
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.
Nella tab Upgrade aggiunta seconda coppia path-display + Sfoglia/Rimuovi
per la cartella archivio opzionale (con hint esplicativo).
In fondo al body: nuovo modal upgradePickerModal con lista candidati e
3 bottoni (Usa selezionato / Cerca YouTube / Salta). Riusa le classi
esistenti .modal-backdrop / .modal / .modal-head / .modal-body / .modal-foot.
start_upgrade accetta 'archive_dir' nel payload e lo inoltra a
upgrade_folder. Il worker passa un resolve_callback che emette
'upgrade:candidates_needed' e blocca su una queue.Queue(1) attendendo
la scelta utente dal modal (timeout 10 min → skip).
Il metodo upgrade_resolve_candidates(choice) è chiamato dalla UI e
deposita la scelta nella queue. stop_upgrade sblocca eventuale attesa
inviando un 'skip' automatico. Log dedicati per gli status nuovi
(local_copy, local_upgraded, skipped_by_user, resolve_wait, scan_archive).
Nuovo flusso: se archive_dir è passato, upgrade_folder cerca versioni HQ
del brano nell'archivio locale prima di scaricare da YouTube. Match via
Jaccard sui token del filename (soglia default 0.5). Se 1 candidato:
copia/converte diretto in mp3 320k. Se >=2 e resolve_callback presente:
chiama la callback bloccante (per la UI) con lista candidati + bitrate
+ similarity, poi rispetta la scelta (use_local / use_youtube / skip).
Fallback su YouTube se nessun match o archive_dir=None.
Test unitari sui 3 helper (_normalize_stem, _scan_archive, _find_candidates).
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.
Nuova tab Traxsource per caricare la Top 100 mensile per genere.
23 generi, bypass Cloudflare via curl_cffi session, HTML scraping con
BeautifulSoup, cache 15min. Riusa pipeline download + tag ID3.
29 nuovi test unitari, 90/90 test verdi totali.
- 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).
Nuove tab Spotify e YouTube per cercare brani per titolo/artista.
Spotify con toggle "Solo artista" per scaricare tutta la discografia.
YouTube per mix DJ, unreleased, bootleg via yt-dlp ytsearch.
40 test verdi. Testato end-to-end in dev su macOS.
Aggiunge download_urls() in core/downloader.py che scarica direttamente da
URL YouTube gia noti (bypassa lo step di ricerca). Usa gli stessi flag di
download_playlist (mp3, embed thumbnail/metadata, output template, cookies,
ffmpeg location) e il parsing progress via stdout.
In api/bridge.py aggiunge start_urls_download() e _urls_worker() speculari a
start_tracks_download/_tracks_worker. Consumati dal tab "YouTube Search"
dopo che l'utente ha selezionato i risultati.
Nuova tab Beatport per scaricare le Top 100 per genere direttamente
dal sito Beatport (bypass Cloudflare via curl_cffi + TLS impersonation).
Riusa il downloader esistente YouTube via yt-dlp.
25 unit test verdi. Testato end-to-end in dev su macOS.