Compare commits

..
2 Commits
Author SHA1 Message Date
luzadevandClaude Opus 4.7 05a2a58ace Tab Scarica: ricerca diretta per titolo o "Artista - Titolo"
L'input URL ora accetta anche query libere: se non sembra un link
(http/https/spotify:/dominio noto), il testo viene parsato come
{artist, name} e scaricato via download_playlist con una traccia.

Riusa lo stesso flusso ytsearch1: gia in uso per le tracklist da file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 23:13:10 +02:00
luzadevandClaude Opus 4.7 8610f4d95c Fix Windows: nascondi finestra cmd per ogni subprocess yt-dlp/ffmpeg
Aggiunto helper subprocess_flags() in core/paths.py che ritorna
CREATE_NO_WINDOW su Windows. Applicato a tutte le subprocess.run/Popen
in downloader, upgrader e recorder per evitare il flash di console nera
ad ogni traccia scaricata.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 23:06:59 +02:00
7 changed files with 77 additions and 14 deletions

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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:
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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">
+33 -1
View File
@@ -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;
} }
if (looksLikeUrl(single)) {
urls = [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,