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:
Labstream
2026-09-15 19:52:58 -05:00
padre 8b482af652
commit b1f65f94c2
Se han modificado 4 ficheros con 152 adiciones y 35 borrados

Ver fichero

@@ -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: