3.6.0: grabaciones siempre por día y por fuente, grabadoras en su carpeta, y «Organizar carpetas»

Las grabaciones van siempre a su día de rodaje y dentro a una carpeta por fuente; el DJI Mic, la Tascam y
las Zoom se reconocen por su nombre y van a DJI_MIC, TASCAM y ZOOM en vez de caer juntas en AUDIO. El plan
repasa lo ya ordenado sin carpeta de día o con la grabadora en AUDIO. «Mapa» pasa a «Organizar carpetas»:
el árbol salía en fila por una regla CSS del ejemplo de tipo de trabajo; ahora va en vertical a toda la
altura y a la derecha, fijos, «Qué va en esta carpeta» y «¿Dónde va a parar este archivo?».

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-10-14 06:11:34 -05:00
padre 91618474a0
commit 93b8f4843d
Se han modificado 6 ficheros con 131 adiciones y 67 borrados

Ver fichero

@@ -18,7 +18,7 @@ static/manrope.woff2 ← la tipografía, para no pedirle nada a nadie fuera de
WebView2Loader.dll ← la entrada a WebView2 (Microsoft, firmada); lo único que viaja y no es de Python WebView2Loader.dll ← la entrada a WebView2 (Microsoft, firmada); lo único que viaja y no es de Python
datos/ ← inventario.sqlite, config.json, diccionario.json (lo aprendido), bitacora.jsonl datos/ ← inventario.sqlite, config.json, diccionario.json (lo aprendido), bitacora.jsonl
empaquetar.py ← arma el paquete: dist\OrganizadorLabstream\ con OrganizadorLabstream.exe y los dos zips empaquetar.py ← arma el paquete: dist\OrganizadorLabstream\ con OrganizadorLabstream.exe y los dos zips
probar.py ← 19 pruebas sin abrir nada · prueba_orden.py: el motor de orden y el convertidor, caso a caso · prueba_funcional.py: las 30 funciones · prueba_ventana.py: la ventana de verdad probar.py ← 19 pruebas sin abrir nada · prueba_orden.py: el motor de orden y el convertidor, caso a caso · prueba_funcional.py: las 32 funciones · prueba_ventana.py: la ventana de verdad
Iniciar.bat ← desde el código, con consola (para ver lo que imprime) Iniciar.bat ← desde el código, con consola (para ver lo que imprime)
``` ```
@@ -905,8 +905,30 @@ deshacer → volver a aplicar deja el árbol exacto y el inventario al día), pe
sistema» (más que antes, porque los proxies y el stock movidos también se marcan como basura). Lo que está en sistema» (más que antes, porque los proxies y el stock movidos también se marcan como basura). Lo que está en
_Para borrar es un grupo aparte, «ya apartado», con su frase: «ahí se quedan hasta que tú los borres». _Para borrar es un grupo aparte, «ya apartado», con su frase: «ahí se quedan hasta que tú los borres».
Y en la misma versión, lo que Munka pidió sobre las grabaciones y la pestaña del mapa:
- **Las grabaciones van siempre por cámara, por día y en carpetas.** Hasta ahora la carpeta del día de rodaje
(«13- Jueves») solo se creaba si ese mes tenía más de un rodaje; ahora va siempre, y dentro una carpeta por
fuente: `URSA_BRAW`, `BLACKMAGIC_MP4`, `SONY`, `OSMO`, `DJI`, `ATOMOS`, `CELULAR`, `FOTOS`. La fuente se
reconoce por el nombre que pone cada cámara (`A006_12171629_C001.braw`, `C0010.MP4` o
`20251111_Labstream5340.MP4`, `DJI_20251113120000_0001_D.MP4`…), y en «Organizar carpetas › ¿Dónde va a parar
este archivo?» se ve de cuál viene cualquier nombre.
- **Las grabadoras de audio, cada una en su carpeta.** El DJI Mic (`DJI_01_20251113_143000.WAV`, con los cortes
`_1`, `_2`), la Tascam (`TASCAM_0389.wav`) y las Zoom (`ZOOM0012.WAV`, `ZOOM0012_Tr1.WAV`) van a `DJI_MIC`,
`TASCAM` y `ZOOM` dentro del día de rodaje; antes las dos primeras caían juntas en `AUDIO`.
- **Lo ya ordenado se repasa.** El plan mira lo que está en `00_Grabacion` sin carpeta de día o con la
grabadora en `AUDIO` y lo lleva a su sitio (marcado, confianza media; solo lo que tiene fecha de fiar: en el
nombre o un original de cámara). Medido el 14-oct-2026 sobre el inventario real: en Diana Méndez, 46 wav de
la Tascam a `TASCAM` y 94 BRAW de abril a `05- Sábado`; en Galeano, 762 clips (578 GB) de 2025 entran en su
día de rodaje.
- **«Mapa» pasa a llamarse «Organizar carpetas»** y aprovecha la pantalla: el árbol a la izquierda, a toda
la altura, y a la derecha, fijos al bajar, «Qué va en esta carpeta» y «¿Dónde va a parar este archivo?».
El árbol salía en fila, con barra horizontal y la leyenda partida, porque la regla CSS del ejemplo de tipo
de trabajo (`.arbol div{display:flex}`) alcanzaba también a los envoltorios del árbol del mapa.
Pruebas: `prueba_funcional.py` comprueba, tras aplicar, deshacer y «hacer todo», lo que dice Inicio (hecho y Pruebas: `prueba_funcional.py` comprueba, tras aplicar, deshacer y «hacer todo», lo que dice Inicio (hecho y
cuándo, nada vuelto a ofrecer, el diagnóstico sin contar lo apartado); 32 de 32. cuándo, nada vuelto a ofrecer, el diagnóstico sin contar lo apartado); 32 de 32. `prueba_orden.py` pasa al
criterio del día siempre (33 casos, y el repaso de lo ya ordenado mete en su día lo que estaba sin él).
3.5.0, «Qué va a pasar» se explica con una carpeta. Munka, con el plan de Diana Méndez delante: «se lee 3.5.0, «Qué va a pasar» se explica con una carpeta. Munka, con el plan de Diana Méndez delante: «se lee
mucho pero se entiende poco», «lo de ordenar por año y por mes, ¿me toca seleccionar uno a uno o lo hace mucho pero se entiende poco», «lo de ordenar por año y por mes, ¿me toca seleccionar uno a uno o lo hace

Ver fichero

@@ -42,7 +42,7 @@ import actualizador
asistente.O = sys.modules[__name__] asistente.O = sys.modules[__name__]
actualizador.O = sys.modules[__name__] actualizador.O = sys.modules[__name__]
VERSION = "3.5.0" VERSION = "3.6.0"
BASE = os.path.dirname(os.path.abspath(sys.executable if getattr(sys, "frozen", False) else __file__)) BASE = os.path.dirname(os.path.abspath(sys.executable if getattr(sys, "frozen", False) else __file__))
# Empaquetado: el programa corre con el motor que viaja en su propia carpeta. OrganizadorLabstream.exe es # Empaquetado: el programa corre con el motor que viaja en su propia carpeta. OrganizadorLabstream.exe es
@@ -1055,9 +1055,11 @@ class Plan:
return dias return dias
def dia_de(self, fecha, anio, mes): 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.""" """El nombre de la carpeta del día de rodaje («13- Jueves»).
if len(self.dias_mes.get((anio, mes), ())) < 2:
return "" Hasta la 3.5.0 se omitía cuando ese mes solo tenía un día de rodaje. Munka, 14-oct-2026: las
grabaciones van «siempre según la cámara, organizado por fecha y por carpetas», así que la carpeta
del día va siempre, y dentro una por fuente (URSA_BRAW, SONY, DJI_MIC, TASCAM…)."""
return R.nombre_dia(fecha) return R.nombre_dia(fecha)
# ---- helpers ---- # ---- helpers ----
@@ -1595,7 +1597,7 @@ class Plan:
continue 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 "")) 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: if dia:
mot += " · y por día, que ese mes hubo varios rodajes" mot += " · y por día de rodaje"
self.agregar(e.path, self.abs_(destino), "GRABACION", "mover archivos", mot + aviso_fecha, fconf, self.agregar(e.path, self.abs_(destino), "GRABACION", "mover archivos", mot + aviso_fecha, fconf,
archivos=archivos) archivos=archivos)
@@ -1691,24 +1693,51 @@ class Plan:
break break
else: else:
resto = segs[len(pat):] resto = segs[len(pat):]
if resto and _RX_DIA.match(resto[0]): tenia_dia = bool(resto and _RX_DIA.match(resto[0]))
if tenia_dia:
resto = resto[1:] # la carpeta del día se vuelve a calcular en destino resto = resto[1:] # la carpeta del día se vuelve a calcular en destino
por_mes[(R.detectar_anio(segs[ia]), R.detectar_mes(segs[im])[0])].append((f, resto)) por_mes[(R.detectar_anio(segs[ia]), R.detectar_mes(segs[im])[0])].append((f, resto, tenia_dia))
grupos = defaultdict(list) grupos = defaultdict(list)
grupos_dia = defaultdict(list) # ya en su mes, pero sin carpeta de día o con la grabadora en «AUDIO»
for (anio_c, mes_c), fs in por_mes.items(): for (anio_c, mes_c), fs in por_mes.items():
fuera = [(f, resto) for f, resto in fs if _discrepa_mes(f["fecha"], f["fecha_origen"], anio_c, mes_c)] fuera = [(f, resto) for f, resto, _ in fs if _discrepa_mes(f["fecha"], f["fecha_origen"], anio_c, mes_c)]
if not fuera: if fuera:
continue # lo que solo tiene fecha de modificación pero es del mismo día que lo que se va (la Sony que
# lo que solo tiene fecha de modificación pero es del mismo día que lo que se va (la Sony que # rodó junto a la URSA) se va con ello: si no, el rodaje quedaría partido entre dos meses
# rodó junto a la URSA) se va con ello: si no, el rodaje quedaría partido entre dos meses dias = {f["fecha"] for f, _ in fuera}
dias = {f["fecha"] for f, _ in fuera} fuera += [(f, resto) for f, resto, _ in fs
fuera += [(f, resto) for f, resto in fs if f["fecha_origen"] == "mtime" and f["fecha"] in dias and _fecha_de_camara(f)]
if f["fecha_origen"] == "mtime" and f["fecha"] in dias and _fecha_de_camara(f)]
for f, resto in fuera: for f, resto in fuera:
ay, am = int(f["fecha"][:4]), int(f["fecha"][5:7]) ay, am = int(f["fecha"][:4]), int(f["fecha"][5:7])
destino = destino_plantilla(self.tipo, "GRABACION", anio=ay, mes=am, destino = destino_plantilla(self.tipo, "GRABACION", anio=ay, mes=am,
dia=self.dia_de(f["fecha"], ay, am), sub=os.sep.join(resto)) dia=self.dia_de(f["fecha"], ay, am), sub=os.sep.join(resto))
grupos[(f["dir"], destino, anio_c, mes_c, ay, am)].append((f["ruta"], f["bytes"], f["fecha_origen"] == "nombre")) grupos[(f["dir"], destino, anio_c, mes_c, ay, am)].append((f["ruta"], f["bytes"], f["fecha_origen"] == "nombre"))
# Lo que sí está en su mes: desde la 3.6.0 las grabaciones van siempre dentro de su día de
# rodaje y cada grabadora en su carpeta (DJI_MIC, TASCAM, ZOOM), así que lo ordenado con
# versiones anteriores (sin carpeta de día, o con la grabadora en «AUDIO») se vuelve a mirar.
# Solo lo que tiene fecha de fiar (en el nombre, o un original de cámara, cuya fecha de
# modificación es la del rodaje): lo demás se queda donde está.
idos = {f["ruta"] for f, _ in fuera}
for f, resto, tenia_dia in fs:
if f["ruta"] in idos or (f["fecha_origen"] == "mtime" and not _fecha_de_camara(f)):
continue
nuevo = list(resto)
fu = R.FUENTE_CARPETA.get(f["fuente"])
if fu and fu != "AUDIO" and f["rol"] == "audio_grabado":
for i in range(len(nuevo)):
if R.norm(nuevo[i]) == "audio":
nuevo[i] = fu
if tenia_dia and nuevo == resto:
continue
destino = destino_plantilla(self.tipo, "GRABACION", anio=anio_c, mes=mes_c,
dia=self.dia_de(f["fecha"], anio_c, mes_c), sub=os.sep.join(nuevo))
if self.abs_(destino).lower() == os.path.normpath(f["dir"]).lower():
continue
grupos_dia[(f["dir"], destino, not tenia_dia, nuevo != resto)].append((f["ruta"], f["bytes"]))
for (origen, destino, sin_dia, grabadora), archivos in grupos_dia.items():
partes = (["sin la carpeta del día de rodaje"] if sin_dia else []) + (["la grabadora va en su propia carpeta, no en «AUDIO»"] if grabadora else [])
self.agregar(origen, self.abs_(destino), "GRABACION", "mover archivos",
"ya en su mes, pero " + " y ".join(partes), "media", archivos=archivos)
n = b = 0 n = b = 0
for (origen, destino, anio_c, mes_c, ay, am), archivos in grupos.items(): for (origen, destino, anio_c, mes_c, ay, am), archivos in grupos.items():
n += len(archivos) n += len(archivos)
@@ -4606,7 +4635,7 @@ def _plural(n, uno, varios):
EJEMPLO_VAR = { EJEMPLO_VAR = {
"anio": ("2026", "el año del material"), "anio": ("2026", "el año del material"),
"mes": ("09- Septiembre", "el mes 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"), "dia": ("15- Viernes", "el día en que se grabó"),
"nombre": ("Nombre de la pieza", "el nombre de la pieza o de la carpeta"), "nombre": ("Nombre de la pieza", "el nombre de la pieza o de la carpeta"),
"fuente": ("URSA_BRAW", "la cámara de la que salió"), "fuente": ("URSA_BRAW", "la cámara de la que salió"),
"app": ("Premiere", "el programa que abre el proyecto"), "app": ("Premiere", "el programa que abre el proyecto"),
@@ -4747,15 +4776,15 @@ def donde_iria(nombre, tipo="contenido"):
fecha, origen = R.fecha_de_nombre(nombre, time.time()) fecha, origen = R.fecha_de_nombre(nombre, time.time())
kw = {"anio": int(fecha[:4]), "mes": int(fecha[5:7])} kw = {"anio": int(fecha[:4]), "mes": int(fecha[5:7])}
if cub == "GRABACION": if cub == "GRABACION":
kw["sub"] = R.FUENTE_CARPETA.get(firma, "OTROS") kw["sub"] = R.FUENTE_CARPETA.get(firma, "AUDIO" if rol == "audio_grabado" else "OTROS")
if cub == "CELULAR" and tipo == "contenido": if cub == "CELULAR" and tipo == "contenido":
cub, kw["sub"] = "GRABACION", "CELULAR" cub, kw["sub"] = "GRABACION", "CELULAR"
if cub in ("GRABACION", "CELULAR"):
kw["dia"] = R.nombre_dia(fecha)
if cub == "PROYECTOS": if cub == "PROYECTOS":
kw["app"] = R.app_proyecto(ext) or ("Diseño" if rol == "diseno" else "Edicion") 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) destino = "_Revisar" if cub == "REVISAR" else destino_plantilla(tipo, cub, **kw)
# el día solo se sabe con la carpeta del cliente delante (depende de cuántos rodajes tuvo ese mes), dia = kw.get("dia", "")
# 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, 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[""]), "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], "destino": destino, "crudo": [x for x in (PLANTILLAS.get(tipo, {}).get(cub, "") or "").split("/") if x],

Ver fichero

@@ -509,7 +509,7 @@ def p_entrada():
assert esperar(lambda: get('/api/estado')['traer'].get('estado') not in ('copiando', 'ordenando'), 120) assert esperar(lambda: get('/api/estado')['traer'].get('estado') not in ('copiando', 'ordenando'), 120)
tr = get('/api/estado')['traer'] tr = get('/api/estado')['traer']
assert tr['estado'] == 'listo' and tr['copiados'] == 1 and not tr['errores_n'], tr assert tr['estado'] == 'listo' and tr['copiados'] == 1 and not tr['errores_n'], tr
final = os.path.join(RAIZ, '2026', '08- Agosto', '00_Grabacion', 'DJI', 'DJI_0007.MP4') final = os.path.join(RAIZ, '2026', '08- Agosto', '00_Grabacion', '10- Lunes', 'DJI', 'DJI_0007.MP4')
assert os.path.isfile(final), 'no llegó a su sitio: %s' % [x for x in arbol() if 'dji' in x] assert os.path.isfile(final), 'no llegó a su sitio: %s' % [x for x in arbol() if 'dji' in x]
assert os.path.getsize(final) == 700_000 and abs(os.path.getmtime(final) - t) < 2, 'la copia no conserva peso o fecha' assert os.path.getsize(final) == 700_000 and abs(os.path.getmtime(final) - t) < 2, 'la copia no conserva peso o fecha'
assert os.path.isfile(clip), 'tocó la tarjeta' assert os.path.isfile(clip), 'tocó la tarjeta'
@@ -524,7 +524,7 @@ def p_entrada():
assert any(x['clave'] == 'entrada' and x['estado'] == 'toca' for x in a['acciones']), [x['clave'] for x in a['acciones']] assert any(x['clave'] == 'entrada' and x['estado'] == 'toca' for x in a['acciones']), [x['clave'] for x in a['acciones']]
c, o = post('/api/entrada/ordenar', {'raiz': RAIZ}) c, o = post('/api/entrada/ordenar', {'raiz': RAIZ})
assert c == 200 and o['movimientos'] >= 1 and not o['errores'] and o['quedan'] == 0, o assert c == 200 and o['movimientos'] >= 1 and not o['errores'] and o['quedan'] == 0, o
assert os.path.isfile(os.path.join(RAIZ, '2026', '08- Agosto', '00_Grabacion', 'SONY', 'C0099.MP4')), [x for x in arbol() if 'c0099' in x] assert os.path.isfile(os.path.join(RAIZ, '2026', '08- Agosto', '00_Grabacion', '10- Lunes', 'SONY', 'C0099.MP4')), [x for x in arbol() if 'c0099' in x]
return 'trae de una tarjeta con copia comprobada y la reparte; lo dejado a mano en la bandeja también' return 'trae de una tarjeta con copia comprobada y la reparte; lo dejado a mano en la bandeja también'

Ver fichero

@@ -171,20 +171,20 @@ class Motor(unittest.TestCase):
self.assertFalse([f for f in p["filas"] if f["origen_rel"] == "Orden"], "sigue queriendo mover «Orden» entera") self.assertFalse([f for f in p["filas"] if f["origen_rel"] == "Orden"], "sigue queriendo mover «Orden» entera")
self.assertTrue(any("guarda dentro carpetas de otras fechas" in a for a in p["avisos"]), p["avisos"]) self.assertTrue(any("guarda dentro carpetas de otras fechas" in a for a in p["avisos"]), p["avisos"])
destinos = {f["destino_rel"] for f in p["filas"]} destinos = {f["destino_rel"] for f in p["filas"]}
self.assertIn(rel("2025", "11- Noviembre", "00_Grabacion", "Tomas consultorio", "SONY"), destinos) self.assertIn(rel("2025", "11- Noviembre", "00_Grabacion", "11- Martes", "Tomas consultorio", "SONY"), destinos)
self.assertIn(rel("2025", "11- Noviembre", "00_Grabacion", "Tomas consultorio", "SONY", "Duplicados"), destinos) self.assertIn(rel("2025", "11- Noviembre", "00_Grabacion", "11- Martes", "Tomas consultorio", "SONY", "Duplicados"), destinos)
# el rodaje del 31 de mayo siguió de madrugada: todo a mayo de 2025, el año que da el .braw intacto # el rodaje del 31 de mayo siguió de madrugada: todo a mayo de 2025, el año que da el .braw intacto
self.assertIn(rel("2025", "05- Mayo", "00_Grabacion", "Maquinas", "BLACKMAGIC_MP4"), destinos) self.assertIn(rel("2025", "05- Mayo", "00_Grabacion", "31- Sábado", "Maquinas", "BLACKMAGIC_MP4"), destinos)
self.assertIn(rel("2025", "05- Mayo", "00_Grabacion", "Maquinas", "URSA_BRAW"), destinos) self.assertIn(rel("2025", "05- Mayo", "00_Grabacion", "31- Sábado", "Maquinas", "URSA_BRAW"), destinos)
self.assertFalse([d for d in destinos if "06- Junio" in d], destinos) self.assertFalse([d for d in destinos if "06- Junio" in d], destinos)
junta = [f for f in p["filas"] if f["accion"] == "renombrar" and f["origen_rel"].endswith("03 - El cuello")] junta = [f for f in p["filas"] if f["accion"] == "renombrar" and f["origen_rel"].endswith("03 - El cuello")]
self.assertTrue(junta and junta[0]["marcado"] and "se junta" in junta[0]["motivo"], junta) self.assertTrue(junta and junta[0]["marcado"] and "se junta" in junta[0]["motivo"], junta)
aplicar(raiz, p) aplicar(raiz, p)
self.assertEqual(nombres(raiz), antes, "se perdió o se duplicó algún archivo") self.assertEqual(nombres(raiz), antes, "se perdió o se duplicó algún archivo")
for esperado in (rel("2025", "11- Noviembre", "00_Grabacion", "Tomas consultorio", "SONY", "20251111_Labstream5320.MP4"), for esperado in (rel("2025", "11- Noviembre", "00_Grabacion", "11- Martes", "Tomas consultorio", "SONY", "20251111_Labstream5320.MP4"),
rel("2025", "11- Noviembre", "00_Grabacion", "Tomas consultorio", "SONY", "20251111_Labstream5321_10bit.mp4"), rel("2025", "11- Noviembre", "00_Grabacion", "11- Martes", "Tomas consultorio", "SONY", "20251111_Labstream5321_10bit.mp4"),
rel("2025", "05- Mayo", "00_Grabacion", "Maquinas", "BLACKMAGIC_MP4", "A006_06010000_C002.mp4"), rel("2025", "05- Mayo", "00_Grabacion", "31- Sábado", "Maquinas", "BLACKMAGIC_MP4", "A006_06010000_C002.mp4"),
rel("2025", "05- Mayo", "00_Grabacion", "Maquinas", "URSA_BRAW", "A006_06010030_C003.braw"), rel("2025", "05- Mayo", "00_Grabacion", "31- Sábado", "Maquinas", "URSA_BRAW", "A006_06010030_C003.braw"),
rel("2026", "05- Mayo", "03- El cuello", "Exportado", "final.mp4"), rel("2026", "05- Mayo", "03- El cuello", "Exportado", "final.mp4"),
rel("2026", "05- Mayo", "03- El cuello", "proyecto.prproj"), rel("2026", "05- Mayo", "03- El cuello", "proyecto.prproj"),
rel("2026", "05- Mayo", "03- El cuello", "portada vieja.png")): rel("2026", "05- Mayo", "03- El cuello", "portada vieja.png")):
@@ -203,7 +203,7 @@ class Motor(unittest.TestCase):
p = plan(raiz) p = plan(raiz)
self.assertFalse([f for f in p["filas"] if "parpado" in f["origen_rel"]], "desarma la pieza") self.assertFalse([f for f in p["filas"] if "parpado" in f["origen_rel"]], "desarma la pieza")
# la sesión del mes, con nombre de fábrica y clips de cámara, sí se reparte # la sesión del mes, con nombre de fábrica y clips de cámara, sí se reparte
self.assertIn(rel("2026", "08- Agosto", "00_Grabacion", "SONY"), {f["destino_rel"] for f in p["filas"]}) self.assertIn(rel("2026", "08- Agosto", "00_Grabacion", "05- Miércoles", "SONY"), {f["destino_rel"] for f in p["filas"]})
def test_lo_que_no_es_rodaje_no_va_a_otros(self): def test_lo_que_no_es_rodaje_no_va_a_otros(self):
"""Dentro de una carpeta de grabación había proyectos e imágenes de IA que acababan en 00_Grabacion\\OTROS.""" """Dentro de una carpeta de grabación había proyectos e imágenes de IA que acababan en 00_Grabacion\\OTROS."""
@@ -215,7 +215,7 @@ class Motor(unittest.TestCase):
mk(raiz, "Grabación evento/Export/final.mp4", 2026, 4, 12) mk(raiz, "Grabación evento/Export/final.mp4", 2026, 4, 12)
p = plan(raiz) p = plan(raiz)
por_archivo = {os.path.basename(a): f for f in p["filas"] for a in (f["archivos"] or [])} por_archivo = {os.path.basename(a): f for f in p["filas"] for a in (f["archivos"] or [])}
self.assertEqual(por_archivo["C0001.MP4"]["destino_rel"], rel("2026", "04- Abril", "00_Grabacion", "evento", "SONY")) self.assertEqual(por_archivo["C0001.MP4"]["destino_rel"], rel("2026", "04- Abril", "00_Grabacion", "10- Viernes", "evento", "SONY"))
self.assertTrue(por_archivo["C0001.MP4"]["marcado"], "un original de cámara fechado por su archivo debe ir marcado") self.assertTrue(por_archivo["C0001.MP4"]["marcado"], "un original de cámara fechado por su archivo debe ir marcado")
self.assertEqual(por_archivo["proyecto.drp"]["destino_rel"], rel("01_Proyectos", "DaVinci Resolve")) self.assertEqual(por_archivo["proyecto.drp"]["destino_rel"], rel("01_Proyectos", "DaVinci Resolve"))
self.assertNotIn("00_Grabacion", por_archivo["guion.docx"]["destino_rel"]) self.assertNotIn("00_Grabacion", por_archivo["guion.docx"]["destino_rel"])
@@ -244,12 +244,13 @@ class Motor(unittest.TestCase):
aplicar(raiz, p) aplicar(raiz, p)
self.assertEqual(nombres(raiz), antes) self.assertEqual(nombres(raiz), antes)
dic = os.path.join(raiz, "2025", "12- Diciembre", "00_Grabacion") dic = os.path.join(raiz, "2025", "12- Diciembre", "00_Grabacion")
for esperado in ("URSA_BRAW" + S + "A014_12091814_C001.braw", "URSA_BRAW" + S + "A014_12100130_C002.braw", for esperado in ("09- Martes" + S + "URSA_BRAW" + S + "A014_12091814_C001.braw", "09- Martes" + S + "URSA_BRAW" + S + "A014_12100130_C002.braw", # la madrugada sigue siendo la noche del 9
"URSA_BRAW" + S + "Duplicados" + S + "A014_12091814_C001.mp4", "SONY" + S + "C0001.MP4"): "09- Martes" + S + "URSA_BRAW" + S + "Duplicados" + S + "A014_12091814_C001.mp4", "09- Martes" + S + "SONY" + S + "C0001.MP4"):
self.assertTrue(os.path.isfile(os.path.join(dic, esperado)), "falta " + esperado + "\n" + "\n".join(arbol(raiz))) self.assertTrue(os.path.isfile(os.path.join(dic, esperado)), "falta " + esperado + "\n" + "\n".join(arbol(raiz)))
# y lo que sí era de marzo, pero estaba sin carpeta de día, entra en la suya (3.6.0: el día va siempre)
mar = os.path.join(raiz, "2025", "03- Marzo", "00_Grabacion") mar = os.path.join(raiz, "2025", "03- Marzo", "00_Grabacion")
self.assertTrue(os.path.isfile(os.path.join(mar, "SONY", "C0002.MP4"))) self.assertTrue(os.path.isfile(os.path.join(mar, "05- Miércoles", "SONY", "C0002.MP4")), "\n".join(arbol(raiz)))
self.assertTrue(os.path.isfile(os.path.join(mar, "URSA_BRAW", "A002_03110342_C036.braw"))) self.assertTrue(os.path.isfile(os.path.join(mar, "11- Martes", "URSA_BRAW", "A002_03110342_C036.braw")), "\n".join(arbol(raiz)))
self.assertFalse([f for f in O.Plan(O.raiz_cfg(raiz)).correr()["filas"] if f["marcado"]], "el repaso no termina") self.assertFalse([f for f in O.Plan(O.raiz_cfg(raiz)).correr()["filas"] if f["marcado"]], "el repaso no termina")
def test_carpetas_gemelas_y_papelera_vieja(self): def test_carpetas_gemelas_y_papelera_vieja(self):
@@ -474,7 +475,7 @@ class Nuevo(unittest.TestCase):
r = O.traer(raiz, tarjeta) r = O.traer(raiz, tarjeta)
self.assertEqual((r["estado"], r["copiados"], r["errores"]), ("listo", 1, []), r) self.assertEqual((r["estado"], r["copiados"], r["errores"]), ("listo", 1, []), r)
self.assertTrue(r["ordenado"]["ok"] and not r["ordenado"]["errores"], r["ordenado"]) 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") final = os.path.join(raiz, "2026", "08- Agosto", "00_Grabacion", "10- Lunes", "DJI", "DJI_0001.MP4")
self.assertTrue(os.path.isfile(final), "\n".join(arbol(raiz))) self.assertTrue(os.path.isfile(final), "\n".join(arbol(raiz)))
with open(final, "rb") as fh: with open(final, "rb") as fh:
self.assertEqual(fh.read(), contenido) self.assertEqual(fh.read(), contenido)
@@ -554,7 +555,7 @@ class Nuevo(unittest.TestCase):
p = O.Plan(O.raiz_cfg(raiz)).correr() 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"]]) self.assertFalse([f for f in p["filas"] if "OJO" in f["motivo"]], [f["motivo"] for f in p["filas"]])
aplicar(raiz, p) aplicar(raiz, p)
mar = os.path.join(raiz, "2025", "03- Marzo", "00_Grabacion", "BLACKMAGIC_MP4") mar = os.path.join(raiz, "2025", "03- Marzo", "00_Grabacion", "02- Domingo", "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, "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))) self.assertTrue(os.path.isfile(os.path.join(mar, "Duplicados", "A002_03020727_C001.mp4")), "\n".join(arbol(raiz)))
@@ -578,7 +579,7 @@ class Nuevo(unittest.TestCase):
self.assertEqual([(c["cliente"], c["estado"]) for c in res["carpetas"]], [("Auto normal", "con dudas"), ("Sin disco", "sin conexion")]) self.assertEqual([(c["cliente"], c["estado"]) for c in res["carpetas"]], [("Auto normal", "con dudas"), ("Sin disco", "sin conexion")])
c = res["carpetas"][0] c = res["carpetas"][0]
self.assertEqual((c["movimientos"], c["dudosos"], c["errores"]), (2, 1, []), c) 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.isfile(os.path.join(normal, "2026", "04- Abril", "00_Grabacion", "10- Viernes", "SONY", "C0001.MP4")))
self.assertTrue(os.path.isdir(os.path.join(normal, "2026", "04- Abril", "01 - Botox")), "tocó lo dudoso") 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.assertEqual(O.estado_automatico()["ultimo"]["origen"], "prueba")
self.assertFalse(O.AUTO["activo"]) self.assertFalse(O.AUTO["activo"])

Ver fichero

@@ -122,8 +122,12 @@ FIRMAS = [
("SONY", r"^c\d{4}(m01)?\.(mp4|mov|xml)$", "Sony"), ("SONY", r"^c\d{4}(m01)?\.(mp4|mov|xml)$", "Sony"),
("OSMO", r"^dji_\d{14}_\d{4}_d\.(mp4|mov|lrf)$", "DJI Osmo Pocket"), ("OSMO", r"^dji_\d{14}_\d{4}_d\.(mp4|mov|lrf)$", "DJI Osmo Pocket"),
("DJI", r"^dji_\d{4}\.(mp4|mov|jpg|dng|lrf|srt)$", "DJI dron / Osmo"), ("DJI", r"^dji_\d{4}\.(mp4|mov|jpg|dng|lrf|srt)$", "DJI dron / Osmo"),
("DJI_MIC", r"^dji_\d{2}_\d{8}_\d{6}\.wav$", "DJI Mic"), # las grabadoras de audio: el DJI Mic nombra «DJI_01_20251113_143000.WAV» (01 o 02 es el transmisor;
("TASCAM", r"^tascam_\d{4}\.wav$", "Tascam"), # los cortes de más de 4 GB llevan _1, _2…), la Tascam «TASCAM_0389.wav» y las Zoom «ZOOM0012.WAV»
# (la H6, con pista: «ZOOM0012_Tr1.WAV»). Cada una va a su propia carpeta del día de rodaje.
("DJI_MIC", r"^dji_(mic_?)?\d{2}_\d{8}_\d{6}(_\d+)?\.(wav|mp3)$", "DJI Mic"),
("TASCAM", r"^tascam_\d{4}(_\d+)?\.(wav|mp3)$", "Tascam"),
("ZOOM", r"^zoom\d{4}(_tr\d+|_lr|_\d+)?\.(wav|mp3)$", "Zoom (grabadora)"),
("ATOMOS", r"^ninjav_", "Atomos Ninja V"), ("ATOMOS", r"^ninjav_", "Atomos Ninja V"),
("CELULAR", r"^img_\d{4}( \(\d+\))?( \d)?(_[a-z0-9]+)?\.(mov|mp4|heic|jpg|jpeg|png)$", "iPhone"), ("CELULAR", r"^img_\d{4}( \(\d+\))?( \d)?(_[a-z0-9]+)?\.(mov|mp4|heic|jpg|jpeg|png)$", "iPhone"),
("CELULAR", r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(mov|heic|jpg|jpeg|mp4|png)$", "iPhone (AirDrop)"), ("CELULAR", r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(mov|heic|jpg|jpeg|mp4|png)$", "iPhone (AirDrop)"),
@@ -142,7 +146,7 @@ FIRMAS_RE = [(k, re.compile(p, re.I), d) for k, p, d in FIRMAS]
FUENTE_CARPETA = { # nombre de subcarpeta al separar una sesión de grabación por fuente FUENTE_CARPETA = { # nombre de subcarpeta al separar una sesión de grabación por fuente
"URSA_BRAW": "URSA_BRAW", "BM_PROXY": "BLACKMAGIC_MP4", "SONY": "SONY", "OSMO": "OSMO", "URSA_BRAW": "URSA_BRAW", "BM_PROXY": "BLACKMAGIC_MP4", "SONY": "SONY", "OSMO": "OSMO",
"DJI": "DJI", "DJI_MIC": "AUDIO", "TASCAM": "AUDIO", "ATOMOS": "ATOMOS", "DJI": "DJI", "DJI_MIC": "DJI_MIC", "TASCAM": "TASCAM", "ZOOM": "ZOOM", "ATOMOS": "ATOMOS",
"CELULAR": "CELULAR", "WHATSAPP": "CELULAR", "PANASONIC": "FOTOS", "SONY_FOTO": "FOTOS", "CELULAR": "CELULAR", "WHATSAPP": "CELULAR", "PANASONIC": "FOTOS", "SONY_FOTO": "FOTOS",
} }
@@ -292,7 +296,7 @@ def rol_archivo(nombre: str, ext: str, firma: str | None) -> str:
return "celular_video" return "celular_video"
if firma in ("CELULAR", "WHATSAPP") and e in EXT["imagen"]: if firma in ("CELULAR", "WHATSAPP") and e in EXT["imagen"]:
return "celular_foto" return "celular_foto"
if firma in ("DJI_MIC", "TASCAM"): if firma in ("DJI_MIC", "TASCAM", "ZOOM"):
return "audio_grabado" return "audio_grabado"
if firma in ("PANASONIC", "SONY_FOTO") or e in EXT["foto_raw"]: if firma in ("PANASONIC", "SONY_FOTO") or e in EXT["foto_raw"]:
return "foto_camara" return "foto_camara"
@@ -571,7 +575,7 @@ def cubeta_carpeta(nombre: str, diccionario: dict):
# palabras que solo dicen "de qué cámara": no aportan etiqueta a una sesión de grabación # palabras que solo dicen "de qué cámara": no aportan etiqueta a una sesión de grabación
FUENTE_PALABRAS = {"grabacion", "grabaciones", "grabado", "grabados", "ursa", "sony", "osmo", "dji", "blackmagic", FUENTE_PALABRAS = {"grabacion", "grabaciones", "grabado", "grabados", "ursa", "sony", "osmo", "dji", "blackmagic",
"braw", "tascam", "audio", "otro", "otros", "a", "c", "camara", "material", "video", "videos", "braw", "tascam", "zoom", "mic", "audio", "otro", "otros", "a", "c", "camara", "material", "video", "videos",
"mp4", "mov", "de", "la", "el", "pocket", "drone", "dron", "mic", "celular", "mp4", "mov", "de", "la", "el", "pocket", "drone", "dron", "mic", "celular",
"fotos", "foto", "atomos", "tarjeta", "tarjetas", "card", "sd"} "fotos", "foto", "atomos", "tarjeta", "tarjetas", "card", "sd"}

Ver fichero

@@ -176,9 +176,14 @@
.tipo span{display:block;font-size:12px;color:var(--mut);margin-top:1px} .tipo span{display:block;font-size:12px;color:var(--mut);margin-top:1px}
.tipo em{display:block;font-size:11.5px;color:var(--mut);font-style:normal;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .tipo em{display:block;font-size:11.5px;color:var(--mut);font-style:normal;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.arbol{margin:6px 0 0;padding:10px 12px;background:var(--panel2);border-radius:8px;font-family:ui-monospace,Consolas,monospace;font-size:11.5px;line-height:1.6} .arbol{margin:6px 0 0;padding:10px 12px;background:var(--panel2);border-radius:8px;font-family:ui-monospace,Consolas,monospace;font-size:11.5px;line-height:1.6}
.arbol div{display:flex;gap:10px;align-items:baseline;white-space:pre} .arbol .ln{display:flex;gap:10px;align-items:baseline;white-space:pre}
.arbol b{font-weight:600} .arbol .ln b{font-weight:600}
.arbol span{font:12px/1.4 Manrope,system-ui,sans-serif;color:var(--mut);white-space:normal} .arbol .ln span{font:12px/1.4 Manrope,system-ui,sans-serif;color:var(--mut);white-space:normal}
.mapa2{grid-template-columns:minmax(0,1.3fr) minmax(300px,1fr);align-items:start}
.mapa2 .lado{position:sticky;top:10px;display:grid;gap:14px}
#mapaArbol{max-height:calc(100vh - 200px);overflow:auto;font-size:12.5px}
#mapaArbol .nd{padding:4px 8px}
@media(max-width:1000px){.mapa2 .lado{position:static}#mapaArbol{max-height:none}}
.arbol{font-family:ui-monospace,Consolas,monospace;font-size:12px;line-height:1.55} .arbol{font-family:ui-monospace,Consolas,monospace;font-size:12px;line-height:1.55}
.arbol .nd{display:flex;gap:10px;align-items:baseline;padding:3px 7px;border-radius:5px;cursor:pointer} .arbol .nd{display:flex;gap:10px;align-items:baseline;padding:3px 7px;border-radius:5px;cursor:pointer}
.arbol .nd:hover{background:var(--panel2)} .arbol .nd:hover{background:var(--panel2)}
@@ -497,7 +502,7 @@
<nav id="nav" aria-label="Vistas"> <nav id="nav" aria-label="Vistas">
<a data-t="carpeta" class="on" tabindex="0">Carpeta</a> <a data-t="carpeta" class="on" tabindex="0">Carpeta</a>
<a data-t="revisar" tabindex="0">Revisar y ordenar</a> <a data-t="revisar" tabindex="0">Revisar y ordenar</a>
<a data-t="mapa" tabindex="0" title="Cómo van a quedar las carpetas y qué va en cada una">Mapa</a> <a data-t="mapa" tabindex="0" title="Cómo van a quedar las carpetas y qué va en cada una">Organizar carpetas</a>
<a data-t="convertir" tabindex="0">Convertidor</a> <a data-t="convertir" tabindex="0">Convertidor</a>
<a data-t="respaldar" tabindex="0">Respaldar</a> <a data-t="respaldar" tabindex="0">Respaldar</a>
<a data-t="historial" tabindex="0">Historial</a> <a data-t="historial" tabindex="0">Historial</a>
@@ -612,7 +617,7 @@
<!-- ============ MAPA ============ --> <!-- ============ MAPA ============ -->
<section id="t-mapa"> <section id="t-mapa">
<div class="cab"><div><h2 class="v">Mapa</h2><div class="sub" id="mapaSub">cómo van a quedar las carpetas y qué va en cada una</div></div></div> <div class="cab"><div><h2 class="v">Organizar carpetas</h2><div class="sub" id="mapaSub">cómo van a quedar las carpetas y qué va en cada una</div></div></div>
<p class="ayuda">Este árbol sale de las plantillas de verdad, así que es exactamente lo que la app va a hacer al ordenar. <span class="q">?</span><span class="mas">Las carpetas en cursiva cambian según el material (el año, el mes, el nombre de la pieza, el programa del proyecto). Las cifras son el material que tienes hoy en esta carpeta, repartido por lo que es cada archivo. Toca cualquier carpeta del árbol para ver qué guarda y cambiar dónde va.</span></p> <p class="ayuda">Este árbol sale de las plantillas de verdad, así que es exactamente lo que la app va a hacer al ordenar. <span class="q">?</span><span class="mas">Las carpetas en cursiva cambian según el material (el año, el mes, el nombre de la pieza, el programa del proyecto). Las cifras son el material que tienes hoy en esta carpeta, repartido por lo que es cada archivo. Toca cualquier carpeta del árbol para ver qué guarda y cambiar dónde va.</span></p>
<div class="toolbar"> <div class="toolbar">
<label>tipo de trabajo <select id="mapaTipo"></select></label> <label>tipo de trabajo <select id="mapaTipo"></select></label>
@@ -621,16 +626,18 @@
<button class="lnk" id="btnDespintar" style="display:none">Quitar los colores</button> <button class="lnk" id="btnDespintar" style="display:none">Quitar los colores</button>
<span id="mapaResumen" class="mut"></span> <span id="mapaResumen" class="mut"></span>
</div> </div>
<div class="box plano"> <div class="row mapa2">
<h3>¿Dónde va a parar este archivo?</h3>
<p class="mut" style="font-size:12.5px;margin:0 0 8px">Escribe el nombre de cualquier archivo, exista o no, y te digo qué es y en qué carpeta lo dejaría. Lo decide el mismo clasificador que usa el escaneo.</p>
<input type="text" id="mapaArchivo" placeholder="A006_12171629_C001.braw" style="width:100%;max-width:460px">
<div class="chips" id="mapaEjemplos"></div>
<div id="mapaRespuesta"></div>
</div>
<div class="row" style="grid-template-columns:1.35fr 1fr">
<div class="box"><h3>Cómo van a quedar las carpetas</h3><div id="mapaArbol" class="arbol mut">cargando…</div></div> <div class="box"><h3>Cómo van a quedar las carpetas</h3><div id="mapaArbol" class="arbol mut">cargando…</div></div>
<div class="box"><h3>Qué va en esta carpeta</h3><div id="mapaDetalle" class="det mut">Toca una carpeta del árbol para ver qué guarda y cambiar dónde va.</div></div> <div class="lado">
<div class="box"><h3>Qué va en esta carpeta</h3><div id="mapaDetalle" class="det mut">Toca una carpeta del árbol para ver qué guarda y cambiar dónde va.</div></div>
<div class="box plano">
<h3>¿Dónde va a parar este archivo?</h3>
<p class="mut" style="font-size:12.5px;margin:0 0 8px">Escribe el nombre de cualquier archivo, exista o no, y te digo qué es, de qué cámara o grabadora salió y en qué carpeta lo dejaría. Lo decide el mismo clasificador que usa el escaneo.</p>
<input type="text" id="mapaArchivo" placeholder="A006_12171629_C001.braw" style="width:100%">
<div class="chips" id="mapaEjemplos"></div>
<div id="mapaRespuesta"></div>
</div>
</div>
</div> </div>
<div class="box" id="mapaSinSitio" style="display:none"><h3>Sin carpeta asignada</h3><div class="mut" style="font-size:12.5px;margin-bottom:6px">Este material no tiene dónde ir con este tipo de trabajo: se queda donde está. Añádele una carpeta y la app empezará a ordenarlo.</div><div id="mapaSinSitioLista"></div></div> <div class="box" id="mapaSinSitio" style="display:none"><h3>Sin carpeta asignada</h3><div class="mut" style="font-size:12.5px;margin-bottom:6px">Este material no tiene dónde ir con este tipo de trabajo: se queda donde está. Añádele una carpeta y la app empezará a ordenarlo.</div><div id="mapaSinSitioLista"></div></div>
</section> </section>
@@ -733,14 +740,15 @@
<dt>Qué va a pasar</dt><dd>El plan: qué haría la app en la carpeta, sin mover nada todavía. Lo explica con una carpeta de ejemplo, que se ve pasar de donde está a donde va, y debajo lista lo demás. Son tres pasos (apartar basura y proxies, ordenar por año y mes, ordenar lo demás) y un solo clic aplica todo lo marcado, en orden: no hay que ir uno por uno. Lo que no va solo queda aparte y contado: los cambios cosméticos (solo el nombre, «01 - Celular» → «01- Celular») y los dudosos. Mientras aplica, una barra dice por dónde va, archivo a archivo, y al acabar, qué quedó sin hacer. <i>Cómo: usa la plantilla de la clase de trabajo y la fecha de cada archivo, la más antigua entre creación y modificación; un render hereda la fecha de su original. Cada cambio va como seguro, probable o dudoso, y solo los dos primeros se marcan solos.</i></dd> <dt>Qué va a pasar</dt><dd>El plan: qué haría la app en la carpeta, sin mover nada todavía. Lo explica con una carpeta de ejemplo, que se ve pasar de donde está a donde va, y debajo lista lo demás. Son tres pasos (apartar basura y proxies, ordenar por año y mes, ordenar lo demás) y un solo clic aplica todo lo marcado, en orden: no hay que ir uno por uno. Lo que no va solo queda aparte y contado: los cambios cosméticos (solo el nombre, «01 - Celular» → «01- Celular») y los dudosos. Mientras aplica, una barra dice por dónde va, archivo a archivo, y al acabar, qué quedó sin hacer. <i>Cómo: usa la plantilla de la clase de trabajo y la fecha de cada archivo, la más antigua entre creación y modificación; un render hereda la fecha de su original. Cada cambio va como seguro, probable o dudoso, y solo los dos primeros se marcan solos.</i></dd>
<dt>Grabaciones</dt><dd>Una carpeta de grabación se reparte por mes, día y cámara, y conserva el nombre de cada sesión («Tomas consultorio»). Un clip ya convertido sigue contando como de su cámara. <i>Cómo: solo se reparte lo que de verdad es una tarjeta volcada; si dentro hay una pieza montada (exportados, portada, proyecto), no se desarma. El año de un clip de Blackmagic, que el nombre no trae, sale del resto de clips de la misma tarjeta y día, y un rodaje que pasa de medianoche cuenta como uno solo.</i></dd> <dt>Grabaciones</dt><dd>Una carpeta de grabación se reparte por mes, día y cámara, y conserva el nombre de cada sesión («Tomas consultorio»). Un clip ya convertido sigue contando como de su cámara. <i>Cómo: solo se reparte lo que de verdad es una tarjeta volcada; si dentro hay una pieza montada (exportados, portada, proyecto), no se desarma. El año de un clip de Blackmagic, que el nombre no trae, sale del resto de clips de la misma tarjeta y día, y un rodaje que pasa de medianoche cuenta como uno solo.</i></dd>
<dt>Cajones</dt><dd>Una carpeta que guarda meses o años dentro («Orden», «Proyectos\Mayo») no se mueve entera: se abre, y cada cosa va a su mes. Si una pieza ya existe en ese mes, se junta con ella en vez de quedar una carpeta gemela al lado.</dd> <dt>Cajones</dt><dd>Una carpeta que guarda meses o años dentro («Orden», «Proyectos\Mayo») no se mueve entera: se abre, y cada cosa va a su mes. Si una pieza ya existe en ese mes, se junta con ella en vez de quedar una carpeta gemela al lado.</dd>
<dt>Repaso de lo ya ordenado</dt><dd>El plan también mira lo que ya está en <span class="path">00_Grabacion</span>: si un clip lleva en el nombre una fecha que no es la del mes donde está, propone llevarlo al suyo. <i>Cómo: solo con fechas escritas por la cámara; la fecha de modificación no cuenta, salvo para lo que se grabó ese mismo día.</i></dd> <dt>Grabaciones</dt><dd>Lo que sale de las cámaras y las grabadoras va siempre a su mes, dentro a su día de rodaje («13- Jueves») y dentro a una carpeta por fuente: <span class="path">URSA_BRAW</span>, <span class="path">BLACKMAGIC_MP4</span>, <span class="path">SONY</span>, <span class="path">OSMO</span>, <span class="path">DJI</span>, <span class="path">DJI_MIC</span>, <span class="path">TASCAM</span>, <span class="path">ZOOM</span>, <span class="path">CELULAR</span>, <span class="path">FOTOS</span>. <i>Cómo: la fuente se reconoce por el nombre que pone cada aparato (el consecutivo de la cámara, «DJI_01_…» del DJI Mic, «TASCAM_0389»); en «Organizar carpetas › ¿Dónde va a parar este archivo?» se ve de cuál viene cualquier nombre.</i></dd>
<dt>Repaso de lo ya ordenado</dt><dd>El plan también mira lo que ya está en <span class="path">00_Grabacion</span>: si un clip lleva en el nombre una fecha que no es la del mes donde está, propone llevarlo al suyo; y lo que está sin carpeta de día, o con la grabadora en «AUDIO», lo lleva a su día y a su carpeta. <i>Cómo: solo con fechas de fiar (escritas por la cámara, o la fecha de un original de cámara); lo demás se queda donde está.</i></dd>
<dt>Aplicar</dt><dd>Hace los movimientos marcados, siempre dentro de la carpeta del cliente. <i>Cómo: no pisa nada; si en el destino ya hay un archivo con ese nombre, lo deja donde está y lo avisa. Cada movimiento queda apuntado para poder deshacerlo.</i></dd> <dt>Aplicar</dt><dd>Hace los movimientos marcados, siempre dentro de la carpeta del cliente. <i>Cómo: no pisa nada; si en el destino ya hay un archivo con ese nombre, lo deja donde está y lo avisa. Cada movimiento queda apuntado para poder deshacerlo.</i></dd>
<dt>Stock viejo</dt><dd>Aparta a <span class="path">_Para borrar\stock</span> las tomas de Envato de piezas de más de 1, 2, 3, 6 o 12 meses, que se vuelven a descargar con la suscripción. <i>Cómo: las reconoce por el nombre (terminan en «-utc») o por estar en una carpeta «Elements».</i></dd> <dt>Stock viejo</dt><dd>Aparta a <span class="path">_Para borrar\stock</span> las tomas de Envato de piezas de más de 1, 2, 3, 6 o 12 meses, que se vuelven a descargar con la suscripción. <i>Cómo: las reconoce por el nombre (terminan en «-utc») o por estar en una carpeta «Elements».</i></dd>
<dt>Repetidos</dt><dd>Cuando el mismo archivo está en varios sitios, deja uno y junta los demás en una subcarpeta <span class="path">Duplicados</span>. También junta el mismo clip guardado con dos calidades: se queda el de más imagen. <i>Cómo: mismo nombre y mismo peso, y antes de apartar compara el contenido (principio, centro y final); si difiere, no lo toca. Dos clips de cámara que se llaman igual y pesan distinto se leen: si duran lo mismo son el mismo clip, y si no, son dos clips distintos y se dejan. Dos exportados con el mismo nombre no se tocan: son dos entregas de la pieza.</i></dd> <dt>Repetidos</dt><dd>Cuando el mismo archivo está en varios sitios, deja uno y junta los demás en una subcarpeta <span class="path">Duplicados</span>. También junta el mismo clip guardado con dos calidades: se queda el de más imagen. <i>Cómo: mismo nombre y mismo peso, y antes de apartar compara el contenido (principio, centro y final); si difiere, no lo toca. Dos clips de cámara que se llaman igual y pesan distinto se leen: si duran lo mismo son el mismo clip, y si no, son dos clips distintos y se dejan. Dos exportados con el mismo nombre no se tocan: son dos entregas de la pieza.</i></dd>
<dt>Carpetas vacías</dt><dd>Lista los huecos que deja ordenar y los borra. Es lo único que el programa borra, porque dentro no hay nada. <i>Cómo: cada una se comprueba en el disco justo antes; se puede deshacer, y no entra en «hacer de una vez».</i></dd> <dt>Carpetas vacías</dt><dd>Lista los huecos que deja ordenar y los borra. Es lo único que el programa borra, porque dentro no hay nada. <i>Cómo: cada una se comprueba en el disco justo antes; se puede deshacer, y no entra en «hacer de una vez».</i></dd>
</dl> </dl>
<h3>Mapa</h3> <h3>Organizar carpetas</h3>
<dl> <dl>
<dt>El árbol</dt><dd>Cómo quedan las carpetas con cada clase de trabajo y qué va en cada una, con el color de lo que guarda. <i>Cómo: se dibuja con las plantillas de verdad, así que enseña exactamente lo que el programa va a hacer.</i></dd> <dt>El árbol</dt><dd>Cómo quedan las carpetas con cada clase de trabajo y qué va en cada una, con el color de lo que guarda. <i>Cómo: se dibuja con las plantillas de verdad, así que enseña exactamente lo que el programa va a hacer.</i></dd>
<dt>Tocar una carpeta</dt><dd>Dice qué guarda y cuánto material de eso hay hoy, y deja cambiar ahí mismo a dónde va. «Sin carpeta asignada» lista lo que todavía no tiene sitio.</dd> <dt>Tocar una carpeta</dt><dd>Dice qué guarda y cuánto material de eso hay hoy, y deja cambiar ahí mismo a dónde va. «Sin carpeta asignada» lista lo que todavía no tiene sitio.</dd>
@@ -778,7 +786,7 @@
<dl> <dl>
<dt>Clientes</dt><dd>Nombre, clase de trabajo, sus carpetas y los otros nombres con que aparece en los discos.</dd> <dt>Clientes</dt><dd>Nombre, clase de trabajo, sus carpetas y los otros nombres con que aparece en los discos.</dd>
<dt>Actualizaciones</dt><dd>«Buscar actualizaciones» mira si hay versión nueva; el interruptor de «consultar al arrancar» viene apagado. <i>Cómo: descarga el paquete de Labstream OS, comprueba su firma y su huella, se cierra, se sustituye sin tocar tus datos y vuelve a abrirse.</i></dd> <dt>Actualizaciones</dt><dd>«Buscar actualizaciones» mira si hay versión nueva; el interruptor de «consultar al arrancar» viene apagado. <i>Cómo: descarga el paquete de Labstream OS, comprueba su firma y su huella, se cierra, se sustituye sin tocar tus datos y vuelve a abrirse.</i></dd>
<dt>Plantillas</dt><dd>En «Avanzado»: la ruta exacta de cada tipo de material en cada clase de trabajo. Es lo mismo que se cambia desde el Mapa.</dd> <dt>Plantillas</dt><dd>En «Avanzado»: la ruta exacta de cada tipo de material en cada clase de trabajo. Es lo mismo que se cambia desde «Organizar carpetas».</dd>
</dl> </dl>
<h3>La ventana</h3> <h3>La ventana</h3>
@@ -2216,9 +2224,9 @@ const probarArchivo = seguro(async () => {
const fecha = d.por_fecha === 'mtime' const fecha = d.por_fecha === 'mtime'
? 'el nombre no trae fecha, así que el mes lo saca de la fecha del archivo' ? 'el nombre no trae fecha, así que el mes lo saca de la fecha del archivo'
: `el nombre dice que es del ${d.fecha}`; : `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}».` : ''; const dia = d.dia ? ` «${d.dia}» es el día del rodaje.` : '';
caja.innerHTML = `<div class="resp"><i class="pt" style="background:${d.color}"></i><div>` + 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><b>${esc(d.nombre)}</b> — ${esc(d.que)}${d.camara ? ` · fuente ${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="d" style="margin:4px 0">${esc(raizN + SEP + d.destino)}${SEP}</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 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>'; '</div></div>';
@@ -2304,12 +2312,12 @@ function ejemploTipo(ruta, tipo){
for (const seg of dest.split('\\')) { if (!n.hijos.has(seg)) n.hijos.set(seg, {hijos: new Map(), que: ''}); n = n.hijos.get(seg); } for (const seg of dest.split('\\')) { if (!n.hijos.has(seg)) n.hijos.set(seg, {hijos: new Map(), que: ''}); n = n.hijos.get(seg); }
n.que = n.que || que; n.que = n.que || que;
} }
const lineas = [`<div><b>${esc((ruta || '').replace(/[\\/]+$/, ''))}\\</b></div>`]; const lineas = [`<div class="ln"><b>${esc((ruta || '').replace(/[\\/]+$/, ''))}\\</b></div>`];
const pinta = (nodo, sangria) => { const pinta = (nodo, sangria) => {
const hs = [...nodo.hijos.entries()]; const hs = [...nodo.hijos.entries()];
hs.forEach(([nombre, h], i) => { hs.forEach(([nombre, h], i) => {
const ultimo = i === hs.length - 1; const ultimo = i === hs.length - 1;
lineas.push(`<div><b>${esc(sangria)}${ultimo ? '└─ ' : '├─ '}${esc(nombre)}\\</b>${h.que ? `<span>${esc(h.que)}</span>` : ''}</div>`); lineas.push(`<div class="ln"><b>${esc(sangria)}${ultimo ? '└─ ' : '├─ '}${esc(nombre)}\\</b>${h.que ? `<span>${esc(h.que)}</span>` : ''}</div>`);
pinta(h, sangria + (ultimo ? ' ' : '│ ')); pinta(h, sangria + (ultimo ? ' ' : '│ '));
}); });
}; };