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:
136
prueba_orden.py
136
prueba_orden.py
@@ -1045,7 +1045,8 @@ class Convertidor(unittest.TestCase):
|
||||
hilo.join(40)
|
||||
self.assertFalse(hilo.is_alive())
|
||||
t = self.trabajo(ids[2])
|
||||
self.assertEqual(t["estado"], "cancelado")
|
||||
self.assertEqual(t["estado"], "pendiente") # vuelve a esperar: al continuar se hace entero
|
||||
self.assertTrue(t["log"].startswith("se detuvo a mitad"), t["log"])
|
||||
self.assertFalse(os.path.exists(t["salida"]) or os.path.exists(t["salida"] + ".parcial.mp4"))
|
||||
self.assertEqual([x for x in os.listdir(O.DATOS) if x.startswith("braw_")], [])
|
||||
|
||||
@@ -1317,8 +1318,9 @@ class Convertidor(unittest.TestCase):
|
||||
self.assertTrue(os.path.isfile(os.path.join(nueva, os.path.basename(tb["salida"]))))
|
||||
|
||||
def _cola_falsa(self, durante=0.4):
|
||||
"""Sustituye la conversión por una que tarda `durante` segundos y cuenta cuántas van a la vez."""
|
||||
cuenta = {"vivos": 0, "maximo": 0, "vistos": []}
|
||||
"""Sustituye la conversión por una que tarda cuenta["durante"] segundos (deja su «.parcial» mientras) y cuenta
|
||||
cuántas van a la vez."""
|
||||
cuenta = {"vivos": 0, "maximo": 0, "vistos": [], "durante": durante}
|
||||
candado = threading.Lock()
|
||||
|
||||
def falso(t):
|
||||
@@ -1326,14 +1328,19 @@ class Convertidor(unittest.TestCase):
|
||||
cuenta["vivos"] += 1
|
||||
cuenta["maximo"] = max(cuenta["maximo"], cuenta["vivos"])
|
||||
cuenta["vistos"].append(t["id"])
|
||||
fin = time.time() + durante
|
||||
with open(t["salida"] + ".parcial.mp4", "wb") as fh:
|
||||
fh.write(b"p" * 100)
|
||||
O.set_trabajo(t["id"], pct=40)
|
||||
fin = time.time() + cuenta["durante"]
|
||||
while time.time() < fin and not O.COLA["cancelar"]:
|
||||
time.sleep(0.02)
|
||||
with candado:
|
||||
cuenta["vivos"] -= 1
|
||||
if O.COLA["cancelar"]:
|
||||
O.set_trabajo(t["id"], estado="cancelado", pct=0)
|
||||
O._quitar_parcial(t["salida"] + ".parcial.mp4")
|
||||
O._devolver_a_la_cola(t["id"])
|
||||
return
|
||||
os.remove(t["salida"] + ".parcial.mp4")
|
||||
with open(t["salida"], "wb") as fh:
|
||||
fh.write(b"x" * 1000)
|
||||
O.set_trabajo(t["id"], estado="listo", pct=100, bytes_out=1000, fin=time.time())
|
||||
@@ -1360,7 +1367,7 @@ class Convertidor(unittest.TestCase):
|
||||
con = O.db()
|
||||
otros = [r[0] for r in con.execute("SELECT id FROM trabajos WHERE estado = 'pendiente'")]
|
||||
with con:
|
||||
con.execute("UPDATE trabajos SET estado = 'cancelado' WHERE estado = 'pendiente'")
|
||||
con.execute("UPDATE trabajos SET estado = 'aparte' WHERE estado = 'pendiente'")
|
||||
con.close()
|
||||
|
||||
def devolver():
|
||||
@@ -1389,13 +1396,128 @@ class Convertidor(unittest.TestCase):
|
||||
O.COLA["cancelar"], O.COLA["activa"] = True, False # lo que hace «Detener»
|
||||
O.COLA["hilo"].join(30)
|
||||
estados = sorted(self.trabajo(i)["estado"] for i in ids)
|
||||
self.assertEqual(estados, ["cancelado", "cancelado", "pendiente"])
|
||||
self.assertEqual(estados, ["pendiente", "pendiente", "pendiente"]) # los cortados vuelven a esperar
|
||||
self.assertFalse(O.COLA["cancelar"])
|
||||
con = O.db()
|
||||
with con:
|
||||
con.execute(f"DELETE FROM trabajos WHERE id IN ({','.join('?' * len(ids))})", ids)
|
||||
con.close()
|
||||
|
||||
def test_cerrar_para_todo_y_continuar(self):
|
||||
"""Munka, 14-oct-2026: «que al cerrar cierre todos los procesos, hasta las colas que faltan y los que quedan a
|
||||
mitad; que al continuar una cola la continúe y vuelva a generar los videos que hagan falta, por donde iba»."""
|
||||
raiz = cliente("CerrarTodo")
|
||||
nr = os.path.normpath(raiz)
|
||||
g = "2025/11- Noviembre/00_Grabacion/SONY/"
|
||||
clips = [mk(raiz, g + f"C01{i:02d}.MP4", 2025, 11, 11, tam=310000 + i) for i in range(3)]
|
||||
O.escanear(raiz)
|
||||
self.leidos(raiz, {os.path.basename(c): {"codec": "h264", "perfil": "High", "kbps": 120000} for c in clips})
|
||||
cuenta = self._cola_falsa(durante=30)
|
||||
O.CONFIG["optimizar"]["simultaneas"] = 2
|
||||
con = O.db()
|
||||
otros = [r[0] for r in con.execute("SELECT id FROM trabajos WHERE estado = 'pendiente'")]
|
||||
with con:
|
||||
con.execute("UPDATE trabajos SET estado = 'aparte' WHERE estado = 'pendiente'")
|
||||
con.close()
|
||||
|
||||
def devolver():
|
||||
c = O.db()
|
||||
with c:
|
||||
c.executemany("UPDATE trabajos SET estado = 'pendiente' WHERE id = ?", [(i,) for i in otros])
|
||||
c.close()
|
||||
self.addCleanup(devolver)
|
||||
ids = O.encolar(clips, "equilibrado", "420", "rec709", nr)
|
||||
self.assertTrue(O.iniciar_cola())
|
||||
fin = time.time() + 10
|
||||
while time.time() < fin and len(cuenta["vistos"]) < 2:
|
||||
time.sleep(0.05)
|
||||
parciales = [self.trabajo(i)["salida"] + ".parcial.mp4" for i in cuenta["vistos"]]
|
||||
self.assertTrue(all(os.path.exists(p) for p in parciales))
|
||||
# cerrar (o actualizar): para todo, lo que iba vuelve a esperar sin su archivo a medias
|
||||
t0 = time.time()
|
||||
r = O.detener_todo(seguir=True)
|
||||
self.assertLess(time.time() - t0, 10)
|
||||
self.assertEqual((r["cola"], r["pendientes"]), (True, 3))
|
||||
self.assertFalse(O.COLA["activa"] or O.COLA["actuales"])
|
||||
self.assertEqual([self.trabajo(i)["estado"] for i in ids], ["pendiente"] * 3)
|
||||
self.assertTrue(all(self.trabajo(i)["log"].startswith("se detuvo a mitad (iba al 40 %)") for i in cuenta["vistos"]))
|
||||
self.assertFalse(any(os.path.exists(p) for p in parciales))
|
||||
self.assertTrue(os.path.exists(O._ruta_seguir()))
|
||||
# al volver a abrir, la cola sigue sola por donde iba: primero los que se cortaron
|
||||
cuenta.update(vistos=[], durante=0.2)
|
||||
primeros = sorted(ids)[:2]
|
||||
self.assertTrue(O._seguir_al_abrir())
|
||||
self.assertFalse(os.path.exists(O._ruta_seguir()))
|
||||
O.COLA["hilo"].join(30)
|
||||
self.assertEqual(sorted(cuenta["vistos"][:2]), primeros)
|
||||
self.assertEqual([self.trabajo(i)["estado"] for i in ids], ["listo"] * 3)
|
||||
self.assertFalse(O._seguir_al_abrir()) # sin nada pendiente de seguir, no arranca
|
||||
|
||||
def test_al_continuar_se_rehace_lo_que_falta(self):
|
||||
"""Lo cortado por un «Detener» de antes (cancelado), lo que falló por un corte («fallo inesperado») y el
|
||||
convertido que ya no está vuelven a la cola; un error de verdad y lo que sigue en su sitio, no."""
|
||||
raiz = cliente("Rehacer")
|
||||
nr = os.path.normpath(raiz)
|
||||
g = "2025/11- Noviembre/00_Grabacion/SONY/"
|
||||
clips = [mk(raiz, g + f"C02{i:02d}.MP4", 2025, 11, 11, tam=320000 + i) for i in range(5)]
|
||||
O.escanear(raiz)
|
||||
self.leidos(raiz, {os.path.basename(c): {"codec": "h264", "perfil": "High", "kbps": 120000} for c in clips})
|
||||
ids = O.encolar(clips, "equilibrado", "420", "rec709", nr)
|
||||
O.set_trabajo(ids[0], estado="cancelado")
|
||||
O.set_trabajo(ids[1], estado="error", log="fallo inesperado: [WinError 5] Acceso denegado: braw_961.json")
|
||||
O.set_trabajo(ids[2], estado="error", log="el decodificador no pudo abrir el clip")
|
||||
for i in ids[3:]:
|
||||
O.set_trabajo(i, estado="verificado", bytes_out=1000)
|
||||
with open(self.trabajo(ids[4])["salida"], "wb") as fh:
|
||||
fh.write(b"x" * 1000)
|
||||
self.assertEqual(O.recuperar_huerfanas(), 2)
|
||||
self.assertEqual(O.rehacer_perdidos(), 1)
|
||||
self.assertEqual([self.trabajo(i)["estado"] for i in ids], ["pendiente", "pendiente", "error", "pendiente", "verificado"])
|
||||
self.assertEqual(self.trabajo(ids[3])["log"], "el convertido ya no estaba: se vuelve a hacer")
|
||||
con = O.db()
|
||||
with con:
|
||||
con.execute(f"DELETE FROM trabajos WHERE id IN ({','.join('?' * len(ids))})", ids)
|
||||
con.close()
|
||||
|
||||
def test_no_se_mueve_lo_que_se_convierte(self):
|
||||
"""«Si muevo y luego convierto no funciona bien»: lo que espera en la cola se mueve y la cola lo sigue, pero
|
||||
lo que se está convirtiendo ahora (ffmpeg lo tiene abierto) se queda, diciendo por qué. Y una carpeta con
|
||||
algo abierto ya no se copia entera para luego dejarla a medias."""
|
||||
raiz = cliente("NoMover")
|
||||
nr = os.path.normpath(raiz)
|
||||
a = mk(raiz, "Material Sony/20250722_Labstream5001.MP4", 2025, 7, 22, tam=300000)
|
||||
b = mk(raiz, "Material Sony/20250731_Labstream5002.MP4", 2025, 7, 31, tam=300001)
|
||||
O.escanear(raiz)
|
||||
self.leidos(raiz, {os.path.basename(x): {"codec": "h264", "perfil": "High", "kbps": 120000} for x in (a, b)})
|
||||
ia, ib = O.encolar([a, b], "equilibrado", "420", "rec709", nr)
|
||||
O.set_trabajo(ia, estado="corriendo")
|
||||
p = O.Plan(O.raiz_cfg(raiz)).correr()
|
||||
r = O.aplicar(raiz, [f for f in p["filas"] if f["marcado"]])
|
||||
self.assertTrue(os.path.exists(a)) # el que se convierte, en su sitio
|
||||
self.assertFalse(os.path.exists(b)) # el que espera, movido (y la cola con él)
|
||||
self.assertEqual(r["motivos"]["convirtiendo"]["n"], 1)
|
||||
self.assertIn("00_Grabacion", self.trabajo(ib)["ruta"])
|
||||
con = O.db()
|
||||
with con:
|
||||
con.execute("DELETE FROM trabajos WHERE id IN (?, ?)", (ia, ib))
|
||||
con.close()
|
||||
# una carpeta con un archivo abierto: no se copia
|
||||
d = os.path.join(nr, "Abierta")
|
||||
os.makedirs(d)
|
||||
f = os.path.join(d, "clip.mov")
|
||||
with open(f, "wb") as fh:
|
||||
fh.write(b"x" * 1000)
|
||||
abierto = open(f, "rb")
|
||||
self.addCleanup(abierto.close)
|
||||
try:
|
||||
O.mover(d, os.path.join(nr, "Destino Abierta"), "prueba", [])
|
||||
except OSError as ex:
|
||||
self.assertIn("en uso", str(ex))
|
||||
self.assertFalse(os.path.exists(os.path.join(nr, "Destino Abierta")))
|
||||
self.assertTrue(os.path.isfile(f))
|
||||
else:
|
||||
self.skipTest("este disco deja renombrar una carpeta con un archivo abierto")
|
||||
|
||||
def test_conversiones_sueltas(self):
|
||||
"""14-oct-2026: el lector de BRAW y el ffmpeg de una conversión de antes de actualizar seguían colgados siete
|
||||
horas después. Al arrancar se cierran los que no tienen quien los espere, y no los de un programa vivo."""
|
||||
|
||||
Referencia en una nueva incidencia
Block a user