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:
26
README.md
26
README.md
@@ -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
|
||||
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
|
||||
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)
|
||||
```
|
||||
|
||||
@@ -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
|
||||
_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
|
||||
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
|
||||
mucho pero se entiende poco», «lo de ordenar por año y por mes, ¿me toca seleccionar uno a uno o lo hace
|
||||
|
||||
@@ -42,7 +42,7 @@ import actualizador
|
||||
asistente.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__))
|
||||
# Empaquetado: el programa corre con el motor que viaja en su propia carpeta. OrganizadorLabstream.exe es
|
||||
@@ -1055,9 +1055,11 @@ class Plan:
|
||||
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 ""
|
||||
"""El nombre de la carpeta del día de rodaje («13- Jueves»).
|
||||
|
||||
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)
|
||||
|
||||
# ---- helpers ----
|
||||
@@ -1595,7 +1597,7 @@ class Plan:
|
||||
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"
|
||||
mot += " · y por día de rodaje"
|
||||
self.agregar(e.path, self.abs_(destino), "GRABACION", "mover archivos", mot + aviso_fecha, fconf,
|
||||
archivos=archivos)
|
||||
|
||||
@@ -1691,24 +1693,51 @@ class Plan:
|
||||
break
|
||||
else:
|
||||
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
|
||||
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_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():
|
||||
fuera = [(f, resto) for f, resto in fs if _discrepa_mes(f["fecha"], f["fecha_origen"], anio_c, mes_c)]
|
||||
if not fuera:
|
||||
continue
|
||||
# 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
|
||||
dias = {f["fecha"] for f, _ in fuera}
|
||||
fuera += [(f, resto) for f, resto in fs
|
||||
if f["fecha_origen"] == "mtime" and f["fecha"] in dias and _fecha_de_camara(f)]
|
||||
fuera = [(f, resto) for f, resto, _ in fs if _discrepa_mes(f["fecha"], f["fecha_origen"], anio_c, mes_c)]
|
||||
if fuera:
|
||||
# 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
|
||||
dias = {f["fecha"] for f, _ in fuera}
|
||||
fuera += [(f, resto) for f, resto, _ in fs
|
||||
if f["fecha_origen"] == "mtime" and f["fecha"] in dias and _fecha_de_camara(f)]
|
||||
for f, resto in fuera:
|
||||
ay, am = int(f["fecha"][:4]), int(f["fecha"][5:7])
|
||||
destino = destino_plantilla(self.tipo, "GRABACION", anio=ay, mes=am,
|
||||
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"))
|
||||
# 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
|
||||
for (origen, destino, anio_c, mes_c, ay, am), archivos in grupos.items():
|
||||
n += len(archivos)
|
||||
@@ -4606,7 +4635,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"),
|
||||
"dia": ("15- Viernes", "el día en que se grabó"),
|
||||
"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"),
|
||||
@@ -4747,15 +4776,15 @@ def donde_iria(nombre, tipo="contenido"):
|
||||
fecha, origen = R.fecha_de_nombre(nombre, time.time())
|
||||
kw = {"anio": int(fecha[:4]), "mes": int(fecha[5:7])}
|
||||
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":
|
||||
cub, kw["sub"] = "GRABACION", "CELULAR"
|
||||
if cub in ("GRABACION", "CELULAR"):
|
||||
kw["dia"] = R.nombre_dia(fecha)
|
||||
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)
|
||||
# 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 ""
|
||||
dia = kw.get("dia", "")
|
||||
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],
|
||||
|
||||
@@ -509,7 +509,7 @@ def p_entrada():
|
||||
assert esperar(lambda: get('/api/estado')['traer'].get('estado') not in ('copiando', 'ordenando'), 120)
|
||||
tr = get('/api/estado')['traer']
|
||||
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.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'
|
||||
@@ -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']]
|
||||
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 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'
|
||||
|
||||
|
||||
|
||||
@@ -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.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"]}
|
||||
self.assertIn(rel("2025", "11- Noviembre", "00_Grabacion", "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"), 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
|
||||
self.assertIn(rel("2025", "05- Mayo", "00_Grabacion", "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", "BLACKMAGIC_MP4"), 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)
|
||||
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)
|
||||
aplicar(raiz, p)
|
||||
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"),
|
||||
rel("2025", "11- Noviembre", "00_Grabacion", "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", "Maquinas", "URSA_BRAW", "A006_06010030_C003.braw"),
|
||||
for esperado in (rel("2025", "11- Noviembre", "00_Grabacion", "11- Martes", "Tomas consultorio", "SONY", "20251111_Labstream5320.MP4"),
|
||||
rel("2025", "11- Noviembre", "00_Grabacion", "11- Martes", "Tomas consultorio", "SONY", "20251111_Labstream5321_10bit.mp4"),
|
||||
rel("2025", "05- Mayo", "00_Grabacion", "31- Sábado", "Maquinas", "BLACKMAGIC_MP4", "A006_06010000_C002.mp4"),
|
||||
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", "proyecto.prproj"),
|
||||
rel("2026", "05- Mayo", "03- El cuello", "portada vieja.png")):
|
||||
@@ -203,7 +203,7 @@ class Motor(unittest.TestCase):
|
||||
p = plan(raiz)
|
||||
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
|
||||
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):
|
||||
"""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)
|
||||
p = plan(raiz)
|
||||
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.assertEqual(por_archivo["proyecto.drp"]["destino_rel"], rel("01_Proyectos", "DaVinci Resolve"))
|
||||
self.assertNotIn("00_Grabacion", por_archivo["guion.docx"]["destino_rel"])
|
||||
@@ -244,12 +244,13 @@ class Motor(unittest.TestCase):
|
||||
aplicar(raiz, p)
|
||||
self.assertEqual(nombres(raiz), antes)
|
||||
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",
|
||||
"URSA_BRAW" + S + "Duplicados" + S + "A014_12091814_C001.mp4", "SONY" + S + "C0001.MP4"):
|
||||
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
|
||||
"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)))
|
||||
# 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")
|
||||
self.assertTrue(os.path.isfile(os.path.join(mar, "SONY", "C0002.MP4")))
|
||||
self.assertTrue(os.path.isfile(os.path.join(mar, "URSA_BRAW", "A002_03110342_C036.braw")))
|
||||
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, "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")
|
||||
|
||||
def test_carpetas_gemelas_y_papelera_vieja(self):
|
||||
@@ -474,7 +475,7 @@ class Nuevo(unittest.TestCase):
|
||||
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")
|
||||
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)))
|
||||
with open(final, "rb") as fh:
|
||||
self.assertEqual(fh.read(), contenido)
|
||||
@@ -554,7 +555,7 @@ class Nuevo(unittest.TestCase):
|
||||
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")
|
||||
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, "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")])
|
||||
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.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.assertEqual(O.estado_automatico()["ultimo"]["origen"], "prueba")
|
||||
self.assertFalse(O.AUTO["activo"])
|
||||
|
||||
14
reglas.py
14
reglas.py
@@ -122,8 +122,12 @@ FIRMAS = [
|
||||
("SONY", r"^c\d{4}(m01)?\.(mp4|mov|xml)$", "Sony"),
|
||||
("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_MIC", r"^dji_\d{2}_\d{8}_\d{6}\.wav$", "DJI Mic"),
|
||||
("TASCAM", r"^tascam_\d{4}\.wav$", "Tascam"),
|
||||
# las grabadoras de audio: el DJI Mic nombra «DJI_01_20251113_143000.WAV» (01 o 02 es el transmisor;
|
||||
# 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"),
|
||||
("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)"),
|
||||
@@ -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
|
||||
"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",
|
||||
}
|
||||
|
||||
@@ -292,7 +296,7 @@ def rol_archivo(nombre: str, ext: str, firma: str | None) -> str:
|
||||
return "celular_video"
|
||||
if firma in ("CELULAR", "WHATSAPP") and e in EXT["imagen"]:
|
||||
return "celular_foto"
|
||||
if firma in ("DJI_MIC", "TASCAM"):
|
||||
if firma in ("DJI_MIC", "TASCAM", "ZOOM"):
|
||||
return "audio_grabado"
|
||||
if firma in ("PANASONIC", "SONY_FOTO") or e in EXT["foto_raw"]:
|
||||
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
|
||||
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",
|
||||
"fotos", "foto", "atomos", "tarjeta", "tarjetas", "card", "sd"}
|
||||
|
||||
|
||||
@@ -176,9 +176,14 @@
|
||||
.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}
|
||||
.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 b{font-weight:600}
|
||||
.arbol span{font:12px/1.4 Manrope,system-ui,sans-serif;color:var(--mut);white-space:normal}
|
||||
.arbol .ln{display:flex;gap:10px;align-items:baseline;white-space:pre}
|
||||
.arbol .ln b{font-weight:600}
|
||||
.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 .nd{display:flex;gap:10px;align-items:baseline;padding:3px 7px;border-radius:5px;cursor:pointer}
|
||||
.arbol .nd:hover{background:var(--panel2)}
|
||||
@@ -497,7 +502,7 @@
|
||||
<nav id="nav" aria-label="Vistas">
|
||||
<a data-t="carpeta" class="on" tabindex="0">Carpeta</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="respaldar" tabindex="0">Respaldar</a>
|
||||
<a data-t="historial" tabindex="0">Historial</a>
|
||||
@@ -612,7 +617,7 @@
|
||||
|
||||
<!-- ============ 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>
|
||||
<div class="toolbar">
|
||||
<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>
|
||||
<span id="mapaResumen" class="mut"></span>
|
||||
</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 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="row mapa2">
|
||||
<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 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>
|
||||
@@ -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>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>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>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>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>
|
||||
|
||||
<h3>Mapa</h3>
|
||||
<h3>Organizar carpetas</h3>
|
||||
<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>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>
|
||||
<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>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>
|
||||
|
||||
<h3>La ventana</h3>
|
||||
@@ -2216,9 +2224,9 @@ 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}».` : '';
|
||||
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>` +
|
||||
`<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="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>';
|
||||
@@ -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); }
|
||||
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 hs = [...nodo.hijos.entries()];
|
||||
hs.forEach(([nombre, h], i) => {
|
||||
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 ? ' ' : '│ '));
|
||||
});
|
||||
};
|
||||
|
||||
Referencia en una nueva incidencia
Block a user