Carica lista: auto-detect URL list vs tracklist
Adesso 'Carica lista' nella tab Scarica riconosce automaticamente il formato del file .txt: - Se la prima riga e un URL (http/https) -> URL list (come prima) - Altrimenti -> tracklist parsata 'Artista - Titolo (variant) (durata)' e scaricata cercando ogni brano su YouTube (riusa download_playlist) Parser robusto: gestisce numerazione iniziale (1. / 1) / 1: / 1-), qualsiasi tipo di dash (-, en-dash, em-dash, ...) e durata finale '(M:SS)'. Testato sulla Beatport Top 100 Melodic House & Techno (100/100 tracce parsate). api/bridge.py: nuovo start_tracks_download, load_url_list ritorna 'kind: urls|tracks'. webui/js/app.js: badge cambia tra 'X URL' e 'X tracce', avvio chiama start_tracks_download quando appropriato. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
e5158d382f
commit
4b1524e5f7
3 files changed
+248
-21
No files matched your search
@@ -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)
|
||||
+114
-2
@@ -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()
|
||||
|
||||
+34
-19
@@ -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"]();
|
||||
|
||||
Reference in new issue
Block a user