From 9d4d1e9cc204b860ee794b2c9ffd8174782d1fef Mon Sep 17 00:00:00 2001 From: Labstream Date: Wed, 16 Sep 2026 23:06:17 -0500 Subject: [PATCH] =?UTF-8?q?2.5.0:=20las=20grabaciones=20bajan=20hasta=20el?= =?UTF-8?q?=20d=C3=ADa=20de=20rodaje?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El año y el mes salían de la fecha de cada archivo desde 2.2.0, pero dentro del mes iba todo junto: en Camilo, 13 días de rodaje y 907 archivos en la misma carpeta. - Nueva variable {dia} («15- Viernes», como los meses) en las plantillas de grabación de los cuatro tipos de trabajo, con migración de las guardadas. - Plan._dias_de_rodaje() cuenta los días distintos por mes y dia_de() devuelve "" cuando el mes tuvo un solo rodaje: la carpeta del día solo aparece donde hace falta. David Reyes la necesita en 1 de 8 meses, Danney en 36 de 57, Camilo en 12 de 26. 186 grupos, cero colisiones. - Un archivo de cámara suelto también se archiva por su propia fecha: plan_archivo hacía «if mes: fm = mes» y se saltaba la regla de 2.2.0. - «¿Dónde va a parar este archivo?» dice también el día. Co-Authored-By: Claude Opus 5 --- README.md | 24 +++++++++++++++++++ manifiesto.json | 20 ++++++++-------- organizador.py | 61 +++++++++++++++++++++++++++++++++++++++++------ probar.py | 9 +++++++ reglas.py | 30 +++++++++++++++++------ static/index.html | 3 ++- 6 files changed, 122 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 9d64f69..0dc487d 100644 --- a/README.md +++ b/README.md @@ -497,6 +497,30 @@ permite — en ese caso la app lo dice y no deja nada escrito. Se quita con un c La carpeta `.organilab` no se escanea ni se ordena, y los `desktop.ini` que pone la app no se cuentan como basura: si no, el primer «apartar basura» se llevaba los colores por delante. +2.5.0, el material de cámara baja hasta el día de rodaje. + +El año y el mes ya salían de la fecha de cada archivo desde la 2.2.0 (Danney se repartía solo en 54 +meses distintos), pero dentro del mes seguía todo junto: en Camilo, un mes con **13 días de rodaje +distintos y 907 archivos** en la misma carpeta. Ahora la plantilla de grabación lleva `{dia}` +(`{anio}/{mes}/00_Grabacion/{dia}/{sub}`), con el día en la misma forma que los meses: `15- Viernes`, +el número delante para que ordene y el nombre detrás para saber de un vistazo si aquello fue un sábado +de rodaje. + +**El día solo aparece cuando hace falta.** `Plan._dias_de_rodaje()` cuenta, mes a mes, en cuántos días +distintos se grabó algo en esa carpeta de cliente, y `dia_de()` devuelve cadena vacía cuando el mes +tuvo un solo rodaje: entonces la variable se lleva su propio segmento y la carpeta del día desaparece +sola. Medido sobre el material real: David Reyes solo la necesita en 1 de sus 8 meses, Danney en 36 de +57 y Camilo en 12 de 26. En el plan de los tres clientes salen 186 grupos por día y ninguna colisión. + +**Un archivo de cámara suelto también se archiva por su propia fecha.** Dentro de una carpeta de +grabación el archivo ya mandaba sobre el nombre de la carpeta desde la 2.2.0, pero un clip traspapelado +fuera de ella se quedaba en el mes que dijera la carpeta: `plan_archivo` hacía `if mes: fm = mes` sin +mirar la fecha. Ahora usa la misma regla y el mismo aviso. + +Probado sobre un árbol de ensayo con un marzo de tres rodajes y un mayo de uno solo: marzo se reparte +en `03- Martes`, `11- Miércoles` y `19- Jueves`, mayo se queda sin carpeta de día, y aplicar y deshacer +dejan el árbol exacto. + Probado el 9 sep 2026 sobre una copia de ensayo: plan → aplicar → deshacer → volver a aplicar deja el árbol exacto; conversión ffmpeg verificada y reemplazo con original a `_Para borrar`. El puente de Resolve conectó con Resolve Studio 21.1 pero no pudo abrir proyecto porque Resolve estaba en el Gestor de proyectos: diff --git a/manifiesto.json b/manifiesto.json index bb1c3d4..b3b33cb 100644 --- a/manifiesto.json +++ b/manifiesto.json @@ -1,19 +1,19 @@ { "nombre": "OrganiLAB", - "version": "2.4.0", - "builtAt": "2026-09-17T03:49:29.000Z", - "archivo": "OrganizadorLabstream-portatil-2.4.0.zip", - "size": 198734267, - "sha256": "02a9dade711cab28953b8e110d3440e4c2262287354e20b0d99ea00e05198b8f", + "version": "2.5.0", + "builtAt": "2026-09-17T04:03:44.000Z", + "archivo": "OrganizadorLabstream-portatil-2.5.0.zip", + "size": 198736121, + "sha256": "ef8054268fafbf35dc4e7336160c59195b382fe1f7e1cf9b6b1db6b852791319", "app": { - "archivo": "OrganizadorLabstream-app-2.4.0.zip", - "size": 14914514, - "sha256": "fc5e250afb755748b3c8a227780fef44c0b231026087247328e7d723cb87135a", - "url": "/api/herramientas/OrganizadorLabstream-app-2.4.0.zip" + "archivo": "OrganizadorLabstream-app-2.5.0.zip", + "size": 14916362, + "sha256": "2d0bf45795699476bcf94ccb6f0633d726320e562c9594e0be62d85ae1648941", + "url": "/api/herramientas/OrganizadorLabstream-app-2.5.0.zip" }, "notes": "", "firma": { "alg": "rsa2048-pkcs1v15-sha256", - "valor": "41707cfc81c53d0f2aa3c2cd2180c49fa588beef835d159f4caf60ef9a3e32aefa56a02f29143867eba7902f1ddf71c9a92332911ee617367be5634daff2eb293bf9a11ab668e827e0012159785b7a1d0f37ac8d76ad588f9d1521bf89c0d2539e6a6812d47fb7beae027493f6b53f844eaa655c81415396413031bd8aff59a49a5592e9263af442a7c853e17a63cb6b274e12f88d2066295ed0f6b3a030a938921e513e539b6fbe968348de5618b6421b48d5b72758c8adef2cdaeec768bf75aa45fecd390392e693038a600dd051f62aa47a62910f5ded9601ba0a3f402feb0f9389b3a27786a1b6e7ca5f7f57c21f93e80d09ecd675b9c3200241ccb7aea4" + "valor": "4b0649e34b9be12b93f46ca15dfd1ef1e0a87365c45fdebf80a3ebe94b6bc1714f438f6c141d3456f0a41b6ce000444229bc1d4c1f2a2ce3b19dc1f21a36de1c7b2657f342906f2d6cc61eaafdfc7c05f2353778abc09dd74c4b4134974aafcf162b2ceac48e1b14536db8ec58990f6aadfaed7baa4d9a30f39e2ca90ebe8c2755b8026c4ff217c8ebc5b61933a657dec61ba1598089f595639b36e4dc7083597002e70317463714a19c889bae2f926ab9dfd6c1f36c45180608c968886ea2881dfb9d29f42b621e58eb583355e634670ab9424f3446092ae636716d86ebdb42d69e10d4fd24db2580acd4e2a01b14b6f4ba401a26d14d779310527c2d7d087f" } } \ No newline at end of file diff --git a/organizador.py b/organizador.py index 982aaf4..dfd362f 100644 --- a/organizador.py +++ b/organizador.py @@ -37,7 +37,7 @@ import actualizador asistente.O = sys.modules[__name__] actualizador.O = sys.modules[__name__] -VERSION = "2.4.0" +VERSION = "2.5.0" 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 @@ -264,6 +264,9 @@ for t, d in R.PLANTILLAS.items(): # los proyectos pasaron a separarse por programa: «01_Proyectos/{nombre}» -> «01_Proyectos/{app}/{nombre}» if b == "PROYECTOS" and PLANTILLAS[t][b] == p.replace("{app}/", ""): PLANTILLAS[t][b] = p + # y las grabaciones, por día de rodaje cuando el mes tiene más de uno + if b in ("GRABACION", "CELULAR") and PLANTILLAS[t][b] == p.replace("{dia}/", ""): + PLANTILLAS[t][b] = p guardar_json(PLANTILLAS_PATH, PLANTILLAS) @@ -627,6 +630,7 @@ def destino_plantilla(tipo, cubeta, **kw): "anio": str(anio) if anio else "", "mes": R.nombre_mes(mes) if mes else "", "nombre": kw.get("nombre", ""), + "dia": kw.get("dia", ""), "sub": kw.get("sub", ""), "rel": kw.get("rel", ""), "fuente": kw.get("fuente", ""), @@ -665,6 +669,31 @@ class Plan: self.sueltos = defaultdict(list) # (cubeta, destino, motivo, confianza) -> archivos self.con = db() self.avisos = [] + self.dias_mes = self._dias_de_rodaje() + + def _dias_de_rodaje(self): + """Para cada mes, los días distintos en que se grabó algo en esta carpeta de cliente. + + Sirve para decidir si el mes necesita carpetas por día. Medido el 16-sep-2026 sobre el material + real: David Reyes tiene un solo día de rodaje en 7 de sus 8 meses (ahí la carpeta del día sería + estorbo), mientras que Danney tiene 36 meses con varios días y Camilo llega a 13 días distintos + en un mismo mes, 907 archivos mezclados en la misma carpeta.""" + dias = defaultdict(set) + for f in self.con.execute( + "SELECT fecha FROM archivos WHERE raiz = ? AND es_basura = 0 AND es_proxy = 0 AND rol IN " + "('camara_raw','camara_video','camara_sidecar','audio_grabado','celular_video')", (self.raiz,)): + f0 = f["fecha"] + try: + dias[(int(f0[:4]), int(f0[5:7]))].add(f0) + except (TypeError, ValueError): + pass + return dias + + def dia_de(self, fecha, anio, mes): + """El nombre de la carpeta del día, o "" si en ese mes solo se grabó un día: entonces sobra.""" + if len(self.dias_mes.get((anio, mes), ())) < 2: + return "" + return R.nombre_dia(fecha) # ---- helpers ---- def rel(self, ruta): @@ -1039,7 +1068,7 @@ class Plan: hoy = datetime.date.today() if (ay, am) > (hoy.year, hoy.month): ay -= 1 - grupos[(ay, am, fuente, fconf)].append((f["ruta"], f["bytes"])) + grupos[(ay, am, self.dia_de(f["fecha"], ay, am), fuente, fconf)].append((f["ruta"], f["bytes"])) etiqueta = "" if generico else R.etiqueta_grabacion(nombre) aviso_fecha = "" # hace falta que discrepe una parte de verdad: un archivo suelto traspapelado no debe dejar @@ -1052,13 +1081,16 @@ class Plan: self.avisos.append(f"«{self.rel(e.path)}» está dentro de {R.nombre_mes(mes)} de {anio}, pero {cuantos} de " f"sus archivos se grabaron en {R.nombre_mes(dm)} de {da}. Se archivan en el mes en que " f"se grabaron, no en el que dice la carpeta.") - for (ay, am, fuente, fconf), archivos in grupos.items(): + for (ay, am, dia, fuente, fconf), archivos in grupos.items(): sub = fuente if not etiqueta else os.path.join(etiqueta, fuente) - destino = destino_plantilla(self.tipo, "GRABACION", anio=ay, mes=am, sub=sub, nombre=limpiar_nombre(nombre)) + destino = destino_plantilla(self.tipo, "GRABACION", anio=ay, mes=am, dia=dia, sub=sub, + nombre=limpiar_nombre(nombre)) # si todos ya están en esa carpeta, no hay nada que hacer if all(os.path.normpath(os.path.dirname(a[0])).lower() == self.abs_(destino).lower() for a in archivos): continue mot = motivo + (" · repartido por fuente" if anio and mes else " · mes por la fecha de cada archivo" + (" (fecha de modificación)" if fconf == "baja" else "")) + if dia: + mot += " · y por día, que ese mes hubo varios rodajes" self.agregar(e.path, self.abs_(destino), "GRABACION", "mover archivos", mot + aviso_fecha, fconf, archivos=archivos) @@ -1078,7 +1110,17 @@ class Plan: if anio and fila["fecha_origen"] == "mtime": fa = anio if mes: - fm = mes + # el archivo manda sobre la carpeta cuando su propia fecha es de fiar, igual que dentro de una + # carpeta de grabación: si no, un clip de diciembre traspapelado en la carpeta de marzo se + # quedaba archivado en marzo, que es justo lo que 2.2.0 dejó de hacer con las grabaciones. + if _discrepa_mes(fila["fecha"], fila["fecha_origen"], anio or fa, mes): + if fila["fecha_origen"] == "nombre+mtime" and anio: + fa = anio if fm <= mes else anio - 1 # el nombre de la cámara no trae año + conf = "media" + motivo += (f" · la carpeta dice {R.nombre_mes(mes)} y el archivo se grabó en " + f"{R.nombre_mes(fm)}: manda el archivo") + else: + fm = mes elif fila["fecha_origen"] == "mtime": conf = "baja" motivo += " · mes por fecha de modificación" @@ -1114,7 +1156,8 @@ class Plan: elif cub == "PROYECTOS": destino = destino_plantilla(self.tipo, "PROYECTOS", app=sub) else: - destino = destino_plantilla(self.tipo, cub, anio=fa, mes=fm, sub=sub, nombre="") + destino = destino_plantilla(self.tipo, cub, anio=fa, mes=fm, sub=sub, nombre="", + dia=self.dia_de(fila["fecha"], fa, fm) if cub in ("GRABACION", "CELULAR") else "") self.suelto(fila, cub, destino, motivo, conf) def plan_basura(self): @@ -2864,6 +2907,7 @@ def _plural(n, uno, varios): EJEMPLO_VAR = { "anio": ("2026", "el año del material"), "mes": ("09- Septiembre", "el mes del material"), + "dia": ("15- Viernes", "el día en que se grabó, y solo si ese mes hubo más de un rodaje"), "nombre": ("Nombre de la pieza", "el nombre de la pieza o de la carpeta"), "fuente": ("URSA_BRAW", "la cámara de la que salió"), "app": ("Premiere", "el programa que abre el proyecto"), @@ -3010,7 +3054,10 @@ def donde_iria(nombre, tipo="contenido"): if cub == "PROYECTOS": kw["app"] = R.app_proyecto(ext) or ("Diseño" if rol == "diseno" else "Edicion") destino = "_Revisar" if cub == "REVISAR" else destino_plantilla(tipo, cub, **kw) - return {"nombre": nombre, "rol": rol, "que": R.ROL_QUE.get(rol, rol), "cubeta": cub, + # el día solo se sabe con la carpeta del cliente delante (depende de cuántos rodajes tuvo ese mes), + # así que aquí se dice cuál sería, no que vaya a estar + dia = R.nombre_dia(fecha) if cub in ("GRABACION", "CELULAR") else "" + return {"nombre": nombre, "dia": dia, "rol": rol, "que": R.ROL_QUE.get(rol, rol), "cubeta": cub, "titulo": R.CUBETAS.get(cub, cub), "color": R.COLOR_CUBETA.get(cub, R.COLOR_CUBETA[""]), "destino": destino, "crudo": [x for x in (PLANTILLAS.get(tipo, {}).get(cub, "") or "").split("/") if x], "fecha": fecha, "por_fecha": origen, "camara": firma or "", diff --git a/probar.py b/probar.py index b51ffe0..950dd70 100644 --- a/probar.py +++ b/probar.py @@ -55,6 +55,13 @@ class Reglas(unittest.TestCase): # la plantilla de proyectos separa por programa self.assertIn("{app}", R.PLANTILLAS["contenido"]["PROYECTOS"]) + def test_dia(self): + self.assertEqual(R.nombre_dia("2026-05-15"), "15- Viernes") + self.assertEqual(R.nombre_dia("2025-12-17"), "17- Miércoles") + self.assertEqual(R.nombre_dia("nada"), "") + # la plantilla de grabación lleva el día, que se rellena solo cuando hace falta + self.assertIn("{dia}", R.PLANTILLAS["contenido"]["GRABACION"]) + def test_basura_y_proxies(self): self.assertTrue(R.es_basura_archivo("Thumbs.db")) self.assertTrue(R.es_basura_archivo(".DS_Store")) @@ -242,6 +249,8 @@ class Servidor(unittest.TestCase): self.assertIn("12- Diciembre", d["destino"]) # la fecha sale del nombre de la cámara self.assertEqual(donde("Proyecto.aep")["destino"], os.path.join("01_Proyectos", "After Effects")) self.assertEqual(donde("cancion.wav")["cubeta"], "MUSICA") + self.assertEqual(d["dia"], "17- Miércoles") # el 17-dic-2025 fue miércoles + self.assertEqual(donde("cancion.wav")["dia"], "") # el día solo aplica a grabaciones self.assertTrue(donde("")["error"]) # 4b-ter. renombrar una carpeta del mapa cambia TODAS las plantillas que pasan por ella, diff --git a/reglas.py b/reglas.py index 2441a92..c1aafd1 100644 --- a/reglas.py +++ b/reglas.py @@ -11,6 +11,7 @@ se guarda en datos/diccionario.json, donde la app va aprendiendo. """ import os import re +import datetime import unicodedata import datetime @@ -70,6 +71,19 @@ def detectar_mes(nombre: str): return None, False +DIAS = ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado", "Domingo"] + + +def nombre_dia(fecha: str) -> str: + """«2026-05-15» -> «15- Viernes». Misma forma que los meses: número delante para que ordene, y el + nombre detrás para saber de un vistazo si aquello fue entre semana o un sábado de rodaje.""" + try: + a, m, d = (int(x) for x in fecha.split("-")[:3]) + return f"{d:02d}- {DIAS[datetime.date(a, m, d).weekday()]}" + except (ValueError, IndexError): + return "" + + def nombre_mes(mes: int) -> str: """convención de la casa: «MM- Mes» (la forma más usada en las carpetas actuales)""" return f"{mes:02d}- {MESES_TITULO[mes - 1]}" @@ -509,15 +523,17 @@ TIPOS_CLIENTE = { "interno": "Es material de Labstream · se ordena por tipo de recurso", } -# Destinos relativos a la raíz del cliente. Variables: {anio} {mes} ("08- Agosto") {nombre} {sub} -# {fuente} (la cámara) {app} (el programa del proyecto) {rel} (la ruta tal cual venía). +# Destinos relativos a la raíz del cliente. Variables: {anio} {mes} ("08- Agosto") {dia} ("15- Viernes") +# {nombre} {sub} {fuente} (la cámara) {app} (el programa del proyecto) {rel} (la ruta tal cual venía). +# {dia} solo se rellena cuando en ese mes se grabó en más de un día: si todo el mes es un solo rodaje, +# la carpeta del día sobraría y desaparece sola. # Una variable vacía se lleva consigo su segmento: «01_Proyectos/{app}/{nombre}» sin programa queda # en «01_Proyectos/Reel botox». PLANTILLAS = { "contenido": { "MARCA": "00_Marca/{sub}", "PROYECTOS": "01_Proyectos/{app}/{nombre}", - "GRABACION": "{anio}/{mes}/00_Grabacion/{sub}", + "GRABACION": "{anio}/{mes}/00_Grabacion/{dia}/{sub}", "PIEZA": "{anio}/{mes}/{nombre}", "CAMPANA": "Campañas/{anio}_{nombre}", "EXPORTADOS": "{anio}/{mes}/_Exportados/{sub}", @@ -531,7 +547,7 @@ PLANTILLAS = { "GUION": "{anio}/{mes}/_Guiones/{sub}", "FOTOS": "{anio}/{mes}/_Fotos/{sub}", "DOCUMENTOS": "_Documentos/{sub}", - "CELULAR": "{anio}/{mes}/00_Grabacion/CELULAR/{sub}", + "CELULAR": "{anio}/{mes}/00_Grabacion/{dia}/CELULAR/{sub}", "PROXY": "_Para borrar/proxies/{rel}", "BASURA": "_Para borrar/basura/{rel}", "REVISAR": "_Revisar/{nombre}", @@ -539,7 +555,7 @@ PLANTILLAS = { "pauta": { "MARCA": "00_Marca/{sub}", "PROYECTOS": "01_Proyectos/{app}/{nombre}", - "GRABACION": "Material/Grabaciones/{anio}/{mes}/{sub}", + "GRABACION": "Material/Grabaciones/{anio}/{mes}/{dia}/{sub}", "PIEZA": "Campañas/{anio}_{nombre}", "CAMPANA": "Campañas/{anio}_{nombre}", "EXPORTADOS": "_Exportados/{anio}/{sub}", @@ -561,7 +577,7 @@ PLANTILLAS = { "proyecto": { "MARCA": "00_Marca/{sub}", "PROYECTOS": "03_Proyecto/{app}/{nombre}", - "GRABACION": "02_Grabacion/{anio}/{mes}/{sub}", + "GRABACION": "02_Grabacion/{anio}/{mes}/{dia}/{sub}", "PIEZA": "04_Exportados/{nombre}", "CAMPANA": "{anio}_{nombre}", "EXPORTADOS": "04_Exportados/{sub}", @@ -583,7 +599,7 @@ PLANTILLAS = { "interno": { "MARCA": "Marca/{sub}", "PROYECTOS": "Proyectos/{app}/{nombre}", - "GRABACION": "Grabaciones/{anio}/{mes}/{sub}", + "GRABACION": "Grabaciones/{anio}/{mes}/{dia}/{sub}", "PIEZA": "Piezas/{anio}/{nombre}", "CAMPANA": "Campañas/{anio}_{nombre}", "EXPORTADOS": "Exportados/{anio}/{sub}", diff --git a/static/index.html b/static/index.html index 36ac31f..edf5cbf 100644 --- a/static/index.html +++ b/static/index.html @@ -1471,10 +1471,11 @@ const probarArchivo = seguro(async () => { const fecha = d.por_fecha === 'mtime' ? 'el nombre no trae fecha, así que el mes lo saca de la fecha del archivo' : `el nombre dice que es del ${d.fecha}`; + const dia = d.dia ? ` Y si ese mes hubo más de un rodaje, dentro de «${d.dia}».` : ''; caja.innerHTML = `
` + `
${esc(d.nombre)} — ${esc(d.que)}${d.camara ? ` · cámara ${esc(d.camara)}` : ''}${d.app ? ` · proyecto de ${esc(d.app)}` : ''}
` + `
${esc(raizN + SEP + d.destino)}${SEP}
` + - `
${esc(fecha)}. Si el archivo está dentro de una carpeta que la app reconoce, esa carpeta puede mandar sobre esto.
` + + `
${esc(fecha)}.${esc(dia)} Si el archivo está dentro de una carpeta que la app reconoce, esa carpeta puede mandar sobre esto.
` + '
'; MAPAE_DESTINO = d.cubeta; pintarMapaEst();