Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05a2a58ace | ||
|
|
8610f4d95c |
No files matched your search
+1
-1
@@ -5,7 +5,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
VERSION = "v1.5.0"
|
VERSION = "v1.5.2"
|
||||||
|
|
||||||
|
|
||||||
APP_NAME = "MusicTools"
|
APP_NAME = "MusicTools"
|
||||||
|
|||||||
+11
-6
@@ -9,7 +9,7 @@ import threading
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
from core.paths import find_ytdlp, find_ffmpeg_dir
|
from core.paths import find_ytdlp, find_ffmpeg_dir, subprocess_flags
|
||||||
|
|
||||||
|
|
||||||
# Flag globale per interruzione
|
# Flag globale per interruzione
|
||||||
@@ -102,7 +102,7 @@ def _search_youtube(query: str, cookies_path: Optional[str] = None) -> tuple[str
|
|||||||
if cookies_path and Path(cookies_path).exists():
|
if cookies_path and Path(cookies_path).exists():
|
||||||
cmd.extend(["--cookies", cookies_path])
|
cmd.extend(["--cookies", cookies_path])
|
||||||
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, **subprocess_flags())
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise ValueError(f"Ricerca fallita: {result.stderr.strip()}")
|
raise ValueError(f"Ricerca fallita: {result.stderr.strip()}")
|
||||||
|
|
||||||
@@ -146,8 +146,10 @@ def download_playlist(
|
|||||||
progress_callback(i, total, "", "stopped", 0)
|
progress_callback(i, total, "", "stopped", 0)
|
||||||
return
|
return
|
||||||
|
|
||||||
query = f"{track['name']} - {track['artist']}"
|
artist = (track.get('artist') or "").strip()
|
||||||
track_only = track['name']
|
name = (track.get('name') or "").strip()
|
||||||
|
query = f"{name} - {artist}" if artist else name
|
||||||
|
track_only = name
|
||||||
|
|
||||||
# Salta brani gia scaricati in precedenza (file di tracking)
|
# Salta brani gia scaricati in precedenza (file di tracking)
|
||||||
if query in done_set:
|
if query in done_set:
|
||||||
@@ -204,6 +206,7 @@ def download_playlist(
|
|||||||
with _process_lock:
|
with _process_lock:
|
||||||
_current_process = subprocess.Popen(
|
_current_process = subprocess.Popen(
|
||||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||||
|
**subprocess_flags(),
|
||||||
)
|
)
|
||||||
|
|
||||||
for line in _current_process.stdout:
|
for line in _current_process.stdout:
|
||||||
@@ -278,7 +281,7 @@ def download_direct_url(
|
|||||||
probe_cmd.extend(["--cookies", cookies_path])
|
probe_cmd.extend(["--cookies", cookies_path])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=60)
|
result = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=60, **subprocess_flags())
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise ValueError(result.stderr.strip())
|
raise ValueError(result.stderr.strip())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -356,6 +359,7 @@ def download_direct_url(
|
|||||||
with _process_lock:
|
with _process_lock:
|
||||||
_current_process = subprocess.Popen(
|
_current_process = subprocess.Popen(
|
||||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||||
|
**subprocess_flags(),
|
||||||
)
|
)
|
||||||
|
|
||||||
for line in _current_process.stdout:
|
for line in _current_process.stdout:
|
||||||
@@ -443,7 +447,7 @@ def download_video(
|
|||||||
probe_cmd.extend(["--cookies", cookies_path])
|
probe_cmd.extend(["--cookies", cookies_path])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=60)
|
result = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=60, **subprocess_flags())
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise ValueError(result.stderr.strip())
|
raise ValueError(result.stderr.strip())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -522,6 +526,7 @@ def download_video(
|
|||||||
with _process_lock:
|
with _process_lock:
|
||||||
_current_process = subprocess.Popen(
|
_current_process = subprocess.Popen(
|
||||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||||
|
**subprocess_flags(),
|
||||||
)
|
)
|
||||||
|
|
||||||
for line in _current_process.stdout:
|
for line in _current_process.stdout:
|
||||||
|
|||||||
@@ -48,6 +48,19 @@ def _exe(name: str) -> str:
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def subprocess_flags() -> dict:
|
||||||
|
"""Kwargs per subprocess.run/Popen che nascondono la console su Windows.
|
||||||
|
|
||||||
|
Senza CREATE_NO_WINDOW, ogni subprocess (yt-dlp, ffmpeg, ffprobe) lanciato
|
||||||
|
da un'app GUI PyInstaller apre una finestra cmd nera per la durata del
|
||||||
|
processo. Su macOS/Linux il flag non esiste, quindi ritorniamo dict vuoto.
|
||||||
|
"""
|
||||||
|
if _IS_WINDOWS:
|
||||||
|
# 0x08000000 == CREATE_NO_WINDOW
|
||||||
|
return {"creationflags": 0x08000000}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def find_ytdlp() -> str:
|
def find_ytdlp() -> str:
|
||||||
"""Trova il path di yt-dlp.
|
"""Trova il path di yt-dlp.
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -15,7 +15,7 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
from core.paths import find_ffmpeg
|
from core.paths import find_ffmpeg, subprocess_flags
|
||||||
|
|
||||||
|
|
||||||
_IS_MAC = sys.platform == "darwin"
|
_IS_MAC = sys.platform == "darwin"
|
||||||
@@ -79,6 +79,7 @@ def _list_macos(ffmpeg: str) -> list[dict]:
|
|||||||
[ffmpeg, "-hide_banner", "-f", "avfoundation",
|
[ffmpeg, "-hide_banner", "-f", "avfoundation",
|
||||||
"-list_devices", "true", "-i", ""],
|
"-list_devices", "true", "-i", ""],
|
||||||
capture_output=True, text=True, timeout=10,
|
capture_output=True, text=True, timeout=10,
|
||||||
|
**subprocess_flags(),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
@@ -116,6 +117,7 @@ def _list_windows(ffmpeg: str) -> list[dict]:
|
|||||||
[ffmpeg, "-hide_banner", "-f", "dshow",
|
[ffmpeg, "-hide_banner", "-f", "dshow",
|
||||||
"-list_devices", "true", "-i", "dummy"],
|
"-list_devices", "true", "-i", "dummy"],
|
||||||
capture_output=True, text=True, timeout=10,
|
capture_output=True, text=True, timeout=10,
|
||||||
|
**subprocess_flags(),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
@@ -247,6 +249,7 @@ def start_recording(
|
|||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=False,
|
text=False,
|
||||||
|
**subprocess_flags(),
|
||||||
)
|
)
|
||||||
|
|
||||||
drain_t = threading.Thread(target=_drain_stderr,
|
drain_t = threading.Thread(target=_drain_stderr,
|
||||||
|
|||||||
+6
-3
@@ -9,7 +9,7 @@ import threading
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
from core.paths import find_ytdlp, find_ffmpeg_dir, find_ffprobe
|
from core.paths import find_ytdlp, find_ffmpeg_dir, find_ffprobe, subprocess_flags
|
||||||
|
|
||||||
AUDIO_EXTENSIONS = {".mp3", ".m4a", ".wav", ".flac"}
|
AUDIO_EXTENSIONS = {".mp3", ".m4a", ".wav", ".flac"}
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ def get_bitrate(filepath: str | Path) -> int:
|
|||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
|
**subprocess_flags(),
|
||||||
)
|
)
|
||||||
data = json.loads(result.stdout)
|
data = json.loads(result.stdout)
|
||||||
bit_rate = int(data.get("format", {}).get("bit_rate", 0))
|
bit_rate = int(data.get("format", {}).get("bit_rate", 0))
|
||||||
@@ -94,7 +95,7 @@ def _search_youtube(query: str, cookies_path: Optional[str] = None) -> str:
|
|||||||
if cookies_path and Path(cookies_path).exists():
|
if cookies_path and Path(cookies_path).exists():
|
||||||
cmd.extend(["--cookies", cookies_path])
|
cmd.extend(["--cookies", cookies_path])
|
||||||
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, **subprocess_flags())
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise ValueError("Ricerca fallita")
|
raise ValueError("Ricerca fallita")
|
||||||
|
|
||||||
@@ -128,7 +129,7 @@ def update_cover_only(
|
|||||||
if cookies_path and Path(cookies_path).exists():
|
if cookies_path and Path(cookies_path).exists():
|
||||||
cmd.extend(["--cookies", cookies_path])
|
cmd.extend(["--cookies", cookies_path])
|
||||||
|
|
||||||
subprocess.run(cmd, capture_output=True, timeout=60)
|
subprocess.run(cmd, capture_output=True, timeout=60, **subprocess_flags())
|
||||||
|
|
||||||
cover_files = list(temp_dir.glob("cover*.jpg"))
|
cover_files = list(temp_dir.glob("cover*.jpg"))
|
||||||
if not cover_files:
|
if not cover_files:
|
||||||
@@ -152,6 +153,7 @@ def update_cover_only(
|
|||||||
],
|
],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
timeout=60,
|
timeout=60,
|
||||||
|
**subprocess_flags(),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
_cleanup_temp(temp_dir)
|
_cleanup_temp(temp_dir)
|
||||||
@@ -284,6 +286,7 @@ def upgrade_folder(
|
|||||||
with _process_lock:
|
with _process_lock:
|
||||||
_current_process = subprocess.Popen(
|
_current_process = subprocess.Popen(
|
||||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||||
|
**subprocess_flags(),
|
||||||
)
|
)
|
||||||
|
|
||||||
for line in _current_process.stdout:
|
for line in _current_process.stdout:
|
||||||
|
|||||||
+9
-2
@@ -75,15 +75,22 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="field-label">
|
<label class="field-label">
|
||||||
URL
|
URL o titolo brano
|
||||||
<span class="badge" id="urlListBadge" hidden></span>
|
<span class="badge" id="urlListBadge" hidden></span>
|
||||||
</label>
|
</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<input type="text" id="urlInput" class="input pill" placeholder="Incolla qui un link Spotify, YouTube o SoundCloud…" />
|
<input type="text" id="urlInput" class="input pill" placeholder="Link Spotify/YouTube… oppure Artista - Titolo · Titolo brano" />
|
||||||
<button class="btn btn-ghost pill" id="loadListBtn">
|
<button class="btn btn-ghost pill" id="loadListBtn">
|
||||||
<span>📂</span> Carica lista
|
<span>📂</span> Carica lista
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hint">
|
||||||
|
<span class="hint-ico">ⓘ</span>
|
||||||
|
<div>
|
||||||
|
Puoi incollare un <strong>link</strong> (Spotify, YouTube, SoundCloud…) oppure scrivere direttamente
|
||||||
|
<strong>Artista - Titolo</strong> o solo il <strong>titolo del brano</strong>: l'app cerca il match migliore su YouTube e scarica in MP3.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|||||||
+34
-2
@@ -27,6 +27,24 @@ function waitApi() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Riconosce se una stringa e un URL (incluso schema spotify:)
|
||||||
|
function looksLikeUrl(s) {
|
||||||
|
if (!s) return false;
|
||||||
|
if (/^(https?|spotify):/i.test(s)) return true;
|
||||||
|
if (s.includes("://")) return true;
|
||||||
|
// Domini comuni anche senza http (es. "youtube.com/...")
|
||||||
|
return /^(www\.)?(youtube\.com|youtu\.be|spotify\.com|soundcloud\.com|tiktok\.com|instagram\.com|facebook\.com|fb\.watch)\//i.test(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trasforma una query libera in {name, artist}.
|
||||||
|
// Formati riconosciuti: "Artista - Titolo" (anche con – o —) oppure solo "Titolo".
|
||||||
|
function parseQueryToTrack(q) {
|
||||||
|
const s = (q || "").trim();
|
||||||
|
const m = s.match(/^(.+?)\s+[-–—]\s+(.+)$/);
|
||||||
|
if (m) return { artist: m[1].trim(), name: m[2].trim() };
|
||||||
|
return { artist: "", name: s };
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Bridge — eventi spinti dal backend
|
// Bridge — eventi spinti dal backend
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -340,11 +358,25 @@ $("#downloadBtn").addEventListener("click", async () => {
|
|||||||
if (!urls.length) {
|
if (!urls.length) {
|
||||||
const single = $("#urlInput").value.trim();
|
const single = $("#urlInput").value.trim();
|
||||||
if (!single) {
|
if (!single) {
|
||||||
toast("Inserisci un URL o carica una lista", "error");
|
toast("Inserisci un URL, un titolo o carica una lista", "error");
|
||||||
bridgeHandlers["download:done"]();
|
bridgeHandlers["download:done"]();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
urls = [single];
|
if (looksLikeUrl(single)) {
|
||||||
|
urls = [single];
|
||||||
|
} else {
|
||||||
|
const track = parseQueryToTrack(single);
|
||||||
|
res = await window.pywebview.api.start_tracks_download({
|
||||||
|
tracks: [track],
|
||||||
|
output_dir: state.dlOutputDir,
|
||||||
|
subfolder: "",
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
toast(res.error || "Errore", "error");
|
||||||
|
bridgeHandlers["download:done"]();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
res = await window.pywebview.api.start_download({
|
res = await window.pywebview.api.start_download({
|
||||||
urls,
|
urls,
|
||||||
|
|||||||
Reference in new issue
Block a user