Compare commits

...
2 Commits
Author SHA1 Message Date
luciano eb20a034e6 fix: stop button e hang su ffmpeg orfano in Upgrade (bump v1.9.2)
Il worker Python era bloccato in `for line in proc.stdout` che non
ritornava mai quando il subprocess yt-dlp veniva killed ma lasciava
figli ffmpeg orfani con pipe stdout ancora aperto. Effetti:
1) Il bottone Stop non fermava l'upgrade
2) Anche il watchdog kill non liberava il thread

Fix:
- Lettura stdout in thread separato + queue.Queue
- Main loop polla ogni 500ms con timeout: controlla is_stopped() e
  proc.poll() -> Stop risponde entro 1s
- Watchdog usa kill() (SIGKILL) invece di terminate() (SIGTERM) per
  garantire cleanup child orfani
2026-08-02 11:37:40 +02:00
luciano cdd380d62f fix: watchdog su download upgrader (bump v1.9.1)
Prima: se yt-dlp si bloccava nel download di un video (geo-restricted,
YouTube throttle, rete lenta), il for loop su stdout attendeva
indefinitamente e la tab Upgrade si fermava dopo 2-3 brani.

Ora: threading.Timer da 5 min uccide il subprocess se in scaduta.
Al kill il for esce -> codice trova temp_dir vuota -> marca il brano
"download_error" e prosegue col successivo.
2026-08-02 10:55:47 +02:00
2 changed files with 77 additions and 10 deletions

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@ import os
import sys
from pathlib import Path
VERSION = "v1.9.0"
VERSION = "v1.9.2"
APP_NAME = "MusicTools"
+76 -9
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import json
import queue
import re
import subprocess
import threading
@@ -13,6 +14,10 @@ from core.paths import find_ytdlp, find_ffmpeg_dir, find_ffprobe, subprocess_fla
AUDIO_EXTENSIONS = {".mp3", ".m4a", ".wav", ".flac"}
# Timeout massimo per singolo download yt-dlp (secondi). Watchdog kill.
# Serve a evitare hang su video geo-restricted, YouTube throttle o rete lenta.
_DOWNLOAD_TIMEOUT_SEC = 300
# Flag globale per interruzione
_stop_event = threading.Event()
_current_process: Optional[subprocess.Popen] = None
@@ -288,17 +293,79 @@ def upgrade_folder(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
**subprocess_flags(),
)
proc = _current_process
for line in _current_process.stdout:
if is_stopped():
_current_process.terminate()
return
pct_match = re.search(r"(\d+(?:\.\d+)?)%", line)
if pct_match and progress_callback:
pct = int(float(pct_match.group(1)))
progress_callback(processed, total, filepath.name, "downloading", current_kbps, pct)
# Watchdog: kill del subprocess se supera _DOWNLOAD_TIMEOUT_SEC.
# Previene hang indefinito su video problematici (geo-block,
# YouTube throttle, rete lenta).
def _watchdog_kill():
try:
if proc.poll() is None:
proc.kill() # SIGKILL — SIGTERM può lasciare ffmpeg orfano che tiene aperto il pipe
except Exception:
pass
watchdog = threading.Timer(_DOWNLOAD_TIMEOUT_SEC, _watchdog_kill)
watchdog.daemon = True
watchdog.start()
# Lettore stdout in thread separato + queue: il main loop polla
# con timeout invece di bloccare su `for line in proc.stdout`.
# Cosi is_stopped() e proc.poll() vengono controllati periodicamente
# -> il bottone Stop risponde in <1s anche se il subprocess ha
# figli orfani (ffmpeg) che tengono aperto il pipe.
output_q: queue.Queue = queue.Queue()
def _reader():
try:
for line in proc.stdout:
output_q.put(line)
except Exception:
pass
finally:
output_q.put(None) # sentinel: pipe closed
reader = threading.Thread(target=_reader, daemon=True)
reader.start()
try:
while True:
if is_stopped():
try:
proc.kill()
except Exception:
pass
return
try:
line = output_q.get(timeout=0.5)
except queue.Empty:
# Se il subprocess e' morto e il pipe non produce piu' output
# (child orfano), esci comunque.
if proc.poll() is not None and output_q.empty():
# Aspetta ancora un attimo per drenare
try:
line = output_q.get(timeout=1.0)
except queue.Empty:
break
else:
continue
if line is None:
break
pct_match = re.search(r"(\d+(?:\.\d+)?)%", line)
if pct_match and progress_callback:
pct = int(float(pct_match.group(1)))
progress_callback(processed, total, filepath.name, "downloading", current_kbps, pct)
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
try:
proc.kill()
proc.wait(timeout=5)
except Exception:
pass
finally:
watchdog.cancel()
_current_process.wait()
with _process_lock:
_current_process = None