From 4b1524e5f793d2968c47c72fdd22ec27b36b6942 Mon Sep 17 00:00:00 2001 From: luzadev Date: Fri, 5 Jun 2026 19:37:17 +0200 Subject: [PATCH] Carica lista: auto-detect URL list vs tracklist Adesso 'Carica lista' nella tab Scarica riconosce automaticamente il formato del file .txt: - Se la prima riga e un URL (http/https) -> URL list (come prima) - Altrimenti -> tracklist parsata 'Artista - Titolo (variant) (durata)' e scaricata cercando ogni brano su YouTube (riusa download_playlist) Parser robusto: gestisce numerazione iniziale (1. / 1) / 1: / 1-), qualsiasi tipo di dash (-, en-dash, em-dash, ...) e durata finale '(M:SS)'. Testato sulla Beatport Top 100 Melodic House & Techno (100/100 tracce parsate). api/bridge.py: nuovo start_tracks_download, load_url_list ritorna 'kind: urls|tracks'. webui/js/app.js: badge cambia tra 'X URL' e 'X tracce', avvio chiama start_tracks_download quando appropriato. Co-Authored-By: Claude Opus 4.7 --- Beatport Top 100 Melodic House & Techno.txt | 100 +++++++++++++++++ api/bridge.py | 116 +++++++++++++++++++- webui/js/app.js | 53 +++++---- 3 files changed, 248 insertions(+), 21 deletions(-) create mode 100644 Beatport Top 100 Melodic House & Techno.txt diff --git a/Beatport Top 100 Melodic House & Techno.txt b/Beatport Top 100 Melodic House & Techno.txt new file mode 100644 index 0000000..f774649 --- /dev/null +++ b/Beatport Top 100 Melodic House & Techno.txt @@ -0,0 +1,100 @@ +1. Kapuchon, Miss Monique & GLZ – Hot Sauce (Extended) (5:36) +2. Brunello, Vargas – Ghost Dance (Original Mix) (6:12) +3. Hardt Antoine – Raw (Original Mix) (5:25) +4. JUNO (DE) & Samm (BE) – Heart Spin (Original Mix) (5:56) +5. AIKON – To You (Original Mix) (6:12) +6. Adam Port, Keinemusik & SG Lewis – Be The One (Extended) (4:45) +7. HotLap – Recall (Extended Mix) (5:48) +8. JUNO (DE) & Samm (BE) – Muralli (Original Mix) (4:59) +9. Brunello – Science Fiction (Original Mix) (6:36) +10. Emanuel Satie, Tim Engelhardt, Maga, Sean Doron, Unfazed, Scenarios – A Gira (Extended Remix) (5:28) +11. DJ Koze – Spiralen (Original Mix) (7:17) +12. Joris Voorn, Taylr Renee & Roddy Lima – Machine (Original Mix) (5:02) +13. KREAM & Korolova – Annihilation (Extended Mix) (4:50) +14. Bedouin, Karl Williams, LUCH – The Only One (Original Mix) (6:49) +15. Massano & Silver Panda – Pa Ca (Original Mix) (4:47) +16. Moeaike, Arodes – Nobody Else (Extended Mix) (5:58) +17. RÜFÜS DU SOL – In the Moment (Adriatique Extended Remix) (5:12) +18. &ME, Black Coffee – The Rapture Pt.III (Original Mix) (8:02) +19. Liva K – Didn’t Miss You (Original Mix) (5:46) +20. Carlo Whale & TH;EN – Echoes of You (Original Mix) (5:22) +21. Magnus, JEWELS, YUMA., SOMMA, LE YORA – EVERYTHING IN ITS RIGHT PLACE (Original Mix) (7:14) +22. Andrea Oliva – Neon Hearts (Original Mix) (6:06) +23. Jackie Hollander – High On You (Extended Mix) (4:57) +24. KREAM, SCRIPT – Turn Up The Dose (Extended Mix) (4:20) +25. JOSEPH (CH) – B4U (Original Mix) (8:00) +26. Ajna (BE), Misha – Body Moving (Original Mix) (6:16) +27. Green Velvet & Harvard Bass – Lazer Beams (Adam Beyer & Massano Remix) (5:02) +28. Argy, Omiki & Lell Nahar – MANTRA (Extended Mix) (5:03) +29. Sasha & Franky Wah – Get On My Level (Original Mix) (8:00) +30. LUCH – UH HUH (Original Mix) (6:45) +31. Fahlberg – Feeling Good (Extended Mix) (6:49) +32. Ivory (IT) – Back Again (Original Mix) (5:42) +33. Massano, Y Do I – Lost My Mind (Original Mix) (5:23) +34. KOWSKI, Eric Reyes – Break Down! (Original Mix) (5:30) +35. Andrea Oliva – Spotlight (Original Mix) (6:21) +36. Tiga vs Meduza – You Gonna Want Me (I Know) (Extended Mix) (5:21) +37. Marsh – Beside (Extended Mix) (6:18) +38. Moderat, &ME, Rampa & Keinemusik – More Love (Rampa &ME Remix) (4:55) +39. Adriatique & Emmit Fenn – Closer (Extended Mix) (4:53) +40. Jimi Jules, Black Coffee – Trippy Yeah (Extended Mix) (6:40) +41. Liva K & OVEOUS – Blessings (Original Mix) (6:35) +42. Township Rebellion, Kasablanca – It’s Chemical (Original Mix) (6:28) +43. MAXI MERAKI, Sharing-DB.club – First Time (Original Mix) (5:19) +44. Pole Position – Touch Me (Hotlap Remix) (5:40) +45. Da Hool, Cassian & YOTTO – Love Parade (Extended Mix) (4:09) +46. Jody Wisternoff, Jamie McDermott – Citadel (Extended Mix) (5:17) +47. Super Flu, Dennis Louvra – Can’t Escape (Original Mix) (5:56) +48. Armin van Buuren, Argy, Marlo Rex – Like A Child (Extended Mix) (4:08) +49. Robert Babicz – The Wave (Original Mix) (5:08) +50. The Temper Trap – Sweet Disposition (John Summit & Silver Panda Extended Remix) (5:44) +51. Yotto, Tallac – Chemtrail Surfers (Extended Mix) (6:26) +52. HotLap – Waitin’ (Extended Mix) (6:35) +53. Miss Monique, Nicolas Taboada & FRANCO BA – Ghosts (Extended Mix) (5:28) +54. Picca & Mars & Moeaike – Move in Time (Original Mix) (5:39) +55. Hardt Antoine – Sing It Back (Extended Mix) (6:57) +56. Shakedown, Layton Giordani & Anyma (ofc) – At Night (Anyma x Layton Giordani Extended Remix) (4:35) +57. Acrobatik & whatlewisdoes – Lamazi (Extended Mix) (4:46) +58. Massano – Abraza (Original Mix) (5:13) +59. Alcatraz – Giv Me Luv (Jerome Isma-Ae Remix) (7:18) +60. Gespona – Komplex (Original Mix) (5:56) +61. Misha & Martim Rola – On Business (Extended Mix) (6:30) +62. Mind Against, Anakim & HUMAN404 – Late at Night (Extended Mix) (4:39) +63. Anyma (ofc), Argy & Son of Son – Voices In My Head (Extended Mix) (4:16) +64. Fred again.., Swedish House Mafia, Future – Turn On The Lights again.. (feat. Future) (Original Mix) (4:27) +65. Sezer Uysal & Axelax – Calling For More (Original Mix) (5:12) +66. LUCH, Marten Lou – Hideaway (Extended Version) (5:57) +67. Giuseppe Ottaviani, ARTBAT & Conor Ross – Break The Loop (Original Mix) (5:22) +68. Rhodes, CamelPhat, ARTBAT – For a Feeling (Adam Sellouk & Y do I Remix – Extended) (4:49) +69. Monolink, Artbat – Return to Oz (ARTBAT Remix) (8:00) +70. Lisa & Anyma (ofc) – Bad Angel (with Lisa) (Extended Mix) (4:08) +71. Florist (BE) – Unite (Original Mix) (6:25) +72. Moderat – Eating Hooks (Deep Dish Extended Remix) (7:13) +73. CamelPhat & Arodes – Cycles (Extended Mix) (6:52) +74. Interplanetary Criminal, Kettama – Yosemite (Extended) (6:02) +75. Leblanc (ofc) – Strings Anthem (Original Mix) (5:22) +76. Mind Against & Jos & Eli – Overture (Original Mix) (5:21) +77. Tripolism & Kingstarlight – Hurricane (Extended Mix) (4:49) +78. Luke Alexander – Last Night (feat. Y4MI) (Original Mix) (3:30) +79. Braxton – There’s Still Time (Extended Mix) (6:31) +80. Township Rebellion – Birds Fly First Class (Original Mix) (6:19) +81. Agents Of Time & Miss Monique – Rajada (Original Mix) (5:00) +82. Fancy Inc – Hypnotic (Extended Mix) (5:09) +83. Boys Noize, &ME, Rampa, Adam Port, Keinemusik, Vinson – Crazy For It (feat. Vinson) (Original Mix) (6:01) +84. Space Motion & Luca Belluco – Baila Conmigo (Original Mix) (4:38) +85. Tom Westy & Dansyn – Feel Your Soul (Extended) (4:33) +86. CamelPhat & Josh Gigante – The One (Extended Mix) (5:32) +87. Argy & Omnya – Aria (Extended Mix) (5:14) +88. Air & Tur, blou., Airsand, TuraniQa – Teach Me (Extended Mix) (4:35) +89. Dilby, Martin Ka – Moving Trough (Extened Mix) (6:16) +90. Pablo Say – Repique (Original Mix) (4:48) +91. Orsay & Kotiēr – 1886 (Original Mix) (5:46) +92. Adriatique & Elderbrook – Lost In The Woods (Extended Mix) (5:01) +93. Ajna (BE) & Sentin – Bringing The Noise (Original Mix) (5:45) +94. Eric Prydz – Pjanoo (Club Mix) (7:30) +95. Eelke Kleijn – Feel It, See It (Extended Mix) (6:07) +96. Aera – Litoral (Original Mix) (6:10) +97. Julian Koerndl – Horses (Original Mix) (6:09) +98. Supermode – Tell Me Why (MEDUZA Extended Remix) (8:28) +99. Laherte – Pump Up The Jam (Extended Mix) (3:58) +100. M.O.S. – Changes (Extended Mix) (6:43) \ No newline at end of file diff --git a/api/bridge.py b/api/bridge.py index eb069ce..6babf5c 100644 --- a/api/bridge.py +++ b/api/bridge.py @@ -4,6 +4,7 @@ from __future__ import annotations import json import os +import re import threading import time import webbrowser @@ -49,6 +50,30 @@ def _js_safe(value: Any) -> str: return json.dumps(value, ensure_ascii=False) +_NUM_PREFIX = re.compile(r"^\s*\d+\s*[\.\)\:\-]\s*") +_DURATION_TAIL = re.compile(r"\s*\(\s*\d{1,2}:\d{2}\s*\)\s*$") +# Tutti i tipi di trattini: hyphen, en-dash, em-dash, horizontal bar, minus +_DASH_SPLIT = re.compile(r"\s+[‐‑‒–—―−\-]\s+") + + +def _parse_track_line(line: str) -> Optional[dict]: + """Parse 'N. Artista - Titolo (variant) (durata)' -> {name, artist}. + Ritorna None se la riga non e parseabile.""" + s = line.strip() + if not s: + return None + s = _NUM_PREFIX.sub("", s) + s = _DURATION_TAIL.sub("", s).strip() + parts = _DASH_SPLIT.split(s, maxsplit=1) + if len(parts) < 2: + return None + artist = parts[0].strip() + name = parts[1].strip() + if not artist or not name: + return None + return {"name": name, "artist": artist} + + class Api: """Espone i metodi a JavaScript tramite pywebview js_api.""" @@ -167,10 +192,25 @@ class Api: return "" def load_url_list(self, path: str) -> dict: + """Carica un file .txt. Riconosce automaticamente se contiene URL + oppure una tracklist 'Artista - Titolo'.""" try: with open(path, "r", encoding="utf-8") as f: - urls = [line.strip() for line in f if line.strip() and not line.strip().startswith("#")] - return {"ok": True, "urls": urls, "count": len(urls)} + lines = [line.strip() for line in f if line.strip() and not line.strip().startswith("#")] + if not lines: + return {"ok": True, "kind": "urls", "urls": [], "count": 0} + + # Detect: se la prima riga e un URL trattiamo tutto come URL list + if lines[0].lower().startswith(("http://", "https://")): + return {"ok": True, "kind": "urls", "urls": lines, "count": len(lines)} + + # Altrimenti parse come tracklist + tracks = [] + for line in lines: + t = _parse_track_line(line) + if t: + tracks.append(t) + return {"ok": True, "kind": "tracks", "tracks": tracks, "count": len(tracks)} except Exception as e: return {"ok": False, "error": str(e)} @@ -215,11 +255,83 @@ class Api: self._download_thread.start() return {"ok": True} + def start_tracks_download(self, payload: dict) -> dict: + """Avvia il download di una tracklist gia parsata + (lista di {name, artist}).""" + if self._any_job_running(): + return {"ok": False, "error": "Un download gia in corso"} + + tracks = payload.get("tracks") or [] + output_dir = (payload.get("output_dir") or "").strip() + if not tracks: + return {"ok": False, "error": "Nessuna traccia fornita"} + if not output_dir: + return {"ok": False, "error": "Cartella output non impostata"} + + self._download_thread = threading.Thread( + target=self._tracks_worker, + args=(list(tracks), output_dir), + daemon=True, + ) + self._download_thread.start() + return {"ok": True} + def stop_download(self) -> dict: request_download_stop() self._log("download", "[INFO] Interruzione richiesta...") return {"ok": True} + def _tracks_worker(self, tracks: list, output_dir: str) -> None: + reset_download_stop() + cfg = load_config() + bitrate = cfg.get("bitrate", "320K") + cookies_path = cfg.get("cookies_path", "") + view = "download" + + self._log(view, f"[INFO] Tracklist: {len(tracks)} brani da cercare su YouTube") + + _last = [0.0] + _THROTTLE = 0.10 + + def progress_cb(idx, total, track_name, status, pct): + if status == "downloading": + now = time.monotonic() + if now - _last[0] < _THROTTLE: + return + _last[0] = now + + payload_evt = { + "idx": idx, "total": total, "track": track_name, + "status": status, "pct": pct, + "url_idx": 0, "url_total": 1, + } + + if status == "searching": + self._log(view, f"[CERCA] {track_name}") + elif status == "skipped": + self._log(view, f"[SKIP] {track_name} (gia scaricato)") + track_progress = (idx + 1) / total if total > 0 else 1 + payload_evt["overall"] = min(track_progress, 1.0) + elif status == "downloading": + track_progress = (idx / total) + (pct / 100 / total) if total > 0 else 0 + payload_evt["overall"] = min(track_progress, 1.0) + elif status == "done": + self._log(view, f"[OK] {track_name}") + track_progress = (idx + 1) / total if total > 0 else 1 + payload_evt["overall"] = min(track_progress, 1.0) + elif status == "stopped": + self._log(view, "[INFO] Download interrotto.") + elif status == "completed": + payload_evt["overall"] = 1.0 + self._log(view, "[INFO] Download completato!") + elif status.startswith("error"): + self._log(view, f"[ERRORE] {track_name}: {status}") + + self._emit("download:progress", payload_evt) + + download_playlist(tracks, output_dir, bitrate, cookies_path, progress_cb) + self._emit("download:done", {"ok": True}) + def _download_worker(self, urls: list, output_dir: str) -> None: reset_download_stop() cfg = load_config() diff --git a/webui/js/app.js b/webui/js/app.js index 21f26c0..2aacb53 100644 --- a/webui/js/app.js +++ b/webui/js/app.js @@ -7,7 +7,7 @@ const $$ = (sel) => Array.from(document.querySelectorAll(sel)); const state = { config: {}, - urlList: [], + loaded: null, // { kind: "urls"|"tracks", urls?, tracks?, count } dlOutputDir: "", upDir: "", videoOutputDir: "", @@ -55,7 +55,7 @@ const bridgeHandlers = { "download:done": () => { state.downloading = false; - state.urlList = []; + state.loaded = null; $("#urlListBadge").hidden = true; $("#urlListBadge").textContent = ""; $("#downloadBtn").disabled = false; @@ -221,27 +221,23 @@ $("#loadListBtn").addEventListener("click", async () => { return; } if (!res.count) { - toast("Il file non contiene URL validi", "error"); + toast("Il file non contiene voci valide", "error"); return; } - state.urlList = res.urls; + state.loaded = res; $("#urlInput").value = path; const badge = $("#urlListBadge"); - badge.textContent = `· ${res.count} URL caricati`; + if (res.kind === "tracks") { + badge.textContent = `· ${res.count} tracce`; + appendLog("download", `[INFO] Caricata tracklist: ${res.count} brani`); + } else { + badge.textContent = `· ${res.count} URL`; + appendLog("download", `[INFO] Caricati ${res.count} URL dal file`); + } badge.hidden = false; - appendLog("download", `[INFO] Caricati ${res.count} URL dal file`); }); $("#downloadBtn").addEventListener("click", async () => { - let urls = state.urlList; - if (!urls.length) { - const single = $("#urlInput").value.trim(); - if (!single) { - toast("Inserisci un URL o carica una lista", "error"); - return; - } - urls = [single]; - } if (!state.dlOutputDir) { toast("Seleziona una cartella di destinazione", "error"); return; @@ -254,10 +250,29 @@ $("#downloadBtn").addEventListener("click", async () => { $("#dlProgressFill").style.width = "0%"; $("#dlPercent").textContent = "0%"; - const res = await window.pywebview.api.start_download({ - urls, - output_dir: state.dlOutputDir, - }); + let res; + if (state.loaded && state.loaded.kind === "tracks") { + res = await window.pywebview.api.start_tracks_download({ + tracks: state.loaded.tracks, + output_dir: state.dlOutputDir, + }); + } else { + let urls = state.loaded && state.loaded.kind === "urls" ? state.loaded.urls : []; + if (!urls.length) { + const single = $("#urlInput").value.trim(); + if (!single) { + toast("Inserisci un URL o carica una lista", "error"); + bridgeHandlers["download:done"](); + return; + } + urls = [single]; + } + res = await window.pywebview.api.start_download({ + urls, + output_dir: state.dlOutputDir, + }); + } + if (!res.ok) { toast(res.error || "Errore", "error"); bridgeHandlers["download:done"]();