Compare commits

...
3 Commits
Author SHA1 Message Date
luzadevandClaude Opus 4.7 d8423ec215 Tracklist: crea sottocartella col nome del file caricato
Quando si carica un file .txt come tracklist, l'app ora scarica i
brani in <output_dir>/<nome_file>/ anziche direttamente in output_dir.
Es: 'Beatport Top 100 Melodic House & Techno.txt' -> cartella
'Beatport Top 100 Melodic House & Techno/' creata dentro la
cartella output predefinita.

- load_url_list ritorna 'name' (basename senza estensione)
- start_tracks_download accetta 'subfolder' opzionale e fa
  os.path.join con sanitizzazione dei separatori
- JS passa state.loaded.name come subfolder

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 00:23:20 +02:00
luzadevandClaude Opus 4.7 4b1524e5f7 Carica lista: auto-detect URL list vs tracklist
Adesso 'Carica lista' nella tab Scarica riconosce automaticamente
il formato del file .txt:

- Se la prima riga e un URL (http/https) -> URL list (come prima)
- Altrimenti -> tracklist parsata 'Artista - Titolo (variant) (durata)'
  e scaricata cercando ogni brano su YouTube (riusa download_playlist)

Parser robusto: gestisce numerazione iniziale (1. / 1) / 1: / 1-),
qualsiasi tipo di dash (-, en-dash, em-dash, ...) e durata finale
'(M:SS)'. Testato sulla Beatport Top 100 Melodic House & Techno
(100/100 tracce parsate).

api/bridge.py: nuovo start_tracks_download, load_url_list ritorna
'kind: urls|tracks'.
webui/js/app.js: badge cambia tra 'X URL' e 'X tracce', avvio
chiama start_tracks_download quando appropriato.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 19:37:17 +02:00
luzadevandClaude Opus 4.7 e5158d382f README: riscritto per UI pywebview + tab Video + CI release
- Aggiornata struttura progetto (webui/, api/, niente piu gui/)
- Documentate nuove dipendenze (pywebview, pyobjc-WebKit, pythonnet)
- Sezione build con GitHub Actions e release automatiche su tag
- Esempio uso cookies.txt per Instagram/Facebook privati

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 13:33:38 +02:00
4 changed files with 353 additions and 75 deletions

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
1. Kapuchon, Miss Monique & GLZ – Hot Sauce (Extended) (5:36)
2. Brunello, Vargas – Ghost Dance (Original Mix) (6:12)
3. Hardt Antoine – Raw (Original Mix) (5:25)
4. JUNO (DE) & Samm (BE) – Heart Spin (Original Mix) (5:56)
5. AIKON – To You (Original Mix) (6:12)
6. Adam Port, Keinemusik & SG Lewis – Be The One (Extended) (4:45)
7. HotLap – Recall (Extended Mix) (5:48)
8. JUNO (DE) & Samm (BE) – Muralli (Original Mix) (4:59)
9. Brunello – Science Fiction (Original Mix) (6:36)
10. Emanuel Satie, Tim Engelhardt, Maga, Sean Doron, Unfazed, Scenarios – A Gira (Extended Remix) (5:28)
11. DJ Koze – Spiralen (Original Mix) (7:17)
12. Joris Voorn, Taylr Renee & Roddy Lima – Machine (Original Mix) (5:02)
13. KREAM & Korolova – Annihilation (Extended Mix) (4:50)
14. Bedouin, Karl Williams, LUCH – The Only One (Original Mix) (6:49)
15. Massano & Silver Panda – Pa Ca (Original Mix) (4:47)
16. Moeaike, Arodes – Nobody Else (Extended Mix) (5:58)
17. RÜFÜS DU SOL – In the Moment (Adriatique Extended Remix) (5:12)
18. &ME, Black Coffee – The Rapture Pt.III (Original Mix) (8:02)
19. Liva K – Didn’t Miss You (Original Mix) (5:46)
20. Carlo Whale & TH;EN – Echoes of You (Original Mix) (5:22)
21. Magnus, JEWELS, YUMA., SOMMA, LE YORA – EVERYTHING IN ITS RIGHT PLACE (Original Mix) (7:14)
22. Andrea Oliva – Neon Hearts (Original Mix) (6:06)
23. Jackie Hollander – High On You (Extended Mix) (4:57)
24. KREAM, SCRIPT – Turn Up The Dose (Extended Mix) (4:20)
25. JOSEPH (CH) – B4U (Original Mix) (8:00)
26. Ajna (BE), Misha – Body Moving (Original Mix) (6:16)
27. Green Velvet & Harvard Bass – Lazer Beams (Adam Beyer & Massano Remix) (5:02)
28. Argy, Omiki & Lell Nahar – MANTRA (Extended Mix) (5:03)
29. Sasha & Franky Wah – Get On My Level (Original Mix) (8:00)
30. LUCH – UH HUH (Original Mix) (6:45)
31. Fahlberg – Feeling Good (Extended Mix) (6:49)
32. Ivory (IT) – Back Again (Original Mix) (5:42)
33. Massano, Y Do I – Lost My Mind (Original Mix) (5:23)
34. KOWSKI, Eric Reyes – Break Down! (Original Mix) (5:30)
35. Andrea Oliva – Spotlight (Original Mix) (6:21)
36. Tiga vs Meduza – You Gonna Want Me (I Know) (Extended Mix) (5:21)
37. Marsh – Beside (Extended Mix) (6:18)
38. Moderat, &ME, Rampa & Keinemusik – More Love (Rampa &ME Remix) (4:55)
39. Adriatique & Emmit Fenn – Closer (Extended Mix) (4:53)
40. Jimi Jules, Black Coffee – Trippy Yeah (Extended Mix) (6:40)
41. Liva K & OVEOUS – Blessings (Original Mix) (6:35)
42. Township Rebellion, Kasablanca – It’s Chemical (Original Mix) (6:28)
43. MAXI MERAKI, Sharing-DB.club – First Time (Original Mix) (5:19)
44. Pole Position – Touch Me (Hotlap Remix) (5:40)
45. Da Hool, Cassian & YOTTO – Love Parade (Extended Mix) (4:09)
46. Jody Wisternoff, Jamie McDermott – Citadel (Extended Mix) (5:17)
47. Super Flu, Dennis Louvra – Can’t Escape (Original Mix) (5:56)
48. Armin van Buuren, Argy, Marlo Rex – Like A Child (Extended Mix) (4:08)
49. Robert Babicz – The Wave (Original Mix) (5:08)
50. The Temper Trap – Sweet Disposition (John Summit & Silver Panda Extended Remix) (5:44)
51. Yotto, Tallac – Chemtrail Surfers (Extended Mix) (6:26)
52. HotLap – Waitin’ (Extended Mix) (6:35)
53. Miss Monique, Nicolas Taboada & FRANCO BA – Ghosts (Extended Mix) (5:28)
54. Picca & Mars & Moeaike – Move in Time (Original Mix) (5:39)
55. Hardt Antoine – Sing It Back (Extended Mix) (6:57)
56. Shakedown, Layton Giordani & Anyma (ofc) – At Night (Anyma x Layton Giordani Extended Remix) (4:35)
57. Acrobatik & whatlewisdoes – Lamazi (Extended Mix) (4:46)
58. Massano – Abraza (Original Mix) (5:13)
59. Alcatraz – Giv Me Luv (Jerome Isma-Ae Remix) (7:18)
60. Gespona – Komplex (Original Mix) (5:56)
61. Misha & Martim Rola – On Business (Extended Mix) (6:30)
62. Mind Against, Anakim & HUMAN404 – Late at Night (Extended Mix) (4:39)
63. Anyma (ofc), Argy & Son of Son – Voices In My Head (Extended Mix) (4:16)
64. Fred again.., Swedish House Mafia, Future – Turn On The Lights again.. (feat. Future) (Original Mix) (4:27)
65. Sezer Uysal & Axelax – Calling For More (Original Mix) (5:12)
66. LUCH, Marten Lou – Hideaway (Extended Version) (5:57)
67. Giuseppe Ottaviani, ARTBAT & Conor Ross – Break The Loop (Original Mix) (5:22)
68. Rhodes, CamelPhat, ARTBAT – For a Feeling (Adam Sellouk & Y do I Remix – Extended) (4:49)
69. Monolink, Artbat – Return to Oz (ARTBAT Remix) (8:00)
70. Lisa & Anyma (ofc) – Bad Angel (with Lisa) (Extended Mix) (4:08)
71. Florist (BE) – Unite (Original Mix) (6:25)
72. Moderat – Eating Hooks (Deep Dish Extended Remix) (7:13)
73. CamelPhat & Arodes – Cycles (Extended Mix) (6:52)
74. Interplanetary Criminal, Kettama – Yosemite (Extended) (6:02)
75. Leblanc (ofc) – Strings Anthem (Original Mix) (5:22)
76. Mind Against & Jos & Eli – Overture (Original Mix) (5:21)
77. Tripolism & Kingstarlight – Hurricane (Extended Mix) (4:49)
78. Luke Alexander – Last Night (feat. Y4MI) (Original Mix) (3:30)
79. Braxton – There’s Still Time (Extended Mix) (6:31)
80. Township Rebellion – Birds Fly First Class (Original Mix) (6:19)
81. Agents Of Time & Miss Monique – Rajada (Original Mix) (5:00)
82. Fancy Inc – Hypnotic (Extended Mix) (5:09)
83. Boys Noize, &ME, Rampa, Adam Port, Keinemusik, Vinson – Crazy For It (feat. Vinson) (Original Mix) (6:01)
84. Space Motion & Luca Belluco – Baila Conmigo (Original Mix) (4:38)
85. Tom Westy & Dansyn – Feel Your Soul (Extended) (4:33)
86. CamelPhat & Josh Gigante – The One (Extended Mix) (5:32)
87. Argy & Omnya – Aria (Extended Mix) (5:14)
88. Air & Tur, blou., Airsand, TuraniQa – Teach Me (Extended Mix) (4:35)
89. Dilby, Martin Ka – Moving Trough (Extened Mix) (6:16)
90. Pablo Say – Repique (Original Mix) (4:48)
91. Orsay & Kotiēr – 1886 (Original Mix) (5:46)
92. Adriatique & Elderbrook – Lost In The Woods (Extended Mix) (5:01)
93. Ajna (BE) & Sentin – Bringing The Noise (Original Mix) (5:45)
94. Eric Prydz – Pjanoo (Club Mix) (7:30)
95. Eelke Kleijn – Feel It, See It (Extended Mix) (6:07)
96. Aera – Litoral (Original Mix) (6:10)
97. Julian Koerndl – Horses (Original Mix) (6:09)
98. Supermode – Tell Me Why (MEDUZA Extended Remix) (8:28)
99. Laherte – Pump Up The Jam (Extended Mix) (3:58)
100. M.O.S. – Changes (Extended Mix) (6:43)
+91 -54
View File
@@ -1,98 +1,112 @@
# MusicDownload
Applicazione desktop per scaricare musica da Spotify, YouTube, SoundCloud e qualsiasi sorgente supportata da yt-dlp, con possibilita di migliorare la qualita audio dei file esistenti.
Applicazione desktop per scaricare **musica** da Spotify, YouTube e SoundCloud, e **video** da YouTube, TikTok, Instagram e Facebook. Include anche un upgrader di qualità per migliorare i file audio già scaricati.
UI moderna basata su **pywebview** (WebKit su macOS, EdgeChromium su Windows) — frontend HTML/CSS/JS, backend Python.
Sviluppata da **LuZa**.
## Funzionalita
Repo: <https://github.com/luzadev/musicdownload>
- **Download da Spotify** — Incolla un link a playlist, album o brano singolo: l'app risolve i brani via API Spotify e li scarica da YouTube in MP3
- **Download diretto** — Incolla un URL YouTube, SoundCloud o qualsiasi sorgente yt-dlp e scarica direttamente in MP3 (singoli video e playlist)
- **Upgrade qualita** — Scansiona una cartella di file audio e riscarica quelli a bassa qualita da YouTube
- **Aggiornamento app** — Controlla la disponibilita di nuove versioni direttamente dalle Impostazioni
- **Gestione impostazioni** — Credenziali Spotify, bitrate, soglia HQ, percorsi, tema
## Funzionalità
- **Scarica musica** — Incolla un link Spotify (playlist/album/brano), YouTube, SoundCloud o qualsiasi sorgente yt-dlp e scarica in MP3 a 320 kbps. Supporta liste di URL da file `.txt`.
- **Scarica video** — Incolla un link **YouTube · TikTok · Instagram · Facebook** (o qualsiasi sorgente yt-dlp) e scarica in MP4 alla qualità scelta (best / 1080p / 720p / 480p).
- **Upgrade qualità** — Scansiona una cartella di file audio e riscarica quelli sotto la soglia kbps impostata.
- **Auto-skip dei duplicati** — Tracking persistente (`.downloaded_tracks` / `.downloaded_videos`) + scan filename token-based: re-eseguire la stessa playlist salta i brani già presenti, anche se il filename ha "Official Video" / "Lyrics" / ecc.
- **Aggiornamento automatico** — Verifica disponibilità nuove versioni dalla pagina Impostazioni.
- **Lista multi-URL** — Carica un file `.txt` con un URL per riga per scaricare più playlist in sequenza.
## Requisiti
- Python 3.8+
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) installato nel sistema (`brew install yt-dlp` su macOS)
- [ffmpeg](https://ffmpeg.org/) installato nel sistema (`brew install ffmpeg` su macOS)
- Credenziali Spotify API (gratuite, vedi sotto) — necessarie solo per URL Spotify
- `ffmpeg` + `ffprobe` (solo per build/sviluppo locale: `brew install ffmpeg` su macOS)
- Credenziali Spotify API gratuite — solo se si usano URL Spotify
## Installazione
Nelle build distribuite (`.app` / `.exe`) **tutto è incluso** — non serve installare nulla.
## Installazione (sviluppo)
```bash
git clone https://github.com/luzadev/musicdownload.git
cd musicdownload
pip install -r requirements.txt
```
Dipendenze Python:
| Pacchetto | Utilizzo |
|---|---|
| `customtkinter` | GUI moderna basata su Tkinter |
| `requests` | Chiamate API Spotify |
| `yt-dlp` | Download audio da YouTube |
| `Pillow` | Gestione immagini (copertine) |
## Avvio
```bash
python3 main.py
```
Dipendenze principali:
| Pacchetto | Utilizzo |
|---|---|
| `pywebview` | Finestra nativa con WebView (WebKit / EdgeChromium) |
| `requests` | Chiamate API Spotify |
| `yt-dlp` | Download audio/video da YouTube e oltre 1000 siti |
| `pyobjc-framework-WebKit` (macOS) | Backend WebKit di pywebview |
| `pythonnet` (Windows) | Backend EdgeChromium di pywebview |
## Credenziali Spotify API
1. Vai su https://developer.spotify.com/dashboard e accedi (anche account gratuito)
Necessarie solo per scaricare da URL `open.spotify.com/...`.
1. Vai su <https://developer.spotify.com/dashboard> e accedi (anche account gratuito)
2. Clicca **Create app**
3. Compila: nome a piacere, Redirect URI `http://localhost:8888/callback`, seleziona **Web API**
4. Nella pagina dell'app, clicca **Settings**
4. Apri la app e clicca **Settings**
5. Copia **Client ID** e **Client Secret** nelle Impostazioni dell'app
Le credenziali sono gratuite e non richiedono Spotify Premium.
Le credenziali sono gratuite, non servono Spotify Premium.
## Cookies per contenuti privati
Per scaricare **Instagram / Facebook privati** o aggirare il rate-limit di YouTube serve un file `cookies.txt` (formato Netscape). Esportalo dal browser con un'estensione tipo "Get cookies.txt LOCALLY" e imposta il percorso in **Impostazioni → Percorsi**.
## Struttura progetto
```
MusicDownload/
├── main.py # Entry point
├── requirements.txt # Dipendenze Python
├── main.py # Entry point — bootstrap pywebview
├── requirements.txt
├── build_macos.py # Script build macOS (.app)
├── build_windows.py # Script build Windows (.exe)
├── .github/workflows/
│ └── build.yml # CI: build macOS + Windows + Release automatica su tag
│
├── core/ # Logica backend
├── core/ # Logica backend (riusabile, no UI)
│ ├── config.py # Configurazione persistente (JSON)
│ ├── paths.py # Ricerca binari (yt-dlp, ffmpeg)
│ ├── spotify_client.py # Autenticazione e API Spotify
│ ├── downloader.py # Download brani (Spotify via YouTube + URL diretti yt-dlp)
│ └── upgrader.py # Upgrade qualita file esistenti
│ ├── spotify_client.py # API Spotify (Client Credentials)
│ ├── downloader.py # Download audio (playlist Spotify, URL diretti, video)
│ └── upgrader.py # Upgrade qualità file esistenti
│
└── gui/ # Interfaccia grafica
├── app.py # Finestra principale con sidebar
├── download_tab.py # Pagina download
├── upgrade_tab.py # Pagina upgrade
└── settings_tab.py # Pagina impostazioni
├── api/
│ └── bridge.py # Ponte JS <-> Python (pywebview.js_api)
│
└── webui/ # Frontend
├── index.html # Single-page, 4 view (Scarica / Video / Upgrade / Impostazioni)
├── css/style.css # Tema scuro Spotify-like, gradient hero, animazioni
└── js/app.js # Routing, eventi, chiamate API
```
## Configurazione
Il file `config.json` viene creato automaticamente:
Il file `config.json` viene creato automaticamente al primo avvio:
| Chiave | Default | Descrizione |
|---|---|---|
| `client_id` | `""` | Spotify Client ID |
| `client_secret` | `""` | Spotify Client Secret |
| `bitrate` | `"320K"` | Bitrate download (128K/192K/256K/320K) |
| `bitrate` | `"320K"` | Bitrate download audio (128K/192K/256K/320K) |
| `hq_threshold` | `310` | Soglia kbps per considerare un file HQ |
| `cookies_path` | `"cookies.txt"` | Path al file cookies YouTube (opzionale) |
| `output_dir` | `"MUSICA/"` | Cartella output predefinita |
| `theme` | `"dark"` | Tema interfaccia (dark/light/system) |
| `cookies_path` | `""` | Path al file `cookies.txt` (opzionale) |
| `output_dir` | `""` | Cartella output predefinita |
| `theme` | `"dark"` | Tema interfaccia |
Posizione del file:
Posizione:
- **Sviluppo**: directory del progetto
- **macOS bundle**: `~/Library/Application Support/MusicDownload/`
- **Windows bundle**: `%APPDATA%/MusicDownload/`
## Build
## Build locale
### macOS
@@ -102,7 +116,7 @@ pip install pyinstaller
python3 build_macos.py
```
Produce `dist/MusicDownload.app` con yt-dlp, ffmpeg e ffprobe inclusi.
Produce `dist/MusicDownload.app` con yt-dlp, ffmpeg, ffprobe e tutte le dylib bundled (rpath patchati con `install_name_tool`).
### Windows
@@ -111,22 +125,45 @@ pip install pyinstaller
python build_windows.py
```
Produce `dist/MusicDownload/MusicDownload.exe` con tutti i binari inclusi.
Produce `dist/MusicDownload/MusicDownload.exe`. Lo script scarica automaticamente `yt-dlp.exe` e `ffmpeg.exe`/`ffprobe.exe` static build.
## Build & Release automatiche (GitHub Actions)
Ogni push di un tag `v*` triggera la pipeline `.github/workflows/build.yml`:
1. **`build-macos`** (runner `macos-latest`) → `MusicDownload-macOS.zip` (la `.app` zippata con `ditto`)
2. **`build-windows`** (runner `windows-latest`) → `MusicDownload-Windows.zip`
3. **`release`** → crea automaticamente una GitHub Release con entrambi gli asset allegati
Per pubblicare una nuova versione:
```bash
git tag v1.2.0
git push origin v1.2.0
```
Per fare solo una build di test (senza release):
```bash
gh workflow run "Build MusicDownload" --ref main
```
oppure dalla UI: **Actions → Build MusicDownload → Run workflow**.
Le release ufficiali sono disponibili su <https://github.com/luzadev/musicdownload/releases>.
## Aggiornamento app
L'app verifica la disponibilita di nuove versioni tramite un file `version.json` remoto. Il formato atteso:
L'app verifica nuove versioni interrogando un `version.json` remoto. URL configurato in `api/bridge.py` (`UPDATE_URL` nel metodo `check_update`):
```json
{
"version": "v1.1",
"download_url": "https://www.djluza.com/musicdownload/MusicDownload.dmg",
"notes": "Descrizione delle novita"
"version": "v1.2.0",
"download_url": "https://github.com/luzadev/musicdownload/releases/latest",
"notes": "Descrizione delle novità"
}
```
L'URL di controllo e configurabile nella costante `UPDATE_URL` in `gui/settings_tab.py`.
## Licenza
Uso personale.
+127 -2
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import json
import os
import re
import threading
import time
import webbrowser
@@ -49,6 +50,30 @@ def _js_safe(value: Any) -> str:
return json.dumps(value, ensure_ascii=False)
_NUM_PREFIX = re.compile(r"^\s*\d+\s*[\.\)\:\-]\s*")
_DURATION_TAIL = re.compile(r"\s*\(\s*\d{1,2}:\d{2}\s*\)\s*$")
# Tutti i tipi di trattini: hyphen, en-dash, em-dash, horizontal bar, minus
_DASH_SPLIT = re.compile(r"\s+[‐‑‒–—―−\-]\s+")
def _parse_track_line(line: str) -> Optional[dict]:
"""Parse 'N. Artista - Titolo (variant) (durata)' -> {name, artist}.
Ritorna None se la riga non e parseabile."""
s = line.strip()
if not s:
return None
s = _NUM_PREFIX.sub("", s)
s = _DURATION_TAIL.sub("", s).strip()
parts = _DASH_SPLIT.split(s, maxsplit=1)
if len(parts) < 2:
return None
artist = parts[0].strip()
name = parts[1].strip()
if not artist or not name:
return None
return {"name": name, "artist": artist}
class Api:
"""Espone i metodi a JavaScript tramite pywebview js_api."""
@@ -167,10 +192,29 @@ class Api:
return ""
def load_url_list(self, path: str) -> dict:
"""Carica un file .txt. Riconosce automaticamente se contiene URL
oppure una tracklist 'Artista - Titolo'."""
try:
with open(path, "r", encoding="utf-8") as f:
urls = [line.strip() for line in f if line.strip() and not line.strip().startswith("#")]
return {"ok": True, "urls": urls, "count": len(urls)}
lines = [line.strip() for line in f if line.strip() and not line.strip().startswith("#")]
if not lines:
return {"ok": True, "kind": "urls", "urls": [], "count": 0}
# Detect: se la prima riga e un URL trattiamo tutto come URL list
if lines[0].lower().startswith(("http://", "https://")):
return {"ok": True, "kind": "urls", "urls": lines, "count": len(lines)}
# Altrimenti parse come tracklist
tracks = []
for line in lines:
t = _parse_track_line(line)
if t:
tracks.append(t)
name = os.path.splitext(os.path.basename(path))[0]
return {
"ok": True, "kind": "tracks", "tracks": tracks,
"count": len(tracks), "name": name,
}
except Exception as e:
return {"ok": False, "error": str(e)}
@@ -215,11 +259,92 @@ class Api:
self._download_thread.start()
return {"ok": True}
def start_tracks_download(self, payload: dict) -> dict:
"""Avvia il download di una tracklist gia parsata
(lista di {name, artist}). Se 'subfolder' e presente,
scarica in output_dir/subfolder."""
if self._any_job_running():
return {"ok": False, "error": "Un download gia in corso"}
tracks = payload.get("tracks") or []
output_dir = (payload.get("output_dir") or "").strip()
subfolder = (payload.get("subfolder") or "").strip()
if not tracks:
return {"ok": False, "error": "Nessuna traccia fornita"}
if not output_dir:
return {"ok": False, "error": "Cartella output non impostata"}
if subfolder:
# Sanitizza: rimuovi separatori di path
safe = subfolder.replace("/", "_").replace("\\", "_").strip()
if safe:
output_dir = os.path.join(output_dir, safe)
self._download_thread = threading.Thread(
target=self._tracks_worker,
args=(list(tracks), output_dir),
daemon=True,
)
self._download_thread.start()
return {"ok": True}
def stop_download(self) -> dict:
request_download_stop()
self._log("download", "[INFO] Interruzione richiesta...")
return {"ok": True}
def _tracks_worker(self, tracks: list, output_dir: str) -> None:
reset_download_stop()
cfg = load_config()
bitrate = cfg.get("bitrate", "320K")
cookies_path = cfg.get("cookies_path", "")
view = "download"
self._log(view, f"[INFO] Tracklist: {len(tracks)} brani da cercare su YouTube")
self._log(view, f"[INFO] Destinazione: {output_dir}")
_last = [0.0]
_THROTTLE = 0.10
def progress_cb(idx, total, track_name, status, pct):
if status == "downloading":
now = time.monotonic()
if now - _last[0] < _THROTTLE:
return
_last[0] = now
payload_evt = {
"idx": idx, "total": total, "track": track_name,
"status": status, "pct": pct,
"url_idx": 0, "url_total": 1,
}
if status == "searching":
self._log(view, f"[CERCA] {track_name}")
elif status == "skipped":
self._log(view, f"[SKIP] {track_name} (gia scaricato)")
track_progress = (idx + 1) / total if total > 0 else 1
payload_evt["overall"] = min(track_progress, 1.0)
elif status == "downloading":
track_progress = (idx / total) + (pct / 100 / total) if total > 0 else 0
payload_evt["overall"] = min(track_progress, 1.0)
elif status == "done":
self._log(view, f"[OK] {track_name}")
track_progress = (idx + 1) / total if total > 0 else 1
payload_evt["overall"] = min(track_progress, 1.0)
elif status == "stopped":
self._log(view, "[INFO] Download interrotto.")
elif status == "completed":
payload_evt["overall"] = 1.0
self._log(view, "[INFO] Download completato!")
elif status.startswith("error"):
self._log(view, f"[ERRORE] {track_name}: {status}")
self._emit("download:progress", payload_evt)
download_playlist(tracks, output_dir, bitrate, cookies_path, progress_cb)
self._emit("download:done", {"ok": True})
def _download_worker(self, urls: list, output_dir: str) -> None:
reset_download_stop()
cfg = load_config()
+35 -19
View File
@@ -7,7 +7,7 @@ const $$ = (sel) => Array.from(document.querySelectorAll(sel));
const state = {
config: {},
urlList: [],
loaded: null, // { kind: "urls"|"tracks", urls?, tracks?, count }
dlOutputDir: "",
upDir: "",
videoOutputDir: "",
@@ -55,7 +55,7 @@ const bridgeHandlers = {
"download:done": () => {
state.downloading = false;
state.urlList = [];
state.loaded = null;
$("#urlListBadge").hidden = true;
$("#urlListBadge").textContent = "";
$("#downloadBtn").disabled = false;
@@ -221,27 +221,23 @@ $("#loadListBtn").addEventListener("click", async () => {
return;
}
if (!res.count) {
toast("Il file non contiene URL validi", "error");
toast("Il file non contiene voci valide", "error");
return;
}
state.urlList = res.urls;
state.loaded = res;
$("#urlInput").value = path;
const badge = $("#urlListBadge");
badge.textContent = `· ${res.count} URL caricati`;
if (res.kind === "tracks") {
badge.textContent = `· ${res.count} tracce`;
appendLog("download", `[INFO] Caricata tracklist: ${res.count} brani`);
} else {
badge.textContent = `· ${res.count} URL`;
appendLog("download", `[INFO] Caricati ${res.count} URL dal file`);
}
badge.hidden = false;
appendLog("download", `[INFO] Caricati ${res.count} URL dal file`);
});
$("#downloadBtn").addEventListener("click", async () => {
let urls = state.urlList;
if (!urls.length) {
const single = $("#urlInput").value.trim();
if (!single) {
toast("Inserisci un URL o carica una lista", "error");
return;
}
urls = [single];
}
if (!state.dlOutputDir) {
toast("Seleziona una cartella di destinazione", "error");
return;
@@ -254,10 +250,30 @@ $("#downloadBtn").addEventListener("click", async () => {
$("#dlProgressFill").style.width = "0%";
$("#dlPercent").textContent = "0%";
const res = await window.pywebview.api.start_download({
urls,
output_dir: state.dlOutputDir,
});
let res;
if (state.loaded && state.loaded.kind === "tracks") {
res = await window.pywebview.api.start_tracks_download({
tracks: state.loaded.tracks,
output_dir: state.dlOutputDir,
subfolder: state.loaded.name || "",
});
} else {
let urls = state.loaded && state.loaded.kind === "urls" ? state.loaded.urls : [];
if (!urls.length) {
const single = $("#urlInput").value.trim();
if (!single) {
toast("Inserisci un URL o carica una lista", "error");
bridgeHandlers["download:done"]();
return;
}
urls = [single];
}
res = await window.pywebview.api.start_download({
urls,
output_dir: state.dlOutputDir,
});
}
if (!res.ok) {
toast(res.error || "Errore", "error");
bridgeHandlers["download:done"]();