2.5.0: las grabaciones bajan hasta el día de rodaje

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 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-09-16 23:06:17 -05:00
padre 1ae7a1bb38
commit 9d4d1e9cc2
Se han modificado 6 ficheros con 122 adiciones y 25 borrados

Ver fichero

@@ -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:

Ver fichero

@@ -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"
}
}

Ver fichero

@@ -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 "",

Ver fichero

@@ -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,

Ver fichero

@@ -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}",

Ver fichero

@@ -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 = `<div class="resp"><i class="pt" style="background:${d.color}"></i><div>` +
`<div><b>${esc(d.nombre)}</b> — ${esc(d.que)}${d.camara ? ` · cámara ${esc(d.camara)}` : ''}${d.app ? ` · proyecto de ${esc(d.app)}` : ''}</div>` +
`<div class="d" style="margin:4px 0">${esc(raizN + SEP + d.destino)}${SEP}</div>` +
`<div class="mut" style="font-size:12px">${esc(fecha)}. Si el archivo está dentro de una carpeta que la app reconoce, esa carpeta puede mandar sobre esto.</div>` +
`<div class="mut" style="font-size:12px">${esc(fecha)}.${esc(dia)} Si el archivo está dentro de una carpeta que la app reconoce, esa carpeta puede mandar sobre esto.</div>` +
'</div></div>';
MAPAE_DESTINO = d.cubeta;
pintarMapaEst();