From 8c06a26ddbb16efdc9d0f46c9c9044f023bfcd39 Mon Sep 17 00:00:00 2001 From: luzadev Date: Sat, 30 May 2026 12:48:58 +0200 Subject: [PATCH] Initial commit: MusicDownload con UI pywebview - Frontend HTML/CSS/JS in webui/ (stile Spotify-like, hero gradient, modal blur) - API bridge JS<->Python in api/bridge.py - Core invariato: downloader yt-dlp, upgrader, spotify client, config - Build scripts macOS (WebKit) e Windows (EdgeChromium) - .gitignore esclude credenziali (config.json, cookies.txt) e artefatti build Co-Authored-By: Claude Opus 4.7 --- .github/workflows/build.yml | 59 +++ .gitignore | 36 ++ PlayList_2026.txt | 7 + PlayList_da_scaricare.txt | 18 + README.md | 132 ++++++ Vibes_List.txt | 5 + api/__init__.py | 0 api/bridge.py | 390 ++++++++++++++++ build_macos.py | 284 +++++++++++ build_windows.py | 179 +++++++ config.example.json | 9 + core/__init__.py | 0 core/config.py | 64 +++ core/downloader.py | 395 ++++++++++++++++ core/paths.py | 137 ++++++ core/spotify_client.py | 177 +++++++ core/upgrader.py | 359 ++++++++++++++ grafica.png | Bin 0 -> 168921 bytes main.py | 42 ++ requirements.txt | 7 + spotify_to_youtube_downloader.sh | 114 +++++ upgrade_quality.sh | 428 +++++++++++++++++ webui/css/style.css | 778 +++++++++++++++++++++++++++++++ webui/index.html | 291 ++++++++++++ webui/js/app.js | 405 ++++++++++++++++ 25 files changed, 4316 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 PlayList_2026.txt create mode 100644 PlayList_da_scaricare.txt create mode 100644 README.md create mode 100644 Vibes_List.txt create mode 100644 api/__init__.py create mode 100644 api/bridge.py create mode 100644 build_macos.py create mode 100644 build_windows.py create mode 100644 config.example.json create mode 100644 core/__init__.py create mode 100644 core/config.py create mode 100644 core/downloader.py create mode 100644 core/paths.py create mode 100644 core/spotify_client.py create mode 100644 core/upgrader.py create mode 100644 grafica.png create mode 100644 main.py create mode 100644 requirements.txt create mode 100755 spotify_to_youtube_downloader.sh create mode 100755 upgrade_quality.sh create mode 100644 webui/css/style.css create mode 100644 webui/index.html create mode 100644 webui/js/app.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..284c67e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,59 @@ +name: Build MusicDownload + +on: + workflow_dispatch: # Lancia manualmente da GitHub + push: + tags: + - 'v*' # Builda automaticamente sui tag (es. v1.0.0) + +jobs: + build-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install pyinstaller + + - name: Install ffmpeg + run: brew install ffmpeg + + - name: Build app + run: python build_macos.py + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: MusicDownload-macOS + path: dist/MusicDownload.app + compression-level: 9 + + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install pyinstaller + + - name: Build app + run: python build_windows.py + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: MusicDownload-Windows + path: dist/MusicDownload + compression-level: 9 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d68f2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.egg-info/ +.venv/ +venv/ + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp + +# PyInstaller +build/ +dist/ +*.spec +bundle_bin/ + +# App data — sensibili o specifici dell'utente +config.json +cookies.txt +MUSICA/ + +# Logs +*.log +.downloaded_tracks +.upgraded_tracks + +# Claude +.claude/ +Apri con claude-social.command diff --git a/PlayList_2026.txt b/PlayList_2026.txt new file mode 100644 index 0000000..39e5e95 --- /dev/null +++ b/PlayList_2026.txt @@ -0,0 +1,7 @@ +https://open.spotify.com/playlist/0Q4JvzeT0q3FLsj5fqffMV?si=d17c4002f6f349c4 +https://open.spotify.com/playlist/4FHKFYziOZF75AkT3aR7QP?si=b013d1cc79ff47a3 +https://open.spotify.com/playlist/0mpX2EakDLmFjifuh1fQKU?si=bab58f6a7e984560 +https://open.spotify.com/playlist/6NW6XlmE10Xk5PLUw9cOaS?si=705e098d59e049fe +https://open.spotify.com/playlist/6NW6XlmE10Xk5PLUw9cOaS?si=705e098d59e049fe +https://open.spotify.com/playlist/4MmZgwwHgrSs2astI4NVxI?si=021eef1a758a42fc +https://open.spotify.com/playlist/4OAx11L58sozuyNVTBi6r4?si=27d4be41e53c4e8a \ No newline at end of file diff --git a/PlayList_da_scaricare.txt b/PlayList_da_scaricare.txt new file mode 100644 index 0000000..8065bcb --- /dev/null +++ b/PlayList_da_scaricare.txt @@ -0,0 +1,18 @@ +https://open.spotify.com/playlist/5HVNSaqs3HgzIsaCVeVGrI?si=dd86ade90d2347ae +https://open.spotify.com/playlist/7ptoSkENfnO1yEQYrly725?si=4d694c3390a54ce6 +https://open.spotify.com/playlist/6VjXyFH9Z5HlGPAjRBKR32?si=e8fc98ee593e4e69 +https://open.spotify.com/playlist/4rdb0iJ7hZhqgBFpXHGVU6?si=96a6838af2a3487f +https://open.spotify.com/playlist/51ireDdqSrt6juqBxkMsrm?si=6c06fc88e7354b50 +https://open.spotify.com/playlist/50SNkybvZnubt5uK4F2kgk?si=b7a146e4d8a54d35 +https://open.spotify.com/playlist/0erLbMzXD0U9xJUPlOeKrX?si=54c87940c7be4c65 +https://open.spotify.com/playlist/3FGGBSXjLGU5kfPCxL9K6f?si=77e1eee04a0745aa +https://open.spotify.com/playlist/2OxJrcgA6AQGk1sSQ8ZZ2a?si=98ede53357124322 +https://open.spotify.com/playlist/345eEKyGNyluygzMsS1n1H?si=9d4be71b28e94339 +https://open.spotify.com/playlist/0Umelkk6D75eZHZuxkTtLF?si=7c2f1ff3d2354abb +https://open.spotify.com/playlist/2k9QmVpMHJfyjVVvJdnRLb?si=765f62a3caf843ef +https://open.spotify.com/playlist/7xVYvU65ezCBiOu49BSV4I?si=608741585f91483d +https://open.spotify.com/playlist/1AOMaSEZf0aChEOK6L5uI3?si=e44863bf2782419b +https://open.spotify.com/playlist/3Zn4AOuMAKFCJD4VLUD0uY?si=2179f9cb5e5b4092 +https://open.spotify.com/playlist/1LA5WDfX5Z4I0fQSIx4L8N?si=9918a8eadb5744b9 +https://open.spotify.com/playlist/0NHDUXVEsoFMwzkrYaRHG0?si=58e61fe6319e48cb +https://open.spotify.com/playlist/2Y6PQB7k73HCrBFwiBy2Jf?si=8da4990576cd444f \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d87293b --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# 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. + +Sviluppata da **LuZa**. + +## Funzionalita + +- **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 + +## 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 + +## Installazione + +```bash +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 +``` + +## Credenziali Spotify API + +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** +5. Copia **Client ID** e **Client Secret** nelle Impostazioni dell'app + +Le credenziali sono gratuite e non richiedono Spotify Premium. + +## Struttura progetto + +``` +MusicDownload/ +├── main.py # Entry point +├── requirements.txt # Dipendenze Python +├── build_macos.py # Script build macOS (.app) +├── build_windows.py # Script build Windows (.exe) +│ +├── core/ # Logica backend +│ ├── 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 +│ +└── gui/ # Interfaccia grafica + ├── app.py # Finestra principale con sidebar + ├── download_tab.py # Pagina download + ├── upgrade_tab.py # Pagina upgrade + └── settings_tab.py # Pagina impostazioni +``` + +## Configurazione + +Il file `config.json` viene creato automaticamente: + +| Chiave | Default | Descrizione | +|---|---|---| +| `client_id` | `""` | Spotify Client ID | +| `client_secret` | `""` | Spotify Client Secret | +| `bitrate` | `"320K"` | Bitrate download (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) | + +Posizione del file: +- **Sviluppo**: directory del progetto +- **macOS bundle**: `~/Library/Application Support/MusicDownload/` +- **Windows bundle**: `%APPDATA%/MusicDownload/` + +## Build + +### macOS + +```bash +brew install ffmpeg +pip install pyinstaller +python3 build_macos.py +``` + +Produce `dist/MusicDownload.app` con yt-dlp, ffmpeg e ffprobe inclusi. + +### Windows + +```bash +pip install pyinstaller +python build_windows.py +``` + +Produce `dist/MusicDownload/MusicDownload.exe` con tutti i binari inclusi. + +## Aggiornamento app + +L'app verifica la disponibilita di nuove versioni tramite un file `version.json` remoto. Il formato atteso: + +```json +{ + "version": "v1.1", + "download_url": "https://www.djluza.com/musicdownload/MusicDownload.dmg", + "notes": "Descrizione delle novita" +} +``` + +L'URL di controllo e configurabile nella costante `UPDATE_URL` in `gui/settings_tab.py`. + +## Licenza + +Uso personale. diff --git a/Vibes_List.txt b/Vibes_List.txt new file mode 100644 index 0000000..6d5f0ef --- /dev/null +++ b/Vibes_List.txt @@ -0,0 +1,5 @@ +https://open.spotify.com/intl-it/album/3lgCqbQpmLscSRuWVMT5wS?si=UPiqK8smRgO_1JAAiZTrYg +https://open.spotify.com/intl-it/album/3pGb3m5KA1Y3gCVT6leCL3?si=Cj_OrTE5QAiEnwMD63EunA +https://open.spotify.com/intl-it/album/2ewhdpODXqoB0fXIKfq4u2?si=e2N7jWHiTWGiudHHMJNHHQ +https://open.spotify.com/intl-it/album/2xGfPfmx6faZUr9IaKEiSG?si=2Q-qMwWCTp-Yp7RIfhE2Ig +https://open.spotify.com/intl-it/album/43pKffJweYUVwEYcHPIfox?si=2E8IoUmOQp-cgsCujVBPMQ \ No newline at end of file diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/bridge.py b/api/bridge.py new file mode 100644 index 0000000..13f1b56 --- /dev/null +++ b/api/bridge.py @@ -0,0 +1,390 @@ +"""Ponte JS <-> Python: tutti i metodi che la UI invoca via pywebview.api.""" + +from __future__ import annotations + +import json +import os +import threading +import time +import webbrowser +from typing import Any, Optional + +from core.config import load_config, save_config, VERSION +from core.downloader import ( + download_playlist, + download_direct_url, + request_stop as request_download_stop, + reset_stop as reset_download_stop, + is_stopped as download_is_stopped, +) +from core.spotify_client import get_access_token, resolve_spotify_url +from core.upgrader import ( + upgrade_folder, + request_stop as request_upgrade_stop, + count_files_info, +) + + +SPOTIFY_GUIDE_TEXT = """\ +1) Vai su https://developer.spotify.com/dashboard e accedi (anche con account free). +2) Clicca "Create app". +3) Compila: + - App name: MusicDownload (o quello che vuoi) + - Description: a piacere + - Redirect URI: http://localhost:8888/callback (obbligatorio ma non usato) + - Seleziona "Web API" + - Accetta i termini, "Save" +4) Apri la app appena creata > "Settings" e copia: + - Client ID + - Client Secret (clicca "View client secret") +5) Incollali qui sotto e premi "Salva". + +Note: credenziali gratuite, no Premium. Limite 100 richieste/minuto. +""" + + +def _js_safe(value: Any) -> str: + """Serializza un valore per inserirlo in evaluate_js.""" + return json.dumps(value, ensure_ascii=False) + + +class Api: + """Espone i metodi a JavaScript tramite pywebview js_api.""" + + def __init__(self): + self.window = None # impostato dopo create_window + self._download_thread: Optional[threading.Thread] = None + self._upgrade_thread: Optional[threading.Thread] = None + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + def _emit(self, channel: str, payload: dict | str | None = None) -> None: + """Invia un evento al frontend.""" + if not self.window: + return + try: + self.window.evaluate_js( + f"window.bridge && window.bridge.emit({_js_safe(channel)}, {_js_safe(payload)});" + ) + except Exception: + pass + + def _log(self, view: str, msg: str) -> None: + self._emit("log", {"view": view, "msg": msg}) + + # ------------------------------------------------------------------ + # Bootstrap + # ------------------------------------------------------------------ + def get_init_data(self) -> dict: + """Chiamato dal frontend all'avvio per popolare lo stato iniziale.""" + cfg = load_config() + return { + "version": VERSION, + "config": cfg, + "spotify_guide": SPOTIFY_GUIDE_TEXT, + } + + # ------------------------------------------------------------------ + # Settings + # ------------------------------------------------------------------ + def save_settings(self, payload: dict) -> dict: + try: + threshold = int(payload.get("hq_threshold", 310)) + except (TypeError, ValueError): + threshold = 310 + + config = { + "client_id": (payload.get("client_id") or "").strip(), + "client_secret": (payload.get("client_secret") or "").strip(), + "bitrate": payload.get("bitrate", "320K"), + "hq_threshold": threshold, + "cookies_path": (payload.get("cookies_path") or "").strip(), + "output_dir": (payload.get("output_dir") or "").strip(), + "theme": payload.get("theme", "dark"), + } + save_config(config) + return {"ok": True} + + def check_update(self) -> dict: + """Controlla aggiornamenti chiamando version.json remoto.""" + import urllib.request + + UPDATE_URL = "https://www.djluza.com/musicdownload/version.json" + try: + req = urllib.request.Request(UPDATE_URL, headers={"User-Agent": "MusicDownload"}) + with urllib.request.urlopen(req, timeout=10) as resp: + data = json.loads(resp.read().decode()) + remote = data.get("version", "") + return { + "ok": True, + "current": VERSION, + "remote": remote, + "is_new": bool(remote and remote != VERSION), + "download_url": data.get("download_url", ""), + "notes": data.get("notes", ""), + } + except Exception as e: + return {"ok": False, "error": str(e), "current": VERSION} + + # ------------------------------------------------------------------ + # File dialogs (delegati a pywebview) + # ------------------------------------------------------------------ + def _folder_dialog_type(self): + import webview + # API nuova (>= 5.x): webview.FileDialog.FOLDER + # API vecchia: webview.FOLDER_DIALOG + if hasattr(webview, "FileDialog"): + return webview.FileDialog.FOLDER + return webview.FOLDER_DIALOG + + def _open_dialog_type(self): + import webview + if hasattr(webview, "FileDialog"): + return webview.FileDialog.OPEN + return webview.OPEN_DIALOG + + def browse_directory(self) -> str: + if not self.window: + return "" + result = self.window.create_file_dialog(self._folder_dialog_type()) + if result: + return result[0] if isinstance(result, (list, tuple)) else result + return "" + + def browse_file(self, filetypes: Optional[list] = None) -> str: + if not self.window: + return "" + if filetypes: + ft = tuple(filetypes) + else: + ft = ("Text files (*.txt)", "All files (*.*)") + result = self.window.create_file_dialog(self._open_dialog_type(), file_types=ft) + if result: + return result[0] if isinstance(result, (list, tuple)) else result + return "" + + def load_url_list(self, path: str) -> dict: + 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)} + except Exception as e: + return {"ok": False, "error": str(e)} + + def open_external_url(self, url: str) -> None: + webbrowser.open(url) + + # ------------------------------------------------------------------ + # Audio folder scan (Upgrade tab) + # ------------------------------------------------------------------ + def scan_audio_folder(self, directory: str, recursive: bool) -> dict: + try: + total, done = count_files_info(directory, bool(recursive)) + return {"ok": True, "total": total, "done": done, "remaining": total - done} + except Exception as e: + return {"ok": False, "error": str(e)} + + # ------------------------------------------------------------------ + # DOWNLOAD + # ------------------------------------------------------------------ + def start_download(self, payload: dict) -> dict: + if self._download_thread and self._download_thread.is_alive(): + return {"ok": False, "error": "Download gia in corso"} + + urls = payload.get("urls") or [] + output_dir = (payload.get("output_dir") or "").strip() + if not urls: + return {"ok": False, "error": "Nessun URL fornito"} + if not output_dir: + return {"ok": False, "error": "Cartella output non impostata"} + + self._download_thread = threading.Thread( + target=self._download_worker, + args=(list(urls), 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 _download_worker(self, urls: list, output_dir: str) -> None: + reset_download_stop() + cfg = load_config() + bitrate = cfg.get("bitrate", "320K") + cookies_path = cfg.get("cookies_path", "") + total_urls = len(urls) + multi = total_urls > 1 + view = "download" + + token = None + has_spotify = any("spotify.com" in u for u in urls) + if has_spotify: + client_id = cfg.get("client_id", "") + client_secret = cfg.get("client_secret", "") + if not client_id or not client_secret: + self._log(view, "[ERRORE] Configura Client ID e Client Secret nelle Impostazioni.") + self._emit("download:done", {"ok": False}) + return + self._log(view, "[INFO] Autenticazione Spotify...") + try: + token = get_access_token(client_id, client_secret) + except Exception as e: + self._log(view, f"[ERRORE] Autenticazione fallita: {e}") + self._emit("download:done", {"ok": False}) + return + + for url_idx, url in enumerate(urls): + if download_is_stopped(): + self._log(view, "[INFO] Download interrotto.") + break + + if multi: + self._log(view, f"\n{'='*50}") + self._log(view, f"[LISTA] URL {url_idx + 1} di {total_urls}") + self._log(view, f"{'='*50}") + + is_spotify = "spotify.com" in url + + _last = [0.0] + _THROTTLE = 0.10 + url_base = url_idx / total_urls if multi else 0 + url_weight = 1 / total_urls if multi else 1 + + def progress_cb(idx, total, track_name, status, pct, + _base=url_base, _weight=url_weight, _uidx=url_idx): + 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": _uidx, "url_total": total_urls, + } + + 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(_base + track_progress * _weight, 1.0) + elif status == "downloading": + track_progress = (idx / total) + (pct / 100 / total) if total > 0 else 0 + payload_evt["overall"] = min(_base + track_progress * _weight, 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(_base + track_progress * _weight, 1.0) + elif status == "stopped": + self._log(view, "[INFO] Download interrotto.") + elif status == "completed": + if not multi: + 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) + + if is_spotify: + self._log(view, "[INFO] Recupero informazioni da Spotify...") + try: + label, name, tracks = resolve_spotify_url(token, url) + except Exception as e: + self._log(view, f"[ERRORE] {e}") + continue + self._log(view, f"[INFO] {label}: {name} ({len(tracks)} brani)") + dest_dir = os.path.join(output_dir, name) + download_playlist(tracks, dest_dir, bitrate, cookies_path, progress_cb) + else: + self._log(view, f"[INFO] Download diretto via yt-dlp: {url}") + download_direct_url(url, output_dir, bitrate, cookies_path, progress_cb) + + if multi and not download_is_stopped(): + self._log(view, f"\n[INFO] Completate tutte le {total_urls} playlist/URL!") + self._emit("download:progress", { + "status": "completed", "overall": 1.0, + "url_total": total_urls, + }) + + self._emit("download:done", {"ok": True}) + + # ------------------------------------------------------------------ + # UPGRADE + # ------------------------------------------------------------------ + def start_upgrade(self, payload: dict) -> dict: + if self._upgrade_thread and self._upgrade_thread.is_alive(): + return {"ok": False, "error": "Upgrade gia in corso"} + + directory = (payload.get("directory") or "").strip() + recursive = bool(payload.get("recursive", False)) + try: + threshold = int(payload.get("threshold", 310)) + except (TypeError, ValueError): + threshold = 310 + + if not directory: + return {"ok": False, "error": "Cartella non impostata"} + + cfg = load_config() + cookies_path = cfg.get("cookies_path", "") + + self._upgrade_thread = threading.Thread( + target=self._upgrade_worker, + args=(directory, threshold, cookies_path, recursive), + daemon=True, + ) + self._upgrade_thread.start() + return {"ok": True} + + def stop_upgrade(self) -> dict: + request_upgrade_stop() + self._log("upgrade", "[INFO] Interruzione richiesta...") + return {"ok": True} + + def _upgrade_worker(self, directory, threshold, cookies_path, recursive): + view = "upgrade" + + def progress_cb(idx, total, filename, status, old_kbps, new_kbps): + payload_evt = { + "idx": idx, "total": total, "filename": filename, + "status": status, "old_kbps": old_kbps, "new_kbps": new_kbps, + } + if total > 0: + payload_evt["overall"] = min(idx / total, 1.0) + + if status == "searching": + self._log(view, f"[CERCA] {filename} ({old_kbps}kbps)") + elif status == "not_found": + self._log(view, f"[NON TROVATO] {filename}") + elif status == "cover_only": + self._log(view, f"[COPERTINA] {filename} (gia {old_kbps}kbps)") + elif status == "cover_done": + self._log(view, f"[OK COPERTINA] {filename}") + elif status == "cover_failed": + self._log(view, f"[SKIP] {filename} - copertina non disponibile ({old_kbps}kbps ok)") + elif status == "downloading": + self._log(view, f"[DOWNLOAD] {filename} ({old_kbps}kbps)") + elif status == "upgraded": + diff = new_kbps - old_kbps + diff_str = f" (+{diff}kbps)" if diff > 0 else "" + self._log(view, f"[UPGRADE] {filename}: {old_kbps} -> {new_kbps}kbps{diff_str}") + elif status == "download_error": + self._log(view, f"[ERRORE] {filename}: download fallito") + elif status == "stopped": + self._log(view, "[INFO] Upgrade interrotto.") + elif status == "completed": + payload_evt["overall"] = 1.0 + self._log(view, "[INFO] Upgrade completato!") + + self._emit("upgrade:progress", payload_evt) + + upgrade_folder(directory, threshold, cookies_path, recursive, progress_cb) + self._emit("upgrade:done", {"ok": True}) diff --git a/build_macos.py b/build_macos.py new file mode 100644 index 0000000..3d8c00d --- /dev/null +++ b/build_macos.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +""" +Build script per creare MusicDownload.app su macOS. + +Uso: + python3 build_macos.py + +Cosa fa: +1. Scarica il binario standalone di yt-dlp per macOS +2. Raccoglie ffmpeg + ffprobe + tutte le loro dylib da Homebrew +3. Esegue PyInstaller per creare MusicDownload.app + +Requisiti: + pip3 install pyinstaller + brew install ffmpeg +""" + +import os +import platform +import re +import shutil +import stat +import subprocess +import sys +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +BUILD_DIR = ROOT / "build" +DIST_DIR = ROOT / "dist" +BUNDLE_DIR = ROOT / "bundle_bin" # binari da includere nell'app + + +def log(msg): + print(f"\n>>> {msg}") + + +# ========================================================================= +# 1. Scarica yt-dlp standalone +# ========================================================================= +def download_ytdlp(): + """Scarica il binario standalone di yt-dlp per macOS.""" + arch = platform.machine() # arm64 o x86_64 + if arch == "arm64": + url = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos" + else: + url = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos_legacy" + + dest = BUNDLE_DIR / "yt-dlp" + if dest.exists(): + log(f"yt-dlp gia presente: {dest}") + return dest + + log(f"Scarico yt-dlp da {url} ...") + BUNDLE_DIR.mkdir(parents=True, exist_ok=True) + + # Usa curl perche urllib di Python 3.8 ha problemi SSL su macOS + subprocess.run( + ["curl", "-L", "-o", str(dest), url], + check=True, + ) + dest.chmod(dest.stat().st_mode | stat.S_IEXEC) + log(f"yt-dlp scaricato: {dest}") + return dest + + +# ========================================================================= +# 2. Raccogli ffmpeg/ffprobe + dylib +# ========================================================================= +def find_brew_binary(name): + """Trova il percorso reale di un binario brew.""" + path = shutil.which(name, path="/opt/homebrew/bin:/usr/local/bin") + if not path: + path = shutil.which(name) + if not path: + print(f"ERRORE: {name} non trovato. Installa con: brew install {name}") + sys.exit(1) + return str(Path(path).resolve()) + + +def collect_dylibs(binary_path, collected=None): + """Raccoglie ricorsivamente tutte le dylib linkate da un binario.""" + if collected is None: + collected = set() + + result = subprocess.run( + ["otool", "-L", binary_path], + capture_output=True, text=True, + ) + + for line in result.stdout.splitlines()[1:]: # skip prima riga (nome binario) + line = line.strip() + match = re.match(r"(.+\.dylib)\s+\(", line) + if not match: + continue + dylib = match.group(1).strip() + # Includi solo librerie homebrew (non di sistema) + if dylib.startswith("/opt/homebrew/") or dylib.startswith("/usr/local/"): + real = str(Path(dylib).resolve()) + if real not in collected and os.path.exists(real): + collected.add(real) + # Raccogli ricorsivamente + collect_dylibs(real, collected) + + return collected + + +def bundle_ffmpeg(): + """Copia ffmpeg, ffprobe e tutte le loro dylib in bundle_bin/.""" + BUNDLE_DIR.mkdir(parents=True, exist_ok=True) + + ffmpeg_path = find_brew_binary("ffmpeg") + ffprobe_path = find_brew_binary("ffprobe") + + log("Raccolgo librerie dinamiche di ffmpeg/ffprobe...") + all_dylibs = set() + collect_dylibs(ffmpeg_path, all_dylibs) + collect_dylibs(ffprobe_path, all_dylibs) + + log(f"Trovate {len(all_dylibs)} dylib") + + # Copia binari + for src in (ffmpeg_path, ffprobe_path): + dst = BUNDLE_DIR / Path(src).name + shutil.copy2(src, dst) + dst.chmod(dst.stat().st_mode | stat.S_IEXEC) + log(f" Copiato: {Path(src).name}") + + # Copia dylib + lib_dir = BUNDLE_DIR / "lib" + lib_dir.mkdir(exist_ok=True) + + for dylib in sorted(all_dylibs): + dst = lib_dir / Path(dylib).name + shutil.copy2(dylib, dst) + + log(f" Copiate {len(all_dylibs)} librerie in bundle_bin/lib/") + + # Fix rpath nei binari: cambia i riferimenti /opt/homebrew/... a @executable_path/lib/ + log("Fix rpath nei binari...") + for binary_name in ("ffmpeg", "ffprobe"): + binary = BUNDLE_DIR / binary_name + _fix_rpaths(binary, all_dylibs) + + # Fix rpath nelle dylib stesse + for dylib in sorted(all_dylibs): + dst = lib_dir / Path(dylib).name + _fix_rpaths(dst, all_dylibs, is_dylib=True) + + log("ffmpeg/ffprobe bundled con successo") + + +def _fix_rpaths(binary, all_dylibs, is_dylib=False): + """Sostituisce i path assoluti delle dylib con path relativi.""" + prefix = "@executable_path/lib/" if not is_dylib else "@loader_path/" + + # Se e una dylib, fix anche il suo install name + if is_dylib: + subprocess.run( + ["install_name_tool", "-id", + f"@loader_path/{binary.name}", str(binary)], + capture_output=True, + ) + + for dylib in all_dylibs: + dylib_name = Path(dylib).name + subprocess.run( + ["install_name_tool", "-change", + dylib, f"{prefix}{dylib_name}", str(binary)], + capture_output=True, + ) + # Prova anche con path non-resolved (symlink) + for brew_prefix in ("/opt/homebrew", "/usr/local"): + for alt in (f"{brew_prefix}/opt/", f"{brew_prefix}/lib/", f"{brew_prefix}/Cellar/"): + result = subprocess.run( + ["otool", "-L", str(binary)], + capture_output=True, text=True, + ) + for line in result.stdout.splitlines(): + line = line.strip() + if dylib_name in line and (alt in line): + old_path = re.match(r"(.+\.dylib)", line) + if old_path: + subprocess.run( + ["install_name_tool", "-change", + old_path.group(1).strip(), + f"{prefix}{dylib_name}", str(binary)], + capture_output=True, + ) + + +# ========================================================================= +# 3. PyInstaller +# ========================================================================= +def run_pyinstaller(): + """Esegue PyInstaller per creare l'app.""" + log("Eseguo PyInstaller...") + + # Genera lista --add-binary per i binari bundled + add_binaries = [] + for f in BUNDLE_DIR.iterdir(): + if f.is_file() and not f.name.startswith("."): + add_binaries.extend(["--add-binary", f"{f}:."]) + # Dylibs in lib/ + lib_dir = BUNDLE_DIR / "lib" + if lib_dir.exists(): + for f in lib_dir.iterdir(): + if f.is_file(): + add_binaries.extend(["--add-binary", f"{f}:lib"]) + + cmd = [ + sys.executable, "-m", "PyInstaller", + "--name", "MusicDownload", + "--windowed", + "--onedir", + "--noconfirm", + # Web UI assets (HTML/CSS/JS) — usati da pywebview + "--add-data", f"{ROOT / 'webui'}:webui", + # Hidden imports per pywebview (WebKit su macOS) + "--hidden-import", "webview", + "--hidden-import", "webview.platforms.cocoa", + "--hidden-import", "requests", + # Binari bundled (yt-dlp, ffmpeg, ffprobe + dylibs) + *add_binaries, + # Entry point + str(ROOT / "main.py"), + ] + + log(f"Comando: {' '.join(cmd[:10])}...") + subprocess.run(cmd, check=True, cwd=str(ROOT)) + + app_path = DIST_DIR / "MusicDownload.app" + if app_path.exists(): + log(f"Build completata: {app_path}") + # Mostra dimensione + size = sum(f.stat().st_size for f in app_path.rglob("*") if f.is_file()) + log(f"Dimensione: {size / 1024 / 1024:.0f} MB") + else: + log("ATTENZIONE: .app non trovata, controlla l'output di PyInstaller") + # Potrebbe essere in dist/MusicDownload/ (non .app) + alt = DIST_DIR / "MusicDownload" + if alt.exists(): + log(f"Trovata directory: {alt}") + + +# ========================================================================= +# Main +# ========================================================================= +def main(): + print("=" * 50) + print(" MusicDownload — Build macOS") + print("=" * 50) + + # Verifica pyinstaller + try: + import PyInstaller + log(f"PyInstaller {PyInstaller.__version__}") + except ImportError: + log("Installo PyInstaller...") + subprocess.run([sys.executable, "-m", "pip", "install", "pyinstaller"], + check=True) + + # Pulisci bundle precedente + if BUNDLE_DIR.exists(): + shutil.rmtree(BUNDLE_DIR) + + download_ytdlp() + bundle_ffmpeg() + run_pyinstaller() + + print("\n" + "=" * 50) + print(" DONE!") + print("=" * 50) + app = DIST_DIR / "MusicDownload.app" + if app.exists(): + print(f"\n L'app e in: {app}") + print(f" Per testarla: open '{app}'") + print(f"\n Per distribuirla, comprimi la cartella:") + print(f" zip -r MusicDownload.zip dist/MusicDownload.app") + print() + + +if __name__ == "__main__": + main() diff --git a/build_windows.py b/build_windows.py new file mode 100644 index 0000000..a032605 --- /dev/null +++ b/build_windows.py @@ -0,0 +1,179 @@ +""" +Build script per creare MusicDownload.exe su Windows. + +Uso: + python build_windows.py + +Cosa fa: +1. Scarica il binario standalone di yt-dlp per Windows +2. Scarica ffmpeg static build per Windows +3. Esegue PyInstaller per creare MusicDownload.exe + +Requisiti: + pip install pyinstaller + pip install -r requirements.txt +""" + +import io +import os +import shutil +import stat +import subprocess +import sys +import urllib.request +import zipfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +BUILD_DIR = ROOT / "build" +DIST_DIR = ROOT / "dist" +BUNDLE_DIR = ROOT / "bundle_bin" + +YTDLP_URL = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe" +FFMPEG_URL = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip" + + +def log(msg): + print(f"\n>>> {msg}") + + +# ========================================================================= +# 1. Scarica yt-dlp.exe +# ========================================================================= +def download_ytdlp(): + dest = BUNDLE_DIR / "yt-dlp.exe" + if dest.exists(): + log(f"yt-dlp.exe gia presente: {dest}") + return dest + + log(f"Scarico yt-dlp.exe ...") + BUNDLE_DIR.mkdir(parents=True, exist_ok=True) + urllib.request.urlretrieve(YTDLP_URL, dest) + log(f"yt-dlp.exe scaricato ({dest.stat().st_size // 1024 // 1024} MB)") + return dest + + +# ========================================================================= +# 2. Scarica ffmpeg static build +# ========================================================================= +def download_ffmpeg(): + ffmpeg_exe = BUNDLE_DIR / "ffmpeg.exe" + ffprobe_exe = BUNDLE_DIR / "ffprobe.exe" + + if ffmpeg_exe.exists() and ffprobe_exe.exists(): + log("ffmpeg.exe e ffprobe.exe gia presenti") + return + + log("Scarico ffmpeg static build per Windows ...") + BUNDLE_DIR.mkdir(parents=True, exist_ok=True) + + # Scarica lo zip in memoria + response = urllib.request.urlopen(FFMPEG_URL) + zip_data = io.BytesIO(response.read()) + log("Download completato, estraggo ffmpeg.exe e ffprobe.exe ...") + + with zipfile.ZipFile(zip_data) as zf: + for member in zf.namelist(): + basename = Path(member).name + if basename in ("ffmpeg.exe", "ffprobe.exe"): + # Estrai solo il file, senza sottocartelle + data = zf.read(member) + dest = BUNDLE_DIR / basename + dest.write_bytes(data) + log(f" Estratto: {basename} ({len(data) // 1024 // 1024} MB)") + + if not ffmpeg_exe.exists(): + print("ERRORE: ffmpeg.exe non trovato nello zip!") + sys.exit(1) + + +# ========================================================================= +# 3. PyInstaller +# ========================================================================= +def run_pyinstaller(): + log("Eseguo PyInstaller...") + + # Genera lista --add-binary per i binari bundled + add_binaries = [] + for f in BUNDLE_DIR.iterdir(): + if f.is_file() and f.suffix in (".exe", ""): + add_binaries.extend(["--add-binary", f"{f};."]) + + cmd = [ + sys.executable, "-m", "PyInstaller", + "--name", "MusicDownload", + "--windowed", + "--onedir", + "--noconfirm", + # Icona (se presente) + # "--icon", str(ROOT / "icon.ico"), + # Web UI assets (HTML/CSS/JS) — usati da pywebview + "--add-data", f"{ROOT / 'webui'};webui", + # Hidden imports per pywebview (EdgeChromium su Windows) + "--hidden-import", "webview", + "--hidden-import", "webview.platforms.edgechromium", + "--hidden-import", "clr", + "--hidden-import", "requests", + # Collegamenti completi alle DLL .NET di pywebview/pythonnet + "--collect-all", "webview", + # Binari bundled + *add_binaries, + # Entry point + str(ROOT / "main.py"), + ] + + log(f"Comando: {' '.join(cmd[:10])}...") + subprocess.run(cmd, check=True, cwd=str(ROOT)) + + exe_path = DIST_DIR / "MusicDownload" / "MusicDownload.exe" + if exe_path.exists(): + log(f"Build completata: {exe_path}") + else: + log("ATTENZIONE: MusicDownload.exe non trovato, controlla l'output") + + +# ========================================================================= +# Main +# ========================================================================= +def main(): + print("=" * 50) + print(" MusicDownload - Build Windows") + print("=" * 50) + + if sys.platform != "win32": + print("\nATTENZIONE: Questo script va eseguito su Windows!") + print("PyInstaller non supporta cross-compilation.") + print("Copia il progetto su una macchina Windows e rilancia.") + sys.exit(1) + + # Verifica pyinstaller + try: + import PyInstaller + log(f"PyInstaller {PyInstaller.__version__}") + except ImportError: + log("Installo PyInstaller...") + subprocess.run([sys.executable, "-m", "pip", "install", "pyinstaller"], + check=True) + + # Pulisci bundle precedente + if BUNDLE_DIR.exists(): + shutil.rmtree(BUNDLE_DIR) + + download_ytdlp() + download_ffmpeg() + run_pyinstaller() + + print("\n" + "=" * 50) + print(" DONE!") + print("=" * 50) + dist = DIST_DIR / "MusicDownload" + if dist.exists(): + print(f"\n L'app e in: {dist}") + print(f" Eseguibile: {dist / 'MusicDownload.exe'}") + print(f"\n Per distribuirla, comprimi la cartella:") + print(f" Comprimi {dist} -> MusicDownload_Windows.zip") + print() + + +if __name__ == "__main__": + main() diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..1e8708d --- /dev/null +++ b/config.example.json @@ -0,0 +1,9 @@ +{ + "client_id": "", + "client_secret": "", + "bitrate": "320K", + "hq_threshold": 310, + "cookies_path": "", + "output_dir": "", + "theme": "dark" +} diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/config.py b/core/config.py new file mode 100644 index 0000000..ada4987 --- /dev/null +++ b/core/config.py @@ -0,0 +1,64 @@ +"""Gestione configurazione persistente (JSON).""" + +import json +import os +import sys +from pathlib import Path + +VERSION = "v1.0" + + +def _get_config_dir() -> Path: + """Ritorna la directory per config.json. + + Frozen (bundle PyInstaller): + - macOS: ~/Library/Application Support/MusicDownload/ + - Windows: %APPDATA%/MusicDownload/ + Dev (non frozen): + - directory del progetto + """ + if getattr(sys, "frozen", False): + if sys.platform == "win32": + base = Path(os.environ.get("APPDATA", Path.home())) + else: + base = Path.home() / "Library" / "Application Support" + config_dir = base / "MusicDownload" + config_dir.mkdir(parents=True, exist_ok=True) + return config_dir + return Path(__file__).resolve().parent.parent + + +CONFIG_PATH = _get_config_dir() / "config.json" + +# Default: i path relativi al progetto (se non frozen) +_project_dir = Path(__file__).resolve().parent.parent + +DEFAULTS = { + "client_id": "", + "client_secret": "", + "bitrate": "320K", + "hq_threshold": 310, + "cookies_path": str(_project_dir / "cookies.txt"), + "output_dir": str(_project_dir / "MUSICA"), + "theme": "dark", +} + + +def load_config() -> dict: + """Carica la configurazione da config.json, con fallback ai default.""" + config = dict(DEFAULTS) + if CONFIG_PATH.exists(): + try: + with open(CONFIG_PATH, "r", encoding="utf-8") as f: + stored = json.load(f) + config.update(stored) + except (json.JSONDecodeError, OSError): + pass + return config + + +def save_config(config: dict) -> None: + """Salva la configurazione su config.json.""" + CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(CONFIG_PATH, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2, ensure_ascii=False) diff --git a/core/downloader.py b/core/downloader.py new file mode 100644 index 0000000..84f2aa6 --- /dev/null +++ b/core/downloader.py @@ -0,0 +1,395 @@ +"""Download da YouTube via yt-dlp (subprocess per usare la versione piu aggiornata).""" + +from __future__ import annotations + +import json +import re +import subprocess +import threading +from pathlib import Path +from typing import Callable, Optional + +from core.paths import find_ytdlp, find_ffmpeg_dir + + +# Flag globale per interruzione +_stop_event = threading.Event() +_current_process: Optional[subprocess.Popen] = None +_process_lock = threading.Lock() + + +def request_stop(): + """Richiede l'interruzione del download in corso.""" + _stop_event.set() + with _process_lock: + if _current_process and _current_process.poll() is None: + _current_process.terminate() + + +def reset_stop(): + _stop_event.clear() + + +def is_stopped() -> bool: + return _stop_event.is_set() + + +def _load_done_set(done_file: Path) -> set[str]: + """Carica l'elenco delle tracce gia scaricate.""" + if done_file.exists(): + try: + return set(done_file.read_text(encoding="utf-8").splitlines()) + except OSError: + return set() + return set() + + +def _mark_done(done_file: Path, key: str) -> None: + """Aggiunge una traccia al file delle tracce scaricate.""" + try: + with open(done_file, "a", encoding="utf-8") as f: + f.write(key + "\n") + except OSError: + pass + + +_STOPWORDS = { + "official", "video", "audio", "music", "lyric", "lyrics", "hd", "4k", + "remix", "edit", "extended", "feat", "ft", "featuring", "with", + "from", "the", "and", "of", "a", "an", "to", "in", +} + + +def _tokenize(s: str) -> set[str]: + """Estrae token alfanumerici significativi da una stringa.""" + s = s.lower() + tokens = re.findall(r"[a-z0-9]+", s) + return {t for t in tokens if len(t) >= 2 and t not in _STOPWORDS} + + +def _scan_existing_files(directory: Path, extensions: tuple[str, ...] = (".mp3", ".m4a", ".opus", ".webm", ".wav", ".flac")) -> list[set[str]]: + """Ritorna lista di set di token dei filename presenti nella directory.""" + if not directory.exists(): + return [] + return [_tokenize(f.stem) for f in directory.iterdir() + if f.is_file() and f.suffix.lower() in extensions] + + +def _file_exists_for_query(query: str, existing_tokens: list[set[str]]) -> bool: + """Verifica se esiste gia un file il cui titolo contiene tutte le parole del query. + Match basato sui token: tutte le parole significative del query devono essere + presenti nel filename (l'ordine non conta, eventuali parole extra nel filename ok).""" + query_tokens = _tokenize(query) + if not query_tokens: + return False + for file_tokens in existing_tokens: + if query_tokens.issubset(file_tokens): + return True + return False + + +def _search_youtube(query: str, cookies_path: Optional[str] = None) -> tuple[str, str]: + """Cerca un video su YouTube. Ritorna (video_url, video_title).""" + ytdlp = find_ytdlp() + cmd = [ + ytdlp, + f"ytsearch1:{query}", + "--dump-json", + "--no-download", + "--no-warnings", + "--flat-playlist", + ] + if cookies_path and Path(cookies_path).exists(): + cmd.extend(["--cookies", cookies_path]) + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode != 0: + raise ValueError(f"Ricerca fallita: {result.stderr.strip()}") + + data = json.loads(result.stdout) + video_id = data.get("id", "") + title = data.get("title", query) + if not video_id: + raise ValueError("Nessun risultato trovato") + + return f"https://www.youtube.com/watch?v={video_id}", title + + +def download_playlist( + tracks: list[dict], + output_dir: str, + bitrate: str = "320K", + cookies_path: Optional[str] = None, + progress_callback: Optional[Callable] = None, +) -> None: + """Scarica tutti i brani dalla lista di tracce. + + Args: + tracks: lista di {"name": ..., "artist": ...} + output_dir: directory di destinazione + bitrate: qualita audio (es. "320K") + cookies_path: percorso file cookies (opzionale) + progress_callback: callback(track_index, total, track_name, status, percent) + """ + reset_stop() + total = len(tracks) + out_path = Path(output_dir) + out_path.mkdir(parents=True, exist_ok=True) + done_file = out_path / ".downloaded_tracks" + done_set = _load_done_set(done_file) + existing_files = _scan_existing_files(out_path) + ytdlp = find_ytdlp() + + for i, track in enumerate(tracks): + if is_stopped(): + if progress_callback: + progress_callback(i, total, "", "stopped", 0) + return + + query = f"{track['name']} - {track['artist']}" + track_only = track['name'] + + # Salta brani gia scaricati in precedenza (file di tracking) + if query in done_set: + if progress_callback: + progress_callback(i, total, query, "skipped", 100) + continue + + # Salta se esiste gia un file corrispondente nella cartella + if (_file_exists_for_query(query, existing_files) + or _file_exists_for_query(track_only, existing_files)): + _mark_done(done_file, query) + done_set.add(query) + if progress_callback: + progress_callback(i, total, query, "skipped", 100) + continue + + if progress_callback: + progress_callback(i, total, query, "searching", 0) + + # Cerca il video su YouTube + try: + video_url, _ = _search_youtube(query, cookies_path) + except Exception as e: + if progress_callback: + progress_callback(i, total, query, f"error: {e}", 0) + continue + + if is_stopped(): + return + + if progress_callback: + progress_callback(i, total, query, "downloading", 0) + + # Scarica con yt-dlp subprocess + cmd = [ + ytdlp, + "--extract-audio", + "--audio-format", "mp3", + "--audio-quality", bitrate.rstrip("Kk"), + "--embed-thumbnail", + "--add-metadata", + "--no-check-certificates", + "--newline", + "--output", str(Path(output_dir) / "%(title)s.%(ext)s"), + video_url, + ] + ffmpeg_dir = find_ffmpeg_dir() + if ffmpeg_dir: + cmd.extend(["--ffmpeg-location", ffmpeg_dir]) + if cookies_path and Path(cookies_path).exists(): + cmd.extend(["--cookies", cookies_path]) + + try: + with _process_lock: + _current_process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + ) + + for line in _current_process.stdout: + if is_stopped(): + _current_process.terminate() + if progress_callback: + progress_callback(i, total, query, "stopped", 0) + return + + # Parse progress da output yt-dlp + pct_match = re.search(r"(\d+(?:\.\d+)?)%", line) + if pct_match and progress_callback: + pct = int(float(pct_match.group(1))) + progress_callback(i, total, query, "downloading", pct) + + return_code = _current_process.wait() + + with _process_lock: + _current_process = None + + if return_code == 0: + _mark_done(done_file, query) + done_set.add(query) + existing_files = _scan_existing_files(out_path) + if progress_callback: + progress_callback(i, total, query, "done", 100) + else: + if progress_callback: + progress_callback(i, total, query, f"error: yt-dlp exit {return_code}", 0) + + except Exception as e: + with _process_lock: + _current_process = None + if progress_callback: + progress_callback(i, total, query, f"error: {e}", 0) + + if progress_callback and not is_stopped(): + progress_callback(total, total, "", "completed", 100) + + +def download_direct_url( + url: str, + output_dir: str, + bitrate: str = "320K", + cookies_path: Optional[str] = None, + progress_callback: Optional[Callable] = None, +) -> None: + """Scarica direttamente da un URL supportato da yt-dlp (YouTube, SoundCloud, ecc.). + + Supporta sia singoli video che playlist. + Callback signature: progress_callback(idx, total, track_name, status, pct) + """ + global _current_process + reset_stop() + out_path = Path(output_dir) + out_path.mkdir(parents=True, exist_ok=True) + done_file = out_path / ".downloaded_tracks" + done_set = _load_done_set(done_file) + existing_files = _scan_existing_files(out_path) + ytdlp = find_ytdlp() + + # 1. Ottieni info tramite yt-dlp --dump-json + probe_cmd = [ + ytdlp, + "--dump-json", + "--flat-playlist", + "--no-download", + "--no-warnings", + url, + ] + if cookies_path and Path(cookies_path).exists(): + probe_cmd.extend(["--cookies", cookies_path]) + + try: + result = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=60) + if result.returncode != 0: + raise ValueError(result.stderr.strip()) + except Exception as e: + if progress_callback: + progress_callback(0, 0, url, f"error: {e}", 0) + return + + # Ogni riga stdout e un JSON (una entry per playlist, una per singolo) + entries = [] + for line in result.stdout.strip().splitlines(): + if line.strip(): + try: + entries.append(json.loads(line)) + except json.JSONDecodeError: + continue + + if not entries: + if progress_callback: + progress_callback(0, 0, url, "error: Nessun contenuto trovato", 0) + return + + total = len(entries) + + # 2. Scarica ogni entry + for i, entry in enumerate(entries): + if is_stopped(): + if progress_callback: + progress_callback(i, total, "", "stopped", 0) + return + + title = entry.get("title", f"Track {i + 1}") + entry_url = entry.get("url") or entry.get("webpage_url") or entry.get("original_url") or url + + # Per singoli video (non flat-playlist), usa l'URL originale + if total == 1: + entry_url = url + + # Salta se gia scaricato (file di tracking) + track_key = entry.get("id") or title + if track_key in done_set: + if progress_callback: + progress_callback(i, total, title, "skipped", 100) + continue + + # Salta se esiste gia un file corrispondente nella cartella + if _file_exists_for_query(title, existing_files): + _mark_done(done_file, track_key) + done_set.add(track_key) + if progress_callback: + progress_callback(i, total, title, "skipped", 100) + continue + + if progress_callback: + progress_callback(i, total, title, "downloading", 0) + + cmd = [ + ytdlp, + "--extract-audio", + "--audio-format", "mp3", + "--audio-quality", bitrate.rstrip("Kk"), + "--embed-thumbnail", + "--add-metadata", + "--no-check-certificates", + "--newline", + "--output", str(Path(output_dir) / "%(title)s.%(ext)s"), + entry_url, + ] + ffmpeg_dir = find_ffmpeg_dir() + if ffmpeg_dir: + cmd.extend(["--ffmpeg-location", ffmpeg_dir]) + if cookies_path and Path(cookies_path).exists(): + cmd.extend(["--cookies", cookies_path]) + + try: + with _process_lock: + _current_process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + ) + + for line in _current_process.stdout: + if is_stopped(): + _current_process.terminate() + if progress_callback: + progress_callback(i, total, title, "stopped", 0) + return + + pct_match = re.search(r"(\d+(?:\.\d+)?)%", line) + if pct_match and progress_callback: + pct = int(float(pct_match.group(1))) + progress_callback(i, total, title, "downloading", pct) + + return_code = _current_process.wait() + + with _process_lock: + _current_process = None + + if return_code == 0: + _mark_done(done_file, track_key) + done_set.add(track_key) + existing_files = _scan_existing_files(out_path) + if progress_callback: + progress_callback(i, total, title, "done", 100) + else: + if progress_callback: + progress_callback(i, total, title, f"error: yt-dlp exit {return_code}", 0) + + except Exception as e: + with _process_lock: + _current_process = None + if progress_callback: + progress_callback(i, total, title, f"error: {e}", 0) + + if progress_callback and not is_stopped(): + progress_callback(total, total, "", "completed", 100) diff --git a/core/paths.py b/core/paths.py new file mode 100644 index 0000000..25b71a1 --- /dev/null +++ b/core/paths.py @@ -0,0 +1,137 @@ +"""Risoluzione path per binari esterni (yt-dlp, ffmpeg). + +Quando l'app gira come bundle PyInstaller, i binari sono inclusi +nella cartella del bundle. Altrimenti cerca nel sistema. + +Cross-platform: macOS e Windows. +""" + +from __future__ import annotations + +import os +import shutil +import sys +from pathlib import Path +from typing import Optional + +_IS_WINDOWS = sys.platform == "win32" + + +def _is_frozen() -> bool: + """True se stiamo girando come bundle PyInstaller.""" + return getattr(sys, "frozen", False) + + +def _bundle_dirs() -> list[Path]: + """Directories dove cercare i binari bundled (PyInstaller).""" + dirs = [] + if _is_frozen(): + # _MEIPASS: directory dove PyInstaller estrae i file + meipass = Path(getattr(sys, "_MEIPASS", "")) + if meipass.exists(): + dirs.append(meipass) + # Directory dell'eseguibile + exe_dir = Path(sys.executable).parent + dirs.append(exe_dir) + # macOS: Contents/Frameworks/ (PyInstaller onedir .app) + if not _IS_WINDOWS: + frameworks = exe_dir.parent / "Frameworks" + if frameworks.exists(): + dirs.append(frameworks) + return dirs + + +def _exe(name: str) -> str: + """Aggiunge .exe su Windows.""" + if _IS_WINDOWS and not name.endswith(".exe"): + return name + ".exe" + return name + + +def find_ytdlp() -> str: + """Trova il path di yt-dlp. + + Ordine di ricerca: + 1. Bundle PyInstaller + 2. Homebrew (macOS) / PATH comune (Windows) + 3. Qualsiasi posizione nel PATH + """ + name = _exe("yt-dlp") + + # 1. Bundle + for d in _bundle_dirs(): + candidate = d / name + if candidate.exists(): + return str(candidate) + + # 2. Percorsi noti per OS + if not _IS_WINDOWS: + brew = shutil.which("yt-dlp", path="/opt/homebrew/bin:/usr/local/bin") + if brew: + return brew + + # 3. PATH generico + system = shutil.which("yt-dlp") + if system: + return system + + msg = "yt-dlp non trovato. " + if _IS_WINDOWS: + msg += "Scaricalo da https://github.com/yt-dlp/yt-dlp/releases" + else: + msg += "Installalo con: brew install yt-dlp" + raise FileNotFoundError(msg) + + +def find_ffmpeg_dir() -> Optional[str]: + """Trova la directory contenente ffmpeg e ffprobe. + + Ordine di ricerca: + 1. Bundle PyInstaller + 2. Percorsi noti per OS + 3. Qualsiasi posizione nel PATH + """ + ffmpeg_name = _exe("ffmpeg") + + # 1. Bundle + for d in _bundle_dirs(): + if (d / ffmpeg_name).exists(): + return str(d) + + # 2. Percorsi noti + if _IS_WINDOWS: + # Posizioni comuni su Windows + for search in ( + Path(os.environ.get("LOCALAPPDATA", "")) / "Programs" / "ffmpeg" / "bin", + Path("C:/ffmpeg/bin"), + Path(os.environ.get("ProgramFiles", "")) / "ffmpeg" / "bin", + ): + if (search / "ffmpeg.exe").exists(): + return str(search) + else: + for search_path in ("/opt/homebrew/bin", "/usr/local/bin"): + if (Path(search_path) / "ffmpeg").exists(): + return search_path + + # 3. PATH generico + ffmpeg = shutil.which("ffmpeg") + if ffmpeg: + return str(Path(ffmpeg).parent) + + return None + + +def find_ffmpeg() -> Optional[str]: + """Ritorna il path completo di ffmpeg.""" + d = find_ffmpeg_dir() + if d: + return str(Path(d) / _exe("ffmpeg")) + return None + + +def find_ffprobe() -> Optional[str]: + """Ritorna il path completo di ffprobe.""" + d = find_ffmpeg_dir() + if d: + return str(Path(d) / _exe("ffprobe")) + return None diff --git a/core/spotify_client.py b/core/spotify_client.py new file mode 100644 index 0000000..d662db3 --- /dev/null +++ b/core/spotify_client.py @@ -0,0 +1,177 @@ +"""Client API Spotify — autenticazione e fetch playlist/track/album.""" + +from __future__ import annotations + +import re +import requests + + +def get_access_token(client_id: str, client_secret: str) -> str: + """Ottiene un access token tramite Client Credentials Flow.""" + resp = requests.post( + "https://accounts.spotify.com/api/token", + data={"grant_type": "client_credentials"}, + auth=(client_id, client_secret), + timeout=15, + ) + resp.raise_for_status() + token = resp.json().get("access_token") + if not token: + raise ValueError("Token non ricevuto da Spotify") + return token + + +# --------------------------------------------------------------------------- +# Riconoscimento tipo URL +# --------------------------------------------------------------------------- + +def detect_url_type(url: str) -> tuple[str, str]: + """Rileva il tipo di URL Spotify e ne estrae l'ID. + + Ritorna (tipo, id) dove tipo e "track", "album" o "playlist". + Solleva ValueError se l'URL non e riconosciuto. + """ + for kind in ("track", "album", "playlist"): + match = re.search(rf"{kind}/([a-zA-Z0-9]+)", url) + if match: + return kind, match.group(1) + raise ValueError( + "URL non riconosciuto. Sono supportati: playlist, album e singoli brani." + ) + + +# --------------------------------------------------------------------------- +# Singolo brano +# --------------------------------------------------------------------------- + +def get_track_info(token: str, track_id: str) -> tuple[str, list[dict]]: + """Ritorna (track_name, [{"name": ..., "artist": ...}]) per un singolo brano.""" + resp = requests.get( + f"https://api.spotify.com/v1/tracks/{track_id}", + headers={"Authorization": f"Bearer {token}"}, + timeout=15, + ) + resp.raise_for_status() + data = resp.json() + name = data.get("name", "Unknown") + artists = data.get("artists", []) + artist = artists[0].get("name", "Unknown") if artists else "Unknown" + return name, [{"name": name, "artist": artist}] + + +# --------------------------------------------------------------------------- +# Album +# --------------------------------------------------------------------------- + +def get_album_info(token: str, album_id: str) -> tuple[str, list[dict]]: + """Ritorna (album_name, lista tracce) per un album.""" + resp = requests.get( + f"https://api.spotify.com/v1/albums/{album_id}", + headers={"Authorization": f"Bearer {token}"}, + timeout=15, + ) + resp.raise_for_status() + data = resp.json() + album_name = data.get("name", "Unknown Album") + + tracks = [] + # Prima pagina inclusa nella risposta + tracks_data = data.get("tracks", {}) + _collect_album_tracks(tracks_data, tracks) + + # Paginazione se ci sono piu di 50 tracce + next_url = tracks_data.get("next") + while next_url: + resp = requests.get( + next_url, + headers={"Authorization": f"Bearer {token}"}, + timeout=15, + ) + resp.raise_for_status() + page = resp.json() + _collect_album_tracks(page, tracks) + next_url = page.get("next") + + return album_name, tracks + + +def _collect_album_tracks(data: dict, out: list[dict]) -> None: + for item in data.get("items", []): + name = item.get("name", "Unknown") + artists = item.get("artists", []) + artist = artists[0].get("name", "Unknown") if artists else "Unknown" + out.append({"name": name, "artist": artist}) + + +# --------------------------------------------------------------------------- +# Playlist +# --------------------------------------------------------------------------- + +def get_playlist_info(token: str, playlist_id: str) -> tuple[str, list[dict]]: + """Ritorna (playlist_name, lista tracce) per una playlist.""" + resp = requests.get( + f"https://api.spotify.com/v1/playlists/{playlist_id}", + headers={"Authorization": f"Bearer {token}"}, + timeout=15, + ) + resp.raise_for_status() + data = resp.json() + name = data.get("name") + if not name: + raise ValueError("Impossibile leggere il nome della playlist") + + tracks = get_all_tracks(token, playlist_id) + return name, tracks + + +def get_all_tracks(token: str, playlist_id: str) -> list[dict]: + """Scarica tutte le tracce con paginazione (100 per pagina). + + Ritorna lista di {"name": ..., "artist": ...}. + """ + tracks: list[dict] = [] + url: str | None = f"https://api.spotify.com/v1/playlists/{playlist_id}/tracks?limit=100&offset=0" + + while url: + resp = requests.get( + url, + headers={"Authorization": f"Bearer {token}"}, + timeout=15, + ) + resp.raise_for_status() + data = resp.json() + + for item in data.get("items", []): + track = item.get("track") + if not track: + continue + name = track.get("name", "Unknown") + artists = track.get("artists", []) + artist = artists[0].get("name", "Unknown") if artists else "Unknown" + tracks.append({"name": name, "artist": artist}) + + url = data.get("next") # None quando non ci sono altre pagine + + return tracks + + +# --------------------------------------------------------------------------- +# Funzione unificata +# --------------------------------------------------------------------------- + +def resolve_spotify_url(token: str, url: str) -> tuple[str, str, list[dict]]: + """Risolve qualsiasi URL Spotify supportato. + + Ritorna (label, nome, lista_tracce) dove label e "Brano"/"Album"/"Playlist". + """ + kind, item_id = detect_url_type(url) + + if kind == "track": + name, tracks = get_track_info(token, item_id) + return "Brano", name, tracks + elif kind == "album": + name, tracks = get_album_info(token, item_id) + return "Album", name, tracks + else: + name, tracks = get_playlist_info(token, item_id) + return "Playlist", name, tracks diff --git a/core/upgrader.py b/core/upgrader.py new file mode 100644 index 0000000..b5c6cf8 --- /dev/null +++ b/core/upgrader.py @@ -0,0 +1,359 @@ +"""Logica upgrade qualita audio — riscrittura di upgrade_quality.sh.""" + +from __future__ import annotations + +import json +import re +import subprocess +import threading +from pathlib import Path +from typing import Callable, Optional + +from core.paths import find_ytdlp, find_ffmpeg_dir, find_ffprobe + +AUDIO_EXTENSIONS = {".mp3", ".m4a", ".wav", ".flac"} + +# Flag globale per interruzione +_stop_event = threading.Event() +_current_process: Optional[subprocess.Popen] = None +_process_lock = threading.Lock() + + +def request_stop(): + _stop_event.set() + with _process_lock: + if _current_process and _current_process.poll() is None: + _current_process.terminate() + + +def reset_stop(): + _stop_event.clear() + + +def is_stopped() -> bool: + return _stop_event.is_set() + + +def scan_audio_files(directory: str, recursive: bool = False) -> list[Path]: + """Trova file audio nella directory.""" + base = Path(directory) + files = [] + if recursive: + for ext in AUDIO_EXTENSIONS: + files.extend(base.rglob(f"*{ext}")) + else: + for ext in AUDIO_EXTENSIONS: + files.extend(base.glob(f"*{ext}")) + return sorted(files) + + +def get_bitrate(filepath: str | Path) -> int: + """Ritorna il bitrate in kbps tramite ffprobe. Ritorna 0 in caso di errore.""" + try: + ffprobe_bin = find_ffprobe() or "ffprobe" + result = subprocess.run( + [ + ffprobe_bin, "-v", "quiet", + "-print_format", "json", + "-show_format", + str(filepath), + ], + capture_output=True, + text=True, + timeout=30, + ) + data = json.loads(result.stdout) + bit_rate = int(data.get("format", {}).get("bit_rate", 0)) + return bit_rate // 1000 + except Exception: + return 0 + + +def _load_done_set(done_file: Path) -> set[str]: + if done_file.exists(): + return set(done_file.read_text(encoding="utf-8").splitlines()) + return set() + + +def _mark_done(done_file: Path, filename: str) -> None: + with open(done_file, "a", encoding="utf-8") as f: + f.write(filename + "\n") + + +def _search_youtube(query: str, cookies_path: Optional[str] = None) -> str: + """Cerca un video su YouTube e ritorna l'URL.""" + ytdlp = find_ytdlp() + cmd = [ + ytdlp, + f"ytsearch1:{query}", + "--dump-json", + "--no-download", + "--no-warnings", + "--flat-playlist", + ] + if cookies_path and Path(cookies_path).exists(): + cmd.extend(["--cookies", cookies_path]) + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode != 0: + raise ValueError("Ricerca fallita") + + data = json.loads(result.stdout) + video_id = data.get("id", "") + if not video_id: + raise ValueError("Nessun risultato") + return f"https://www.youtube.com/watch?v={video_id}" + + +def update_cover_only( + filepath: Path, + video_url: str, + temp_dir: Path, + cookies_path: Optional[str] = None, +) -> bool: + """Aggiorna solo la copertina di un file audio. Ritorna True se riuscito.""" + ytdlp = find_ytdlp() + cmd = [ + ytdlp, + "--skip-download", + "--write-thumbnail", + "--convert-thumbnails", "jpg", + "--no-warnings", + "--output", str(temp_dir / "cover"), + video_url, + ] + ffmpeg_dir = _find_ffmpeg_dir() + if ffmpeg_dir: + cmd.extend(["--ffmpeg-location", ffmpeg_dir]) + if cookies_path and Path(cookies_path).exists(): + cmd.extend(["--cookies", cookies_path]) + + subprocess.run(cmd, capture_output=True, timeout=60) + + cover_files = list(temp_dir.glob("cover*.jpg")) + if not cover_files: + return False + + cover_file = cover_files[0] + temp_output = temp_dir / "temp_output.mp3" + + try: + subprocess.run( + [ + "ffmpeg", "-y", + "-i", str(filepath), + "-i", str(cover_file), + "-map", "0:a", "-map", "1:0", + "-c:a", "copy", + "-id3v2_version", "3", + "-metadata:s:v", "title=Album cover", + "-metadata:s:v", "comment=Cover (front)", + str(temp_output), + ], + capture_output=True, + timeout=60, + ) + except Exception: + _cleanup_temp(temp_dir) + return False + + if temp_output.exists(): + temp_output.replace(filepath) + _cleanup_temp(temp_dir) + return True + + _cleanup_temp(temp_dir) + return False + + +def _cleanup_temp(temp_dir: Path) -> None: + for pattern in ("*.jpg", "*.webp", "*.mp3", "*.webm", "*.m4a"): + for f in temp_dir.glob(pattern): + f.unlink(missing_ok=True) + + +def upgrade_folder( + directory: str, + threshold: int = 310, + cookies_path: Optional[str] = None, + recursive: bool = False, + progress_callback: Optional[Callable] = None, +) -> None: + """Logica principale di upgrade qualita.""" + reset_stop() + ytdlp = find_ytdlp() + + if recursive: + base = Path(directory) + folders = set() + for ext in AUDIO_EXTENSIONS: + for f in base.rglob(f"*{ext}"): + folders.add(f.parent) + folders = sorted(folders) + else: + folders = [Path(directory)] + + all_items: list[tuple[Path, Path]] = [] + for folder in folders: + for ext in AUDIO_EXTENSIONS: + for f in sorted(folder.glob(f"*{ext}")): + all_items.append((f, folder)) + + total = len(all_items) + if total == 0: + if progress_callback: + progress_callback(0, 0, "", "no_files", 0, 0) + return + + processed = 0 + + for filepath, folder in all_items: + if is_stopped(): + if progress_callback: + progress_callback(processed, total, "", "stopped", 0, 0) + return + + done_file = folder / ".upgraded_tracks" + temp_dir = folder / ".temp_download" + temp_dir.mkdir(exist_ok=True) + + done_set = _load_done_set(done_file) + filename = filepath.stem + + if filename in done_set: + processed += 1 + if progress_callback: + progress_callback(processed, total, filepath.name, "skipped", 0, 0) + continue + + current_kbps = get_bitrate(filepath) + + if progress_callback: + progress_callback(processed, total, filepath.name, "searching", current_kbps, 0) + + query = filename + try: + video_url = _search_youtube(query, cookies_path) + except Exception: + _mark_done(done_file, filename) + processed += 1 + if progress_callback: + progress_callback(processed, total, filepath.name, "not_found", current_kbps, 0) + continue + + if is_stopped(): + return + + # Se qualita gia alta, aggiorna solo copertina + if current_kbps >= threshold: + if progress_callback: + progress_callback(processed, total, filepath.name, "cover_only", current_kbps, 0) + + success = update_cover_only(filepath, video_url, temp_dir, cookies_path) + _mark_done(done_file, filename) + processed += 1 + + status = "cover_done" if success else "cover_failed" + if progress_callback: + progress_callback(processed, total, filepath.name, status, current_kbps, current_kbps) + continue + + # Scarica versione HQ + if progress_callback: + progress_callback(processed, total, filepath.name, "downloading", current_kbps, 0) + + cmd = [ + ytdlp, + "--extract-audio", + "--audio-format", "mp3", + "--audio-quality", "0", + "--embed-thumbnail", + "--add-metadata", + "--no-warnings", + "--newline", + "--output", str(temp_dir / "%(title)s.%(ext)s"), + video_url, + ] + ffmpeg_dir = find_ffmpeg_dir() + if ffmpeg_dir: + cmd.extend(["--ffmpeg-location", ffmpeg_dir]) + if cookies_path and Path(cookies_path).exists(): + cmd.extend(["--cookies", cookies_path]) + + try: + with _process_lock: + _current_process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + ) + + for line in _current_process.stdout: + if is_stopped(): + _current_process.terminate() + return + pct_match = re.search(r"(\d+(?:\.\d+)?)%", line) + if pct_match and progress_callback: + pct = int(float(pct_match.group(1))) + progress_callback(processed, total, filepath.name, "downloading", current_kbps, pct) + + _current_process.wait() + with _process_lock: + _current_process = None + + except Exception: + with _process_lock: + _current_process = None + _mark_done(done_file, filename) + processed += 1 + if progress_callback: + progress_callback(processed, total, filepath.name, "download_error", current_kbps, 0) + _cleanup_temp(temp_dir) + continue + + # Trova il file scaricato + new_files = list(temp_dir.glob("*.mp3")) + if not new_files: + _mark_done(done_file, filename) + processed += 1 + if progress_callback: + progress_callback(processed, total, filepath.name, "download_error", current_kbps, 0) + _cleanup_temp(temp_dir) + continue + + newest = max(new_files, key=lambda p: p.stat().st_mtime) + new_kbps = get_bitrate(newest) + + filepath.unlink() + newest.replace(filepath.parent / f"{filename}.mp3") + + _mark_done(done_file, filename) + _cleanup_temp(temp_dir) + processed += 1 + + if progress_callback: + progress_callback(processed, total, filepath.name, "upgraded", current_kbps, new_kbps) + + for folder in folders: + td = folder / ".temp_download" + if td.exists(): + try: + td.rmdir() + except OSError: + pass + + if progress_callback and not is_stopped(): + progress_callback(total, total, "", "completed", 0, 0) + + +def count_files_info(directory: str, recursive: bool = False) -> tuple[int, int]: + """Ritorna (total_audio_files, already_upgraded).""" + files = scan_audio_files(directory, recursive) + total = len(files) + + already_done = 0 + for f in files: + done_file = f.parent / ".upgraded_tracks" + done_set = _load_done_set(done_file) + if f.stem in done_set: + already_done += 1 + + return total, already_done diff --git a/grafica.png b/grafica.png new file mode 100644 index 0000000000000000000000000000000000000000..40fed8e1260da733fa38f09a4c25277f77919c96 GIT binary patch literal 168921 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

|KgA&a3tKuP-ARg(|Zy&TsA8nop22 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 str: + """Trova il path di una risorsa sia in dev che bundled (PyInstaller).""" + if getattr(sys, "frozen", False): + base = Path(sys._MEIPASS) # type: ignore[attr-defined] + else: + base = Path(__file__).resolve().parent + return str(base.joinpath(*parts)) + + +def main(): + api = Api() + index_path = _resource_path("webui", "index.html") + + window = webview.create_window( + title="MusicDownload", + url=index_path, + js_api=api, + width=1080, + height=720, + min_size=(900, 600), + background_color="#0A0A0A", + resizable=True, + ) + api.window = window + + # Disable native menu / set dark on macOS + webview.start(debug=False) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ed571a3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +pywebview>=5.0 +requests>=2.31.0 +yt-dlp>=2024.0.0 +pyobjc-core>=10.0 ; sys_platform == "darwin" +pyobjc-framework-Cocoa>=10.0 ; sys_platform == "darwin" +pyobjc-framework-WebKit>=10.0 ; sys_platform == "darwin" +pythonnet>=3.0 ; sys_platform == "win32" diff --git a/spotify_to_youtube_downloader.sh b/spotify_to_youtube_downloader.sh new file mode 100755 index 0000000..99e60fc --- /dev/null +++ b/spotify_to_youtube_downloader.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# ============================================ +# 🎵 Spotify → YouTube Downloader (API edition) +# by LuZa / GPT-5 +# ============================================ + +# Requisiti: yt-dlp, ffmpeg, jq, curl +# Installazione rapida (macOS): +# brew install yt-dlp ffmpeg jq curl + +# ====== CONFIGURAZIONE ====== +CLIENT_ID="379c38c32a5b43259cf55125e6efd472" +CLIENT_SECRET="e58c9a468b624eb3bdd27f701fef3aa2" +BITRATE="320K" +COOKIE_FILE="cookies.txt" +LOGFILE="download.log" + +# ====== INPUT ====== +if [ -z "$1" ]; then + echo "Uso: $0 [directory_output]" + exit 1 +fi + +SPOTIFY_URL="$1" +CUSTOM_DIR="$2" + +# ====== SCELTA DIRECTORY ====== +if [ -z "$CUSTOM_DIR" ]; then + echo "📁 Inserisci il percorso della cartella di destinazione:" + read -r CUSTOM_DIR +fi + +mkdir -p "$CUSTOM_DIR" || { echo "❌ Impossibile creare la directory $CUSTOM_DIR"; exit 1; } + +# ====== OTTIENI TOKEN ACCESSO ====== +echo "🔑 Recupero token di accesso Spotify..." +ACCESS_TOKEN=$(curl -s -X POST -u "$CLIENT_ID:$CLIENT_SECRET" \ + -d grant_type=client_credentials https://accounts.spotify.com/api/token \ + | jq -r '.access_token') + +if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then + echo "❌ Errore durante il recupero del token Spotify." + exit 1 +fi + +# ====== ESTRAI ID PLAYLIST ====== +PLAYLIST_ID=$(echo "$SPOTIFY_URL" | awk -F'playlist/' '{print $2}' | cut -d'?' -f1) +PLAYLIST_API="https://api.spotify.com/v1/playlists/$PLAYLIST_ID" + +# ====== OTTIENI INFO PLAYLIST ====== +PLAYLIST_NAME=$(curl -s -H "Authorization: Bearer $ACCESS_TOKEN" "$PLAYLIST_API" | jq -r '.name') +if [ "$PLAYLIST_NAME" = "null" ] || [ -z "$PLAYLIST_NAME" ]; then + echo "❌ Impossibile leggere la playlist. Controlla il link o le credenziali." + exit 1 +fi + +DEST_DIR="$CUSTOM_DIR/$PLAYLIST_NAME" +mkdir -p "$DEST_DIR" + +echo "🎧 Playlist: $PLAYLIST_NAME" +echo "📂 Download in: $DEST_DIR" +echo "------------------------------------------" +echo "📝 Log: $DEST_DIR/$LOGFILE" + +# ====== OTTIENI TUTTI I BRANI (paginazione API) ====== +OFFSET=0 +LIMIT=100 + +while :; do + TRACKS_JSON=$(curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \ + "$PLAYLIST_API/tracks?limit=$LIMIT&offset=$OFFSET") + + ITEMS=$(echo "$TRACKS_JSON" | jq -r '.items[]?.track.name + " - " + (.items[]?.track.artists[0].name // "Unknown")') + if [ -z "$ITEMS" ]; then + break + fi + + echo "$TRACKS_JSON" | jq -r '.items[] | "\(.track.name) - \(.track.artists[0].name)"' >> "$DEST_DIR/tracks.txt" + + NEXT=$(echo "$TRACKS_JSON" | jq -r '.next') + [ "$NEXT" = "null" ] && break + OFFSET=$((OFFSET + LIMIT)) +done + +# ====== SCARICA I BRANI ====== +INDEX=0 +while IFS= read -r QUERY; do + ((INDEX++)) + echo "🔍 [$INDEX] Cerco su YouTube: $QUERY" + VIDEO_ID=$(yt-dlp "ytsearch1:$QUERY" --get-id 2>/dev/null | head -n 1) + if [ -z "$VIDEO_ID" ]; then + echo "❌ Non trovato: $QUERY" | tee -a "$DEST_DIR/$LOGFILE" + continue + fi + VIDEO_URL="https://www.youtube.com/watch?v=$VIDEO_ID" + echo "🎬 Trovato: $VIDEO_URL" + + yt-dlp \ + --extract-audio \ + --audio-format mp3 \ + --audio-quality "$BITRATE" \ + --embed-thumbnail \ + --add-metadata \ + --no-check-certificates \ + --cookies "$COOKIE_FILE" \ + --output "$DEST_DIR/%(title)s.%(ext)s" \ + "$VIDEO_URL" >>"$DEST_DIR/$LOGFILE" 2>&1 + + echo "✅ Scaricato: $QUERY" + echo "------------------------------------------" +done < "$DEST_DIR/tracks.txt" + +echo "🏁 Download completato!" +echo "📂 File salvati in: $DEST_DIR" \ No newline at end of file diff --git a/upgrade_quality.sh b/upgrade_quality.sh new file mode 100755 index 0000000..8cf9dd6 --- /dev/null +++ b/upgrade_quality.sh @@ -0,0 +1,428 @@ +#!/bin/bash +# ============================================ +# Upgrade Quality - Cerca versioni HQ dei brani +# Sovrascrive i file originali +# Con progress bar e ripresa automatica +# ============================================ + +# Requisiti: yt-dlp, ffmpeg, ffprobe +# Installazione: brew install yt-dlp ffmpeg + +# Percorso script (per trovare cookies.txt nella stessa cartella) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COOKIE_FILE="$SCRIPT_DIR/cookies.txt" + +# Colori +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +GRAY='\033[0;90m' +BLUE='\033[0;34m' +NC='\033[0m' +BOLD='\033[1m' + +# Soglia qualita (kbps) - sopra questa aggiorna solo copertina +HQ_THRESHOLD=310 + +# ============================================ +# Funzione progress bar +# ============================================ +progress_bar() { + local current=$1 + local total=$2 + local width=30 + local percent=$((current * 100 / total)) + local filled=$((current * width / total)) + local empty=$((width - filled)) + + printf "\r [" + printf "%${filled}s" | tr ' ' '#' + printf "%${empty}s" | tr ' ' '-' + printf "] %3d%% (%d/%d)" "$percent" "$current" "$total" +} + +# ============================================ +# Funzione per aggiornare solo la copertina +# ============================================ +update_cover_only() { + local FILE="$1" + local VIDEO_URL="$2" + local TEMP_DIR="$3" + local FILENAME="$4" + + # Scarica thumbnail + yt-dlp \ + --skip-download \ + --write-thumbnail \ + --convert-thumbnails jpg \ + --no-warnings \ + $COOKIE_OPTS \ + --output "$TEMP_DIR/cover" \ + "$VIDEO_URL" 2>/dev/null + + local COVER_FILE=$(ls "$TEMP_DIR"/cover*.jpg 2>/dev/null | head -n 1) + + if [ -n "$COVER_FILE" ] && [ -f "$COVER_FILE" ]; then + # Applica copertina con ffmpeg + local TEMP_OUTPUT="$TEMP_DIR/temp_output.mp3" + + ffmpeg -y -i "$FILE" -i "$COVER_FILE" \ + -map 0:a -map 1:0 \ + -c:a copy \ + -id3v2_version 3 \ + -metadata:s:v title="Album cover" \ + -metadata:s:v comment="Cover (front)" \ + "$TEMP_OUTPUT" 2>/dev/null + + if [ -f "$TEMP_OUTPUT" ]; then + mv "$TEMP_OUTPUT" "$FILE" + rm -f "$COVER_FILE" + return 0 + fi + fi + + rm -f "$TEMP_DIR"/cover*.jpg 2>/dev/null + return 1 +} + +# ============================================ +# Funzione per processare una singola cartella +# ============================================ +process_folder() { + local SOURCE_DIR="$1" + local LOGFILE="$SOURCE_DIR/upgrade.log" + local DONE_FILE="$SOURCE_DIR/.upgraded_tracks" + local TEMP_DIR="$SOURCE_DIR/.temp_download" + + echo "" + echo -e "${CYAN}========================================${NC}" + echo -e "${CYAN} Cartella: ${YELLOW}$(basename "$SOURCE_DIR")${NC}" + echo -e "${CYAN}========================================${NC}" + + # Crea file done se non esiste + touch "$DONE_FILE" + + # Conta file totali e da processare + local ALL_FILES=$(find "$SOURCE_DIR" -maxdepth 1 -type f \( -iname "*.mp3" -o -iname "*.m4a" -o -iname "*.wav" -o -iname "*.flac" \) | wc -l | tr -d ' ') + + if [ "$ALL_FILES" -eq 0 ]; then + echo -e " ${YELLOW}Nessun file audio, salto${NC}" + return + fi + + # Conta quanti sono gia stati convertiti + local ALREADY_DONE=0 + while IFS= read -r FILE; do + local BASENAME=$(basename "$FILE") + local FILENAME="${BASENAME%.*}" + if grep -qxF "$FILENAME" "$DONE_FILE" 2>/dev/null; then + ((ALREADY_DONE++)) + fi + done < <(find "$SOURCE_DIR" -maxdepth 1 -type f \( -iname "*.mp3" -o -iname "*.m4a" -o -iname "*.wav" -o -iname "*.flac" \)) + + local TO_PROCESS=$((ALL_FILES - ALREADY_DONE)) + + if [ "$TO_PROCESS" -eq 0 ]; then + echo -e " ${GREEN}Tutti i $ALL_FILES brani sono gia stati convertiti${NC}" + return + fi + + if [ "$ALREADY_DONE" -gt 0 ]; then + echo -e " Totale: $ALL_FILES file | ${GREEN}Gia convertiti: $ALREADY_DONE${NC} | Da fare: $TO_PROCESS" + else + echo " Trovati $ALL_FILES file audio da processare" + fi + + # Crea directory temporanea + mkdir -p "$TEMP_DIR" + + local INDEX=0 + local SUCCESS=0 + local FAILED=0 + local SKIPPED=0 + + # Processa ogni file audio + while IFS= read -r FILE; do + ((INDEX++)) + + # Estrai nome file senza estensione + local BASENAME=$(basename "$FILE") + local FILENAME="${BASENAME%.*}" + + # Controlla se gia convertito + if grep -qxF "$FILENAME" "$DONE_FILE" 2>/dev/null; then + ((SKIPPED++)) + continue + fi + + # Analizza qualita attuale + local CURRENT_BITRATE=$(ffprobe -v quiet -print_format json -show_format "$FILE" 2>/dev/null | grep -o '"bit_rate": "[^"]*"' | grep -o '[0-9]*') + local CURRENT_KBPS=$((CURRENT_BITRATE / 1000)) + + # Mostra stato + echo "" + local DONE_COUNT=$((SKIPPED + SUCCESS + FAILED)) + echo -e " ${YELLOW}[$((DONE_COUNT + 1))/$ALL_FILES]${NC} $FILENAME" + echo -e " ${GRAY}Qualita attuale: ${CURRENT_KBPS}kbps${NC}" + + # Cerca su YouTube + echo -ne " ${GRAY}Cercando su YouTube...${NC}" + local VIDEO_INFO=$(yt-dlp "ytsearch1:$FILENAME" \ + --get-id --get-title \ + --no-warnings 2>/dev/null) + + if [ -z "$VIDEO_INFO" ]; then + echo -e "\r ${RED}Non trovato su YouTube ${NC}" + echo "FALLITO: $FILENAME - Non trovato" >> "$LOGFILE" + # Segna come fatto comunque per non riprovare + echo "$FILENAME" >> "$DONE_FILE" + ((FAILED++)) + continue + fi + + local VIDEO_TITLE=$(echo "$VIDEO_INFO" | head -n 1) + local VIDEO_ID=$(echo "$VIDEO_INFO" | tail -n 1) + local VIDEO_URL="https://www.youtube.com/watch?v=$VIDEO_ID" + + echo -e "\r ${GRAY}Trovato: ${VIDEO_TITLE:0:50}...${NC} " + + # Se qualita gia alta, aggiorna solo copertina + if [ "$CURRENT_KBPS" -ge "$HQ_THRESHOLD" ]; then + echo -ne " ${BLUE}Qualita gia alta, aggiorno solo copertina...${NC}" + + if update_cover_only "$FILE" "$VIDEO_URL" "$TEMP_DIR" "$FILENAME"; then + echo -e "\r ${GREEN}Copertina aggiornata (audio ${CURRENT_KBPS}kbps mantenuto) ${NC}" + echo "COVER: $FILENAME - Copertina aggiornata (${CURRENT_KBPS}kbps)" >> "$LOGFILE" + else + echo -e "\r ${YELLOW}Copertina non disponibile (audio ${CURRENT_KBPS}kbps ok) ${NC}" + echo "SKIP: $FILENAME - Gia HQ ${CURRENT_KBPS}kbps" >> "$LOGFILE" + fi + + echo "$FILENAME" >> "$DONE_FILE" + ((SUCCESS++)) + + # Pulisci temp + rm -f "$TEMP_DIR"/*.jpg 2>/dev/null + rm -f "$TEMP_DIR"/*.webp 2>/dev/null + continue + fi + + # Scarica con progress + echo -ne " ${CYAN}Scaricando...${NC} " + + yt-dlp \ + --extract-audio \ + --audio-format mp3 \ + --audio-quality 0 \ + --embed-thumbnail \ + --add-metadata \ + --no-warnings \ + --progress \ + --newline \ + $COOKIE_OPTS \ + --output "$TEMP_DIR/%(title)s.%(ext)s" \ + "$VIDEO_URL" 2>&1 | while IFS= read -r line; do + # Estrai percentuale dal output di yt-dlp + if [[ "$line" =~ ([0-9]+\.[0-9]+)% ]]; then + pct="${BASH_REMATCH[1]}" + pct_int=${pct%.*} + bar_width=20 + filled=$((pct_int * bar_width / 100)) + empty=$((bar_width - filled)) + printf "\r ${CYAN}Scaricando${NC} [" + printf "%${filled}s" | tr ' ' '#' + printf "%${empty}s" | tr ' ' '-' + printf "] %3d%%" "$pct_int" + fi + done + + if [ $? -eq 0 ] || [ -n "$(ls -A "$TEMP_DIR" 2>/dev/null)" ]; then + # Trova il file appena scaricato + local NEWEST_FILE=$(ls -t "$TEMP_DIR"/*.mp3 2>/dev/null | head -n 1) + + if [ -n "$NEWEST_FILE" ] && [ -f "$NEWEST_FILE" ]; then + local NEW_BITRATE=$(ffprobe -v quiet -print_format json -show_format "$NEWEST_FILE" 2>/dev/null | grep -o '"bit_rate": "[^"]*"' | grep -o '[0-9]*') + local NEW_KBPS=$((NEW_BITRATE / 1000)) + + # Elimina file originale e sposta quello nuovo + rm -f "$FILE" + mv "$NEWEST_FILE" "$SOURCE_DIR/$FILENAME.mp3" + + # Segna come completato + echo "$FILENAME" >> "$DONE_FILE" + + local DIFF=$((NEW_KBPS - CURRENT_KBPS)) + if [ "$DIFF" -gt 0 ]; then + echo -e "\r ${GREEN}Completato: ${NEW_KBPS}kbps (+${DIFF}kbps) ${NC}" + else + echo -e "\r ${GREEN}Completato: ${NEW_KBPS}kbps ${NC}" + fi + + echo "OK: $FILENAME - ${CURRENT_KBPS}kbps -> ${NEW_KBPS}kbps" >> "$LOGFILE" + ((SUCCESS++)) + else + echo -e "\r ${RED}Errore: file non trovato dopo download ${NC}" + echo "$FILENAME" >> "$DONE_FILE" + ((FAILED++)) + fi + else + echo -e "\r ${RED}Errore durante il download ${NC}" + echo "ERRORE: $FILENAME - Download fallito" >> "$LOGFILE" + echo "$FILENAME" >> "$DONE_FILE" + ((FAILED++)) + fi + + # Pulisci temp + rm -f "$TEMP_DIR"/*.mp3 2>/dev/null + rm -f "$TEMP_DIR"/*.webm 2>/dev/null + rm -f "$TEMP_DIR"/*.m4a 2>/dev/null + + done < <(find "$SOURCE_DIR" -maxdepth 1 -type f \( -iname "*.mp3" -o -iname "*.m4a" -o -iname "*.wav" -o -iname "*.flac" \)) + + # Rimuovi directory temporanea + rmdir "$TEMP_DIR" 2>/dev/null + + # Sommario cartella + echo "" + echo -e " ${GRAY}---${NC}" + echo -e " Convertiti: ${GREEN}$SUCCESS${NC} | Falliti: ${RED}$FAILED${NC} | Saltati: ${GRAY}$SKIPPED${NC}" +} + +# ============================================ +# MAIN +# ============================================ + +echo "" +echo "========================================" +echo " UPGRADE QUALITY AUDIO" +echo "========================================" + +# Verifica cookies YouTube Premium +if [ -f "$COOKIE_FILE" ]; then + echo -e "Cookies: ${GREEN}YouTube Premium attivo${NC}" + COOKIE_OPTS="--cookies $COOKIE_FILE" +else + echo -e "Cookies: ${YELLOW}Non trovati (qualita standard)${NC}" + COOKIE_OPTS="" +fi + +# Gestione parametri +if [ "$1" = "-r" ] || [ "$1" = "--recursive" ]; then + # Modalita ricorsiva: processa tutte le sottocartelle + if [ -z "$2" ]; then + echo -e "${CYAN}Inserisci la cartella principale:${NC}" + read -r BASE_DIR + else + BASE_DIR="$2" + fi + + if [ ! -d "$BASE_DIR" ]; then + echo -e "${RED}Errore: Directory '$BASE_DIR' non trovata${NC}" + exit 1 + fi + + # Trova tutte le sottocartelle con file audio + echo "" + echo "Scansione cartelle in: $BASE_DIR" + + FOLDERS=$(find "$BASE_DIR" -type d | while read -r dir; do + count=$(find "$dir" -maxdepth 1 -type f \( -iname "*.mp3" -o -iname "*.m4a" -o -iname "*.wav" -o -iname "*.flac" \) 2>/dev/null | wc -l | tr -d ' ') + if [ "$count" -gt 0 ]; then + echo "$dir" + fi + done) + + if [ -z "$FOLDERS" ]; then + echo -e "${RED}Nessuna cartella con file audio trovata${NC}" + exit 1 + fi + + FOLDER_COUNT=$(echo "$FOLDERS" | wc -l | tr -d ' ') + echo "" + echo "Trovate $FOLDER_COUNT cartelle con file audio:" + echo "$FOLDERS" | while read -r f; do + count=$(find "$f" -maxdepth 1 -type f \( -iname "*.mp3" -o -iname "*.m4a" -o -iname "*.wav" -o -iname "*.flac" \) | wc -l | tr -d ' ') + # Conta gia convertiti + done_file="$f/.upgraded_tracks" + if [ -f "$done_file" ]; then + done_count=$(wc -l < "$done_file" | tr -d ' ') + remaining=$((count - done_count)) + if [ "$remaining" -le 0 ]; then + echo -e " ${GREEN}$(basename "$f")${NC} ($count file - tutti convertiti)" + else + echo -e " ${YELLOW}$(basename "$f")${NC} ($count file - $remaining da convertire)" + fi + else + echo -e " ${YELLOW}$(basename "$f")${NC} ($count file)" + fi + done + + echo "" + echo -e "${YELLOW}I file originali verranno sovrascritti!${NC}" + echo -e "${GRAY}I brani gia convertiti verranno saltati automaticamente.${NC}" + read -p "Vuoi continuare? (s/n): " CONFIRM + if [[ ! "$CONFIRM" =~ ^[sS]$ ]]; then + echo "Operazione annullata." + exit 0 + fi + + # Processa ogni cartella + FOLDER_INDEX=0 + echo "$FOLDERS" | while read -r folder; do + ((FOLDER_INDEX++)) + echo "" + echo -e "${BOLD}>>> Cartella $FOLDER_INDEX/$FOLDER_COUNT <<<${NC}" + process_folder "$folder" + done + +else + # Modalita singola cartella + if [ -z "$1" ]; then + echo -e "${CYAN}Inserisci il percorso della cartella da convertire:${NC}" + read -r SOURCE_DIR + else + SOURCE_DIR="$1" + fi + + if [ ! -d "$SOURCE_DIR" ]; then + echo -e "${RED}Errore: Directory '$SOURCE_DIR' non trovata${NC}" + exit 1 + fi + + # Conta file + TOTAL=$(find "$SOURCE_DIR" -maxdepth 1 -type f \( -iname "*.mp3" -o -iname "*.m4a" -o -iname "*.wav" -o -iname "*.flac" \) | wc -l | tr -d ' ') + + if [ "$TOTAL" -eq 0 ]; then + echo -e "${RED}Nessun file audio trovato nella cartella${NC}" + exit 1 + fi + + # Conta gia convertiti + DONE_FILE="$SOURCE_DIR/.upgraded_tracks" + if [ -f "$DONE_FILE" ]; then + DONE_COUNT=$(wc -l < "$DONE_FILE" | tr -d ' ') + REMAINING=$((TOTAL - DONE_COUNT)) + echo -e "Cartella: ${YELLOW}$SOURCE_DIR${NC}" + echo -e "Totale: $TOTAL file | ${GREEN}Gia convertiti: $DONE_COUNT${NC} | Da fare: $REMAINING" + else + echo -e "Cartella: ${YELLOW}$SOURCE_DIR${NC}" + echo "Trovati $TOTAL file audio" + fi + + echo "" + echo -e "${YELLOW}I file originali verranno sovrascritti!${NC}" + echo -e "${GRAY}I brani gia convertiti verranno saltati automaticamente.${NC}" + read -p "Vuoi continuare? (s/n): " CONFIRM + if [[ ! "$CONFIRM" =~ ^[sS]$ ]]; then + echo "Operazione annullata." + exit 0 + fi + + process_folder "$SOURCE_DIR" +fi + +echo "" +echo "========================================" +echo -e " ${GREEN}COMPLETATO${NC}" +echo "========================================" diff --git a/webui/css/style.css b/webui/css/style.css new file mode 100644 index 0000000..5632568 --- /dev/null +++ b/webui/css/style.css @@ -0,0 +1,778 @@ +/* =============================== + MusicDownload — Spotify-like UI + =============================== */ + +:root { + --bg: #0A0A0A; + --bg-elev: #121212; + --bg-card: #181818; + --bg-card-2: #1F1F1F; + --bg-input: #2A2A2A; + --bg-input-hover: #3A3A3A; + + --border: #232323; + --divider: #1F1F1F; + + --green: #1DB954; + --green-hover: #1ED760; + --green-press: #169B46; + --green-dim: #0F3B22; + + --blue: #509BF5; + --blue-hover: #6FB1F7; + --blue-dim: #1A2F4A; + + --purple: #A855F7; + --purple-hover: #C084FC; + --purple-dim: #2D1B4D; + + --red: #E22134; + --red-hover: #FF4055; + + --text: #FFFFFF; + --text-2: #B3B3B3; + --text-3: #727272; + + --r-sm: 8px; + --r-md: 12px; + --r-lg: 14px; + --r-pill: 999px; + + --shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + --shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.7); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + height: 100%; + width: 100%; + overflow: hidden; +} + +body { + background: var(--bg); + color: var(--text); + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + user-select: none; +} + +button, input, select, textarea { font-family: inherit; font-size: inherit; } + +/* =============================== + LAYOUT + =============================== */ +.app { + display: grid; + grid-template-columns: 240px 1fr; + height: 100vh; +} + +/* =============================== + SIDEBAR + =============================== */ +.sidebar { + background: var(--bg-elev); + display: flex; + flex-direction: column; + padding: 24px 12px; + border-right: 1px solid var(--border); +} + +.brand { + display: flex; + align-items: center; + gap: 12px; + padding: 0 8px; + margin-bottom: 20px; +} + +.brand-logo { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, var(--green), var(--green-press)); + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + font-weight: 900; + color: #000; + box-shadow: 0 4px 12px rgba(29, 185, 84, 0.35); +} + +.brand-name { + font-size: 17px; + font-weight: 700; + letter-spacing: -0.2px; +} + +.divider { + height: 1px; + background: var(--divider); + margin: 0 8px 16px; +} + +.nav { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; +} + +.nav-item { + display: flex; + align-items: center; + gap: 14px; + padding: 12px 14px; + border: none; + background: transparent; + color: var(--text-2); + cursor: pointer; + border-radius: var(--r-sm); + font-size: 14px; + font-weight: 700; + text-align: left; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.nav-item:hover { + background: #1A1A1A; + color: var(--text); +} + +.nav-item.active { + background: #1F1F1F; + color: var(--green); +} + +.nav-icon { + font-size: 16px; + width: 20px; + text-align: center; +} + +.footer { + text-align: center; + padding-top: 16px; + border-top: 1px solid var(--divider); +} + +.footer-by { + color: var(--green); + font-weight: 700; + font-size: 12px; + letter-spacing: 0.4px; +} + +.footer-version { + color: var(--text-3); + font-size: 11px; + margin-top: 2px; +} + +/* =============================== + MAIN + =============================== */ +.main { + overflow-y: auto; + padding: 24px 32px 32px; + scroll-behavior: smooth; +} + +.view { + display: none; + animation: fadeIn 0.25s ease both; +} + +.view.active { + display: block; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +/* =============================== + HERO + =============================== */ +.hero { + position: relative; + border-radius: var(--r-lg); + padding: 28px 32px; + margin-bottom: 24px; + overflow: hidden; + box-shadow: var(--shadow); +} + +.hero-green { + background: linear-gradient(135deg, #16893E 0%, #0F3B22 60%, #062614 100%); +} + +.hero-blue { + background: linear-gradient(135deg, #2D4D7A 0%, #1A2F4A 60%, #0D1A2B 100%); +} + +.hero-purple { + background: linear-gradient(135deg, #5D2C9C 0%, #2D1B4D 60%, #1A0F2E 100%); +} + +.hero-content { position: relative; z-index: 2; max-width: 70%; } + +.hero-eyebrow { + font-size: 11px; + font-weight: 800; + letter-spacing: 1.5px; + color: var(--green); + margin-bottom: 6px; +} + +.hero-eyebrow.blue { color: var(--blue); } +.hero-eyebrow.purple { color: var(--purple); } + +.hero-title { + font-size: 36px; + font-weight: 900; + letter-spacing: -0.8px; + line-height: 1.1; + margin-bottom: 8px; +} + +.hero-subtitle { + font-size: 14px; + color: rgba(255, 255, 255, 0.7); + font-weight: 500; +} + +.hero-deco { + position: absolute; + right: -10px; + top: 50%; + transform: translateY(-50%); + font-size: 180px; + font-weight: 900; + opacity: 0.08; + line-height: 1; + user-select: none; + pointer-events: none; +} + +/* =============================== + SECTION LABELS + =============================== */ +.section-label { + font-size: 11px; + font-weight: 800; + letter-spacing: 1.2px; + color: var(--text-3); + text-transform: uppercase; + margin: 8px 4px 10px; +} + +/* =============================== + CARDS + =============================== */ +.card { + background: var(--bg-card); + border-radius: var(--r-md); + padding: 18px 20px; + margin-bottom: 18px; + transition: background-color 0.2s ease; +} + +.card-accent-purple { + background: linear-gradient(180deg, #1F1532 0%, #181818 80%); + border: 1px solid rgba(168, 85, 247, 0.2); +} + +.card.actions { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.log-card { + padding: 12px; +} + +/* =============================== + FIELDS + =============================== */ +.field { margin-bottom: 14px; } +.field:last-child { margin-bottom: 0; } + +.field.inline { + display: flex; + align-items: center; + gap: 14px; +} +.field.inline .field-label { margin-bottom: 0; } + +.field-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + font-weight: 700; + color: var(--text-2); + text-transform: uppercase; + letter-spacing: 0.6px; + margin-bottom: 8px; +} + +.row { + display: flex; + align-items: center; + gap: 10px; +} + +.row.gap { gap: 12px; flex-wrap: wrap; margin-bottom: 14px; } +.row.spread { justify-content: space-between; } + +.badge { + background: rgba(29, 185, 84, 0.15); + color: var(--green); + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 700; +} + +/* =============================== + INPUTS + =============================== */ +.input { + width: 100%; + background: var(--bg-input); + color: var(--text); + border: 1px solid transparent; + padding: 10px 16px; + border-radius: var(--r-sm); + outline: none; + transition: background-color 0.15s, border-color 0.15s; +} + +.input.pill { border-radius: var(--r-pill); } + +.input::placeholder { color: var(--text-3); } + +.input:focus { + background: #333; + border-color: rgba(29, 185, 84, 0.4); +} + +.input-sm { max-width: 200px; } + +select.input { + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: right 14px center; + background-size: 12px; + padding-right: 36px; +} + +input[type="number"] { -moz-appearance: textfield; } +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.path-display { + flex: 1; + background: var(--bg-input); + color: var(--text); + padding: 10px 16px; + border-radius: var(--r-pill); + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.path-display:empty, +.path-display.empty { color: var(--text-3); } + +/* =============================== + BUTTONS + =============================== */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border: none; + cursor: pointer; + padding: 10px 22px; + font-weight: 700; + border-radius: var(--r-sm); + white-space: nowrap; + transition: transform 0.06s ease, background-color 0.15s, color 0.15s; +} + +.btn.pill { border-radius: var(--r-pill); } +.btn.btn-sm { padding: 8px 16px; font-size: 12px; } +.btn.btn-lg { padding: 14px 32px; font-size: 14px; } + +.btn:active { transform: scale(0.96); } +.btn:disabled { cursor: not-allowed; opacity: 0.4; } +.btn:not(:disabled):hover { filter: brightness(1.06); } + +.btn-primary { + background: var(--green); + color: #000; +} + +.btn-primary:not(:disabled):hover { + background: var(--green-hover); + transform: scale(1.02); +} + +.btn-blue { + background: var(--blue); + color: #fff; +} + +.btn-purple { + background: var(--purple); + color: #fff; +} + +.btn-success { + background: #00CEC9; + color: #000; + margin-top: 8px; +} + +.btn-danger { + background: transparent; + color: var(--red); + border: 1px solid var(--red); +} + +.btn-danger:not(:disabled):hover { + background: var(--red); + color: #fff; +} + +.btn-ghost { + background: var(--bg-input); + color: var(--text); +} + +.btn-ghost:not(:disabled):hover { + background: var(--bg-input-hover); +} + +.ico { + font-size: 12px; + line-height: 1; +} + +/* =============================== + COUNTER / PROGRESS + =============================== */ +.counter { + color: var(--text-2); + font-size: 13px; + font-weight: 600; + margin-left: auto; +} + +.progress-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.micro-label { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.8px; + color: var(--text-2); + text-transform: uppercase; +} + +.percent { + font-size: 14px; + font-weight: 800; +} + +.percent.green { color: var(--green); } +.percent.blue { color: var(--blue); } + +.progress { + width: 100%; + height: 6px; + background: var(--bg-input); + border-radius: 3px; + overflow: hidden; +} + +.progress-fill { + width: 0%; + height: 100%; + border-radius: 3px; + transition: width 0.2s ease-out; +} + +.progress-fill.green { + background: linear-gradient(90deg, var(--green), var(--green-hover)); +} + +.progress-fill.blue { + background: linear-gradient(90deg, var(--blue), var(--blue-hover)); +} + +/* =============================== + LOG BOX + =============================== */ +.log { + background: var(--bg-card-2); + border-radius: var(--r-sm); + padding: 14px 16px; + font-family: "SF Mono", Menlo, Consolas, monospace; + font-size: 12px; + color: var(--text-2); + height: 220px; + overflow-y: auto; + white-space: pre-wrap; + user-select: text; + line-height: 1.6; +} + +.log .l-ok { color: var(--green); } +.log .l-err { color: var(--red); } +.log .l-info { color: var(--blue); } +.log .l-warn { color: #FDCB6E; } +.log .l-muted { color: var(--text-3); } + +/* =============================== + CHECKBOX + =============================== */ +.checkbox { + display: inline-flex; + align-items: center; + gap: 10px; + cursor: pointer; + color: var(--text); + font-size: 13px; + font-weight: 600; + margin: 6px 0 14px; + user-select: none; +} + +.checkbox input { display: none; } + +.check-box { + width: 18px; + height: 18px; + border-radius: 4px; + background: var(--bg-input); + border: 1px solid var(--bg-input-hover); + position: relative; + transition: background-color 0.15s, border-color 0.15s; +} + +.checkbox input:checked + .check-box { + background: var(--blue); + border-color: var(--blue); +} + +.checkbox input:checked + .check-box::after { + content: ""; + position: absolute; + left: 5px; + top: 1px; + width: 5px; + height: 11px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +/* =============================== + INFO ROW (Upgrade tab) + =============================== */ +.info-row { + color: var(--green); + font-size: 13px; + font-weight: 700; + margin-bottom: 14px; + min-height: 18px; +} + +/* =============================== + SETTINGS + =============================== */ +.version-line { + color: var(--text-2); + font-size: 13px; +} + +.strong { color: var(--text); font-weight: 700; } + +.update-status { + font-size: 13px; + font-weight: 600; + margin-top: 12px; + min-height: 18px; +} + +.update-status.ok { color: var(--green); } +.update-status.warn { color: #FDCB6E; } +.update-status.err { color: var(--red); } + +.update-notes { + color: var(--text-3); + font-size: 12px; + margin-top: 8px; + white-space: pre-line; +} + +.save-row { + display: flex; + align-items: center; + gap: 16px; + margin: 20px 0 32px; +} + +.save-status { + color: var(--green); + font-weight: 700; + font-size: 13px; + opacity: 0; + transition: opacity 0.25s; +} + +.save-status.show { opacity: 1; } + +/* =============================== + MODAL + =============================== */ +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.65); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + animation: fadeIn 0.18s ease both; +} + +.modal-backdrop[hidden] { display: none; } + +.modal { + background: var(--bg-card); + border-radius: var(--r-lg); + width: min(560px, calc(100% - 40px)); + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: var(--shadow-lg); + overflow: hidden; +} + +.modal-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18px 22px; + border-bottom: 1px solid var(--divider); +} + +.modal-head h2 { + font-size: 17px; + font-weight: 800; +} + +.modal-close { + background: transparent; + border: none; + color: var(--text-2); + font-size: 26px; + cursor: pointer; + line-height: 1; + padding: 0 6px; +} + +.modal-close:hover { color: var(--text); } + +.modal-body { + padding: 18px 22px; + font-family: "SF Mono", Menlo, Consolas, monospace; + font-size: 13px; + color: var(--text-2); + white-space: pre-wrap; + overflow-y: auto; + line-height: 1.6; + user-select: text; + flex: 1; +} + +.modal-foot { + display: flex; + gap: 10px; + padding: 14px 22px; + border-top: 1px solid var(--divider); +} + +/* =============================== + TOAST + =============================== */ +.toast { + position: fixed; + bottom: 32px; + left: 50%; + transform: translate(-50%, 100px); + background: var(--bg-card); + border: 1px solid var(--border); + color: var(--text); + padding: 14px 22px; + border-radius: var(--r-pill); + font-weight: 600; + font-size: 13px; + box-shadow: var(--shadow-lg); + opacity: 0; + transition: transform 0.3s cubic-bezier(0.2, 0.7, 0.3, 1), opacity 0.3s; + z-index: 300; + pointer-events: none; +} + +.toast.show { + opacity: 1; + transform: translate(-50%, 0); +} + +.toast.toast-error { border-color: var(--red); color: var(--red); } +.toast.toast-success { border-color: var(--green); color: var(--green); } + +/* =============================== + SCROLLBARS (WebKit) + =============================== */ +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background: #404040; + border-radius: 5px; + border: 2px solid var(--bg); +} +::-webkit-scrollbar-thumb:hover { background: #606060; } + +.log::-webkit-scrollbar-thumb { border-color: var(--bg-card-2); } +.main::-webkit-scrollbar-thumb { border-color: var(--bg); } diff --git a/webui/index.html b/webui/index.html new file mode 100644 index 0000000..9487148 --- /dev/null +++ b/webui/index.html @@ -0,0 +1,291 @@ + + + + + + MusicDownload + + + +

+ + + + + +
+ + +
+
+
+
DOWNLOAD
+

Scarica la tua musica

+

Spotify · YouTube · SoundCloud · qualsiasi link supportato da yt-dlp

+
+
♫
+
+ + +
+
+ +
+ + +
+
+ +
+ +
+
Nessuna cartella selezionata
+ +
+
+
+ +
+ + +
+
+ +
+
+ Progresso + 0% +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
UPGRADE QUALITÀ
+

Migliora i tuoi audio

+

Riscarica i file a bitrate più alto. Imposta la soglia minima accettabile.

+
+
⚡
+
+ + +
+
+
+
Nessuna cartella selezionata
+ +
+
+ + + +
+ +
+ + +
+
+ +
+ + +
+
+ +
+
+ Progresso + 0% +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
IMPOSTAZIONI
+

Configurazione

+

Credenziali Spotify, qualità audio, percorsi e aggiornamenti.

+
+
⚙
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ Versione corrente: v1.0 +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+
+ + + + + +
+ + + + diff --git a/webui/js/app.js b/webui/js/app.js new file mode 100644 index 0000000..824a656 --- /dev/null +++ b/webui/js/app.js @@ -0,0 +1,405 @@ +/* ====================================================== + MusicDownload — Frontend (vanilla JS) + ====================================================== */ + +const $ = (sel) => document.querySelector(sel); +const $$ = (sel) => Array.from(document.querySelectorAll(sel)); + +const state = { + config: {}, + urlList: [], + dlOutputDir: "", + upDir: "", + showSecret: false, + downloading: false, + upgrading: false, +}; + +// Wait for pywebview ready +function waitApi() { + return new Promise((resolve) => { + if (window.pywebview && window.pywebview.api) return resolve(); + window.addEventListener("pywebviewready", resolve, { once: true }); + }); +} + +// ============================================================ +// Bridge — eventi spinti dal backend +// ============================================================ +window.bridge = { + emit(channel, payload) { + const handler = bridgeHandlers[channel]; + if (handler) handler(payload); + }, +}; + +const bridgeHandlers = { + "log": ({ view, msg }) => appendLog(view, msg), + + "download:progress": (p) => { + if (typeof p.overall === "number") { + const pct = Math.round(p.overall * 100); + $("#dlProgressFill").style.width = pct + "%"; + $("#dlPercent").textContent = pct + "%"; + } + if (p.status === "searching" && typeof p.idx === "number") { + const prefix = p.url_total > 1 ? `[${(p.url_idx ?? 0) + 1}/${p.url_total}] ` : ""; + $("#dlCounter").textContent = `${prefix}Brano ${p.idx + 1} di ${p.total}`; + } + if (p.status === "completed" && p.url_total > 1) { + $("#dlCounter").textContent = `Completato: ${p.url_total} URL`; + } + }, + + "download:done": () => { + state.downloading = false; + state.urlList = []; + $("#urlListBadge").hidden = true; + $("#urlListBadge").textContent = ""; + $("#downloadBtn").disabled = false; + $("#loadListBtn").disabled = false; + $("#stopDownloadBtn").disabled = true; + }, + + "upgrade:progress": (p) => { + if (typeof p.overall === "number") { + const pct = Math.round(p.overall * 100); + $("#upProgressFill").style.width = pct + "%"; + $("#upPercent").textContent = pct + "%"; + } + if (typeof p.idx === "number" && typeof p.total === "number" && p.total > 0) { + $("#upCounter").textContent = `File ${p.idx} di ${p.total}`; + } + if (p.status === "completed") { + $("#upCounter").textContent = `Completato: ${p.total} file`; + } + }, + + "upgrade:done": () => { + state.upgrading = false; + $("#upgradeBtn").disabled = false; + $("#stopUpgradeBtn").disabled = true; + }, +}; + +// ============================================================ +// Logging +// ============================================================ +const logEls = { download: () => $("#dlLog"), upgrade: () => $("#upLog") }; + +function classifyLog(msg) { + if (msg.startsWith("[ERRORE]")) return "l-err"; + if (msg.startsWith("[OK]") || msg.startsWith("[UPGRADE]")) return "l-ok"; + if (msg.startsWith("[INFO]")) return "l-info"; + if (msg.startsWith("[SKIP]") || msg.startsWith("[CERCA]") || msg.startsWith("[DOWNLOAD]") || msg.startsWith("[CONV]")) return "l-muted"; + if (msg.startsWith("[NON TROVATO]")) return "l-warn"; + if (msg.startsWith("[LISTA]")) return "l-info"; + return ""; +} + +function appendLog(view, msg) { + const el = logEls[view] && logEls[view](); + if (!el) return; + const cls = classifyLog(msg); + const line = document.createElement("div"); + if (cls) line.className = cls; + line.textContent = msg; + el.appendChild(line); + el.scrollTop = el.scrollHeight; +} + +// ============================================================ +// Toast +// ============================================================ +let toastTimer = null; +function toast(msg, kind = "") { + const el = $("#toast"); + el.textContent = msg; + el.className = "toast show " + (kind ? `toast-${kind}` : ""); + clearTimeout(toastTimer); + toastTimer = setTimeout(() => { el.className = "toast"; }, 3000); +} + +// ============================================================ +// Routing (sidebar) +// ============================================================ +function showView(name) { + $$(".view").forEach((v) => v.classList.toggle("active", v.id === `view-${name}`)); + $$(".nav-item").forEach((b) => b.classList.toggle("active", b.dataset.view === name)); +} + +$$(".nav-item").forEach((btn) => { + btn.addEventListener("click", () => showView(btn.dataset.view)); +}); + +// ============================================================ +// Init +// ============================================================ +async function init() { + await waitApi(); + const data = await window.pywebview.api.get_init_data(); + state.config = data.config || {}; + $("#footerVersion").textContent = data.version; + $("#currentVersionLabel").textContent = data.version; + $("#guideBody").textContent = data.spotify_guide; + + // Populate Settings fields + $("#clientIdInput").value = state.config.client_id || ""; + $("#clientSecretInput").value = state.config.client_secret || ""; + $("#bitrateSelect").value = state.config.bitrate || "320K"; + $("#hqThresholdInput").value = state.config.hq_threshold || 310; + $("#cookiesInput").value = state.config.cookies_path || ""; + $("#outputInput").value = state.config.output_dir || ""; + $("#themeSelect").value = state.config.theme || "dark"; + + // Download tab — usa output_dir come default + state.dlOutputDir = state.config.output_dir || ""; + if (state.dlOutputDir) { + $("#dlPathDisplay").textContent = state.dlOutputDir; + $("#dlPathDisplay").classList.remove("empty"); + } + + // Upgrade tab — default threshold + $("#upThreshold").value = state.config.hq_threshold || 310; +} + +// ============================================================ +// DOWNLOAD tab +// ============================================================ +$("#dlBrowseBtn").addEventListener("click", async () => { + const path = await window.pywebview.api.browse_directory(); + if (path) { + state.dlOutputDir = path; + $("#dlPathDisplay").textContent = path; + $("#dlPathDisplay").classList.remove("empty"); + } +}); + +$("#loadListBtn").addEventListener("click", async () => { + const path = await window.pywebview.api.browse_file([ + "Text files (*.txt)", + "All files (*.*)", + ]); + if (!path) return; + const res = await window.pywebview.api.load_url_list(path); + if (!res.ok) { + toast("Impossibile leggere il file: " + (res.error || ""), "error"); + return; + } + if (!res.count) { + toast("Il file non contiene URL validi", "error"); + return; + } + state.urlList = res.urls; + $("#urlInput").value = path; + const badge = $("#urlListBadge"); + badge.textContent = `· ${res.count} URL caricati`; + 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; + } + + state.downloading = true; + $("#downloadBtn").disabled = true; + $("#loadListBtn").disabled = true; + $("#stopDownloadBtn").disabled = false; + $("#dlProgressFill").style.width = "0%"; + $("#dlPercent").textContent = "0%"; + + const res = await window.pywebview.api.start_download({ + urls, + output_dir: state.dlOutputDir, + }); + if (!res.ok) { + toast(res.error || "Errore", "error"); + bridgeHandlers["download:done"](); + } +}); + +$("#stopDownloadBtn").addEventListener("click", async () => { + await window.pywebview.api.stop_download(); + $("#stopDownloadBtn").disabled = true; +}); + +// ============================================================ +// UPGRADE tab +// ============================================================ +$("#upBrowseBtn").addEventListener("click", async () => { + const path = await window.pywebview.api.browse_directory(); + if (path) { + state.upDir = path; + $("#upPathDisplay").textContent = path; + $("#upPathDisplay").classList.remove("empty"); + await refreshUpgradeScan(); + } +}); + +$("#upRecursive").addEventListener("change", refreshUpgradeScan); + +async function refreshUpgradeScan() { + if (!state.upDir) return; + const res = await window.pywebview.api.scan_audio_folder(state.upDir, $("#upRecursive").checked); + const info = $("#upInfo"); + if (!res.ok) { + info.textContent = "Errore scansione: " + (res.error || ""); + return; + } + if (!res.total) { + info.textContent = "Nessun file audio trovato"; + return; + } + if (res.done > 0) { + info.textContent = `● ${res.total} file audio · ${res.done} già convertiti · ${res.remaining} da fare`; + } else { + info.textContent = `● ${res.total} file audio trovati`; + } +} + +$("#upgradeBtn").addEventListener("click", async () => { + if (!state.upDir) { + toast("Seleziona una cartella", "error"); + return; + } + state.upgrading = true; + $("#upgradeBtn").disabled = true; + $("#stopUpgradeBtn").disabled = false; + $("#upProgressFill").style.width = "0%"; + $("#upPercent").textContent = "0%"; + + const res = await window.pywebview.api.start_upgrade({ + directory: state.upDir, + recursive: $("#upRecursive").checked, + threshold: parseInt($("#upThreshold").value, 10) || 310, + }); + if (!res.ok) { + toast(res.error || "Errore", "error"); + bridgeHandlers["upgrade:done"](); + } +}); + +$("#stopUpgradeBtn").addEventListener("click", async () => { + await window.pywebview.api.stop_upgrade(); + $("#stopUpgradeBtn").disabled = true; +}); + +// ============================================================ +// SETTINGS tab +// ============================================================ +$("#toggleSecretBtn").addEventListener("click", () => { + state.showSecret = !state.showSecret; + $("#clientSecretInput").type = state.showSecret ? "text" : "password"; + $("#toggleSecretBtn").textContent = state.showSecret ? "Nascondi" : "Mostra"; +}); + +$("#browseCookiesBtn").addEventListener("click", async () => { + const path = await window.pywebview.api.browse_file([ + "Text files (*.txt)", + "All files (*.*)", + ]); + if (path) $("#cookiesInput").value = path; +}); + +$("#browseOutputBtn").addEventListener("click", async () => { + const path = await window.pywebview.api.browse_directory(); + if (path) $("#outputInput").value = path; +}); + +$("#saveBtn").addEventListener("click", async () => { + const payload = { + client_id: $("#clientIdInput").value, + client_secret: $("#clientSecretInput").value, + bitrate: $("#bitrateSelect").value, + hq_threshold: parseInt($("#hqThresholdInput").value, 10) || 310, + cookies_path: $("#cookiesInput").value, + output_dir: $("#outputInput").value, + theme: $("#themeSelect").value, + }; + const res = await window.pywebview.api.save_settings(payload); + if (res.ok) { + state.config = payload; + // sync Download tab default path + if (!state.dlOutputDir && payload.output_dir) { + state.dlOutputDir = payload.output_dir; + $("#dlPathDisplay").textContent = payload.output_dir; + $("#dlPathDisplay").classList.remove("empty"); + } + const status = $("#saveStatus"); + status.textContent = "✓ Impostazioni salvate"; + status.classList.add("show"); + setTimeout(() => status.classList.remove("show"), 2500); + } else { + toast("Errore nel salvataggio", "error"); + } +}); + +$("#openDashboardBtn").addEventListener("click", () => { + window.pywebview.api.open_external_url("https://developer.spotify.com/dashboard"); +}); + +$("#showGuideBtn").addEventListener("click", () => { + $("#guideModal").hidden = false; +}); + +$("#guideCloseBtn").addEventListener("click", () => { $("#guideModal").hidden = true; }); +$("#guideCloseBtn2").addEventListener("click", () => { $("#guideModal").hidden = true; }); +$("#guideOpenDashBtn").addEventListener("click", () => { + window.pywebview.api.open_external_url("https://developer.spotify.com/dashboard"); +}); + +$("#checkUpdateBtn").addEventListener("click", async () => { + const btn = $("#checkUpdateBtn"); + const status = $("#updateStatus"); + const notes = $("#updateNotes"); + const dlBtn = $("#downloadUpdateBtn"); + + btn.disabled = true; + btn.textContent = "Controllo..."; + status.className = "update-status"; + status.textContent = ""; + notes.textContent = ""; + dlBtn.hidden = true; + + const res = await window.pywebview.api.check_update(); + btn.disabled = false; + btn.textContent = "Controlla aggiornamenti"; + + if (!res.ok) { + status.className = "update-status err"; + status.textContent = "Errore: " + (res.error || ""); + return; + } + if (res.is_new) { + status.className = "update-status warn"; + status.textContent = `Nuova versione disponibile: ${res.remote}`; + if (res.download_url) { + dlBtn.hidden = false; + dlBtn.onclick = () => window.pywebview.api.open_external_url(res.download_url); + } + if (res.notes) notes.textContent = res.notes; + } else { + status.className = "update-status ok"; + status.textContent = "✓ Sei aggiornato!"; + } +}); + +// ============================================================ +// Boot +// ============================================================ +init().catch((e) => { + console.error(e); + toast("Errore di inizializzazione: " + e.message, "error"); +});