Video tab: modalita 'Solo audio' (estrazione MP3)

Aggiunto toggle Video/Solo audio nella tab Video. In modalita audio
yt-dlp scarica solo bestaudio ed estrae in MP3 al bitrate impostato
in Impostazioni (default 320 kbps), con embed thumbnail come la tab
Scarica.

- core/downloader.py: download_video() accetta audio_only e
  audio_bitrate; quando True usa format 'bestaudio/best' e
  --extract-audio --audio-format mp3
- api/bridge.py: start_video_download passa audio_only nel worker;
  log mostra 'Solo audio MP3 320K' vs 'Video 1080p'
- webui: segmented control 'Video (MP4)' / 'Solo audio (MP3)' rosa,
  selettore qualita nascosto in modalita audio, hint aggiornato

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
luzadevandClaude Opus 4.7 committed 2026-06-08 11:11:50 +02:00
1 parent 39c92f44f4
commit 6f5c32165a
5 files changed
+95 -18

No files matched your search

+9 -4
View File
@@ -640,6 +640,7 @@ class Api:
urls = payload.get("urls") or [] urls = payload.get("urls") or []
output_dir = (payload.get("output_dir") or "").strip() output_dir = (payload.get("output_dir") or "").strip()
quality = payload.get("quality") or "1080p" quality = payload.get("quality") or "1080p"
audio_only = bool(payload.get("audio_only", False))
if not urls: if not urls:
return {"ok": False, "error": "Nessun URL fornito"} return {"ok": False, "error": "Nessun URL fornito"}
@@ -648,7 +649,7 @@ class Api:
self._video_thread = threading.Thread( self._video_thread = threading.Thread(
target=self._video_worker, target=self._video_worker,
args=(list(urls), output_dir, quality), args=(list(urls), output_dir, quality, audio_only),
daemon=True, daemon=True,
) )
self._video_thread.start() self._video_thread.start()
@@ -659,10 +660,12 @@ class Api:
self._log("video", "[INFO] Interruzione richiesta...") self._log("video", "[INFO] Interruzione richiesta...")
return {"ok": True} return {"ok": True}
def _video_worker(self, urls: list, output_dir: str, quality: str) -> None: def _video_worker(self, urls: list, output_dir: str, quality: str,
audio_only: bool = False) -> None:
reset_download_stop() reset_download_stop()
cfg = load_config() cfg = load_config()
cookies_path = cfg.get("cookies_path", "") cookies_path = cfg.get("cookies_path", "")
bitrate = cfg.get("bitrate", "320K")
total_urls = len(urls) total_urls = len(urls)
multi = total_urls > 1 multi = total_urls > 1
view = "video" view = "video"
@@ -718,9 +721,11 @@ class Api:
self._emit("video:progress", payload_evt) self._emit("video:progress", payload_evt)
self._log(view, f"[INFO] Video: {url}") mode_lbl = f"Solo audio MP3 {bitrate}" if audio_only else f"Video {quality}"
self._log(view, f"[INFO] {mode_lbl}: {url}")
try: try:
download_video(url, output_dir, quality, cookies_path, progress_cb) download_video(url, output_dir, quality, cookies_path, progress_cb,
audio_only=audio_only, audio_bitrate=bitrate)
except Exception as e: except Exception as e:
self._log(view, f"[ERRORE] {e}") self._log(view, f"[ERRORE] {e}")
continue continue
+32 -13
View File
@@ -418,10 +418,14 @@ def download_video(
quality: str = "1080p", quality: str = "1080p",
cookies_path: Optional[str] = None, cookies_path: Optional[str] = None,
progress_callback: Optional[Callable] = None, progress_callback: Optional[Callable] = None,
audio_only: bool = False,
audio_bitrate: str = "320K",
) -> None: ) -> None:
"""Scarica video (con audio) da YouTube/TikTok/Instagram/Facebook/etc. """Scarica video (con audio) o solo audio da YouTube/TikTok/Instagram/Facebook/etc.
quality: "best" | "1080p" | "720p" | "480p" quality: "best" | "1080p" | "720p" | "480p" (ignorato se audio_only=True)
audio_only: True -> estrae solo audio in MP3
audio_bitrate: bitrate MP3 quando audio_only=True
Per Instagram/Facebook privati servono cookies validi. Per Instagram/Facebook privati servono cookies validi.
""" """
global _current_process global _current_process
@@ -461,7 +465,7 @@ def download_video(
return return
total = len(entries) total = len(entries)
fmt = _video_format(quality) fmt = _video_format(quality) if not audio_only else "bestaudio/best"
for i, entry in enumerate(entries): for i, entry in enumerate(entries):
if is_stopped(): if is_stopped():
@@ -483,16 +487,31 @@ def download_video(
if progress_callback: if progress_callback:
progress_callback(i, total, title, "downloading", 0) progress_callback(i, total, title, "downloading", 0)
cmd = [ if audio_only:
ytdlp, cmd = [
"-f", fmt, ytdlp,
"--merge-output-format", "mp4", "-f", fmt,
"--add-metadata", "--extract-audio",
"--no-check-certificates", "--audio-format", "mp3",
"--newline", "--audio-quality", audio_bitrate.rstrip("Kk"),
"--output", str(Path(output_dir) / "%(title)s [%(id)s].%(ext)s"), "--embed-thumbnail",
entry_url, "--add-metadata",
] "--no-check-certificates",
"--newline",
"--output", str(Path(output_dir) / "%(title)s [%(id)s].%(ext)s"),
entry_url,
]
else:
cmd = [
ytdlp,
"-f", fmt,
"--merge-output-format", "mp4",
"--add-metadata",
"--no-check-certificates",
"--newline",
"--output", str(Path(output_dir) / "%(title)s [%(id)s].%(ext)s"),
entry_url,
]
ffmpeg_dir = find_ffmpeg_dir() ffmpeg_dir = find_ffmpeg_dir()
if ffmpeg_dir: if ffmpeg_dir:
cmd.extend(["--ffmpeg-location", ffmpeg_dir]) cmd.extend(["--ffmpeg-location", ffmpeg_dir])
+30
View File
@@ -615,6 +615,36 @@ input[type="number"]::-webkit-inner-spin-button {
.log .l-warn { color: #FDCB6E; } .log .l-warn { color: #FDCB6E; }
.log .l-muted { color: var(--text-3); } .log .l-muted { color: var(--text-3); }
/* ===============================
SEGMENTED CONTROL
=============================== */
.segmented {
display: inline-flex;
background: var(--bg-input);
padding: 4px;
border-radius: var(--r-pill);
gap: 2px;
}
.seg-btn {
border: none;
background: transparent;
color: var(--text-2);
padding: 8px 18px;
border-radius: var(--r-pill);
cursor: pointer;
font-weight: 700;
font-size: 13px;
transition: background-color 0.15s, color 0.15s;
}
.seg-btn:hover:not(.active) { color: var(--text); }
.seg-btn.active {
background: var(--pink);
color: #fff;
}
/* =============================== /* ===============================
HINT BOX HINT BOX
=============================== */ =============================== */
+12 -1
View File
@@ -144,6 +144,14 @@
</div> </div>
<div class="field inline"> <div class="field inline">
<label class="field-label">Modalità</label>
<div class="segmented" id="videoMode">
<button type="button" class="seg-btn active" data-mode="video">🎬 Video (MP4)</button>
<button type="button" class="seg-btn" data-mode="audio">🎵 Solo audio (MP3)</button>
</div>
</div>
<div class="field inline" id="videoQualityField">
<label class="field-label">Qualità</label> <label class="field-label">Qualità</label>
<select id="videoQuality" class="input pill input-sm"> <select id="videoQuality" class="input pill input-sm">
<option value="best">Migliore disponibile</option> <option value="best">Migliore disponibile</option>
@@ -155,7 +163,10 @@
<div class="hint"> <div class="hint">
<span class="hint-ico">ⓘ</span> <span class="hint-ico">ⓘ</span>
Per Instagram/Facebook privati serve un file <strong>cookies.txt</strong> valido (Impostazioni → Percorsi). <div>
In modalità <strong>Solo audio</strong> il file viene salvato in MP3 al bitrate impostato in Impostazioni → Download.<br>
Per Instagram/Facebook privati serve un file <strong>cookies.txt</strong> valido (Impostazioni → Percorsi).
</div>
</div> </div>
</div> </div>
+12
View File
@@ -375,6 +375,17 @@ $("#videoBrowseBtn").addEventListener("click", async () => {
} }
}); });
let videoMode = "video"; // "video" | "audio"
$$("#videoMode .seg-btn").forEach((b) => {
b.addEventListener("click", () => {
$$("#videoMode .seg-btn").forEach((x) => x.classList.remove("active"));
b.classList.add("active");
videoMode = b.dataset.mode;
$("#videoQualityField").style.display = videoMode === "audio" ? "none" : "";
});
});
$("#videoDownloadBtn").addEventListener("click", async () => { $("#videoDownloadBtn").addEventListener("click", async () => {
const url = $("#videoUrlInput").value.trim(); const url = $("#videoUrlInput").value.trim();
if (!url) { if (!url) {
@@ -396,6 +407,7 @@ $("#videoDownloadBtn").addEventListener("click", async () => {
urls: [url], urls: [url],
output_dir: state.videoOutputDir, output_dir: state.videoOutputDir,
quality: $("#videoQuality").value, quality: $("#videoQuality").value,
audio_only: videoMode === "audio",
}); });
if (!res.ok) { if (!res.ok) {
toast(res.error || "Errore", "error"); toast(res.error || "Errore", "error");