3.8.0: «database is locked» y los 753 que no se movieron, arreglados; fechas raras, escaneo profundo, buscar, otros discos, banco por fecha y compilar en el estudio

- Tras «Aplicar» en una carpeta grande el inventario se ponía al día con la base tomada más de diez minutos y
  «Buscar clips» se rendía con «database is locked». Ahora las preguntas al disco van antes y en paralelo y
  lo de dentro de cada carpeta se busca por índice: 1,3 s para 2.493 movimientos, mismas filas de archivos
  que antes y 183 carpetas que antes se perdían. La lectura de clips reintenta si la base está ocupada.
- Las fotos de «Alta» y «Baja» chocaban al juntarse en una carpeta: las subcarpetas con sentido se
  conservan, el plan avisa antes de dos archivos iguales en una fila, y un número entre dos palabras
  («Fiesta 6 Años») ya no se quita.
- Los fallos de «Aplicar» se agrupan por motivo y dicen qué hacer; el banco de material se explica en llano.
- Fechas raras (el reloj de una cámara), escaneo profundo, buscar en todos los clientes, traer lo que el
  cliente tiene en otros discos, el banco por temas o por fecha, y «Calcular de nuevo» más rápido.
- Mover reintenta un momento si el archivo está abierto un instante (lectura de clips, antivirus).
- .github/workflows/publicar.yml para compilar y publicar desde un PC del estudio; las pruebas devuelven
  error si algo falla.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-10-14 08:27:27 -05:00
padre 41155cd834
commit e74b8f7ece
Se han modificado 9 ficheros con 931 adiciones y 63 borrados

Ver fichero

@@ -402,6 +402,77 @@ class Motor(unittest.TestCase):
self.assertTrue(d["niveles"] >= 3 and "Lugares" in [h["nombre"] for h in d["hijos"]], d)
self.assertIn("banco", [a["clave"] for a in O.acciones(raiz)["acciones"]])
def test_alta_y_baja_no_chocan(self):
"""Vía Sumapaz: cada evento guardaba la misma foto en «Alta» y en «Baja»; el plan las juntaba en una
sola carpeta y 753 no se movieron. Las subcarpetas con sentido se conservan."""
raiz = cliente("AltaBaja")
for sub, tam in (("Alta", 8192), ("Baja", 2048)):
for n in ("IMG_5154.jpg", "IMG_5155.jpg"):
mk(raiz, f"Fotos varias/2022/Enero/Evento recaudadoras/{sub}/{n}", 2022, 1, 2, tam)
mk(raiz, "Fotos varias/2022/Diciembre/Fiesta 6 Años/IMG_3135.jpg", 2022, 12, 14)
antes = nombres(raiz)
p = plan(raiz)
self.assertFalse([f for f in p["filas"] if "OJO" in f["motivo"]], [f["motivo"] for f in p["filas"]])
por = {}
for f in p["filas"]:
for a in f["archivos"] or []:
por[a] = f["destino_rel"]
alta = [d for a, d in por.items() if os.sep + "Alta" + os.sep in a]
baja = [d for a, d in por.items() if os.sep + "Baja" + os.sep in a]
self.assertTrue(alta and baja and all(d.endswith("Alta") for d in alta) and all(d.endswith("Baja") for d in baja), por)
self.assertTrue(any("Fiesta 6 Años" in d for d in por.values()), por)
aplicar(raiz, p) # sin errores: ninguna foto choca con su gemela
self.assertEqual(nombres(raiz), antes)
def test_reloj_de_una_camara(self):
"""Una cámara con el reloj sin poner: se corrige con la fecha real del primer clip, sigue corregida al
volver a escanear, el plan la lleva a su mes de verdad, y se deshace."""
raiz = cliente("Reloj")
mk(raiz, "Vía Mujeres/Drone/DJI_0001.MP4", 2009, 1, 19, 4096)
mk(raiz, "Vía Mujeres/Drone/DJI_0002.MP4", 2009, 1, 20, 4096)
plan(raiz) # escanea
r = O.relojes(raiz)
self.assertEqual([(g["fuente"], g["n"], g["desde"]) for g in r["grupos"]], [("DJI", 2, "2009-01-19")], r)
c = O.corregir_reloj(raiz, r["grupos"][0]["dir"], "DJI", "2024-03-12")
self.assertTrue(c.get("ok") and c["n"] == 2, c)
con = O.db()
fechas = sorted((x["fecha"][:10], x["fecha_origen"]) for x in con.execute("SELECT fecha, fecha_origen FROM archivos WHERE raiz = ? AND nombre LIKE 'DJI_%'", (raiz,)))
con.close()
self.assertEqual(fechas, [("2024-03-12", "corregida"), ("2024-03-13", "corregida")])
p = plan(raiz) # vuelve a escanear: la corrección se mantiene
self.assertFalse(O.relojes(raiz)["grupos"])
self.assertTrue(any("2024" in f["destino_rel"] and "03- Marzo" in f["destino_rel"] for f in p["filas"]), [f["destino_rel"] for f in p["filas"]])
d = O.deshacer_reloj(raiz, O.relojes(raiz)["corregidos"][0]["grupo"])
self.assertTrue(d.get("ok"), d)
self.assertEqual(O.relojes(raiz)["grupos"][0]["desde"], "2009-01-19")
# y buscar la encuentra, con tilde o sin ella
b = O.buscar("via mujeres dji_0002")
self.assertEqual([x["nombre"] for x in b["resultados"]], ["DJI_0002.MP4"], b)
self.assertEqual(O.buscar("x").get("error"), "escribe al menos dos letras")
def test_banco_por_fecha(self):
"""El banco también se puede ordenar por fecha: a la 00_Grabacion de su mes, con el tema de etiqueta."""
raiz = cliente("BancoFecha")
mk(raiz, "Archivo (No tocar)/Lugares/Peajes/Drone/DJI_20240312101500_0001_D.MP4", 2024, 3, 12)
mk(raiz, "Archivo (No tocar)/Lugares/Peajes/Final.mp4", 2024, 4, 2)
O.CONFIG.setdefault("bancos", {})[os.path.normpath(raiz).lower()] = {O.R.norm("Archivo (No tocar)"): "fecha"}
self.addCleanup(lambda: O.CONFIG["bancos"].pop(os.path.normpath(raiz).lower(), None))
p = plan(raiz)
self.assertEqual([b["modo"] for b in p["bancos"]], ["fecha"])
banco = [f for f in p["filas"] if f["paso"] == "banco"]
self.assertEqual([f["destino_rel"] for f in banco], [rel("2024", "03- Marzo", "00_Grabacion", "12- Martes", "Peajes", "OSMO")])
self.assertFalse(banco[0]["marcado"])
def test_otros_discos(self):
"""El mismo cliente en otros discos: se ofrece traerlo, sin contar las carpetas ya registradas."""
raiz = cliente("Repartido")
previo = dict(O._DISCOS_CACHE)
self.addCleanup(lambda: (O._DISCOS_CACHE.clear(), O._DISCOS_CACHE.update(previo)))
O._DISCOS_CACHE.update(ts=time.time(), datos={"discos": [{"letra": "K:", "unc": r"\\nas\Entregas", "nombre": ""}],
"clientes": {"Repartido": [r"K:\Repartido viejo", raiz]}})
s = O.otros_sitios(raiz)
self.assertEqual([(x["ruta"], x["unc"]) for x in s], [(r"K:\Repartido viejo", r"\\nas\Entregas")])
def test_el_convertidor_no_repite(self):
"""22 clips se habían convertido dos veces y algunos tres («…_10bit_10bit_10bit.mp4»)."""
raiz = cliente("Convertidos")
@@ -1092,6 +1163,7 @@ class Convertidor(unittest.TestCase):
if __name__ == "__main__":
try:
unittest.main(verbosity=2, exit=False)
r = unittest.main(verbosity=2, exit=False)
finally:
shutil.rmtree(TMP, ignore_errors=True)
sys.exit(0 if r.result.wasSuccessful() else 1) # la compilación del estudio se detiene si algo falla