3.3.0: lo que entra nuevo ya entra ordenado
Las cuatro funciones que eligió Munka tras la 3.2.0: - Nueva pieza: crea «NN- Título» en su mes con el número que toca y sus subcarpetas; si la pieza ya existe, lo dice. Se deshace mientras siga vacía. - Bandeja de entrada: lo que se deje en _Entrada se reparte por mes, día de rodaje y cámara. «Traer material» copia una tarjeta comprobando cada archivo (se vuelve a leer y se compara la huella), conserva la fecha del rodaje y no toca el origen. - Versiones del mismo clip: dos clips de cámara que se llaman igual, duran lo mismo y tienen la misma forma son el mismo clip; se queda el de más imagen y el otro va a Duplicados. - Orden automático, apagado de fábrica: una tarea del Programador de tareas de Windows lanza «automatico», que lee todas las carpetas y hace solo lo seguro. No toca una carpeta que no se pueda leer entera ni una cuyo plan mueva más de 3.000 archivos, y si el programa ya está abierto se lo pide a él. Desinstalar quita la tarea. De paso: una ruta que empieza por guion bajo ya no pierde el guion al ir a la papelera. Pruebas: prueba_orden.py 24, probar.py 19, prueba_funcional.py 29, prueba_ventana.py 18 desde el código y 17 con el paquete. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
219
prueba_orden.py
219
prueba_orden.py
@@ -382,6 +382,225 @@ class Motor(unittest.TestCase):
|
||||
self.assertTrue(any("versión anterior" in x for x in O.Plan(O.raiz_cfg(raiz)).correr()["avisos"]))
|
||||
|
||||
|
||||
class Nuevo(unittest.TestCase):
|
||||
"""Lo que entra nuevo: una pieza, el material de una tarjeta, y la pasada automática."""
|
||||
|
||||
def test_pieza_nueva(self):
|
||||
"""Las piezas se creaban a mano: dos «01» en el mismo mes, «03 - X» junto a «03- X», «portasda»."""
|
||||
raiz = cliente("Piezas")
|
||||
mk(raiz, "2026/10- Octubre/01- Uno/a.mp4", 2026, 10, 1)
|
||||
mk(raiz, "2026/10- Octubre/02- Dos/b.mp4", 2026, 10, 2)
|
||||
plan(raiz)
|
||||
previa = O.nueva_pieza(raiz, "Ozempic face", 2026, 10, crear=False)
|
||||
self.assertEqual((previa["rel"], previa["existe"]), (rel("2026", "10- Octubre", "03- Ozempic face"), ""))
|
||||
self.assertFalse(os.path.exists(previa["ruta"]), "la vista previa no debe crear nada")
|
||||
r = O.nueva_pieza(raiz, "Ozempic face", 2026, 10)
|
||||
self.assertTrue(r.get("ok"), r)
|
||||
subs = sorted(os.listdir(r["ruta"]))
|
||||
self.assertEqual(subs, sorted(O.CONFIG_DEFECTO["pieza"]["subcarpetas"]))
|
||||
self.assertIn("ya existe", O.nueva_pieza(raiz, "ozempic FACE", 2026, 10).get("error", "")) # la misma, escrita distinto
|
||||
self.assertEqual(O.nueva_pieza(raiz, "07- Otra cosa", 2026, 10, crear=False)["nombre"], "04- Otra cosa") # el número lo pone la app
|
||||
for malo in ("", "a/b", "x" * 90):
|
||||
self.assertTrue(O.nueva_pieza(raiz, malo, 2026, 10).get("error"), malo)
|
||||
mes_nuevo = O.nueva_pieza(raiz, "Primera", 2026, 11)
|
||||
self.assertEqual(mes_nuevo["rel"], rel("2026", "11- Noviembre", "01- Primera"))
|
||||
# una pieza recién creada no es un hueco que borrar, ni el mes que se creó para ella
|
||||
self.assertEqual(O.carpetas_vacias(raiz)["carpetas"], [])
|
||||
self.assertFalse([f for f in O.Plan(O.raiz_cfg(raiz)).correr()["filas"]], "el plan quiere mover una pieza recién creada")
|
||||
# deshacer la quita si sigue vacía; si ya tiene algo dentro, la deja
|
||||
mk(raiz, "2026/11- Noviembre/01- Primera/Guion/guion.docx", 2026, 11, 2)
|
||||
d = O.deshacer(mes_nuevo["lote"])
|
||||
self.assertTrue(os.path.isfile(os.path.join(mes_nuevo["ruta"], "Guion", "guion.docx")) and d["errores"], d)
|
||||
d = O.deshacer(r["lote"])
|
||||
self.assertFalse(os.path.exists(r["ruta"]) or d["errores"], d)
|
||||
# donde las piezas no van por mes (campañas), sin número
|
||||
anuncios = cliente("Piezas de pauta", "pauta")
|
||||
self.assertEqual(O.nueva_pieza(anuncios, "Navidad", 2026, 12)["rel"], rel("Campañas", "2026_Navidad"))
|
||||
|
||||
def test_bandeja_de_entrada(self):
|
||||
"""Lo que se deja en _Entrada se reparte solo por rodaje y cámara, y la bandeja queda vacía."""
|
||||
raiz = cliente("Bandeja")
|
||||
mk(raiz, "2026/09- Septiembre/01- Pieza/a.png", 2026, 9, 1)
|
||||
t = "_Entrada/Tarjeta A/PRIVATE/M4ROOT/"
|
||||
mk(raiz, t + "CLIP/C0001.MP4", 2026, 9, 20)
|
||||
mk(raiz, t + "CLIP/C0001M01.XML", 2026, 9, 20)
|
||||
mk(raiz, t + "MEDIAPRO.XML", 2026, 9, 20)
|
||||
mk(raiz, t + "THMBNL/C0001T01.JPG", 2026, 9, 20)
|
||||
mk(raiz, t + "SUB/C0001S03.MP4", 2026, 9, 20)
|
||||
mk(raiz, "_Entrada/A001_09211200_C001.braw", 2026, 9, 21)
|
||||
mk(raiz, "_Entrada/Entrevista Dra/C0005.MP4", 2026, 9, 22)
|
||||
antes = nombres(raiz)
|
||||
plan(raiz)
|
||||
self.assertEqual([a["clave"] for a in O.acciones(raiz)["acciones"] if a["clave"] == "entrada"], ["entrada"])
|
||||
self.assertGreaterEqual(O.entrada(raiz)["n"], 4)
|
||||
r = O.ordenar_entrada(raiz)
|
||||
self.assertTrue(r.get("ok") and r["movimientos"] and not r["errores"], r)
|
||||
self.assertEqual(nombres(raiz), antes)
|
||||
g = os.path.join(raiz, "2026", "09- Septiembre", "00_Grabacion")
|
||||
for esperado in (os.path.join(g, R.nombre_dia("2026-09-20"), "SONY", "C0001.MP4"),
|
||||
os.path.join(g, R.nombre_dia("2026-09-20"), "SONY", "C0001M01.XML"),
|
||||
os.path.join(g, R.nombre_dia("2026-09-21"), "URSA_BRAW", "A001_09211200_C001.braw"),
|
||||
os.path.join(g, R.nombre_dia("2026-09-22"), "Entrevista Dra", "SONY", "C0005.MP4"),
|
||||
os.path.join(raiz, "_Para borrar", "proxies", "_Entrada", "Tarjeta A", "PRIVATE", "M4ROOT", "SUB", "C0001S03.MP4"),
|
||||
os.path.join(raiz, "_Para borrar", "basura", "_Entrada", "Tarjeta A", "PRIVATE", "M4ROOT", "THMBNL", "C0001T01.JPG"),
|
||||
os.path.join(raiz, "_Para borrar", "basura", "_Entrada", "Tarjeta A", "PRIVATE", "M4ROOT", "MEDIAPRO.XML")):
|
||||
self.assertTrue(os.path.isfile(esperado), "falta " + os.path.relpath(esperado, raiz) + "\n" + "\n".join(arbol(raiz)))
|
||||
# la bandeja se queda, vacía y sin el esqueleto de la tarjeta, y no sale como hueco que borrar
|
||||
self.assertEqual(os.listdir(os.path.join(raiz, "_Entrada")), [])
|
||||
self.assertEqual((r["quedan"], O.entrada(raiz)["n"]), (0, 0))
|
||||
self.assertFalse([a for a in O.acciones(raiz)["acciones"] if a["clave"] == "entrada"])
|
||||
self.assertNotIn("_Entrada", [c["rel"] for c in O.carpetas_vacias(raiz)["carpetas"]])
|
||||
# y se deshace: cada cosa vuelve a su sitio dentro de la tarjeta
|
||||
d = O.deshacer(r["lote"])
|
||||
self.assertFalse(d["errores"], d)
|
||||
self.assertTrue(os.path.isfile(os.path.join(raiz, "_Entrada", "Tarjeta A", "PRIVATE", "M4ROOT", "CLIP", "C0001.MP4")))
|
||||
self.assertTrue(os.path.isfile(os.path.join(raiz, "_Entrada", "Entrevista Dra", "C0005.MP4")))
|
||||
|
||||
def test_traer_de_una_tarjeta(self):
|
||||
"""La copia desde la tarjeta se comprueba, conserva la fecha del rodaje y no toca el origen."""
|
||||
raiz = cliente("Traer")
|
||||
mk(raiz, "2026/08- Agosto/01- Pieza/a.png", 2026, 8, 1)
|
||||
tarjeta = os.path.join(TMP, "tarjeta-sd")
|
||||
clip = mk(tarjeta, "DCIM/100MEDIA/DJI_0001.MP4", 2026, 8, 10, tam=300_000)
|
||||
mk(tarjeta, "System Volume Information/indice.dat", 2026, 8, 10)
|
||||
with open(clip, "rb") as fh:
|
||||
contenido = fh.read()
|
||||
self.assertTrue(O.plan_traer(raiz, os.path.join(raiz, "2026")).get("error")) # de dentro no se trae
|
||||
self.assertTrue(O.plan_traer(raiz, os.path.join(TMP, "no-existe")).get("error"))
|
||||
p = O.plan_traer(raiz, tarjeta)
|
||||
self.assertEqual((p["n"], p["bytes"]), (1, 300_000), p)
|
||||
self.assertTrue(p["carpeta"].startswith("Tarjeta "))
|
||||
r = O.traer(raiz, tarjeta)
|
||||
self.assertEqual((r["estado"], r["copiados"], r["errores"]), ("listo", 1, []), r)
|
||||
self.assertTrue(r["ordenado"]["ok"] and not r["ordenado"]["errores"], r["ordenado"])
|
||||
final = os.path.join(raiz, "2026", "08- Agosto", "00_Grabacion", "DJI", "DJI_0001.MP4")
|
||||
self.assertTrue(os.path.isfile(final), "\n".join(arbol(raiz)))
|
||||
with open(final, "rb") as fh:
|
||||
self.assertEqual(fh.read(), contenido)
|
||||
self.assertLess(abs(os.path.getmtime(final) - os.path.getmtime(clip)), 2, "no conservó la fecha del rodaje")
|
||||
with open(clip, "rb") as fh:
|
||||
self.assertEqual(fh.read(), contenido, "tocó el origen")
|
||||
self.assertEqual(arbol(tarjeta), [rel("DCIM", "100MEDIA", "DJI_0001.MP4"), rel("System Volume Information", "indice.dat")])
|
||||
actas = os.listdir(os.path.join(raiz, ".organilab", "entradas"))
|
||||
self.assertEqual(len(actas), 1)
|
||||
with open(os.path.join(raiz, ".organilab", "entradas", actas[0]), encoding="utf-8-sig") as fh:
|
||||
self.assertIn(rel("DCIM", "100MEDIA", "DJI_0001.MP4") + ";300000;", fh.read())
|
||||
# una copia que no queda igual que el original no se da por buena
|
||||
malo = mk(tarjeta, "DCIM/100MEDIA/DJI_0002.MP4", 2026, 8, 10, tam=50_000)
|
||||
real = O.hashlib.blake2b
|
||||
vez = []
|
||||
|
||||
class Trucado: # la segunda lectura (la de comprobación) sale distinta
|
||||
def __init__(self, **kw):
|
||||
self.h = real(**kw)
|
||||
vez.append(self)
|
||||
self.malo = len(vez) % 2 == 0
|
||||
|
||||
def update(self, b):
|
||||
self.h.update(b[::-1] if self.malo else b)
|
||||
|
||||
def digest(self):
|
||||
return self.h.digest()
|
||||
|
||||
def hexdigest(self):
|
||||
return self.h.hexdigest()
|
||||
O.hashlib.blake2b = Trucado
|
||||
try:
|
||||
estado = {"bytes": 0}
|
||||
destino = os.path.join(raiz, "_Entrada", "prueba", "DJI_0002.MP4")
|
||||
with self.assertRaises(OSError):
|
||||
O._copiar_comprobando(malo, destino, estado)
|
||||
finally:
|
||||
O.hashlib.blake2b = real
|
||||
self.assertFalse(os.path.exists(destino) or os.path.exists(destino + ".copiando"), "dejó una copia sin comprobar")
|
||||
self.assertEqual(estado["bytes"], 0)
|
||||
|
||||
def test_versiones_del_mismo_clip(self):
|
||||
"""39 clips estaban dos veces con el mismo nombre y distinta calidad, y no cabían juntos en su mes."""
|
||||
raiz = cliente("Versiones")
|
||||
oct_, may = "2025/10- Octubre/00_Grabacion/BLACKMAGIC_MP4/", "2026/05- Mayo/00_Grabacion/BLACKMAGIC_MP4/"
|
||||
buena = mk(raiz, oct_ + "A002_03020727_C001.mp4", 2025, 12, 1, tam=9000)
|
||||
peor = mk(raiz, may + "A002_03020727_C001.mp4", 2026, 7, 1, tam=3000)
|
||||
mk(raiz, oct_ + "C0001.MP4", 2025, 10, 5, tam=5000) # se llaman igual…
|
||||
mk(raiz, may + "C0001.MP4", 2026, 5, 5, tam=4000) # …pero son clips distintos: duran distinto
|
||||
# dos exportados con el mismo nombre no son un duplicado: son dos entregas de la pieza (la vertical y
|
||||
# la horizontal). No son material de cámara, así que ni se leen: `sonda` fallaría si se los pasaran.
|
||||
mk(raiz, "2026/02- Febrero/02- Liftera/Exportados/Liftera VR3 Final.mp4", 2026, 2, 20, tam=7000)
|
||||
mk(raiz, "2026/02- Febrero/01- Otra/Exportados/Liftera VR3 Final.mp4", 2026, 2, 21, tam=6000)
|
||||
p = plan(raiz)
|
||||
self.assertTrue([f for f in p["filas"] if "OJO" in f["motivo"]], "sin comparar, las dos versiones chocan en su mes")
|
||||
medidas = {buena: (12.0, 3840, 2160), peor: (12.02, 1920, 1080),
|
||||
os.path.join(raiz, oct_.replace("/", S), "C0001.MP4"): (10.0, 1920, 1080),
|
||||
os.path.join(raiz, may.replace("/", S), "C0001.MP4"): (25.0, 1920, 1080)}
|
||||
|
||||
def sonda(ruta):
|
||||
d, an, al = medidas[ruta]
|
||||
return {"codec": "h264", "perfil": "", "pix_fmt": "yuv420p", "ancho": an, "alto": al, "fps": 25.0, "dur": d,
|
||||
"kbps": 1000, "alfa": 0, "acodec": "aac", "pistas_audio": 1}
|
||||
viejos = (O.ffprobe, O.herramientas)
|
||||
O.ffprobe, O.herramientas = sonda, lambda: {"ffprobe": {"ok": True}}
|
||||
try:
|
||||
self.assertEqual(O.duplicados(raiz, minimo=0)["por_comparar"], 2)
|
||||
d = O.duplicados(raiz, minimo=0, calidades=True)
|
||||
finally:
|
||||
O.ffprobe, O.herramientas = viejos
|
||||
self.assertEqual([(g["tipo"], g["queda"], g["mueven"]) for g in d["grupos"]], [("calidad", buena, [peor])], d["grupos"])
|
||||
r = O.apartar_duplicados(raiz, d["grupos"])
|
||||
self.assertEqual((r["movimientos"], r["errores"]), (1, []), r)
|
||||
self.assertTrue(os.path.isfile(os.path.join(os.path.dirname(buena), "Duplicados", "A002_03020727_C001.mp4")))
|
||||
self.assertTrue(os.path.isfile(os.path.join(raiz, may.replace("/", S), "C0001.MP4")), "apartó un clip que solo se llamaba igual")
|
||||
# ya no chocan: las dos van a su mes, la peor dentro de Duplicados
|
||||
p = O.Plan(O.raiz_cfg(raiz)).correr()
|
||||
self.assertFalse([f for f in p["filas"] if "OJO" in f["motivo"]], [f["motivo"] for f in p["filas"]])
|
||||
aplicar(raiz, p)
|
||||
mar = os.path.join(raiz, "2025", "03- Marzo", "00_Grabacion", "BLACKMAGIC_MP4")
|
||||
self.assertTrue(os.path.isfile(os.path.join(mar, "A002_03020727_C001.mp4")), "\n".join(arbol(raiz)))
|
||||
self.assertTrue(os.path.isfile(os.path.join(mar, "Duplicados", "A002_03020727_C001.mp4")), "\n".join(arbol(raiz)))
|
||||
|
||||
def test_orden_automatico(self):
|
||||
"""La pasada automática hace lo seguro en cada carpeta, y se frena sola cuando no debe seguir."""
|
||||
normal = cliente("Auto normal")
|
||||
for n in ("C0001.MP4", "C0002.MP4"):
|
||||
mk(normal, "2026/04- Abril/Grabacion/" + n, 2026, 4, 10)
|
||||
mk(normal, "2026/04- Abril/01 - Botox/final.mp4", 2026, 4, 12) # un retoque de nombre: dudoso, no se toca
|
||||
mucho = cliente("Auto demasiado")
|
||||
for i in range(6):
|
||||
mk(mucho, "2026/04- Abril/Grabacion/C%04d.MP4" % i, 2026, 4, 10)
|
||||
todas, ajuste = O.CONFIG["raices"], dict(O.CONFIG["automatico"])
|
||||
desconectada = {"ruta": os.path.join(TMP, "no-conectada"), "cliente": "Sin disco", "tipo": "contenido"}
|
||||
O.CONFIG["raices"] = [r for r in todas if r["cliente"] == "Auto normal"] + [desconectada]
|
||||
ensayo = os.path.join(TMP, "ensayo-tarea")
|
||||
os.makedirs(ensayo, exist_ok=True)
|
||||
os.environ["ORGANILAB_ENSAYO"] = ensayo
|
||||
try:
|
||||
res = O.automatico(origen="prueba")
|
||||
self.assertEqual([(c["cliente"], c["estado"]) for c in res["carpetas"]], [("Auto normal", "con dudas"), ("Sin disco", "sin conexion")])
|
||||
c = res["carpetas"][0]
|
||||
self.assertEqual((c["movimientos"], c["dudosos"], c["errores"]), (2, 1, []), c)
|
||||
self.assertTrue(os.path.isfile(os.path.join(normal, "2026", "04- Abril", "00_Grabacion", "SONY", "C0001.MP4")))
|
||||
self.assertTrue(os.path.isdir(os.path.join(normal, "2026", "04- Abril", "01 - Botox")), "tocó lo dudoso")
|
||||
self.assertEqual(O.estado_automatico()["ultimo"]["origen"], "prueba")
|
||||
self.assertFalse(O.AUTO["activo"])
|
||||
# con más movimientos que el límite, la carpeta no se toca y se dice por qué
|
||||
O.CONFIG["raices"] = [r for r in todas if r["cliente"] == "Auto demasiado"]
|
||||
O.CONFIG["automatico"]["limite"] = 5
|
||||
c = O.automatico(origen="prueba")["carpetas"][0]
|
||||
self.assertEqual(c["estado"], "demasiado", c)
|
||||
self.assertTrue(os.path.isfile(os.path.join(mucho, "2026", "04- Abril", "Grabacion", "C0000.MP4")), "movió pese al límite")
|
||||
# encenderlo da de alta la tarea de Windows (aquí, en una carpeta de ensayo) con la orden «automatico»
|
||||
self.assertIn("error", O.configurar_automatico(True, "25:00"))
|
||||
r = O.configurar_automatico(True, "03:30")
|
||||
self.assertTrue(r.get("ok") and r["activo"] and r["hora"] == "03:30", r)
|
||||
with open(os.path.join(ensayo, "tarea.json"), encoding="utf-8") as fh:
|
||||
tarea = json.load(fh)
|
||||
self.assertTrue(tarea["activo"] and tarea["hora"] == "03:30" and tarea["orden"].endswith(" automatico"), tarea)
|
||||
self.assertFalse(O.configurar_automatico(False, "03:30")["activo"])
|
||||
finally:
|
||||
os.environ.pop("ORGANILAB_ENSAYO", None)
|
||||
O.CONFIG["raices"] = todas
|
||||
O.CONFIG["automatico"] = ajuste
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
unittest.main(verbosity=2, exit=False)
|
||||
|
||||
Referencia en una nueva incidencia
Block a user