Cerrar lo para todo, continuar sigue por donde iba, convertir antes de ordenar, y lo grande a la vista
Munka: «que al cerrar cierre todos los procesos… que al continuar la cola siga por donde iba y vuelva a generar los videos que hagan falta… que identifique bien el material… y convertir antes de mover». Cerrar con trabajo avisa y lo para todo (cola, lectura, respaldo, traer), cierra lo que quede con lo que lanzó y borra lo que quedó a medias. Lo cortado vuelve a esperar en su sitio (no «cancelado») y al continuar se hace entero; también vuelven los fallos por un corte y los convertidos que ya no están. Actualizar con la cola en marcha la detiene y sigue sola al volver. El Convertidor enseña lo grande de la carpeta y qué pasa con cada parte (exportados sin incluir, con un botón). Convertir va antes de ordenar en los pasos y en Inicio; lo que se está convirtiendo no se mueve, y una carpeta con algo abierto ya no se copia entera. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
172
organizador.py
172
organizador.py
@@ -2443,6 +2443,12 @@ def mover(origen, destino, lote, movidos):
|
||||
except OSError:
|
||||
# entre unidades o con el archivo bloqueado: copia, comprueba el tamaño y solo entonces borra el origen
|
||||
if os.path.isdir(origen):
|
||||
# Una carpeta en la misma unidad que no se deja renombrar tiene algo abierto (un clip que se está
|
||||
# convirtiendo, un proyecto de Resolve). Antes se copiaba entera y luego se borraba lo que se dejaba:
|
||||
# media carpeta duplicada y el resto a medias. Ahora se queda donde está.
|
||||
if os.path.splitdrive(origen)[0].lower() == os.path.splitdrive(destino)[0].lower():
|
||||
raise OSError(f"«{os.path.basename(origen)}» tiene algo en uso dentro (¿un clip que se está convirtiendo, o abierto "
|
||||
"en Resolve?): la carpeta se queda donde está")
|
||||
shutil.move(origen, destino)
|
||||
else:
|
||||
shutil.copy2(origen, destino)
|
||||
@@ -2473,6 +2479,8 @@ def _motivo_fallo(ex):
|
||||
w = getattr(ex, "winerror", None)
|
||||
if isinstance(ex, FileExistsError) or "ya existe" in t:
|
||||
return "existe"
|
||||
if "se está convirtiendo" in t:
|
||||
return "convirtiendo"
|
||||
if "en uso" in t or w in (32, 33):
|
||||
return "en_uso"
|
||||
if isinstance(ex, PermissionError) or w == 5 or "acceso denegado" in t:
|
||||
@@ -2510,6 +2518,16 @@ def aplicar(raiz, filas):
|
||||
con.close()
|
||||
en_plan = {os.path.normpath(x).lower() for f in filas for x in (f.get("archivos") or ())}
|
||||
marcas = [os.sep + p + os.sep for p in R.PAPELERAS]
|
||||
# lo que se está convirtiendo ahora mismo no se puede mover (ffmpeg lo tiene abierto), ni la carpeta que lo
|
||||
# tiene: se queda, y se dice por qué. Lo que solo espera en la cola sí se mueve (la cola lo sigue).
|
||||
con = db()
|
||||
convirtiendo = [os.path.normpath(r[0]).lower() for r in con.execute("SELECT ruta FROM trabajos WHERE estado = 'corriendo'")]
|
||||
con.close()
|
||||
en_conversion = set(convirtiendo)
|
||||
|
||||
def con_conversion(carpeta):
|
||||
pre = os.path.normpath(carpeta).lower() + os.sep
|
||||
return any(c.startswith(pre) for c in convirtiendo)
|
||||
fallos = 0
|
||||
APLICANDO.update(hechas=0, total=len(filas), actual="", archivo="", raiz=_clave(raiz), unidades=0,
|
||||
unidades_total=unidades_total, movidos=0, fallos=0, lote=lote, inicio=time.time(), fin=0)
|
||||
@@ -2532,6 +2550,8 @@ def aplicar(raiz, filas):
|
||||
if not os.path.exists(a):
|
||||
continue
|
||||
try:
|
||||
if os.path.normpath(a).lower() in en_conversion:
|
||||
raise OSError(f"«{os.path.basename(a)}» se está convirtiendo ahora mismo: se queda hasta que acabe")
|
||||
antes = len(movidos)
|
||||
mover(a, especiales.get(a) or os.path.join(f["destino"], os.path.basename(a)), lote, movidos)
|
||||
llego = movidos[-1][1] if len(movidos) > antes else ""
|
||||
@@ -2551,6 +2571,8 @@ def aplicar(raiz, filas):
|
||||
hechas += 1
|
||||
else:
|
||||
try:
|
||||
if con_conversion(f["origen"]) or os.path.normpath(f["origen"]).lower() in en_conversion:
|
||||
raise OSError(f"«{os.path.basename(f['origen'])}» tiene un clip que se está convirtiendo ahora mismo: se queda hasta que acabe")
|
||||
if os.path.exists(f["origen"]):
|
||||
mover(f["origen"], f["destino"], lote, movidos)
|
||||
hechas += 1
|
||||
@@ -4115,6 +4137,7 @@ def correr_ffmpeg(t):
|
||||
except OSError as ex:
|
||||
set_trabajo(t["id"], estado="error", log=f"no se pudo lanzar ffmpeg: {ex}")
|
||||
return
|
||||
_HIJOS.add(p)
|
||||
ultimo = 0
|
||||
for linea in p.stdout:
|
||||
if COLA["cancelar"]:
|
||||
@@ -4131,9 +4154,10 @@ def correr_ffmpeg(t):
|
||||
pass
|
||||
err = p.stderr.read()
|
||||
p.wait()
|
||||
_HIJOS.discard(p)
|
||||
if COLA["cancelar"]:
|
||||
set_trabajo(t["id"], estado="cancelado", pct=0)
|
||||
_quitar_parcial(tmp)
|
||||
_devolver_a_la_cola(t["id"])
|
||||
return
|
||||
if p.returncode == 0 and os.path.exists(tmp):
|
||||
break
|
||||
@@ -4204,6 +4228,7 @@ def correr_braw(t):
|
||||
errores.close()
|
||||
set_trabajo(t["id"], estado="error", log=f"no se pudo lanzar el lector de BRAW: {ex}")
|
||||
return
|
||||
_HIJOS.add(p)
|
||||
|
||||
def vigia():
|
||||
# Detener: se le pide al lector que pare (él cierra ffmpeg y borra lo que llevaba); si no hace caso, se
|
||||
@@ -4244,9 +4269,10 @@ def correr_braw(t):
|
||||
os.remove(resto)
|
||||
except OSError:
|
||||
pass
|
||||
_HIJOS.discard(p)
|
||||
if COLA["cancelar"] or fin.get("estado") == "cancelado":
|
||||
set_trabajo(t["id"], estado="cancelado", pct=0)
|
||||
_quitar_parcial(tmp)
|
||||
_devolver_a_la_cola(t["id"])
|
||||
return
|
||||
if fin.get("estado") != "listo" or not os.path.exists(tmp):
|
||||
set_trabajo(t["id"], estado="error",
|
||||
@@ -4328,9 +4354,10 @@ def correr_resolve(trabajos):
|
||||
con = db()
|
||||
for t in trabajos:
|
||||
r = con.execute("SELECT estado FROM trabajos WHERE id = ?", (t["id"],)).fetchone()
|
||||
if r and r["estado"] == "corriendo":
|
||||
set_trabajo(t["id"], estado="cancelado" if COLA["cancelar"] else "error",
|
||||
log=("\n".join(log_general))[-3000:] or "el puente terminó sin reportar el trabajo")
|
||||
if r and r["estado"] == "corriendo" and COLA["cancelar"]:
|
||||
_devolver_a_la_cola(t["id"])
|
||||
elif r and r["estado"] == "corriendo":
|
||||
set_trabajo(t["id"], estado="error", log=("\n".join(log_general))[-3000:] or "el puente terminó sin reportar el trabajo")
|
||||
con.close()
|
||||
|
||||
|
||||
@@ -4699,6 +4726,108 @@ def calidad_de(id_, segundos=10, ancho=1920):
|
||||
return {"id": id_, "vmaf": nota, "minimo": minimo, "cuadros": cuadros, "desde_seg": round(desde / fps, 2), "seg": round(time.time() - t0, 1)}
|
||||
|
||||
|
||||
_HIJOS = set() # las conversiones en marcha (ffmpeg, el lector de BRAW): se cierran al cerrar o actualizar
|
||||
|
||||
|
||||
def _parcial_de(t):
|
||||
fm = FORMATOS.get(formato_de(t), FORMATOS["h265_10bit"])
|
||||
return (t["salida"] or "") + ".parcial" + fm["ext"] if t["salida"] else ""
|
||||
|
||||
|
||||
def _devolver_a_la_cola(id_, motivo="se detuvo a mitad"):
|
||||
"""Lo que se cortó a medias (Detener, cerrar el programa, actualizar) vuelve a esperar en su sitio, delante de
|
||||
los demás: al continuar se hace entero otra vez (un MP4 a medio escribir no se puede seguir), y su archivo a
|
||||
medias se borra. Hasta la 3.9 quedaba «cancelado» y «Reanudar» no lo retomaba: el 14-oct-2026, dos BRAW
|
||||
cortados al actualizar se quedaron fuera de la cola."""
|
||||
con = db()
|
||||
t = con.execute("SELECT * FROM trabajos WHERE id = ?", (id_,)).fetchone()
|
||||
con.close()
|
||||
if not t:
|
||||
return
|
||||
parcial = _parcial_de(dict(t))
|
||||
if parcial:
|
||||
_quitar_parcial(parcial)
|
||||
set_trabajo(id_, estado="pendiente", pct=0, log=f"{motivo} (iba al {round(t['pct'] or 0)} %): al continuar se hace entero otra vez")
|
||||
|
||||
|
||||
def _ruta_seguir():
|
||||
return os.path.join(DATOS, "cola_seguir.json")
|
||||
|
||||
|
||||
def detener_todo(seguir=False, espera=30):
|
||||
"""Para todo lo que está en marcha, para cerrar el programa o actualizarlo. Munka, 14-oct-2026: «que al cerrar
|
||||
cierre todos los procesos, hasta las colas que faltan y los que quedan a mitad». La cola se detiene y lo que iba
|
||||
vuelve a esperar, la lectura de clips, el respaldo y lo que se trae de una tarjeta se cortan; un «Aplicar» se
|
||||
deja acabar (son segundos, y a medias dejaría el plan a medias). Lo que no se cierre por las buenas se cierra
|
||||
a la fuerza, con lo que haya lanzado. Con `seguir` (al actualizar), la cola sigue sola al volver a abrirse."""
|
||||
habia_cola = bool(COLA["activa"] or COLA["actuales"])
|
||||
if habia_cola:
|
||||
COLA["cancelar"] = True
|
||||
COLA["activa"] = False
|
||||
for a in list(ANALISIS.values()):
|
||||
if a and a.get("estado") == "corriendo":
|
||||
a["cancelar"] = True
|
||||
if RESPALDO.get("estado") == "corriendo":
|
||||
RESPALDO["cancelar"] = True
|
||||
if TRAER.get("estado") in ("copiando", "ordenando"):
|
||||
TRAER["cancelar"] = True
|
||||
|
||||
def ocupado():
|
||||
hilo = COLA.get("hilo")
|
||||
return bool((hilo and hilo.is_alive()) or (APLICANDO.get("total") and APLICANDO.get("hechas", 0) < APLICANDO["total"]))
|
||||
limite = time.time() + espera
|
||||
while time.time() < limite and ocupado():
|
||||
time.sleep(0.3)
|
||||
for p in list(_HIJOS): # lo que no se cerró por las buenas
|
||||
if p.poll() is None:
|
||||
_matar_arbol(p.pid)
|
||||
_HIJOS.discard(p)
|
||||
con = db()
|
||||
cortados = [r[0] for r in con.execute("SELECT id FROM trabajos WHERE estado = 'corriendo'")]
|
||||
con.close()
|
||||
for i in cortados:
|
||||
_devolver_a_la_cola(i)
|
||||
con = db()
|
||||
pendientes = con.execute("SELECT COUNT(*) FROM trabajos WHERE estado = 'pendiente'").fetchone()[0]
|
||||
con.close()
|
||||
if seguir and habia_cola and pendientes:
|
||||
guardar_json(_ruta_seguir(), {"ts": time.time(), "pendientes": pendientes})
|
||||
return {"cola": habia_cola, "pendientes": pendientes}
|
||||
|
||||
|
||||
def _seguir_al_abrir():
|
||||
"""Si la cola se detuvo para actualizar, sigue sola al volver a abrirse, por donde iba."""
|
||||
ruta = _ruta_seguir()
|
||||
if not os.path.exists(ruta):
|
||||
return False
|
||||
try:
|
||||
os.remove(ruta)
|
||||
except OSError:
|
||||
pass
|
||||
return iniciar_cola()
|
||||
|
||||
|
||||
def rehacer_perdidos():
|
||||
"""Los convertidos que ya no están (alguien los borró o los movió fuera de la app) se vuelven a hacer: el trabajo
|
||||
vuelve a la cola. Solo en carpetas conectadas (si no, que no esté no dice nada), y después de reengancharlos por
|
||||
si solo se movieron. Munka: «que al continuar … vuelva a generar los videos que hagan falta»."""
|
||||
con = db()
|
||||
ts = [dict(r) for r in con.execute("SELECT id, raiz, salida FROM trabajos WHERE estado IN ('listo','verificado') AND salida IS NOT NULL")]
|
||||
con.close()
|
||||
conectadas = {}
|
||||
ts = [t for t in ts if conectadas.setdefault(t["raiz"], os.path.isdir(t["raiz"] or ""))]
|
||||
if not ts:
|
||||
return 0
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
with ThreadPoolExecutor(max_workers=16) as ex:
|
||||
estan = list(ex.map(lambda t: os.path.isfile(t["salida"]), ts))
|
||||
perdidos = [t for t, esta in zip(ts, estan) if not esta]
|
||||
for t in perdidos:
|
||||
set_trabajo(t["id"], estado="pendiente", pct=0, bytes_out=None, nota=None, nota_min=None,
|
||||
log="el convertido ya no estaba: se vuelve a hacer")
|
||||
return len(perdidos)
|
||||
|
||||
|
||||
def _matar_arbol(pid):
|
||||
"""Cierra un proceso y lo que lanzó (el lector de BRAW lanza su propio ffmpeg)."""
|
||||
try:
|
||||
@@ -4772,20 +4901,19 @@ def cerrar_conversores_sueltos():
|
||||
|
||||
|
||||
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."""
|
||||
"""Al arrancar y al continuar, vuelve a esperar en su sitio lo que quedó a medias: lo que estaba «corriendo» sin
|
||||
una cola viva (el programa se cerró a mitad), lo «cancelado» por un «Detener» de antes de la 3.9 y lo que falló
|
||||
por un corte (un «fallo inesperado»: el 14-oct-2026, el lector de BRAW chocó con su propio archivo de una
|
||||
conversión anterior que había quedado colgada). Su archivo a medias se borra; al continuar se hace entero."""
|
||||
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"]))
|
||||
ids = [r[0] for r in con.execute("SELECT id FROM trabajos WHERE estado IN ('corriendo','cancelado') "
|
||||
"OR (estado = 'error' AND log LIKE 'fallo inesperado%')")]
|
||||
con.close()
|
||||
return len(filas)
|
||||
for i in ids:
|
||||
_devolver_a_la_cola(i, "se interrumpió")
|
||||
return len(ids)
|
||||
|
||||
|
||||
def braws_al_lector():
|
||||
@@ -4834,6 +4962,7 @@ def iniciar_cola():
|
||||
recuperar_huerfanas()
|
||||
try:
|
||||
braws_al_lector()
|
||||
rehacer_perdidos()
|
||||
except Exception: # noqa
|
||||
pass
|
||||
COLA["activa"] = True
|
||||
@@ -6920,8 +7049,10 @@ def _nombre_tipo(t):
|
||||
|
||||
|
||||
# el orden de la lista de Inicio: lo hecho se queda en su sitio, no se va al final ni desaparece
|
||||
_ORDEN_ACCIONES = ["escanear", "tipo", "entrada", "limpiar", "nombrar", "relojes", "fechas", "resto", "banco", "vacias", "stock",
|
||||
"repetidos", "convertir", "juntar", "respaldar"]
|
||||
# Convertir va antes de ordenar (Munka, 14-oct-2026: «colocaría convertir antes de mover los videos»), y lo repetido
|
||||
# se aparta antes de convertir: no se convierte lo que se va a borrar.
|
||||
_ORDEN_ACCIONES = ["escanear", "tipo", "entrada", "limpiar", "nombrar", "relojes", "repetidos", "convertir", "fechas", "resto", "banco",
|
||||
"vacias", "stock", "juntar", "respaldar"]
|
||||
|
||||
|
||||
def _historial_carpeta(raiz):
|
||||
@@ -8453,6 +8584,7 @@ def _ventana(url):
|
||||
base=BASE, marca=_marca(), relanzar=instalar.orden(BASE),
|
||||
depurar=bool(os.environ.get("ORGANIZADOR_DEPURAR")))
|
||||
v.al_cerrar, v.al_soltar = en_marcha, _al_soltar
|
||||
v.al_detener = lambda: detener_todo(seguir=False)
|
||||
v.al_abrir = lambda: _recien_abierta(v)
|
||||
fin = threading.Event()
|
||||
threading.Thread(target=_vigilar, args=(v, fin), daemon=True).start()
|
||||
@@ -8546,10 +8678,10 @@ def main():
|
||||
except OSError:
|
||||
pass
|
||||
huerfanas = recuperar_huerfanas()
|
||||
threading.Thread(target=lambda: (cerrar_conversores_sueltos(), reubicar_trabajos(), repasar_verificados(), braws_al_lector()),
|
||||
daemon=True).start()
|
||||
threading.Thread(target=lambda: (cerrar_conversores_sueltos(), reubicar_trabajos(), repasar_verificados(), braws_al_lector(),
|
||||
rehacer_perdidos(), _seguir_al_abrir()), daemon=True).start()
|
||||
if huerfanas:
|
||||
print(f"{huerfanas} conversiones quedaron a medias la última vez: vuelven a la cola en espera (Reanudar).")
|
||||
print(f"{huerfanas} conversiones quedaron a medias la última vez: vuelven a la cola en espera (Continuar).")
|
||||
try:
|
||||
srv = ServidorLocal(("127.0.0.1", PUERTO), Manejador)
|
||||
except OSError:
|
||||
|
||||
Referencia en una nueva incidencia
Block a user