1.6.3: botón Elegir carpeta con el selector de Windows
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
@@ -33,7 +33,7 @@ import actualizador
|
||||
asistente.O = sys.modules[__name__]
|
||||
actualizador.O = sys.modules[__name__]
|
||||
|
||||
VERSION = "1.6.2"
|
||||
VERSION = "1.6.3"
|
||||
|
||||
CONGELADO = getattr(sys, "frozen", False)
|
||||
SIN_VENTANA = getattr(subprocess, "CREATE_NO_WINDOW", 0) # ffmpeg/ffprobe/net sin abrir consolas negras # empaquetado como .exe con PyInstaller
|
||||
@@ -1560,9 +1560,34 @@ def bucle_cola():
|
||||
COLA["hilo"] = None
|
||||
|
||||
|
||||
def recuperar_huerfanas():
|
||||
"""Trabajos que quedaron «corriendo» sin una cola viva: el programa se cerró (o se actualizó) a mitad de
|
||||
una conversión y nadie volvía a tocarlos. Bloqueaban «Desconectar», no salían en «En curso» y el paso
|
||||
Convertir decía «nada» (visto el 2026-09-15: 5 clips de V:\\Galeano al 18-89 % desde el día 10).
|
||||
Vuelven a «pendiente» con el motivo en el log; Reanudar los convierte desde cero."""
|
||||
if COLA["hilo"] and COLA["hilo"].is_alive():
|
||||
return 0
|
||||
con = db()
|
||||
with con:
|
||||
filas = con.execute("SELECT id, pct FROM trabajos WHERE estado = 'corriendo'").fetchall()
|
||||
for f in filas:
|
||||
con.execute("UPDATE trabajos SET estado = 'pendiente', pct = 0, log = ? WHERE id = ?",
|
||||
(f"se interrumpió al cerrar OrganiLAB (iba al {round(f['pct'] or 0)} %); pulsa Reanudar para convertirlo de nuevo", f["id"]))
|
||||
con.close()
|
||||
return len(filas)
|
||||
|
||||
|
||||
class ErrorCola(ValueError):
|
||||
"""Un ValueError que además lleva datos para la interfaz (qué trabajos estorban), no solo un texto."""
|
||||
def __init__(self, mensaje, datos):
|
||||
super().__init__(mensaje)
|
||||
self.datos = datos
|
||||
|
||||
|
||||
def iniciar_cola():
|
||||
if COLA["hilo"] and COLA["hilo"].is_alive():
|
||||
return False
|
||||
recuperar_huerfanas()
|
||||
COLA["activa"] = True
|
||||
COLA["cancelar"] = False
|
||||
COLA["hilo"] = threading.Thread(target=bucle_cola, daemon=True)
|
||||
@@ -2032,8 +2057,31 @@ def _clave(ruta):
|
||||
return os.path.normpath(ruta)
|
||||
|
||||
|
||||
def quitar_raiz(ruta, borrar_datos=False):
|
||||
"""Desconecta una carpeta de la lista. Con borrar_datos también olvida su inventario (se puede volver a escanear)."""
|
||||
_ELIGIENDO = threading.Lock()
|
||||
|
||||
|
||||
def elegir_carpeta():
|
||||
"""Abre el selector de carpetas de Windows (en un PowerShell aparte, para no bloquear el servidor) y devuelve la ruta o None."""
|
||||
if not _ELIGIENDO.acquire(blocking=False):
|
||||
raise ValueError("ya hay un selector de carpetas abierto")
|
||||
try:
|
||||
ps = ("Add-Type -AssemblyName System.Windows.Forms; "
|
||||
"$d = New-Object System.Windows.Forms.FolderBrowserDialog; "
|
||||
"$d.Description = 'Elige la carpeta del cliente'; $d.ShowNewFolderButton = $false; "
|
||||
"$f = New-Object System.Windows.Forms.Form; $f.TopMost = $true; "
|
||||
"if ($d.ShowDialog($f) -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::Out.Write($d.SelectedPath) }")
|
||||
flags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||
out = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-STA", "-Command", ps], capture_output=True, timeout=600, creationflags=flags)
|
||||
ruta = out.stdout.decode("utf-8", "replace").strip() or out.stdout.decode("mbcs", "replace").strip()
|
||||
return os.path.normpath(ruta) if ruta and os.path.isdir(ruta) else None
|
||||
finally:
|
||||
_ELIGIENDO.release()
|
||||
|
||||
|
||||
def quitar_raiz(ruta, borrar_datos=False, descartar_cola=False):
|
||||
"""Desconecta una carpeta de la lista. Con borrar_datos también olvida su inventario (se puede volver a escanear).
|
||||
Con descartar_cola se quitan de la cola sus conversiones EN ESPERA (no toca archivos); las que se están
|
||||
convirtiendo ahora mismo nunca se descartan desde aquí: hay que detener la cola primero."""
|
||||
k = _clave(ruta)
|
||||
r = raiz_cfg(ruta)
|
||||
if not r:
|
||||
@@ -2042,11 +2090,27 @@ def quitar_raiz(ruta, borrar_datos=False):
|
||||
raise ValueError("la carpeta se está escaneando o analizando; espera a que termine")
|
||||
if APLICANDO.get("total") and APLICANDO.get("hechas", 0) < APLICANDO["total"] and (APLICANDO.get("raiz") or "").lower() == k.lower():
|
||||
raise ValueError("se está aplicando el plan de esa carpeta; espera a que termine")
|
||||
recuperar_huerfanas()
|
||||
con = db()
|
||||
activos = con.execute("SELECT COUNT(*) n FROM trabajos WHERE lower(raiz) = ? AND estado IN ('pendiente','corriendo')", (k.lower(),)).fetchone()["n"]
|
||||
activos = [dict(x) for x in con.execute("SELECT id, ruta, estado, pct FROM trabajos WHERE lower(raiz) = ? AND estado IN ('pendiente','corriendo') ORDER BY id", (k.lower(),)).fetchall()]
|
||||
if activos:
|
||||
con.close()
|
||||
raise ValueError(f"hay {activos} conversiones pendientes o en curso de esa carpeta; detén la cola o límpiala primero")
|
||||
en_curso = [x for x in activos if x["estado"] == "corriendo"]
|
||||
if en_curso or not descartar_cola:
|
||||
con.close()
|
||||
n = len(activos)
|
||||
por_estado = {"corriendo": len(en_curso), "pendiente": n - len(en_curso)}
|
||||
que = ", ".join(f"{v} {'convirtiéndose ahora' if e == 'corriendo' else 'en espera'}" for e, v in por_estado.items() if v)
|
||||
# El texto dice QUÉ hay y QUÉ hacer; los datos (archivos, estados) van aparte para que la
|
||||
# interfaz los enseñe y ofrezca descartar. Antes solo decía «detén la cola o límpiala».
|
||||
if en_curso:
|
||||
msg = f"hay {n} conversiones de esa carpeta sin terminar ({que}): detén la cola en Convertir a 10 bits y vuelve a intentarlo"
|
||||
else:
|
||||
msg = f"hay {n} conversiones de esa carpeta sin terminar ({que}): termínalas con Reanudar en Convertir a 10 bits, o descártalas al desconectar"
|
||||
raise ErrorCola(msg, {"cola": {"n": n, "por_estado": por_estado, "en_curso": bool(en_curso),
|
||||
"archivos": [os.path.basename(x["ruta"]) for x in activos[:6]]}})
|
||||
with con:
|
||||
con.execute("DELETE FROM trabajos WHERE lower(raiz) = ? AND estado = 'pendiente'", (k.lower(),))
|
||||
bitacora({"op": "cola_descartada", "raiz": k, "n": len(activos), "archivos": [x["ruta"] for x in activos]})
|
||||
CONFIG["raices"] = [x for x in CONFIG["raices"] if _clave(x["ruta"]).lower() != k.lower()]
|
||||
guardar_json(CONFIG_PATH, CONFIG)
|
||||
PLANES.pop(k, None); PROGRESO.pop(k, None); ANALISIS.pop(k, None)
|
||||
@@ -2283,9 +2347,16 @@ class Manejador(BaseHTTPRequestHandler):
|
||||
try:
|
||||
if ruta == "/api/escanear":
|
||||
return self._json({"lanzado": lanzar_escaneo(d["raiz"])})
|
||||
if ruta == "/api/elegir-carpeta":
|
||||
try:
|
||||
return self._json({"ruta": elegir_carpeta()})
|
||||
except ValueError as e:
|
||||
return self._json({"error": str(e)}, 409)
|
||||
if ruta == "/api/raiz/quitar":
|
||||
try:
|
||||
return self._json(quitar_raiz(d["raiz"], bool(d.get("datos"))))
|
||||
return self._json(quitar_raiz(d["raiz"], bool(d.get("datos")), bool(d.get("descartar_cola"))))
|
||||
except ErrorCola as e:
|
||||
return self._json({"error": str(e), **e.datos}, 409)
|
||||
except ValueError as e:
|
||||
return self._json({"error": str(e)}, 409)
|
||||
if ruta == "/api/raices":
|
||||
@@ -2698,6 +2769,9 @@ def main():
|
||||
h = herramientas()
|
||||
print("ffmpeg:", "OK" if h["ffmpeg"]["ok"] else "NO ENCONTRADO", h["ffmpeg"]["ruta"])
|
||||
print("Resolve:", "OK" if h["resolve"]["ok"] else "no instalado (solo hace falta para BRAW)")
|
||||
huerfanas = recuperar_huerfanas()
|
||||
if huerfanas:
|
||||
print(f"{huerfanas} conversiones quedaron a medias la última vez: vuelven a la cola en espera (Reanudar).")
|
||||
try:
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", PUERTO), Manejador)
|
||||
except OSError:
|
||||
|
||||
Referencia en una nueva incidencia
Block a user