diff --git a/README.md b/README.md index 5a65b76..a07b065 100644 --- a/README.md +++ b/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 diff --git a/organizador.py b/organizador.py index ddf8a58..fca5a13 100644 --- a/organizador.py +++ b/organizador.py @@ -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], diff --git a/prueba_funcional.py b/prueba_funcional.py index 8867403..e26d654 100644 --- a/prueba_funcional.py +++ b/prueba_funcional.py @@ -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' diff --git a/prueba_orden.py b/prueba_orden.py index 8220e98..a001b09 100644 --- a/prueba_orden.py +++ b/prueba_orden.py @@ -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"]) diff --git a/reglas.py b/reglas.py index 9d92ffc..7422392 100644 --- a/reglas.py +++ b/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"} diff --git a/static/index.html b/static/index.html index be1d0b5..7c0c970 100644 --- a/static/index.html +++ b/static/index.html @@ -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 @@