3.7.0: auditoría de Vía Sumapaz: «Calcular» ya no se queda gris, banco de material por temas, proxies sin original, y «Qué pesa» por dentro
- El botón «Calcular» (y «Reanudar» la cola, y «Volver a pintar») quedaba deshabilitado para siempre tras la primera vez: al cambiar de carpeta «Qué va a pasar» no calculaba nada. - «material» suelto ya no ignora una carpeta; «No tocar» es un banco de material: los temas se quedan y, dentro, las grabaciones van por fecha y cámara, todo sin marcar. - «Hacer de una vez» y la pasada de la noche ya no mueven basura ni proxies sin marcar; un proxy sin su original va sin marcar. - «Qué pesa» entra nivel a nivel con el detalle de cada carpeta; repetidos, plan e Inicio mucho más rápidos; aviso cuando el tipo de trabajo de la carpeta no es el del cliente. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
51
README.md
51
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
|
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 32 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 33 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)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -880,6 +880,55 @@ Pruebas: `prueba_orden.py` 24 casos (5 nuevos: pieza, bandeja, tarjeta con una c
|
|||||||
que rechazar, versiones con ffprobe simulado, y la pasada automática con sus frenos); `prueba_funcional.py`
|
que rechazar, versiones con ffprobe simulado, y la pasada automática con sus frenos); `prueba_funcional.py`
|
||||||
29 de 29 (3 nuevas, por la interfaz de verdad del servidor); `probar.py` 19 y `prueba_ventana.py` 18.
|
29 de 29 (3 nuevas, por la interfaz de verdad del servidor); `probar.py` 19 y `prueba_ventana.py` 18.
|
||||||
|
|
||||||
|
3.7.0, la auditoría de Vía Sumapaz. Munka: «creo que el escáner de archivos fue superficial», «no me sale la
|
||||||
|
forma de organización del material», «no hay reorganización y faltan funciones», y una captura de «Qué va a
|
||||||
|
pasar» con el botón «Calcular» gris y ningún plan. Lo medido el 14-oct-2026 sobre `Y:\Sumapaz` (76.559
|
||||||
|
archivos, 4,27 TB), solo leyendo:
|
||||||
|
|
||||||
|
- **El escaneo no era superficial**: un recorrido aparte del disco encontró los mismos 76.559 archivos y los
|
||||||
|
mismos 4,270 TB, hasta 11 niveles. Lo que fallaba era lo que la app hacía y enseñaba con ellos.
|
||||||
|
- **«Calcular» se quedaba gris para siempre** después del primer cálculo, en cualquier carpeta: al acabar, su
|
||||||
|
etiqueta pasa a «Calcular de nuevo», y la rutina que desbloquea los botones solo lo hacía si la etiqueta no
|
||||||
|
había cambiado. Al cambiar de carpeta, «Qué va a pasar» intentaba calcular, veía el botón bloqueado y no
|
||||||
|
hacía nada (con la lista de la carpeta anterior a la vista). Lo mismo «Reanudar» la cola después de
|
||||||
|
«Detener», y «Volver a pintar». Ahora un botón re-etiquetado se desbloquea salvo que alguien lo bloquee a
|
||||||
|
propósito, y la lista del plan se vacía al cambiar de carpeta.
|
||||||
|
- **El 85 % del disco quedaba fuera del orden**: «Material Vía (No tocar)», 3,6 TB, y dentro «Material
|
||||||
|
Importante», 2,6 TB, se ignoraban porque la palabra «material» suelta contaba como «dejar como está». Ahora
|
||||||
|
solo «Material» a secas es estructura. Y una carpeta cuyo nombre dice «No tocar» (o «No mover») es un
|
||||||
|
**banco de material por temas**: los temas se quedan donde están y, dentro de cada uno, las grabaciones van
|
||||||
|
por fecha de rodaje y cámara (`Lugares\Peajes\Drone` → `Lugares\Peajes\2024-03-12\DJI`); las carpetas
|
||||||
|
que solo dicen la cámara o la tarjeta («Drone», «URSA», «Pocket», «100MEDIA», «Grabado») se funden en su
|
||||||
|
tema. Todo lo que el plan propone dentro de un «No tocar» (sus proxies y su basura incluidos) va sin marcar
|
||||||
|
y no entra en «hacer de una vez»: en «Qué va a pasar» es un paso propio, «Banco de material», con «marcar
|
||||||
|
los N». Una fecha de antes de 2014 sacada del reloj de la cámara va como dudosa (250 clips de la Blackmagic
|
||||||
|
«del 28 de julio de 2008»). Otra carpeta se puede marcar como banco en «Por nombrar». En Sumapaz: 381
|
||||||
|
cambios, 10.902 grabaciones, 2,06 TB.
|
||||||
|
- **«Hacer de una vez» y la pasada de la noche movían basura y proxies sin marcar** (las demás filas sí
|
||||||
|
respetaban la marca). Ahora solo lo marcado.
|
||||||
|
- **Proxies sin su original**: de 12.645 proxies (449 GB), 126 no tenían el original en ningún sitio de la
|
||||||
|
carpeta (clips Fuji en una carpeta «Proxy», el proxy de un reel cuyo master ya no está, .LRF de dron sin su
|
||||||
|
MP4). Iban marcados a `_Para borrar`; ahora van aparte, sin marcar y con un aviso. Se emparejan por nombre,
|
||||||
|
también los que graba la Sony en `M4ROOT\SUB` (`C0001S03.MP4` es el proxy de `C0001.MP4`).
|
||||||
|
- **«Qué pesa» entra hasta el fondo**: antes solo el primer nivel, y «Material Vía» era un bloque gris de
|
||||||
|
3,64 TB sin nada dentro. Ahora se toca un bloque y se entra, nivel a nivel, con migas para volver; al lado,
|
||||||
|
qué guarda, de qué cámaras o grabadoras, de qué fechas, cuántos proxies y basura se pueden apartar, qué hace
|
||||||
|
la app con esa carpeta y cuántos cambios tiene en el plan (`/api/mapa/carpeta`). En Inicio: «Escaneo
|
||||||
|
completo: 76.559 archivos en 3.285 carpetas, hasta 11 niveles de profundidad. Ver qué hay dentro».
|
||||||
|
- **Más rápido**: Inicio tardaba 18 s en SUMAPAZ y «Qué hay» 21 s, casi todo comprobando en el disco de red
|
||||||
|
que existía cada uno de 30.963 archivos para buscar repetidos. Ahora solo se comprueban los que pueden
|
||||||
|
formar grupo, en paralelo, y el resultado se guarda hasta que algo se mueve: 2,3 s la primera vez, nada
|
||||||
|
las siguientes; Inicio, 4,4 s. Mientras carga, Inicio dice «Leyendo la carpeta» y «Qué hay» «Leyendo el
|
||||||
|
inventario» (antes decía «Esta carpeta todavía no está escaneada»). El plan de SUMAPAZ, de 6,5 s a 2,4 s
|
||||||
|
(apartar la basura comparaba cada archivo con cada carpeta ya cubierta: 5 millones de comparaciones).
|
||||||
|
- **El tipo de trabajo no cuadraba**: Vía Sumapaz estaba guardado como «proyecto» y su carpeta como
|
||||||
|
«contenido», sin que nada lo dijera. Inicio lo dice y deja elegir uno; elegirlo iguala los dos
|
||||||
|
(`/api/raiz/tipo`).
|
||||||
|
|
||||||
|
Pruebas: `prueba_orden.py` 34 casos (uno nuevo: el banco de material, de punta a punta, con «No tocar»,
|
||||||
|
reloj sin poner y proxies sin original); `prueba_funcional.py` 33 de 33 (una nueva: «Qué pesa» por dentro y el
|
||||||
|
tipo de trabajo); `probar.py` 19 y `prueba_ventana.py` 18.
|
||||||
|
|
||||||
3.6.0, Inicio dice lo que ya se hizo. Munka pidió auditar «mover y reorganizar»: «si hago el cambio debería
|
3.6.0, Inicio dice lo que ya se hizo. Munka pidió auditar «mover y reorganizar»: «si hago el cambio debería
|
||||||
decir que el cambio se hizo y no mandarme para hacerlo y mostrarme lo que pasaría o lo que ahorraría cuando ya
|
decir que el cambio se hizo y no mandarme para hacerlo y mostrarme lo que pasaría o lo que ahorraría cuando ya
|
||||||
lo había movido». Reproducido el 14-oct-2026 sobre una carpeta de ensayo: el motor mueve bien (plan → aplicar →
|
lo había movido». Reproducido el 14-oct-2026 sobre una carpeta de ensayo: el motor mueve bien (plan → aplicar →
|
||||||
|
|||||||
@@ -191,7 +191,9 @@ def filas_elegidas(plan_filas, dec):
|
|||||||
for f in plan_filas:
|
for f in plan_filas:
|
||||||
c = f["cubeta"]
|
c = f["cubeta"]
|
||||||
if c in ("BASURA", "PROXY"):
|
if c in ("BASURA", "PROXY"):
|
||||||
if dec.get("limpieza", "papelera") == "papelera":
|
# también aquí, solo lo marcado: un proxy sin su original o lo que está dentro de una carpeta
|
||||||
|
# «No tocar» van sin marcar, y «hacer de una vez» (y la pasada de la noche) los movían igual
|
||||||
|
if dec.get("limpieza", "papelera") == "papelera" and f["marcado"]:
|
||||||
out.append(f)
|
out.append(f)
|
||||||
elif c in ("GRABACION", "CELULAR", "FOTOS"):
|
elif c in ("GRABACION", "CELULAR", "FOTOS"):
|
||||||
# solo lo marcado: antes entraba todo el material aunque el plan lo diera por dudoso (el mes
|
# solo lo marcado: antes entraba todo el material aunque el plan lo diera por dudoso (el mes
|
||||||
|
|||||||
438
organizador.py
438
organizador.py
@@ -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.6.0"
|
VERSION = "3.7.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
|
||||||
@@ -1000,6 +1000,40 @@ def _fecha_de_camara(f):
|
|||||||
|
|
||||||
|
|
||||||
_RX_DIA = re.compile(r"^\d{2}- (" + "|".join(R.DIAS) + r")$", re.I)
|
_RX_DIA = re.compile(r"^\d{2}- (" + "|".join(R.DIAS) + r")$", re.I)
|
||||||
|
_RX_FECHA_DIA = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||||
|
# en un banco de material, lo que se reordena por fecha y cámara: el material grabado, no lo montado
|
||||||
|
_ROLES_BANCO = ("camara_raw", "camara_video", "camara_sidecar", "audio_grabado", "celular_video")
|
||||||
|
# carpetas que solo dicen con qué se grabó (además de las de R.FUENTE_PALABRAS)
|
||||||
|
_CAMARAS_BANCO = {"fuji", "gopro", "go pro", "mavic", "osmos", "insta360", "iphone", "ronin", "fx3", "fx6", "a7", "a7s"}
|
||||||
|
_NOMBRES_FUENTE = {v.upper() for v in R.FUENTE_CARPETA.values()} | {"AUDIO", "OTROS", "FOTOS"}
|
||||||
|
|
||||||
|
|
||||||
|
def _solo_fuente(seg):
|
||||||
|
"""True si la carpeta solo dice la cámara, la tarjeta, el día o que son repetidos: no es un tema."""
|
||||||
|
s = (seg or "").strip()
|
||||||
|
n = R.norm(s)
|
||||||
|
return (n in R.FUENTE_PALABRAS or n in _CAMARAS_BANCO or R.es_carpeta_de_tarjeta(s) or bool(_RX_DIA.match(s))
|
||||||
|
or bool(_RX_FECHA_DIA.match(s)) or s.upper() in _NOMBRES_FUENTE or n == R.norm(R.DUPLICADOS))
|
||||||
|
|
||||||
|
|
||||||
|
def _bajo_alguna(ruta, conjunto):
|
||||||
|
"""True si `ruta` o alguna carpeta por encima de ella está en `conjunto` (rutas en minúsculas)."""
|
||||||
|
d = os.path.normpath(ruta).lower()
|
||||||
|
while True:
|
||||||
|
if d in conjunto:
|
||||||
|
return True
|
||||||
|
p = os.path.dirname(d)
|
||||||
|
if p == d:
|
||||||
|
return False
|
||||||
|
d = p
|
||||||
|
|
||||||
|
|
||||||
|
def _tallo(nombre):
|
||||||
|
"""«C0001_Proxy.mov», «C0001S03.MP4» (el proxy que graba la Sony en M4ROOT/SUB) y «C0001.MP4» -> «c0001»:
|
||||||
|
para emparejar un proxy con su original."""
|
||||||
|
t = re.sub(r"[ _.-]?proxy$", "", os.path.splitext(nombre or "")[0].lower())
|
||||||
|
m = re.match(r"^(c\d{4})s\d{2}$", t)
|
||||||
|
return m.group(1) if m else t
|
||||||
|
|
||||||
|
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
@@ -1014,6 +1048,9 @@ class Plan:
|
|||||||
self.dicc = DICC["carpetas"]
|
self.dicc = DICC["carpetas"]
|
||||||
self.reglas = [r for r in DICC["reglas"] if r.get("cliente") in (None, "", self.cliente)]
|
self.reglas = [r for r in DICC["reglas"] if r.get("cliente") in (None, "", self.cliente)]
|
||||||
self.filas = []
|
self.filas = []
|
||||||
|
self.bancos = [] # (ruta normcase + sep, nombre, protegido): los bancos de material
|
||||||
|
self._carpeta_existe = {} # destino -> si ya existe en el disco (para no preguntar dos veces)
|
||||||
|
self._bytes = None # ruta -> bytes, para partir filas de archivos
|
||||||
self.sueltos = defaultdict(list) # (cubeta, destino, motivo, confianza) -> archivos
|
self.sueltos = defaultdict(list) # (cubeta, destino, motivo, confianza) -> archivos
|
||||||
self.con = db()
|
self.con = db()
|
||||||
self.avisos = []
|
self.avisos = []
|
||||||
@@ -1076,8 +1113,14 @@ class Plan:
|
|||||||
return r
|
return r
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _hay_carpeta(self, d):
|
||||||
|
d = os.path.normcase(os.path.normpath(d))
|
||||||
|
if d not in self._carpeta_existe:
|
||||||
|
self._carpeta_existe[d] = os.path.isdir(d)
|
||||||
|
return self._carpeta_existe[d]
|
||||||
|
|
||||||
def agregar(self, origen, destino, cubeta, accion, motivo, confianza, archivos=None, n=None, bytes_=None,
|
def agregar(self, origen, destino, cubeta, accion, motivo, confianza, archivos=None, n=None, bytes_=None,
|
||||||
marcado=None, kw_destinos=None):
|
marcado=None, kw_destinos=None, paso=None):
|
||||||
destino = os.path.normpath(destino)
|
destino = os.path.normpath(destino)
|
||||||
if archivos is None and os.path.normpath(origen).lower() == destino.lower():
|
if archivos is None and os.path.normpath(origen).lower() == destino.lower():
|
||||||
# el mismo sitio con otras mayúsculas solo tiene sentido como renombrado («10- octubre» ->
|
# el mismo sitio con otras mayúsculas solo tiene sentido como renombrado («10- octubre» ->
|
||||||
@@ -1101,7 +1144,8 @@ class Plan:
|
|||||||
# solo los primeros: cada comprobación es una consulta al disco de red, y para avisar
|
# solo los primeros: cada comprobación es una consulta al disco de red, y para avisar
|
||||||
# basta con encontrar uno
|
# basta con encontrar uno
|
||||||
choques = [os.path.basename(a[0]) for a in archivos[:40]
|
choques = [os.path.basename(a[0]) for a in archivos[:40]
|
||||||
if os.path.isfile(especiales.get(a[0]) or os.path.join(destino, os.path.basename(a[0])))]
|
if self._hay_carpeta(os.path.dirname(especiales.get(a[0]) or os.path.join(destino, "x")))
|
||||||
|
and os.path.isfile(especiales.get(a[0]) or os.path.join(destino, os.path.basename(a[0])))]
|
||||||
if choques:
|
if choques:
|
||||||
aviso_colision = (f" · OJO: en el destino ya hay archivos con el mismo nombre "
|
aviso_colision = (f" · OJO: en el destino ya hay archivos con el mismo nombre "
|
||||||
f"(«{choques[0]}»): esos no se moverían")
|
f"(«{choques[0]}»): esos no se moverían")
|
||||||
@@ -1128,8 +1172,8 @@ class Plan:
|
|||||||
"cosmetico": marcado is False and accion == "renombrar",
|
"cosmetico": marcado is False and accion == "renombrar",
|
||||||
# en qué paso del recorrido se aplica: 1 limpiar (basura/proxies), 2 por fecha (años, meses, grabaciones,
|
# en qué paso del recorrido se aplica: 1 limpiar (basura/proxies), 2 por fecha (años, meses, grabaciones,
|
||||||
# piezas), 3 el resto (exportados, proyectos, marca, música, dudosos)
|
# piezas), 3 el resto (exportados, proyectos, marca, música, dudosos)
|
||||||
"paso": "limpiar" if accion == "papelera" or cubeta in ("BASURA", "PROXY") else
|
"paso": paso or ("limpiar" if accion == "papelera" or cubeta in ("BASURA", "PROXY") else
|
||||||
"fechas" if cubeta in ("MES", "ANIO", "GRABACION", "PIEZA", "CELULAR") else "resto",
|
"fechas" if cubeta in ("MES", "ANIO", "GRABACION", "PIEZA", "CELULAR") else "resto"),
|
||||||
})
|
})
|
||||||
|
|
||||||
def suelto(self, fila, cubeta, destino_rel, motivo, confianza):
|
def suelto(self, fila, cubeta, destino_rel, motivo, confianza):
|
||||||
@@ -1185,6 +1229,8 @@ class Plan:
|
|||||||
self.plan_auditoria()
|
self.plan_auditoria()
|
||||||
self.volcar_sueltos()
|
self.volcar_sueltos()
|
||||||
self.plan_basura()
|
self.plan_basura()
|
||||||
|
self.revisar_proxies()
|
||||||
|
self.proteger_bancos()
|
||||||
self.con.close()
|
self.con.close()
|
||||||
# primero las filas de archivos sueltos (rutas exactas), luego carpetas de más profunda a menos,
|
# primero las filas de archivos sueltos (rutas exactas), luego carpetas de más profunda a menos,
|
||||||
# para que un renombrado de mes no deje rutas viejas en filas que aún no se aplicaron
|
# para que un renombrado de mes no deje rutas viejas en filas que aún no se aplicaron
|
||||||
@@ -1383,6 +1429,9 @@ class Plan:
|
|||||||
cub, motivo = R.cubeta_carpeta(nombre, self.dicc)
|
cub, motivo = R.cubeta_carpeta(nombre, self.dicc)
|
||||||
if cub == "IGNORAR":
|
if cub == "IGNORAR":
|
||||||
return
|
return
|
||||||
|
if cub == "BANCO":
|
||||||
|
self.plan_banco(e)
|
||||||
|
return
|
||||||
if cub == "CONTENEDOR":
|
if cub == "CONTENEDOR":
|
||||||
m_anio = re.search(r"(20\d\d)", nombre)
|
m_anio = re.search(r"(20\d\d)", nombre)
|
||||||
if self.est.fechas_dentro(e.path):
|
if self.est.fechas_dentro(e.path):
|
||||||
@@ -1488,6 +1537,153 @@ class Plan:
|
|||||||
return None, ""
|
return None, ""
|
||||||
return (a, m), origenes.most_common(1)[0][0]
|
return (a, m), origenes.most_common(1)[0][0]
|
||||||
|
|
||||||
|
def plan_banco(self, e):
|
||||||
|
"""Un banco de material por temas («Material Vía (No tocar)\\Lugares\\Peajes»): el cliente lo consulta
|
||||||
|
por tema, así que los temas se quedan donde están. Lo que se ordena son las grabaciones de dentro de
|
||||||
|
cada tema: por fecha de rodaje y por cámara, como en cualquier 00_Grabacion. «Lugares\\Peajes\\Drone»
|
||||||
|
pasa a «Lugares\\Peajes\\2024-03-12\\DJI». Las carpetas que solo dicen la cámara, la tarjeta o el día
|
||||||
|
(«Drone», «URSA», «Pocket», «100MEDIA», «Grabado») no son un tema: se funden en el tema de encima.
|
||||||
|
|
||||||
|
Medido el 14-oct-2026 en Vía Sumapaz: el banco eran 3,6 TB, 50.076 archivos, con temas como Lugares,
|
||||||
|
Social, Ambiental o Unidades Funcionales; tratado como piezas sueltas, el plan quería llevar cada tema
|
||||||
|
entero a un mes inventado. Si el nombre dice «No tocar», todo lo de dentro va sin marcar."""
|
||||||
|
protegido = R.es_no_tocar(e.name)
|
||||||
|
self.bancos.append((os.path.normcase(os.path.normpath(e.path)) + os.sep, e.name, protegido))
|
||||||
|
grupos = defaultdict(list)
|
||||||
|
for f in archivos_bajo(self.con, e.path):
|
||||||
|
if f["es_basura"] or f["es_proxy"] or f["rol"] not in _ROLES_BANCO or not f["fecha"]:
|
||||||
|
continue
|
||||||
|
rel = os.path.relpath(f["dir"], e.path)
|
||||||
|
segs = [] if rel == "." else rel.split(os.sep)
|
||||||
|
repetido = False
|
||||||
|
while segs and _solo_fuente(segs[-1]):
|
||||||
|
repetido = repetido or R.norm(segs[-1]) == R.norm(R.DUPLICADOS)
|
||||||
|
segs.pop()
|
||||||
|
fuente = R.FUENTE_CARPETA.get(f["fuente"]) or ("AUDIO" if f["rol"] == "audio_grabado" else "OTROS")
|
||||||
|
fiable = f["fecha_origen"] != "mtime" or _fecha_de_camara(f)
|
||||||
|
# una cámara con el reloj sin poner fecha en 2008 o 2009: medido en Vía Sumapaz, 250 clips de la
|
||||||
|
# Blackmagic «del 28 de julio de 2008» y 100 del dron «del 19 de enero de 2009»
|
||||||
|
reloj = f["fecha_origen"] != "nombre" and int(f["fecha"][:4]) < 2014
|
||||||
|
destino = os.path.join(e.path, *segs, f["fecha"][:10], fuente, *([R.DUPLICADOS] if repetido else []))
|
||||||
|
if os.path.normcase(os.path.normpath(destino)) == os.path.normcase(os.path.normpath(f["dir"])):
|
||||||
|
continue
|
||||||
|
tema = segs[-1] if segs else e.name
|
||||||
|
por_que = "reloj" if reloj else "" if fiable else "mtime"
|
||||||
|
grupos[(f["dir"], destino, tema, "media" if fiable and not reloj else "baja", por_que)].append((f["ruta"], f["bytes"]))
|
||||||
|
for (origen, destino, tema, conf, por_que), archivos in sorted(grupos.items()):
|
||||||
|
self.agregar(origen, destino, "GRABACION", "mover archivos",
|
||||||
|
f"banco de material «{e.name}»: el tema «{tema}» se queda; dentro, por fecha de rodaje y cámara"
|
||||||
|
+ {"": "", "mtime": " (la fecha sale de la fecha de modificación: puede fallar)",
|
||||||
|
"reloj": f" (la fecha, {next((x for x in destino.split(os.sep) if _RX_FECHA_DIA.match(x)), '')}, parece de una cámara con el reloj sin poner: no es de fiar)"}[por_que],
|
||||||
|
conf, archivos=archivos, marcado=False if protegido else None, paso="banco")
|
||||||
|
self.filas[-1]["no_tocar"] = protegido
|
||||||
|
|
||||||
|
def _partir(self, f, aparte, motivo, confianza=None, no_tocar=False):
|
||||||
|
"""Saca de una fila de archivos los que cumplen `aparte` a una fila propia, sin marcar. Si son todos,
|
||||||
|
la fila entera queda sin marcar. Devuelve la fila nueva, si hace falta una."""
|
||||||
|
sep = [a for a in f["archivos"] if aparte(a)]
|
||||||
|
if not sep:
|
||||||
|
return None
|
||||||
|
if len(sep) == len(f["archivos"]):
|
||||||
|
f["marcado"] = False
|
||||||
|
f["no_tocar"] = f.get("no_tocar") or no_tocar
|
||||||
|
f["confianza"] = confianza or f["confianza"]
|
||||||
|
if motivo not in f["motivo"]:
|
||||||
|
f["motivo"] += motivo
|
||||||
|
return None
|
||||||
|
if self._bytes is None:
|
||||||
|
self._bytes = {r["ruta"]: r["bytes"] or 0 for r in self.con.execute("SELECT ruta, bytes FROM archivos WHERE raiz = ?", (self.raiz,))}
|
||||||
|
quedan = [a for a in f["archivos"] if not aparte(a)]
|
||||||
|
nueva = dict(f)
|
||||||
|
destinos = dict(f.get("destinos_archivo") or {})
|
||||||
|
for fila, lista in ((f, quedan), (nueva, sep)):
|
||||||
|
fila["archivos"] = lista
|
||||||
|
fila["n"] = len(lista)
|
||||||
|
fila["bytes"] = sum(self._bytes.get(a, 0) for a in lista)
|
||||||
|
if destinos:
|
||||||
|
fila["destinos_archivo"] = {a: destinos[a] for a in lista if a in destinos}
|
||||||
|
nueva.update(marcado=False, confianza=confianza or f["confianza"], motivo=f["motivo"] + motivo, cosmetico=False,
|
||||||
|
no_tocar=f.get("no_tocar") or no_tocar)
|
||||||
|
return nueva
|
||||||
|
|
||||||
|
def revisar_proxies(self):
|
||||||
|
"""Un proxy sin su original puede ser la única copia que queda de ese clip. Medido el 14-oct-2026 en Vía
|
||||||
|
Sumapaz: de 12.645 proxies (449 GB), 126 (4,4 GB) no tenían el original en ningún sitio de la carpeta
|
||||||
|
(clips Fuji de una carpeta «Proxy», el proxy de un reel cuyo master ya no está, .LRF de un dron sin su
|
||||||
|
MP4). Esos van aparte y sin marcar; los demás siguen yendo a _Para borrar como siempre."""
|
||||||
|
filas = [f for f in self.filas if f["cubeta"] == "PROXY"]
|
||||||
|
if not filas:
|
||||||
|
return
|
||||||
|
originales = {_tallo(r["nombre"]) for r in self.con.execute(
|
||||||
|
"SELECT nombre FROM archivos WHERE raiz = ? AND es_proxy = 0 AND es_basura = 0", (self.raiz,))}
|
||||||
|
|
||||||
|
def huerfano(ruta):
|
||||||
|
n = os.path.basename(ruta)
|
||||||
|
ext = os.path.splitext(n)[1].lower()
|
||||||
|
return (ext in R.EXT["video"] or ext == ".lrf") and _tallo(n) not in originales
|
||||||
|
bajo = defaultdict(list) # carpeta (minúsculas) -> los proxies que tiene debajo
|
||||||
|
tope = self.raiz.lower()
|
||||||
|
for r in self.con.execute("SELECT ruta, nombre, bytes FROM archivos WHERE raiz = ? AND es_proxy = 1", (self.raiz,)).fetchall():
|
||||||
|
d = os.path.dirname(r["ruta"]).lower()
|
||||||
|
while True:
|
||||||
|
bajo[d].append(r)
|
||||||
|
p = os.path.dirname(d)
|
||||||
|
if d == tope or p == d:
|
||||||
|
break
|
||||||
|
d = p
|
||||||
|
nuevas, n_sin, b_sin = [], 0, 0
|
||||||
|
for f in filas:
|
||||||
|
if f["archivos"]:
|
||||||
|
sin = [a for a in f["archivos"] if huerfano(a)]
|
||||||
|
if not sin:
|
||||||
|
continue
|
||||||
|
nueva = self._partir(f, huerfano, f" · OJO: {_plural(len(sin), 'proxy no tiene', 'proxies no tienen')} su original en la "
|
||||||
|
f"carpeta (p. ej. «{os.path.basename(sin[0])}»): puede ser la única copia; míralos antes", "baja")
|
||||||
|
if nueva:
|
||||||
|
nuevas.append(nueva)
|
||||||
|
n_sin += len(sin)
|
||||||
|
else:
|
||||||
|
sin = [r for r in bajo.get(os.path.normpath(f["origen"]).lower(), []) if huerfano(r["ruta"])]
|
||||||
|
if not sin:
|
||||||
|
continue
|
||||||
|
f["motivo"] += (f" · OJO: {_plural(len(sin), 'proxy no tiene', 'proxies no tienen')} su original en la carpeta "
|
||||||
|
f"(p. ej. «{sin[0]['nombre']}»): puede ser la única copia; míralos antes")
|
||||||
|
f["confianza"], f["marcado"] = "baja", False
|
||||||
|
n_sin += len(sin)
|
||||||
|
b_sin += sum(r["bytes"] or 0 for r in sin)
|
||||||
|
self.filas += nuevas
|
||||||
|
if n_sin:
|
||||||
|
self.avisos.append(f"{_plural(n_sin, 'proxy no tiene', 'proxies no tienen')} su original en ningún sitio de esta carpeta. "
|
||||||
|
f"Pueden ser la única copia de esos clips: van sin marcar, para que los mires antes de apartarlos.")
|
||||||
|
|
||||||
|
def proteger_bancos(self):
|
||||||
|
"""Todo lo que el plan propone dentro de una carpeta «No tocar» (sus proxies, su basura) va sin marcar."""
|
||||||
|
prot = [(p, nombre) for p, nombre, protegido in self.bancos if protegido]
|
||||||
|
if not prot:
|
||||||
|
return
|
||||||
|
|
||||||
|
def de_cual(ruta):
|
||||||
|
rc = os.path.normcase(os.path.normpath(ruta))
|
||||||
|
return next((nombre for p, nombre in prot if rc.startswith(p)), None)
|
||||||
|
nuevas = []
|
||||||
|
for f in list(self.filas):
|
||||||
|
if f.get("paso") == "banco":
|
||||||
|
continue
|
||||||
|
if f["archivos"]:
|
||||||
|
dentro = [a for a in f["archivos"] if de_cual(a)]
|
||||||
|
if dentro:
|
||||||
|
nueva = self._partir(f, lambda a: de_cual(a) is not None,
|
||||||
|
f" · dentro de «{de_cual(dentro[0])}», que dice «No tocar»: márcalo tú si quieres", no_tocar=True)
|
||||||
|
if nueva:
|
||||||
|
nuevas.append(nueva)
|
||||||
|
else:
|
||||||
|
nombre = de_cual(f["origen"])
|
||||||
|
if nombre:
|
||||||
|
f["marcado"] = False
|
||||||
|
f["no_tocar"] = True
|
||||||
|
f["motivo"] += f" · dentro de «{nombre}», que dice «No tocar»: márcalo tú si quieres"
|
||||||
|
self.filas += nuevas
|
||||||
|
|
||||||
def plan_grabacion(self, e, nivel, anio, mes, motivo):
|
def plan_grabacion(self, e, nivel, anio, mes, motivo):
|
||||||
"""Una sesión de grabación: reparte sus archivos por fuente (y por mes si no hay mes de contexto)."""
|
"""Una sesión de grabación: reparte sus archivos por fuente (y por mes si no hay mes de contexto)."""
|
||||||
nombre = e.name
|
nombre = e.name
|
||||||
@@ -1763,11 +1959,16 @@ class Plan:
|
|||||||
# aquí van las que están más adentro (dentro de piezas) y los archivos sueltos de basura
|
# aquí van las que están más adentro (dentro de piezas) y los archivos sueltos de basura
|
||||||
carpetas = con.execute("SELECT * FROM carpetas WHERE raiz = ? AND (es_basura = 1 OR es_proxy = 1)", (self.raiz,)).fetchall()
|
carpetas = con.execute("SELECT * FROM carpetas WHERE raiz = ? AND (es_basura = 1 OR es_proxy = 1)", (self.raiz,)).fetchall()
|
||||||
cubiertas = []
|
cubiertas = []
|
||||||
|
# se mira subiendo por la ruta de cada uno, no recorriendo la lista entera: en Vía Sumapaz (13.899
|
||||||
|
# archivos de proxy o basura, 540 carpetas cubiertas) eran 5 millones de comparaciones y 13 s
|
||||||
|
cub_set = set()
|
||||||
|
origenes = {os.path.normpath(f["origen"]).lower() for f in self.filas}
|
||||||
for c in sorted(carpetas, key=lambda c: len(c["ruta"])):
|
for c in sorted(carpetas, key=lambda c: len(c["ruta"])):
|
||||||
if any(c["ruta"].lower().startswith(p.lower() + os.sep) for p in cubiertas):
|
if _bajo_alguna(os.path.dirname(c["ruta"]), cub_set):
|
||||||
continue
|
continue
|
||||||
if any(os.path.normpath(f["origen"]).lower() == c["ruta"].lower() for f in self.filas):
|
if c["ruta"].lower() in origenes:
|
||||||
cubiertas.append(c["ruta"])
|
cubiertas.append(c["ruta"])
|
||||||
|
cub_set.add(c["ruta"].lower())
|
||||||
continue
|
continue
|
||||||
if any(R.norm(self.rel(c["ruta"])).startswith(R.norm(p)) for p in R.PAPELERAS):
|
if any(R.norm(self.rel(c["ruta"])).startswith(R.norm(p)) for p in R.PAPELERAS):
|
||||||
continue
|
continue
|
||||||
@@ -1775,10 +1976,11 @@ class Plan:
|
|||||||
self.agregar(c["ruta"], self.abs_(destino_plantilla(self.tipo, cub, rel=self.rel(c["ruta"]))), cub, "papelera",
|
self.agregar(c["ruta"], self.abs_(destino_plantilla(self.tipo, cub, rel=self.rel(c["ruta"]))), cub, "papelera",
|
||||||
"proxies: se regeneran" if cub == "PROXY" else "caché / autoguardado", "alta")
|
"proxies: se regeneran" if cub == "PROXY" else "caché / autoguardado", "alta")
|
||||||
cubiertas.append(c["ruta"])
|
cubiertas.append(c["ruta"])
|
||||||
|
cub_set.add(c["ruta"].lower())
|
||||||
sueltos = con.execute("SELECT * FROM archivos WHERE raiz = ? AND (es_basura = 1 OR es_proxy = 1)", (self.raiz,)).fetchall()
|
sueltos = con.execute("SELECT * FROM archivos WHERE raiz = ? AND (es_basura = 1 OR es_proxy = 1)", (self.raiz,)).fetchall()
|
||||||
grupos = defaultdict(list)
|
grupos = defaultdict(list)
|
||||||
for f in sueltos:
|
for f in sueltos:
|
||||||
if any(f["dir"].lower() == p.lower() or f["dir"].lower().startswith(p.lower() + os.sep) for p in cubiertas):
|
if _bajo_alguna(f["dir"], cub_set):
|
||||||
continue
|
continue
|
||||||
if any(R.norm(self.rel(f["dir"])).startswith(R.norm(p)) for p in R.PAPELERAS):
|
if any(R.norm(self.rel(f["dir"])).startswith(R.norm(p)) for p in R.PAPELERAS):
|
||||||
continue
|
continue
|
||||||
@@ -2329,7 +2531,7 @@ def vocabulario(raiz):
|
|||||||
apunta(h, p[1], "", "", "está numerada como una pieza, pero dentro guarda carpetas de varias fechas")
|
apunta(h, p[1], "", "", "está numerada como una pieza, pero dentro guarda carpetas de varias fechas")
|
||||||
continue # una pieza: no se toca por dentro
|
continue # una pieza: no se toca por dentro
|
||||||
cub, mot = R.cubeta_carpeta(nombre, dicc)
|
cub, mot = R.cubeta_carpeta(nombre, dicc)
|
||||||
if cub == "IGNORAR":
|
if cub in ("IGNORAR", "BANCO"):
|
||||||
apunta(h, nombre, cub, mot)
|
apunta(h, nombre, cub, mot)
|
||||||
continue
|
continue
|
||||||
if cub == "CONTENEDOR":
|
if cub == "CONTENEDOR":
|
||||||
@@ -2456,7 +2658,7 @@ def _sugerir(est, resumen, raiz, t):
|
|||||||
# c) por el nombre: se parece a una palabra del diccionario
|
# c) por el nombre: se parece a una palabra del diccionario
|
||||||
dicc = DICC["carpetas"]
|
dicc = DICC["carpetas"]
|
||||||
cerca = difflib.get_close_matches(t["token"], list(dicc.keys()), n=1, cutoff=0.82)
|
cerca = difflib.get_close_matches(t["token"], list(dicc.keys()), n=1, cutoff=0.82)
|
||||||
if cerca and dicc[cerca[0]] not in ("CONTENEDOR", "IGNORAR", "GRABACION"):
|
if cerca and dicc[cerca[0]] not in ("CONTENEDOR", "IGNORAR", "GRABACION", "BANCO"):
|
||||||
sug, conf, por_que = dicc[cerca[0]], "media", f"el nombre se parece a «{cerca[0]}»"
|
sug, conf, por_que = dicc[cerca[0]], "media", f"el nombre se parece a «{cerca[0]}»"
|
||||||
# d) por el contenido (manda si es contundente)
|
# d) por el contenido (manda si es contundente)
|
||||||
if total[0] >= 3 and orden:
|
if total[0] >= 3 and orden:
|
||||||
@@ -4176,7 +4378,56 @@ def _mismas_tomas(candidatos, tope=400):
|
|||||||
return grupos
|
return grupos
|
||||||
|
|
||||||
|
|
||||||
|
_DUP_CACHE = {}
|
||||||
|
|
||||||
|
|
||||||
def duplicados(raiz, minimo=5_000_000, calidades=False):
|
def duplicados(raiz, minimo=5_000_000, calidades=False):
|
||||||
|
"""Los repetidos de una carpeta, guardados hasta que algo se mueva en ella (o pasen 10 minutos): Inicio,
|
||||||
|
«Qué hay» y el diagnóstico los piden cada vez, y en Vía Sumapaz (76.559 archivos) costaban 22 s cada vez."""
|
||||||
|
k = (os.path.normcase(os.path.normpath(raiz)), minimo, bool(calidades))
|
||||||
|
marca = (sum(v for kk, v in PLAN_GEN.items() if kk.lower() == k[0]), _ts_escaneo(raiz))
|
||||||
|
c = _DUP_CACHE.get(k)
|
||||||
|
if c and c[0] == marca and time.time() - c[1] < 600:
|
||||||
|
return c[2]
|
||||||
|
r = _duplicados(raiz, minimo, calidades)
|
||||||
|
_DUP_CACHE[k] = (marca, time.time(), r)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def _ts_escaneo(raiz):
|
||||||
|
con = db()
|
||||||
|
f = con.execute("SELECT ts FROM escaneos WHERE raiz = ?", (os.path.normpath(raiz),)).fetchone()
|
||||||
|
con.close()
|
||||||
|
return f["ts"] if f else 0
|
||||||
|
|
||||||
|
|
||||||
|
def _solo_existentes(filas):
|
||||||
|
"""Quita los que ya no están en el disco, pero solo pregunta por los que pueden formar grupo (mismo nombre
|
||||||
|
y peso, el mismo clip en dos formatos, o con dos pesos), y en paralelo. Medido el 14-oct-2026 en Vía
|
||||||
|
Sumapaz: comprobar uno a uno los 30.963 archivos de más de 5 MB por la red tardaba 20 de 22 segundos."""
|
||||||
|
por_nb = Counter((f["nombre"].lower(), f["bytes"]) for f in filas)
|
||||||
|
exts, pesos = defaultdict(set), defaultdict(set)
|
||||||
|
for f in filas:
|
||||||
|
e = (f["ext"] or "").lower()
|
||||||
|
if e in _EXT_VIDEO_DUP:
|
||||||
|
exts[os.path.splitext(f["nombre"])[0].lower()].add(e)
|
||||||
|
pesos[f["nombre"].lower()].add(f["bytes"])
|
||||||
|
|
||||||
|
def puede(f):
|
||||||
|
if por_nb[(f["nombre"].lower(), f["bytes"])] > 1:
|
||||||
|
return True
|
||||||
|
return (f["ext"] or "").lower() in _EXT_VIDEO_DUP and (
|
||||||
|
len(exts[os.path.splitext(f["nombre"])[0].lower()]) > 1 or len(pesos[f["nombre"].lower()]) > 1)
|
||||||
|
dudosos = [f["ruta"] for f in filas if puede(f)]
|
||||||
|
if not dudosos:
|
||||||
|
return filas
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
with ThreadPoolExecutor(max_workers=24) as ex:
|
||||||
|
existe = dict(zip(dudosos, ex.map(os.path.exists, dudosos)))
|
||||||
|
return [f for f in filas if existe.get(f["ruta"], True)]
|
||||||
|
|
||||||
|
|
||||||
|
def _duplicados(raiz, minimo=5_000_000, calidades=False):
|
||||||
"""Grupos de archivos repetidos. Tres clases:
|
"""Grupos de archivos repetidos. Tres clases:
|
||||||
identico = mismo nombre y mismo tamaño en sitios distintos.
|
identico = mismo nombre y mismo tamaño en sitios distintos.
|
||||||
version = mismo nombre con otra extensión de video (p. ej. el MP4 y el MOV del mismo clip): puede
|
version = mismo nombre con otra extensión de video (p. ej. el MP4 y el MOV del mismo clip): puede
|
||||||
@@ -4189,7 +4440,7 @@ def duplicados(raiz, minimo=5_000_000, calidades=False):
|
|||||||
"SELECT ruta, dir, nombre, ext, bytes, rol FROM archivos WHERE raiz = ? AND bytes >= ? AND es_proxy = 0 AND es_basura = 0",
|
"SELECT ruta, dir, nombre, ext, bytes, rol FROM archivos WHERE raiz = ? AND bytes >= ? AND es_proxy = 0 AND es_basura = 0",
|
||||||
(os.path.normpath(raiz), minimo)).fetchall()]
|
(os.path.normpath(raiz), minimo)).fetchall()]
|
||||||
con.close()
|
con.close()
|
||||||
filas = [f for f in filas if _fuera_de_dup(f["ruta"]) and os.path.exists(f["ruta"])]
|
filas = _solo_existentes([f for f in filas if _fuera_de_dup(f["ruta"])])
|
||||||
grupos, usados = [], set()
|
grupos, usados = [], set()
|
||||||
por_nombre = {}
|
por_nombre = {}
|
||||||
for f in filas:
|
for f in filas:
|
||||||
@@ -4709,7 +4960,7 @@ def mapa_estructura(tipo, raiz=None):
|
|||||||
plantilla = pl.get(cub, "")
|
plantilla = pl.get(cub, "")
|
||||||
n, b = hay.get(cub, [0, 0])
|
n, b = hay.get(cub, [0, 0])
|
||||||
cubetas.append({"clave": cub, "titulo": titulo, "plantilla": plantilla,
|
cubetas.append({"clave": cub, "titulo": titulo, "plantilla": plantilla,
|
||||||
"asignable": cub not in ("CONTENEDOR", "IGNORAR"),
|
"asignable": cub not in ("CONTENEDOR", "IGNORAR", "BANCO"),
|
||||||
"color": R.COLOR_CUBETA.get(cub, R.COLOR_CUBETA[""]),
|
"color": R.COLOR_CUBETA.get(cub, R.COLOR_CUBETA[""]),
|
||||||
"ejemplos": ejemplos.get(cub, []),
|
"ejemplos": ejemplos.get(cub, []),
|
||||||
"defecto": R.PLANTILLAS.get(tipo, {}).get(cub, ""),
|
"defecto": R.PLANTILLAS.get(tipo, {}).get(cub, ""),
|
||||||
@@ -5810,6 +6061,18 @@ def acciones(raiz):
|
|||||||
cifra="", detalle="El inventario de esta carpeta es de la versión anterior. Solo lee: no mueve nada.",
|
cifra="", detalle="El inventario de esta carpeta es de la versión anterior. Solo lee: no mueve nada.",
|
||||||
ir="escanear")
|
ir="escanear")
|
||||||
|
|
||||||
|
# --- el tipo de trabajo de la carpeta no es el del cliente ---
|
||||||
|
# Vía Sumapaz estaba guardado como «proyecto» y su carpeta como «contenido»: el plan, el mapa y «Organizar
|
||||||
|
# carpetas» usan el de la carpeta, y nada decía que no cuadraban.
|
||||||
|
tc = ((CONFIG.get("clientes") or {}).get(r["cliente"]) or {}).get("tipo")
|
||||||
|
tr = r.get("tipo", "contenido")
|
||||||
|
if tc and tc != tr and tc in PLANTILLAS:
|
||||||
|
nom = lambda t: (R.TIPOS_CLIENTE.get(t) or t).split(" · ")[0] # noqa
|
||||||
|
añadir(clave="tipo", titulo="Elegir cómo se ordena esta carpeta", boton="Elegir",
|
||||||
|
que=f"{r['cliente']} está guardado como «{nom(tc)}», pero esta carpeta se ordena como «{nom(tr)}».",
|
||||||
|
cifra="", detalle="El plan, el mapa y «Organizar carpetas» usan el de la carpeta. Elige uno y quedan los dos iguales.",
|
||||||
|
ir="tipo", tipos=[tr, tc])
|
||||||
|
|
||||||
# --- la bandeja de entrada: material nuevo esperando a que lo repartan ---
|
# --- la bandeja de entrada: material nuevo esperando a que lo repartan ---
|
||||||
ent = entrada(raiz)
|
ent = entrada(raiz)
|
||||||
if ent["n"]:
|
if ent["n"]:
|
||||||
@@ -5827,7 +6090,7 @@ def acciones(raiz):
|
|||||||
añadir(clave="limpiar", titulo="Apartar basura y proxies", boton="Ver y apartar",
|
añadir(clave="limpiar", titulo="Apartar basura y proxies", boton="Ver y apartar",
|
||||||
que="Archivos de sistema y proxies, que Resolve y Premiere regeneran solos.",
|
que="Archivos de sistema y proxies, que Resolve y Premiere regeneran solos.",
|
||||||
cifra=f"{_plural(fila['n'], 'archivo', 'archivos')} · {_gb(fila['b'])}",
|
cifra=f"{_plural(fila['n'], 'archivo', 'archivos')} · {_gb(fila['b'])}",
|
||||||
detalle=f"Van a «{R.PAPELERA}» dentro de la misma carpeta. No se borra nada.",
|
detalle=f"Van a «{R.PAPELERA}» dentro de la misma carpeta. No se borra nada." + _no_tocar_dentro(con, raiz),
|
||||||
ir="plan", paso="limpiar")
|
ir="plan", paso="limpiar")
|
||||||
elif vieja:
|
elif vieja:
|
||||||
añadir(clave="limpiar", titulo="Juntar las dos papeleras", boton="Ver y juntar",
|
añadir(clave="limpiar", titulo="Juntar las dos papeleras", boton="Ver y juntar",
|
||||||
@@ -5898,6 +6161,33 @@ def acciones(raiz):
|
|||||||
añadir(clave=clave, titulo=titulo, boton="Calcular", que=que, cifra="", estado="espera",
|
añadir(clave=clave, titulo=titulo, boton="Calcular", que=que, cifra="", estado="espera",
|
||||||
detalle=(hecho_det + " " if hecho_det else "") + "Hay que calcular el plan para saber si queda algo.", ir="plan", paso=clave)
|
detalle=(hecho_det + " " if hecho_det else "") + "Hay que calcular el plan para saber si queda algo.", ir="plan", paso=clave)
|
||||||
|
|
||||||
|
# --- el banco de material: los temas se quedan; dentro, por fecha y cámara ---
|
||||||
|
bancos = _bancos_de(con, raiz)
|
||||||
|
if bancos:
|
||||||
|
b0 = bancos[0]
|
||||||
|
nombre_b = b0["nombre"]
|
||||||
|
prot = any(b["protegido"] for b in bancos)
|
||||||
|
que_b = "Los temas se quedan como están; dentro de cada uno, las grabaciones por fecha de rodaje y cámara."
|
||||||
|
nota_b = ("Su nombre dice «No tocar»: la app lo propone todo sin marcar y no entra en «hacer de una vez». "
|
||||||
|
"Revísalo en «Qué va a pasar» y marca lo que quieras." if prot else "")
|
||||||
|
if plan:
|
||||||
|
fs = [f for f in plan["filas"] if f.get("paso") == "banco"]
|
||||||
|
marc = [f for f in fs if f.get("marcado")]
|
||||||
|
if fs:
|
||||||
|
añadir(clave="banco", titulo=f"Ordenar el banco de material «{nombre_b}»" + (f" y {len(bancos) - 1} más" if len(bancos) > 1 else ""), boton="Ver", que=que_b,
|
||||||
|
estado="toca" if marc else "espera",
|
||||||
|
cifra=f"{_plural(len(fs), 'cambio', 'cambios')} · {_gb(sum(f['bytes'] for f in fs))}",
|
||||||
|
detalle=nota_b or f"{len(marc)} marcados.", ir="plan", paso="banco")
|
||||||
|
else:
|
||||||
|
añadir(clave="banco", titulo=f"Banco de material «{nombre_b}»", estado="hecho", que=que_b, cifra="",
|
||||||
|
detalle="Sus grabaciones ya están por fecha y cámara.")
|
||||||
|
else:
|
||||||
|
añadir(clave="banco", titulo=f"Ordenar el banco de material «{nombre_b}»" + (f" y {len(bancos) - 1} más" if len(bancos) > 1 else ""), que=que_b, estado="espera",
|
||||||
|
cifra=_gb(sum(b["bytes"] for b in bancos)), calculando=bool(REPLAN.get(raiz)),
|
||||||
|
boton="" if REPLAN.get(raiz) else "Calcular",
|
||||||
|
detalle=("Comprobando qué queda… " if REPLAN.get(raiz) else "Hay que calcular el plan para verlo. ") + nota_b,
|
||||||
|
ir="plan", paso="banco")
|
||||||
|
|
||||||
# --- 5. carpetas que quedaron vacías ---
|
# --- 5. carpetas que quedaron vacías ---
|
||||||
vac = carpetas_vacias(raiz)
|
vac = carpetas_vacias(raiz)
|
||||||
if vac["n"]:
|
if vac["n"]:
|
||||||
@@ -6017,6 +6307,110 @@ def acciones(raiz):
|
|||||||
"automatico": auto, "asistente": asistente.ESTADO if asistente.ESTADO.get("raiz", "").lower() == raiz.lower() else None}
|
"automatico": auto, "asistente": asistente.ESTADO if asistente.ESTADO.get("raiz", "").lower() == raiz.lower() else None}
|
||||||
|
|
||||||
|
|
||||||
|
def _no_tocar_dentro(con, raiz):
|
||||||
|
"""« De ellos, 366 GB están dentro de «Material Vía (No tocar)»: van sin marcar.» para la ficha de basura y proxies."""
|
||||||
|
partes = []
|
||||||
|
for b in _bancos_de(con, raiz):
|
||||||
|
if not b["protegido"]:
|
||||||
|
continue
|
||||||
|
x = con.execute("SELECT COALESCE(SUM(bytes), 0) b FROM archivos WHERE raiz = ? AND (es_basura = 1 OR es_proxy = 1) "
|
||||||
|
"AND ruta LIKE ? ESCAPE '!'", (raiz, esc_like(con_sep(b["ruta"])) + "%")).fetchone()["b"]
|
||||||
|
if x:
|
||||||
|
partes.append(f"{_gb(x)} están dentro de «{b['nombre']}», que dice «No tocar»")
|
||||||
|
return (" De ellos, " + "; y ".join(partes) + ": van sin marcar, para que decidas tú.") if partes else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _bancos_de(con, raiz):
|
||||||
|
"""Los bancos de material de una carpeta de cliente: los de su primer nivel. Una copia dentro de otra
|
||||||
|
carpeta (en Vía Sumapaz, «_Duplicados_para_borrar\\Material Vía (No tocar)») no es el banco."""
|
||||||
|
base = raiz.rstrip(os.sep).count(os.sep) + 1
|
||||||
|
out = []
|
||||||
|
for c in con.execute("SELECT ruta, nombre, n, bytes FROM carpetas WHERE raiz = ? ORDER BY length(ruta)", (raiz,)).fetchall():
|
||||||
|
if c["ruta"].count(os.sep) != base:
|
||||||
|
continue
|
||||||
|
if R.cubeta_carpeta(c["nombre"], DICC["carpetas"])[0] == "BANCO":
|
||||||
|
out.append({"ruta": c["ruta"], "nombre": c["nombre"], "n": c["n"] or 0, "bytes": c["bytes"] or 0,
|
||||||
|
"protegido": R.es_no_tocar(c["nombre"])})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def carpeta_detalle(raiz, ruta=None):
|
||||||
|
"""Lo que hay dentro de cualquier carpeta del cliente, a cualquier profundidad: sus subcarpetas con su peso
|
||||||
|
(para entrar en «Qué pesa» nivel a nivel), de qué es lo que guarda, de qué cámaras y de qué fechas, y qué
|
||||||
|
hace la app con ella. Antes «Qué pesa» solo enseñaba el primer nivel, y una carpeta de 3,6 TB era un bloque
|
||||||
|
gris sin nada dentro: parecía que el escaneo no había entrado."""
|
||||||
|
raiz = _clave(raiz)
|
||||||
|
ruta = os.path.normpath(ruta or raiz)
|
||||||
|
if ruta.lower() != raiz.lower() and not dentro_de(raiz, ruta):
|
||||||
|
return {"error": "esa carpeta no es de este cliente"}
|
||||||
|
con = db()
|
||||||
|
q = lambda sql, *a: [dict(x) for x in con.execute(sql, a).fetchall()] # noqa
|
||||||
|
pref = esc_like(con_sep(ruta)) + "%"
|
||||||
|
hijos = q("SELECT ruta, nombre, n, bytes, es_proxy, es_basura FROM carpetas WHERE raiz = ? AND ruta LIKE ? ESCAPE '!' "
|
||||||
|
"AND ruta NOT LIKE ? ESCAPE '!' ORDER BY bytes DESC", raiz, pref, pref + esc_like(os.sep) + "%")
|
||||||
|
rel0 = os.path.relpath(ruta, raiz)
|
||||||
|
en_banco = rel0 != "." and any(R.cubeta_carpeta(s, DICC["carpetas"])[0] == "BANCO" for s in rel0.split(os.sep))
|
||||||
|
for h in hijos:
|
||||||
|
cub = "PROXY" if h["es_proxy"] else "BASURA" if h["es_basura"] else \
|
||||||
|
"BANCO" if en_banco else (R.cubeta_carpeta(h["nombre"], DICC["carpetas"])[0] or "")
|
||||||
|
h.update(cubeta=cub, color=R.COLOR_CUBETA.get(cub, R.COLOR_CUBETA[""]))
|
||||||
|
donde = "(dir = ? OR dir LIKE ? ESCAPE '!')"
|
||||||
|
total = q(f"SELECT COUNT(*) n, COALESCE(SUM(bytes), 0) bytes FROM archivos WHERE raiz = ? AND {donde}", raiz, ruta, pref)[0]
|
||||||
|
sueltos = q("SELECT COUNT(*) n, COALESCE(SUM(bytes), 0) bytes FROM archivos WHERE raiz = ? AND dir = ?", raiz, ruta)[0]
|
||||||
|
roles = q(f"SELECT rol, COUNT(*) n, SUM(bytes) bytes FROM archivos WHERE raiz = ? AND {donde} GROUP BY rol ORDER BY bytes DESC", raiz, ruta, pref)
|
||||||
|
nombres_f = {}
|
||||||
|
for k, _, d in R.FIRMAS:
|
||||||
|
nombres_f.setdefault(k, d)
|
||||||
|
fuentes = q(f"SELECT fuente, COUNT(*) n, SUM(bytes) bytes FROM archivos WHERE raiz = ? AND {donde} AND fuente != '' "
|
||||||
|
f"AND fuente != 'STOCK' GROUP BY fuente ORDER BY bytes DESC", raiz, ruta, pref)
|
||||||
|
fechas = q(f"SELECT MIN(substr(fecha,1,7)) desde, MAX(substr(fecha,1,7)) hasta, COUNT(DISTINCT substr(fecha,1,7)) meses "
|
||||||
|
f"FROM archivos WHERE raiz = ? AND {donde} AND rol IN ('camara_raw','camara_video','celular_video','audio_grabado')",
|
||||||
|
raiz, ruta, pref)[0]
|
||||||
|
apartable = q(f"SELECT COALESCE(SUM(CASE WHEN es_proxy = 1 THEN bytes END), 0) proxies, "
|
||||||
|
f"COALESCE(SUM(CASE WHEN es_basura = 1 AND es_proxy = 0 THEN bytes END), 0) basura FROM archivos "
|
||||||
|
f"WHERE raiz = ? AND {donde}", raiz, ruta, pref)[0]
|
||||||
|
sub = [x["ruta"] for x in q("SELECT ruta FROM carpetas WHERE raiz = ? AND ruta LIKE ? ESCAPE '!'", raiz, pref)]
|
||||||
|
niveles = max((s.count(os.sep) - ruta.rstrip(os.sep).count(os.sep) for s in sub), default=0)
|
||||||
|
grandes = q("SELECT nombre, bytes FROM archivos WHERE raiz = ? AND dir = ? ORDER BY bytes DESC LIMIT 6", raiz, ruta)
|
||||||
|
con.close()
|
||||||
|
# qué hace la app con ella: si está dentro de un banco de material, eso manda
|
||||||
|
trato = {"cubeta": "", "texto": ""}
|
||||||
|
rel = os.path.relpath(ruta, raiz)
|
||||||
|
if rel != ".":
|
||||||
|
segs = rel.split(os.sep)
|
||||||
|
for s in segs:
|
||||||
|
if R.cubeta_carpeta(s, DICC["carpetas"])[0] == "BANCO":
|
||||||
|
trato = {"cubeta": "BANCO", "texto": f"Está en el banco de material «{s}»: los temas se quedan como están y, "
|
||||||
|
f"dentro, las grabaciones van por fecha de rodaje y cámara."
|
||||||
|
+ (" Su nombre dice «No tocar»: todo se propone sin marcar." if R.es_no_tocar(s) else "")}
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
cub, mot = R.cubeta_carpeta(segs[-1], DICC["carpetas"])
|
||||||
|
trato = {"cubeta": cub or "", "texto": (f"La app la reconoce como «{R.CUBETAS.get(cub, cub)}» ({mot})." if cub else
|
||||||
|
"La app no reconoce el nombre: el plan decide por lo que tiene dentro.")}
|
||||||
|
plan = PLANES.get(raiz)
|
||||||
|
en_plan = None
|
||||||
|
if plan:
|
||||||
|
rc = os.path.normcase(con_sep(ruta)) if rel != "." else ""
|
||||||
|
fs = [f for f in plan["filas"] if not rc or os.path.normcase(f["origen"] + os.sep).startswith(rc)
|
||||||
|
or any(os.path.normcase(a).startswith(rc) for a in (f["archivos"] or [])[:1])]
|
||||||
|
en_plan = {"filas": len(fs), "marcadas": sum(1 for f in fs if f["marcado"]), "bytes": sum(f["bytes"] for f in fs)}
|
||||||
|
return {"raiz": raiz, "ruta": ruta, "rel": "" if rel == "." else rel, "nombre": os.path.basename(ruta) or ruta,
|
||||||
|
"n": total["n"], "bytes": total["bytes"], "sueltos": sueltos, "hijos": hijos, "niveles": niveles,
|
||||||
|
"carpetas": len(sub), "roles": [dict(x, que=R.ROL_QUE.get(x["rol"], x["rol"])) for x in roles],
|
||||||
|
"fuentes": [dict(x, que=nombres_f.get(x["fuente"], x["fuente"])) for x in fuentes],
|
||||||
|
"fechas": fechas, "apartable": apartable, "grandes": grandes, "trato": trato, "en_plan": en_plan}
|
||||||
|
|
||||||
|
|
||||||
|
def _inventario_resumen(raiz):
|
||||||
|
"""Cuántas carpetas leyó el escaneo y hasta qué profundidad: para que se vea que entró en todas."""
|
||||||
|
con = db()
|
||||||
|
rutas = [x[0] for x in con.execute("SELECT ruta FROM carpetas WHERE raiz = ?", (raiz,)).fetchall()]
|
||||||
|
con.close()
|
||||||
|
base = raiz.rstrip(os.sep).count(os.sep)
|
||||||
|
return {"carpetas": len(rutas), "niveles": max((x.count(os.sep) - base for x in rutas), default=0)}
|
||||||
|
|
||||||
|
|
||||||
def estado_carpeta(ruta):
|
def estado_carpeta(ruta):
|
||||||
"""El recorrido de una carpeta: qué paso está hecho, cuál toca y qué lo bloquea."""
|
"""El recorrido de una carpeta: qué paso está hecho, cuál toca y qué lo bloquea."""
|
||||||
raiz = _clave(ruta)
|
raiz = _clave(ruta)
|
||||||
@@ -6054,7 +6448,8 @@ def estado_carpeta(ruta):
|
|||||||
"diagnostico": diag,
|
"diagnostico": diag,
|
||||||
"sin_nombre": [{"token": t["token"], "ejemplo": t["ejemplo"], "n": t["n"], "bytes": t["bytes"]} for t in sin_nombre[:12]],
|
"sin_nombre": [{"token": t["token"], "ejemplo": t["ejemplo"], "n": t["n"], "bytes": t["bytes"]} for t in sin_nombre[:12]],
|
||||||
"sin_nombre_n": len(sin_nombre),
|
"sin_nombre_n": len(sin_nombre),
|
||||||
"plan": plan_res, "lotes": lotes[:6],
|
"plan": plan_res, "lotes": lotes[:6], "inventario": _inventario_resumen(raiz) if esc else None,
|
||||||
|
"tipo_cliente": ((CONFIG.get("clientes") or {}).get(r["cliente"]) or {}).get("tipo"),
|
||||||
"convertir": {"camara_bytes": cam_bytes, "raw_bytes": g.get("camara_raw", {}).get("bytes", 0), "trabajos": trabajos,
|
"convertir": {"camara_bytes": cam_bytes, "raw_bytes": g.get("camara_raw", {}).get("bytes", 0), "trabajos": trabajos,
|
||||||
"analisis": ANALISIS.get(raiz), "analizados": analizados["ok"] or 0, "sin_analizar": analizados["sin"] or 0,
|
"analisis": ANALISIS.get(raiz), "analizados": analizados["ok"] or 0, "sin_analizar": analizados["sin"] or 0,
|
||||||
"cola_activa": COLA["activa"]},
|
"cola_activa": COLA["activa"]},
|
||||||
@@ -6156,6 +6551,9 @@ class Manejador(BaseHTTPRequestHandler):
|
|||||||
return self._json(estado_carpeta(q["raiz"]))
|
return self._json(estado_carpeta(q["raiz"]))
|
||||||
if ruta == "/api/mapa":
|
if ruta == "/api/mapa":
|
||||||
return self._json(mapa(q["raiz"]))
|
return self._json(mapa(q["raiz"]))
|
||||||
|
if ruta == "/api/mapa/carpeta": # «Qué pesa», nivel a nivel
|
||||||
|
r = carpeta_detalle(q["raiz"], q.get("ruta"))
|
||||||
|
return self._json(r, 400 if r.get("error") else 200)
|
||||||
if ruta == "/api/actualizacion":
|
if ruta == "/api/actualizacion":
|
||||||
return self._json({**actualizador.ESTADO, "version": VERSION}) # ESTADO trae "version" vacía hasta la primera consulta
|
return self._json({**actualizador.ESTADO, "version": VERSION}) # ESTADO trae "version" vacía hasta la primera consulta
|
||||||
if ruta == "/api/asistente/pasos":
|
if ruta == "/api/asistente/pasos":
|
||||||
@@ -6343,6 +6741,18 @@ class Manejador(BaseHTTPRequestHandler):
|
|||||||
CONFIG["raices"] = d["raices"]
|
CONFIG["raices"] = d["raices"]
|
||||||
guardar_json(CONFIG_PATH, CONFIG)
|
guardar_json(CONFIG_PATH, CONFIG)
|
||||||
return self._json({"ok": True})
|
return self._json({"ok": True})
|
||||||
|
if ruta == "/api/raiz/tipo":
|
||||||
|
# el tipo de trabajo de una carpeta, y el de su cliente con él: así no vuelven a quedar distintos
|
||||||
|
k = _clave(d.get("raiz") or "")
|
||||||
|
rc = raiz_cfg(k)
|
||||||
|
tipo = d.get("tipo")
|
||||||
|
if not rc or tipo not in PLANTILLAS:
|
||||||
|
return self._json({"error": "carpeta o tipo de trabajo desconocido"}, 400)
|
||||||
|
rc["tipo"] = tipo
|
||||||
|
CONFIG.setdefault("clientes", {}).setdefault(rc["cliente"], {})["tipo"] = tipo
|
||||||
|
guardar_json(CONFIG_PATH, CONFIG)
|
||||||
|
olvidar_plan(k)
|
||||||
|
return self._json({"ok": True, "tipo": tipo})
|
||||||
if ruta == "/api/clientes":
|
if ruta == "/api/clientes":
|
||||||
CONFIG["clientes"] = d["clientes"]
|
CONFIG["clientes"] = d["clientes"]
|
||||||
guardar_json(CONFIG_PATH, CONFIG)
|
guardar_json(CONFIG_PATH, CONFIG)
|
||||||
|
|||||||
@@ -192,6 +192,27 @@ def p_que_hay():
|
|||||||
return 'basura %d · proxies %d · %d ramas' % (m['basura']['n'], m['proxies']['n'], len(m['arbol']))
|
return 'basura %d · proxies %d · %d ramas' % (m['basura']['n'], m['proxies']['n'], len(m['arbol']))
|
||||||
|
|
||||||
|
|
||||||
|
def p_tipo_y_pesa():
|
||||||
|
# «Qué pesa» entra nivel a nivel
|
||||||
|
d = get('/api/mapa/carpeta' + q(raiz=RAIZ))
|
||||||
|
assert d['hijos'] and d['n'] == raiz_estado()['escaneo']['n'], (d['n'], len(d['hijos']))
|
||||||
|
d2 = get('/api/mapa/carpeta' + q(raiz=RAIZ, ruta=os.path.join(RAIZ, '2026')))
|
||||||
|
assert d2['rel'] == '2026' and d2['niveles'] >= 2 and d2['roles'], d2
|
||||||
|
# el tipo de trabajo de la carpeta y el del cliente no cuadran: Inicio lo dice, y elegir uno los iguala
|
||||||
|
cl = get('/api/estado')['config'].get('clientes') or {}
|
||||||
|
cl['Cliente Smoke'] = {'tipo': 'proyecto'}
|
||||||
|
c, r = post('/api/clientes', {'clientes': cl})
|
||||||
|
assert c == 200, r
|
||||||
|
a = {x['clave']: x for x in get('/api/carpeta/acciones' + q(raiz=RAIZ))['acciones']}
|
||||||
|
assert 'tipo' in a and a['tipo']['tipos'] == ['contenido', 'proyecto'], a.get('tipo')
|
||||||
|
c, r = post('/api/raiz/tipo', {'raiz': RAIZ, 'tipo': 'contenido'})
|
||||||
|
assert c == 200, r
|
||||||
|
e = get('/api/estado')
|
||||||
|
assert e['config']['clientes']['Cliente Smoke']['tipo'] == 'contenido' and raiz_estado()['tipo'] == 'contenido'
|
||||||
|
assert 'tipo' not in [x['clave'] for x in get('/api/carpeta/acciones' + q(raiz=RAIZ))['acciones']]
|
||||||
|
return '«Qué pesa» entra hasta %d niveles bajo 2026; el tipo de la carpeta y el del cliente se igualan' % d2['niveles']
|
||||||
|
|
||||||
|
|
||||||
def p_nombrar():
|
def p_nombrar():
|
||||||
v = get('/api/vocabulario' + q(raiz=RAIZ))
|
v = get('/api/vocabulario' + q(raiz=RAIZ))
|
||||||
toks = {t['token']: t for t in v['tokens']}
|
toks = {t['token']: t for t in v['tokens']}
|
||||||
@@ -590,7 +611,7 @@ def p_quitar():
|
|||||||
|
|
||||||
for nombre, fn in (('Interfaz sin nada externo', p_estatico), ('Registrar carpeta', p_registrar), ('Escanear', p_escanear),
|
for nombre, fn in (('Interfaz sin nada externo', p_estatico), ('Registrar carpeta', p_registrar), ('Escanear', p_escanear),
|
||||||
('Estado y lista de acciones', p_carpeta), ('Qué hay (mapa de peso, discos)', p_que_hay),
|
('Estado y lista de acciones', p_carpeta), ('Qué hay (mapa de peso, discos)', p_que_hay),
|
||||||
('Por nombrar (vocabulario, diccionario)', p_nombrar), ('Calcular el plan', p_plan),
|
('Qué pesa por dentro y tipo de trabajo', p_tipo_y_pesa), ('Por nombrar (vocabulario, diccionario)', p_nombrar), ('Calcular el plan', p_plan),
|
||||||
('Aplicar el plan', p_aplicar), ('Deshacer', p_deshacer), ('Volver a aplicar', p_reaplicar),
|
('Aplicar el plan', p_aplicar), ('Deshacer', p_deshacer), ('Volver a aplicar', p_reaplicar),
|
||||||
('Historial y papelera', p_historial), ('Repetidos', p_repetidos), ('Stock viejo', p_stock),
|
('Historial y papelera', p_historial), ('Repetidos', p_repetidos), ('Stock viejo', p_stock),
|
||||||
('Carpetas vacías', p_vacias), ('Mapa interactivo', p_mapa), ('Colores en Windows', p_colores),
|
('Carpetas vacías', p_vacias), ('Mapa interactivo', p_mapa), ('Colores en Windows', p_colores),
|
||||||
|
|||||||
@@ -337,10 +337,71 @@ class Motor(unittest.TestCase):
|
|||||||
filas = [{"cubeta": "GRABACION", "marcado": False, "confianza": "baja"},
|
filas = [{"cubeta": "GRABACION", "marcado": False, "confianza": "baja"},
|
||||||
{"cubeta": "GRABACION", "marcado": True, "confianza": "media"},
|
{"cubeta": "GRABACION", "marcado": True, "confianza": "media"},
|
||||||
{"cubeta": "BASURA", "marcado": True, "confianza": "alta"},
|
{"cubeta": "BASURA", "marcado": True, "confianza": "alta"},
|
||||||
|
{"cubeta": "PROXY", "marcado": False, "confianza": "baja"}, # sin su original, o dentro de «No tocar»
|
||||||
{"cubeta": "EXPORTADOS", "marcado": False, "confianza": "baja"}]
|
{"cubeta": "EXPORTADOS", "marcado": False, "confianza": "baja"}]
|
||||||
elegidas = asistente.filas_elegidas(filas, {"carpetas": "seguras"})
|
elegidas = asistente.filas_elegidas(filas, {"carpetas": "seguras"})
|
||||||
self.assertEqual([(f["cubeta"], f["marcado"]) for f in elegidas], [("GRABACION", True), ("BASURA", True)])
|
self.assertEqual([(f["cubeta"], f["marcado"]) for f in elegidas], [("GRABACION", True), ("BASURA", True)])
|
||||||
|
|
||||||
|
def test_banco_de_material(self):
|
||||||
|
"""Vía Sumapaz: 3,6 TB en «Material Vía (No tocar)», por temas, que el plan dejaba fuera por la palabra
|
||||||
|
«material». Los temas se quedan; dentro, las grabaciones por fecha y cámara; todo sin marcar."""
|
||||||
|
raiz = cliente("Banco")
|
||||||
|
b = "Material Vía (No tocar)/"
|
||||||
|
mk(raiz, b + "Lugares/Peajes/Drone/DJI_20240312101500_0001_D.MP4", 2024, 3, 12)
|
||||||
|
mk(raiz, b + "Lugares/Peajes/URSA/A001_03121015_C001.mp4", 2024, 3, 12)
|
||||||
|
mk(raiz, b + "Lugares/Peajes/URSA/Proxy/A001_03121015_C001.mov", 2024, 3, 12) # proxy con su original
|
||||||
|
mk(raiz, b + "Lugares/Peajes/Final peajes.mp4", 2024, 4, 2) # montado: no se toca
|
||||||
|
mk(raiz, b + "Material Importante/Social/Mercados/C0001.MP4", 2025, 5, 2)
|
||||||
|
mk(raiz, b + "Material Importante/UF9/Drone/DJI_0001.MP4", 2009, 1, 19) # reloj sin poner
|
||||||
|
mk(raiz, "2026/03- Marzo/Grabacion/C0010.MP4", 2026, 3, 12)
|
||||||
|
mk(raiz, "2026/03- Marzo/Grabacion/Proxy/C0010.MOV", 2026, 3, 12) # proxy con original
|
||||||
|
mk(raiz, "2026/04- Abril/01- Viejo/Proxy/C0099.MOV", 2026, 4, 2) # el único que queda
|
||||||
|
dicc = O.DICC["carpetas"]
|
||||||
|
self.assertEqual(O.R.cubeta_carpeta("Material Vía (No tocar)", dicc)[0], "BANCO")
|
||||||
|
self.assertIsNone(O.R.cubeta_carpeta("Material juan", dicc)[0], "«material» suelto ya no ignora la carpeta")
|
||||||
|
self.assertEqual(O.R.cubeta_carpeta("Material", dicc)[0], "IGNORAR")
|
||||||
|
antes = nombres(raiz)
|
||||||
|
p = plan(raiz)
|
||||||
|
banco = [f for f in p["filas"] if f["paso"] == "banco"]
|
||||||
|
destinos = {f["destino_rel"] for f in banco}
|
||||||
|
B = "Material Vía (No tocar)"
|
||||||
|
self.assertIn(rel(B, "Lugares", "Peajes", "2024-03-12", "OSMO"), destinos)
|
||||||
|
self.assertIn(rel(B, "Lugares", "Peajes", "2024-03-12", "BLACKMAGIC_MP4"), destinos)
|
||||||
|
self.assertIn(rel(B, "Material Importante", "Social", "Mercados", "2025-05-02", "SONY"), destinos)
|
||||||
|
self.assertFalse([f for f in banco if f["marcado"]], "en «No tocar» nada va marcado")
|
||||||
|
self.assertFalse([f for f in p["filas"] if any("Final peajes" in a for a in (f["archivos"] or []))], "movió lo montado")
|
||||||
|
reloj = [f for f in banco if "reloj sin poner" in f["motivo"]]
|
||||||
|
self.assertTrue(reloj and reloj[0]["confianza"] == "baja", [f["motivo"] for f in banco])
|
||||||
|
prox = [f for f in p["filas"] if f["cubeta"] == "PROXY"]
|
||||||
|
dentro = [f for f in prox if "Peajes" in f["origen_rel"]]
|
||||||
|
self.assertTrue(dentro and not dentro[0]["marcado"] and "No tocar" in dentro[0]["motivo"], dentro)
|
||||||
|
sin = [f for f in prox if "01- Viejo" in f["origen_rel"]]
|
||||||
|
self.assertTrue(sin and not sin[0]["marcado"] and "OJO" in sin[0]["motivo"], sin)
|
||||||
|
con = [f for f in prox if "03- Marzo" in f["origen_rel"]]
|
||||||
|
self.assertTrue(con and con[0]["marcado"], con)
|
||||||
|
# «hacer de una vez» no toca nada de lo que va sin marcar
|
||||||
|
self.assertFalse([f for f in asistente.filas_elegidas(p["filas"], {"carpetas": "seguras"}) if not f["marcado"]])
|
||||||
|
dentro_antes = [x for x in arbol(raiz) if x.startswith(B)]
|
||||||
|
aplicar(raiz, p)
|
||||||
|
self.assertEqual([x for x in arbol(raiz) if x.startswith(B)], dentro_antes, "tocó la carpeta «No tocar» sin que nadie lo marcara")
|
||||||
|
self.assertTrue(os.path.isfile(os.path.join(raiz, "2026", "04- Abril", "01- Viejo", "Proxy", "C0099.MOV")), "apartó el proxy sin original")
|
||||||
|
# si alguien lo marca, queda donde dice
|
||||||
|
p2 = O.Plan(O.raiz_cfg(raiz)).correr()
|
||||||
|
for f in p2["filas"]:
|
||||||
|
f["marcado"] = f["paso"] == "banco"
|
||||||
|
aplicar(raiz, p2)
|
||||||
|
self.assertEqual(nombres(raiz), antes, "se perdió o se duplicó algún archivo")
|
||||||
|
for esperado in (rel(B, "Lugares", "Peajes", "2024-03-12", "OSMO", "DJI_20240312101500_0001_D.MP4"),
|
||||||
|
rel(B, "Lugares", "Peajes", "2024-03-12", "BLACKMAGIC_MP4", "A001_03121015_C001.mp4"),
|
||||||
|
rel(B, "Lugares", "Peajes", "Final peajes.mp4"),
|
||||||
|
rel(B, "Material Importante", "Social", "Mercados", "2025-05-02", "SONY", "C0001.MP4")):
|
||||||
|
self.assertTrue(os.path.isfile(os.path.join(raiz, esperado)), "falta " + esperado + "\n" + "\n".join(arbol(raiz)))
|
||||||
|
# y «Qué pesa» entra hasta el fondo y dice cómo la trata la app
|
||||||
|
d = O.carpeta_detalle(raiz, os.path.join(raiz, B))
|
||||||
|
self.assertEqual(d["trato"]["cubeta"], "BANCO")
|
||||||
|
self.assertTrue(d["niveles"] >= 3 and "Lugares" in [h["nombre"] for h in d["hijos"]], d)
|
||||||
|
self.assertIn("banco", [a["clave"] for a in O.acciones(raiz)["acciones"]])
|
||||||
|
|
||||||
def test_el_convertidor_no_repite(self):
|
def test_el_convertidor_no_repite(self):
|
||||||
"""22 clips se habían convertido dos veces y algunos tres («…_10bit_10bit_10bit.mp4»)."""
|
"""22 clips se habían convertido dos veces y algunos tres («…_10bit_10bit_10bit.mp4»)."""
|
||||||
raiz = cliente("Convertidos")
|
raiz = cliente("Convertidos")
|
||||||
|
|||||||
21
reglas.py
21
reglas.py
@@ -451,6 +451,7 @@ CUBETAS = {
|
|||||||
"CELULAR": "Material de celular",
|
"CELULAR": "Material de celular",
|
||||||
"PROXY": "Proxies (se pueden regenerar)",
|
"PROXY": "Proxies (se pueden regenerar)",
|
||||||
"BASURA": "Basura de sistema / cachés",
|
"BASURA": "Basura de sistema / cachés",
|
||||||
|
"BANCO": "Banco de material por temas: los temas se quedan; dentro, las grabaciones por fecha y cámara",
|
||||||
"CONTENEDOR": "Carpeta contenedora: mirar adentro",
|
"CONTENEDOR": "Carpeta contenedora: mirar adentro",
|
||||||
"REVISAR": "Revisar a mano",
|
"REVISAR": "Revisar a mano",
|
||||||
"IGNORAR": "Dejar como está",
|
"IGNORAR": "Dejar como está",
|
||||||
@@ -544,6 +545,21 @@ MARCA_SUB = {
|
|||||||
_SEMILLA_POR_LARGO = sorted(DICCIONARIO_SEMILLA, key=lambda k: -len(k))
|
_SEMILLA_POR_LARGO = sorted(DICCIONARIO_SEMILLA, key=lambda k: -len(k))
|
||||||
|
|
||||||
|
|
||||||
|
# «Material» a secas es estructura (la carpeta de material de una campaña o de la raíz de pauta), pero como
|
||||||
|
# palabra suelta dentro de otro nombre no dice nada. Medido el 14-oct-2026 en Vía Sumapaz: «Material Vía (No
|
||||||
|
# tocar)», 3,6 TB y el 85 % del disco, y dentro «Material Importante», 2,6 TB, se quedaban fuera del orden
|
||||||
|
# por llevar esa palabra delante.
|
||||||
|
SOLO_NOMBRE_COMPLETO = {"material"}
|
||||||
|
|
||||||
|
# «(No tocar)», «NO MOVER»: alguien dejó dicho que esa carpeta se respeta. Es un banco de material: sus
|
||||||
|
# temas no se mueven y lo que la app propone dentro va sin marcar, para que lo decida una persona.
|
||||||
|
_RX_NO_TOCAR = re.compile(r"(^| )no (tocar|mover|borrar)( |$)")
|
||||||
|
|
||||||
|
|
||||||
|
def es_no_tocar(nombre: str) -> bool:
|
||||||
|
return bool(_RX_NO_TOCAR.search(norm(nombre)))
|
||||||
|
|
||||||
|
|
||||||
def cubeta_carpeta(nombre: str, diccionario: dict):
|
def cubeta_carpeta(nombre: str, diccionario: dict):
|
||||||
"""(cubeta, motivo) de una carpeta por su nombre; None si no se reconoce."""
|
"""(cubeta, motivo) de una carpeta por su nombre; None si no se reconoce."""
|
||||||
if es_basura_dir(nombre):
|
if es_basura_dir(nombre):
|
||||||
@@ -555,6 +571,8 @@ def cubeta_carpeta(nombre: str, diccionario: dict):
|
|||||||
n = norm(nombre)
|
n = norm(nombre)
|
||||||
if n in diccionario:
|
if n in diccionario:
|
||||||
return diccionario[n], f"diccionario: «{n}»"
|
return diccionario[n], f"diccionario: «{n}»"
|
||||||
|
if _RX_NO_TOCAR.search(n):
|
||||||
|
return "BANCO", "el nombre dice «No tocar»: es un banco de material"
|
||||||
# tokens de varias palabras primero, luego palabras sueltas
|
# tokens de varias palabras primero, luego palabras sueltas
|
||||||
# (CONTENEDOR solo por coincidencia exacta: "Material juan manuel" no es un cajón de sastre)
|
# (CONTENEDOR solo por coincidencia exacta: "Material juan manuel" no es un cajón de sastre)
|
||||||
#
|
#
|
||||||
@@ -568,7 +586,7 @@ def cubeta_carpeta(nombre: str, diccionario: dict):
|
|||||||
return diccionario[k], f"diccionario: contiene «{k}»"
|
return diccionario[k], f"diccionario: contiene «{k}»"
|
||||||
palabras = n.split()
|
palabras = n.split()
|
||||||
for k in claves:
|
for k in claves:
|
||||||
if " " not in k and diccionario[k] != "CONTENEDOR" and k in palabras:
|
if " " not in k and diccionario[k] != "CONTENEDOR" and k not in SOLO_NOMBRE_COMPLETO and k in palabras:
|
||||||
return diccionario[k], f"diccionario: palabra «{k}»"
|
return diccionario[k], f"diccionario: palabra «{k}»"
|
||||||
return None, ""
|
return None, ""
|
||||||
|
|
||||||
@@ -753,6 +771,7 @@ VERSION_REGLAS = 2
|
|||||||
# Un color por tipo de material. Se usa en el mapa, en el gráfico de peso y —si se quiere— en el
|
# Un color por tipo de material. Se usa en el mapa, en el gráfico de peso y —si se quiere— en el
|
||||||
# propio Explorador de Windows, poniéndole a cada carpeta un icono de ese color.
|
# propio Explorador de Windows, poniéndole a cada carpeta un icono de ese color.
|
||||||
COLOR_CUBETA = {
|
COLOR_CUBETA = {
|
||||||
|
"BANCO": "#b07d4f",
|
||||||
"ANIO": "#4f8cff", "GRABACION": "#e0574f", "PIEZA": "#7c5cff", "CAMPANA": "#ff8c42",
|
"ANIO": "#4f8cff", "GRABACION": "#e0574f", "PIEZA": "#7c5cff", "CAMPANA": "#ff8c42",
|
||||||
"EXPORTADOS": "#3ecf8e", "PORTADAS": "#f5b84b", "APOYOS": "#d96fd9", "MARCA": "#39b8c9",
|
"EXPORTADOS": "#3ecf8e", "PORTADAS": "#f5b84b", "APOYOS": "#d96fd9", "MARCA": "#39b8c9",
|
||||||
"PROYECTOS": "#8fa1b8", "MUSICA": "#c9b339", "SFX": "#c9b339", "VOZ": "#c9b339",
|
"PROYECTOS": "#8fa1b8", "MUSICA": "#c9b339", "SFX": "#c9b339", "VOZ": "#c9b339",
|
||||||
|
|||||||
@@ -145,6 +145,19 @@
|
|||||||
.tm{position:relative;height:300px;border-radius:6px;overflow:hidden;background:var(--panel2)}
|
.tm{position:relative;height:300px;border-radius:6px;overflow:hidden;background:var(--panel2)}
|
||||||
.tm div{position:absolute;overflow:hidden;padding:4px 6px;font-size:11px;border:1px solid rgba(255,255,255,.6);color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5);cursor:default}
|
.tm div{position:absolute;overflow:hidden;padding:4px 6px;font-size:11px;border:1px solid rgba(255,255,255,.6);color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5);cursor:default}
|
||||||
.tm div b{display:block;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
.tm div b{display:block;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.tm div.pick{cursor:pointer}.tm div.pick:hover{filter:brightness(1.15)}
|
||||||
|
.tm .tmvacio{position:static;color:var(--mut);text-shadow:none;border:0;padding:14px;font-size:12.5px}
|
||||||
|
.tmrow{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(250px,1fr);gap:18px;align-items:start}
|
||||||
|
.tmrow .tm{height:380px}
|
||||||
|
@media(max-width:1000px){.tmrow{grid-template-columns:1fr}}
|
||||||
|
.migas{display:flex;flex-wrap:wrap;gap:4px 6px;align-items:center;margin:0 0 8px;font-size:12.5px}
|
||||||
|
.migas .lnk{padding:0}
|
||||||
|
.tmdet{font-size:12.5px}
|
||||||
|
.tmdet h4{margin:0 0 2px;font-size:14px;word-break:break-word}
|
||||||
|
.tmdet .tit{margin:10px 0 3px;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--mut)}
|
||||||
|
.tmdet .ln{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px;padding:1px 0}
|
||||||
|
.tmdet .trato{margin:8px 0;padding:8px 10px;border-radius:8px;background:var(--panel2)}
|
||||||
|
.tmdet .acts{margin-top:10px;display:flex;gap:14px}
|
||||||
.toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
|
.toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
|
||||||
.toolbar label{color:var(--mut);font-size:12px;display:flex;align-items:center;gap:6px}
|
.toolbar label{color:var(--mut);font-size:12px;display:flex;align-items:center;gap:6px}
|
||||||
.toolbar .sep{flex:1}
|
.toolbar .sep{flex:1}
|
||||||
@@ -545,16 +558,16 @@
|
|||||||
<section id="t-revisar">
|
<section id="t-revisar">
|
||||||
<h2 class="v">Revisar y ordenar</h2>
|
<h2 class="v">Revisar y ordenar</h2>
|
||||||
<p class="ayuda">Todo se mueve <b>dentro de la misma carpeta</b>, y lo único que se borra son las carpetas que se quedan vacías, si las marcas tú. <span class="q" title="Más">?</span><span class="mas">Basura y proxies van a <span class="path">_Para borrar</span>. Los nombres de archivo no cambian, así Resolve y Premiere pueden re-enlazar. Cada aplicación es un cambio que se deshace desde Historial. Las carpetas que la app no reconoce salen en el plan como «dudoso» y no se marcan: nómbralas en «Por nombrar» y se ordenan solas, en cualquier disco.</span></p>
|
<p class="ayuda">Todo se mueve <b>dentro de la misma carpeta</b>, y lo único que se borra son las carpetas que se quedan vacías, si las marcas tú. <span class="q" title="Más">?</span><span class="mas">Basura y proxies van a <span class="path">_Para borrar</span>. Los nombres de archivo no cambian, así Resolve y Premiere pueden re-enlazar. Cada aplicación es un cambio que se deshace desde Historial. Las carpetas que la app no reconoce salen en el plan como «dudoso» y no se marcan: nómbralas en «Por nombrar» y se ordenan solas, en cualquier disco.</span></p>
|
||||||
|
<div id="mapaCargando" class="hint" style="display:none"><span class="spin"></span> Leyendo el inventario de la carpeta… En una carpeta grande tarda unos segundos.</div>
|
||||||
<div id="mapaVacio" class="hint">Esta carpeta todavía no está escaneada. Vuelve a <b>Carpeta</b> y pulsa <b>Escanear</b> (solo lee nombres y tamaños; no toca nada).</div>
|
<div id="mapaVacio" class="hint">Esta carpeta todavía no está escaneada. Vuelve a <b>Carpeta</b> y pulsa <b>Escanear</b> (solo lee nombres y tamaños; no toca nada).</div>
|
||||||
<div id="revisarLleno" style="display:none">
|
<div id="revisarLleno" style="display:none">
|
||||||
<div class="tabs" id="tabsRev"><a data-s="hay" class="on">Qué hay</a><a data-s="plan">Qué va a pasar</a><a data-s="nombrar">Por nombrar <span class="badge" id="badgeNombrar">…</span></a><a data-s="stock">Stock viejo</a><a data-s="vacias">Carpetas vacías</a></div>
|
<div class="tabs" id="tabsRev"><a data-s="hay" class="on">Qué hay</a><a data-s="plan">Qué va a pasar</a><a data-s="nombrar">Por nombrar <span class="badge" id="badgeNombrar">…</span></a><a data-s="stock">Stock viejo</a><a data-s="vacias">Carpetas vacías</a></div>
|
||||||
|
|
||||||
<div class="sub on" id="s-hay">
|
<div class="sub on" id="s-hay">
|
||||||
<div class="cards" id="cards"></div>
|
<div class="cards" id="cards"></div>
|
||||||
<div class="row">
|
<div class="box" style="margin-bottom:18px"><h3>Qué pesa · toca una carpeta para entrar en ella</h3><div class="migas" id="tmMigas"></div>
|
||||||
<div class="box"><h3>Qué pesa (primer nivel)</h3><div class="tm" id="treemap"></div><div class="legend" id="legend" style="margin-top:8px"></div></div>
|
<div class="tmrow"><div><div class="tm" id="treemap"></div><div class="legend" id="legend" style="margin-top:8px"></div></div><div class="tmdet" id="tmDetalle"></div></div></div>
|
||||||
<div class="box"><h3>Avisos</h3><div id="alertas"></div></div>
|
<div class="box" style="margin-bottom:18px"><h3>Avisos</h3><div id="alertas"></div></div>
|
||||||
</div>
|
|
||||||
<details class="mas"><summary>Más detalle: cámaras, meses, tipos de archivo, repetidos, dónde vive cada cliente</summary>
|
<details class="mas"><summary>Más detalle: cámaras, meses, tipos de archivo, repetidos, dónde vive cada cliente</summary>
|
||||||
<div class="row" style="margin-top:12px"><div class="box"><h3>Por fuente (de qué cámara viene)</h3><div id="fuentes"></div></div><div class="box"><h3>Material de cámara por mes</h3><div id="meses"></div></div></div>
|
<div class="row" style="margin-top:12px"><div class="box"><h3>Por fuente (de qué cámara viene)</h3><div id="fuentes"></div></div><div class="box"><h3>Material de cámara por mes</h3><div id="meses"></div></div></div>
|
||||||
<div class="row"><div class="box"><h3>Por tipo de archivo</h3><div id="exts"></div></div><div class="box"><h3>Repetidos (mismo nombre y peso, el mismo clip en MP4 y MOV, o con dos calidades)</h3><div id="dups"></div><div class="toolbar" style="margin-top:10px"><button id="btnComparar">Comparar y ver todos</button><button class="p" id="btnJuntar" disabled>Juntar en Duplicados</button><span class="mut" id="dupsResumen"></span></div></div></div>
|
<div class="row"><div class="box"><h3>Por tipo de archivo</h3><div id="exts"></div></div><div class="box"><h3>Repetidos (mismo nombre y peso, el mismo clip en MP4 y MOV, o con dos calidades)</h3><div id="dups"></div><div class="toolbar" style="margin-top:10px"><button id="btnComparar">Comparar y ver todos</button><button class="p" id="btnJuntar" disabled>Juntar en Duplicados</button><span class="mut" id="dupsResumen"></span></div></div></div>
|
||||||
@@ -735,6 +748,8 @@
|
|||||||
|
|
||||||
<h3>Revisar y ordenar</h3>
|
<h3>Revisar y ordenar</h3>
|
||||||
<dl>
|
<dl>
|
||||||
|
<dt>Qué pesa</dt><dd>Toca cualquier bloque para entrar en esa carpeta, nivel a nivel, hasta el fondo. Al lado dice qué guarda, de qué cámaras o grabadoras, de qué fechas, cuántos proxies y basura se pueden apartar, y qué hace la app con ella. <i>Cómo: sale del inventario del escaneo, que entra en todas las carpetas; en Inicio se ve cuántas leyó y hasta qué profundidad.</i></dd>
|
||||||
|
<dt>Banco de material</dt><dd>Una carpeta de material ordenada por temas (Lugares, Social, Ambiental…). Los temas se quedan como están; dentro de cada uno, las grabaciones van por fecha de rodaje y por cámara: «Lugares\Peajes\Drone» pasa a «Lugares\Peajes\2024-03-12\DJI». Una carpeta cuyo nombre diga «No tocar» se trata así, y todo lo que la app propone dentro va sin marcar: lo marcas tú en «Qué va a pasar», donde el banco es un paso propio. Otra carpeta se puede marcar como banco en «Por nombrar».</dd>
|
||||||
<dt>Qué hay</dt><dd>Qué pesa y de qué clase es, por cámara, por mes y por tipo de archivo, con avisos de lo raro: material suelto, meses mal escritos, dos sistemas de orden a la vez.</dd>
|
<dt>Qué hay</dt><dd>Qué pesa y de qué clase es, por cámara, por mes y por tipo de archivo, con avisos de lo raro: material suelto, meses mal escritos, dos sistemas de orden a la vez.</dd>
|
||||||
<dt>Por nombrar</dt><dd>Las carpetas que no sabe qué son y que no moverá hasta que se lo digas. Propone qué es cada una y tú lo confirmas. No pregunta por las carpetas de la propia app, ni por lo de dentro de las piezas, ni por lo que ya ordena solo. <i>Cómo: mira la forma (si trae sus exportados, su portada y su música es una pieza; si guarda meses dentro, un cajón) y lo que tiene dentro contando archivos, no peso. Lo confirmado se guarda en un diccionario y vale para las carpetas que se llamen exactamente igual, en cualquier disco.</i></dd>
|
<dt>Por nombrar</dt><dd>Las carpetas que no sabe qué son y que no moverá hasta que se lo digas. Propone qué es cada una y tú lo confirmas. No pregunta por las carpetas de la propia app, ni por lo de dentro de las piezas, ni por lo que ya ordena solo. <i>Cómo: mira la forma (si trae sus exportados, su portada y su música es una pieza; si guarda meses dentro, un cajón) y lo que tiene dentro contando archivos, no peso. Lo confirmado se guarda en un diccionario y vale para las carpetas que se llamen exactamente igual, en cualquier disco.</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>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>
|
||||||
@@ -851,8 +866,8 @@ const fechaCorta = ts => ts ? new Date(ts*1000).toLocaleDateString('es-CO', {day
|
|||||||
const hora = ts => ts ? new Date(ts*1000).toLocaleTimeString('es-CO', {hour:'2-digit', minute:'2-digit'}) : '';
|
const hora = ts => ts ? new Date(ts*1000).toLocaleTimeString('es-CO', {hour:'2-digit', minute:'2-digit'}) : '';
|
||||||
const num = n => (n || 0).toLocaleString('es-CO');
|
const num = n => (n || 0).toLocaleString('es-CO');
|
||||||
const dur = s => { s = Math.round(s||0); return s >= 3600 ? `${Math.floor(s/3600)}h ${Math.floor(s%3600/60)}m` : `${Math.floor(s/60)}m ${s%60}s`; };
|
const dur = s => { s = Math.round(s||0); return s >= 3600 ? `${Math.floor(s/3600)}h ${Math.floor(s%3600/60)}m` : `${Math.floor(s/60)}m ${s%60}s`; };
|
||||||
const COLORES = {ANIO:'#4f8cff',GRABACION:'#e0574f',PIEZA:'#7c5cff',CAMPANA:'#ff8c42',EXPORTADOS:'#3ecf8e',PORTADAS:'#f5b84b',APOYOS:'#d96fd9',MARCA:'#39b8c9',PROYECTOS:'#8fa1b8',MUSICA:'#c9b339',SFX:'#c9b339',VOZ:'#c9b339',IA:'#a78bfa',STOCK:'#5aa8e0',CONTENEDOR:'#6b7280',REVISAR:'#ff6b6b',PROXY:'#555c6b',BASURA:'#3a3f4a',APARTADO:'#9aa3ad','':'#4b5563'};
|
const COLORES = {BANCO:'#b07d4f',ANIO:'#4f8cff',GRABACION:'#e0574f',PIEZA:'#7c5cff',CAMPANA:'#ff8c42',EXPORTADOS:'#3ecf8e',PORTADAS:'#f5b84b',APOYOS:'#d96fd9',MARCA:'#39b8c9',PROYECTOS:'#8fa1b8',MUSICA:'#c9b339',SFX:'#c9b339',VOZ:'#c9b339',IA:'#a78bfa',STOCK:'#5aa8e0',CONTENEDOR:'#6b7280',REVISAR:'#ff6b6b',PROXY:'#555c6b',BASURA:'#3a3f4a',APARTADO:'#9aa3ad','':'#4b5563'};
|
||||||
const NOMBRE_CUBETA = {ANIO:'año',GRABACION:'grabación',PIEZA:'pieza',CAMPANA:'campaña',EXPORTADOS:'exportados',PORTADAS:'portadas',APOYOS:'apoyos',MARCA:'marca',PROYECTOS:'proyectos',MUSICA:'música',SFX:'sonidos',VOZ:'voz',IA:'IA',STOCK:'stock',CONTENEDOR:'contenedor',REVISAR:'por revisar',PROXY:'proxies',BASURA:'basura',CELULAR:'celular',FOTOS:'fotos',APARTADO:'apartado por la app'};
|
const NOMBRE_CUBETA = {BANCO:'banco de material (temas)',DOCUMENTOS:'documentos',GUION:'guion',IGNORAR:'se deja como está',ANIO:'año',GRABACION:'grabación',PIEZA:'pieza',CAMPANA:'campaña',EXPORTADOS:'exportados',PORTADAS:'portadas',APOYOS:'apoyos',MARCA:'marca',PROYECTOS:'proyectos',MUSICA:'música',SFX:'sonidos',VOZ:'voz',IA:'IA',STOCK:'stock',CONTENEDOR:'contenedor',REVISAR:'por revisar',PROXY:'proxies',BASURA:'basura',CELULAR:'celular',FOTOS:'fotos',APARTADO:'apartado por la app'};
|
||||||
const SEGURIDAD = {alta:'seguro', media:'revisar', baja:'dudoso'};
|
const SEGURIDAD = {alta:'seguro', media:'revisar', baja:'dudoso'};
|
||||||
let ESTADO = null, MAPA = null, PLAN = null, CAND = [], VOC = [], STOCK = [], RESP = null, CARPETA = null;
|
let ESTADO = null, MAPA = null, PLAN = null, CAND = [], VOC = [], STOCK = [], RESP = null, CARPETA = null;
|
||||||
|
|
||||||
@@ -885,15 +900,23 @@ async function accion(btn, fn){
|
|||||||
// se marca lo que pusimos nosotros: si la acción deja OTRA etiqueta (porque recalculó el plan, vació la
|
// se marca lo que pusimos nosotros: si la acción deja OTRA etiqueta (porque recalculó el plan, vació la
|
||||||
// lista o volvió a bloquear el botón), esa manda. Antes el finally la pisaba y quedaban botones
|
// lista o volvió a bloquear el botón), esa manda. Antes el finally la pisaba y quedaban botones
|
||||||
// habilitados con la cuenta vieja: «Aplicar todo lo marcado (23)» sobre un plan que ya no tenía 23.
|
// habilitados con la cuenta vieja: «Aplicar todo lo marcado (23)» sobre un plan que ya no tenía 23.
|
||||||
const antes = btn.innerHTML, marca = '<span class="spin"></span>' + antes;
|
const antes = btn.innerHTML, marca = '<span class="spin"></span>' + antes, ficha = {};
|
||||||
btn.disabled = true; btn.innerHTML = marca;
|
btn.disabled = true; btn.innerHTML = marca; btn._accion = ficha;
|
||||||
try { await fn(); }
|
try { await fn(); }
|
||||||
catch (e) { console.error(e); aviso(e instanceof ApiError ? e.message : 'Algo falló: ' + (e.message || e), 'bad', null, detalleDe(e)); }
|
catch (e) { console.error(e); aviso(e instanceof ApiError ? e.message : 'Algo falló: ' + (e.message || e), 'bad', null, detalleDe(e)); }
|
||||||
finally { if (btn.innerHTML === marca) { btn.disabled = false; btn.innerHTML = antes; } }
|
finally {
|
||||||
|
if (btn.innerHTML === marca) { btn.disabled = false; btn.innerHTML = antes; }
|
||||||
|
// La etiqueta nueva manda, pero si nadie decidió bloquear el botón (con `bloquear`), se vuelve a poder
|
||||||
|
// pulsar. Medido el 14-oct-2026: tras el primer cálculo «Calcular» se quedaba gris para siempre (su
|
||||||
|
// etiqueta pasa a «Calcular de nuevo»), y al cambiar de carpeta «Qué va a pasar» ya no calculaba nada;
|
||||||
|
// lo mismo «Reanudar» la cola después de «Detener», y «Volver a pintar».
|
||||||
|
else if (btn._accion === ficha) btn.disabled = false;
|
||||||
|
if (btn._accion === ficha) btn._accion = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// qué se guarda como detalle de un error: la traza del servidor si la mandó, o la pila del navegador si fue aquí
|
// qué se guarda como detalle de un error: la traza del servidor si la mandó, o la pila del navegador si fue aquí
|
||||||
const detalleDe = e => (e && e.datos && e.datos.traza) ? String(e.datos.traza) : (e instanceof ApiError ? '' : String((e && e.stack) || ''));
|
const detalleDe = e => (e && e.datos && e.datos.traza) ? String(e.datos.traza) : (e instanceof ApiError ? '' : String((e && e.stack) || ''));
|
||||||
function bloquear(id, motivo){ const b = $('#' + id); if (!b) return; b.disabled = !!motivo; const p = $('#' + id + 'PorQue'); if (p) p.textContent = motivo && motivo !== 'x' ? motivo : ''; }
|
function bloquear(id, motivo){ const b = $('#' + id); if (!b) return; b._accion = null; b.disabled = !!motivo; const p = $('#' + id + 'PorQue'); if (p) p.textContent = motivo && motivo !== 'x' ? motivo : ''; }
|
||||||
const seguro = fn => async (...a) => { try { await fn(...a); } catch (e) { console.error(e); aviso(e instanceof ApiError ? e.message : 'Algo falló: ' + (e.message || e), 'bad', null, detalleDe(e)); } };
|
const seguro = fn => async (...a) => { try { await fn(...a); } catch (e) { console.error(e); aviso(e instanceof ApiError ? e.message : 'Algo falló: ' + (e.message || e), 'bad', null, detalleDe(e)); } };
|
||||||
$$('.ayuda .q').forEach(q => q.onclick = () => q.closest('.ayuda').classList.toggle('abierta'));
|
$$('.ayuda .q').forEach(q => q.onclick = () => q.closest('.ayuda').classList.toggle('abierta'));
|
||||||
|
|
||||||
@@ -978,7 +1001,7 @@ async function sondear(){ if (SONDEO) return; SONDEO = cargarEstado().catch(e =>
|
|||||||
setInterval(sondear, 2500);
|
setInterval(sondear, 2500);
|
||||||
$('#raiz').onchange = () => { PLAN = null; CAND = []; STOCK = []; RESP = null; MAPA = null; CARPETA = null; VOC = []; limpiarTablas(); cargarEstado().then(() => cargarVista()).catch(e => aviso(e.message, 'bad')); };
|
$('#raiz').onchange = () => { PLAN = null; CAND = []; STOCK = []; RESP = null; MAPA = null; CARPETA = null; VOC = []; limpiarTablas(); cargarEstado().then(() => cargarVista()).catch(e => aviso(e.message, 'bad')); };
|
||||||
function limpiarTablas(){
|
function limpiarTablas(){
|
||||||
$('#planPasos').innerHTML = ''; $('#planAvisos').innerHTML = ''; PLAN_ABIERTOS = new Set();
|
$('#planPasos').innerHTML = ''; $('#planAvisos').innerHTML = ''; $('#planLista').innerHTML = ''; $('#planEjemplo').innerHTML = ''; $('#planAvance').innerHTML = ''; PLAN_ABIERTOS = new Set(); TM_RUTA = null;
|
||||||
$('#planIntro').innerHTML = 'Pulsa <b>Calcular</b> para ver, paso a paso, qué haría la app en esta carpeta.'; estadoPlan();
|
$('#planIntro').innerHTML = 'Pulsa <b>Calcular</b> para ver, paso a paso, qué haría la app en esta carpeta.'; estadoPlan();
|
||||||
$('#optCards').innerHTML = ''; $('#anaEstado').textContent = ''; $('#convLista').innerHTML = '<span class="mut">Pulsa «Buscar clips» para ver qué se puede convertir en esta carpeta.</span>'; bloquear('btnEncolar', 'x'); $('#btnEncolar').textContent = 'Convertir';
|
$('#optCards').innerHTML = ''; $('#anaEstado').textContent = ''; $('#convLista').innerHTML = '<span class="mut">Pulsa «Buscar clips» para ver qué se puede convertir en esta carpeta.</span>'; bloquear('btnEncolar', 'x'); $('#btnEncolar').textContent = 'Convertir';
|
||||||
$('#tablaStock tbody').innerHTML = ''; $('#stockCaja').style.display = 'none'; $('#stockResumen').textContent = ''; bloquear('btnStockPapelera', 'Busca primero');
|
$('#tablaStock tbody').innerHTML = ''; $('#stockCaja').style.display = 'none'; $('#stockResumen').textContent = ''; bloquear('btnStockPapelera', 'Busca primero');
|
||||||
@@ -1165,7 +1188,10 @@ async function cargarCarpeta(){
|
|||||||
$('#inicioLleno').style.display = raiz ? '' : 'none';
|
$('#inicioLleno').style.display = raiz ? '' : 'none';
|
||||||
if (!raiz) { pintarPortada(); return; }
|
if (!raiz) { pintarPortada(); return; }
|
||||||
const r = cfgRaiz();
|
const r = cfgRaiz();
|
||||||
|
// en una carpeta grande la primera lectura tarda: mejor decirlo que dejar la de antes, o nada
|
||||||
|
if ($('#acciones').dataset.raiz !== raiz) { $('#acciones').dataset.raiz = ''; $('#siguiente').innerHTML = ''; $('#acciones').innerHTML = '<div class="cabsec">Leyendo la carpeta</div><div class="mut"><span class="spin"></span> En una carpeta grande tarda unos segundos.</div>'; }
|
||||||
CARPETA = await api('/api/carpeta/estado?raiz=' + encodeURIComponent(raiz));
|
CARPETA = await api('/api/carpeta/estado?raiz=' + encodeURIComponent(raiz));
|
||||||
|
if (raizActual() !== raiz) return;
|
||||||
$('#cabTitulo').textContent = r.cliente;
|
$('#cabTitulo').textContent = r.cliente;
|
||||||
const e = CARPETA.escaneo;
|
const e = CARPETA.escaneo;
|
||||||
$('#cabSub').innerHTML = `<span class="path">${esc(r.ruta)}</span> · ${esc(ESTADO.tipos[r.tipo] || r.tipo)}${e ? ` · ${num(e.n)} archivos · ${gb(e.bytes)} · escaneada ${fecha(e.ts)}` : ' · sin escanear'}`;
|
$('#cabSub').innerHTML = `<span class="path">${esc(r.ruta)}</span> · ${esc(ESTADO.tipos[r.tipo] || r.tipo)}${e ? ` · ${num(e.n)} archivos · ${gb(e.bytes)} · escaneada ${fecha(e.ts)}` : ' · sin escanear'}`;
|
||||||
@@ -1177,6 +1203,10 @@ async function cargarCarpeta(){
|
|||||||
$('#pesoBarra').innerHTML = d.grupos.map(g => `<i style="width:${Math.max(g.pct, 0.4)}%;background:${COLOR[g.clave] || '#ccc'}" title="${esc(g.titulo)} · ${gb(g.bytes)} · ${num(g.n)} archivos"></i>`).join('');
|
$('#pesoBarra').innerHTML = d.grupos.map(g => `<i style="width:${Math.max(g.pct, 0.4)}%;background:${COLOR[g.clave] || '#ccc'}" title="${esc(g.titulo)} · ${gb(g.bytes)} · ${num(g.n)} archivos"></i>`).join('');
|
||||||
$('#pesoLinea').innerHTML = d.grupos.slice(0, 5).map(g => `<b>${gb(g.bytes)}</b> ${esc(NOMBRE_CORTO[g.clave] || g.titulo)}`).join(' · ');
|
$('#pesoLinea').innerHTML = d.grupos.slice(0, 5).map(g => `<b>${gb(g.bytes)}</b> ${esc(NOMBRE_CORTO[g.clave] || g.titulo)}`).join(' · ');
|
||||||
$('#inicioDiag').innerHTML = d.frases.slice(0, 3).map(f => `<div>${esc(f)}</div>`).join('') + (d.frases.length > 3 ? `<div class="mut">Más en Revisar y ordenar.</div>` : '');
|
$('#inicioDiag').innerHTML = d.frases.slice(0, 3).map(f => `<div>${esc(f)}</div>`).join('') + (d.frases.length > 3 ? `<div class="mut">Más en Revisar y ordenar.</div>` : '');
|
||||||
|
const inv = CARPETA.inventario;
|
||||||
|
// que se vea que el escaneo entró en todas: cuántas carpetas y hasta qué profundidad
|
||||||
|
if (inv && e) $('#inicioDiag').insertAdjacentHTML('afterbegin', `<div class="mut" style="margin-bottom:4px">Escaneo completo: ${num(e.n)} archivos en ${num(inv.carpetas)} carpetas, hasta ${inv.niveles} nivel${inv.niveles !== 1 ? 'es' : ''} de profundidad. <button class="lnk" id="verDentro" style="padding:0">Ver qué hay dentro</button></div>`);
|
||||||
|
const vd = $('#verDentro'); if (vd) vd.onclick = () => { irA('revisar'); subRev('hay'); };
|
||||||
}
|
}
|
||||||
const lotes = CARPETA.lotes || [];
|
const lotes = CARPETA.lotes || [];
|
||||||
$('#inicioLotes').innerHTML = lotes.length ? lotes.map(l => `<div class="ev"><span class="h">${esc(fechaCorta(l.ts))}</span><span class="q"><b>${esc(queSeHizo(l.lote))}</b> · ${cuentaLote(l, true)}${l.errores ? ` <span class="bad">· ${l.errores} avisos</span>` : ''}</span>${l.lote && (l.lote.startsWith('optimizar') || l.lote.startsWith('entrada-') || l.lote.startsWith('tanda-')) ? '' : `<button data-l="${esc(l.lote)}">Deshacer</button>`}</div>`).join('') : '<span class="mut">Todavía no se ha movido nada en esta carpeta.</span>';
|
$('#inicioLotes').innerHTML = lotes.length ? lotes.map(l => `<div class="ev"><span class="h">${esc(fechaCorta(l.ts))}</span><span class="q"><b>${esc(queSeHizo(l.lote))}</b> · ${cuentaLote(l, true)}${l.errores ? ` <span class="bad">· ${l.errores} avisos</span>` : ''}</span>${l.lote && (l.lote.startsWith('optimizar') || l.lote.startsWith('entrada-') || l.lote.startsWith('tanda-')) ? '' : `<button data-l="${esc(l.lote)}">Deshacer</button>`}</div>`).join('') : '<span class="mut">Todavía no se ha movido nada en esta carpeta.</span>';
|
||||||
@@ -1186,7 +1216,19 @@ async function cargarCarpeta(){
|
|||||||
// Una sola lista, en orden, con la cifra de cada cosa y un botón. Sustituye a repartir el trabajo
|
// Una sola lista, en orden, con la cifra de cada cosa y un botón. Sustituye a repartir el trabajo
|
||||||
// entre pestañas dentro de pestañas: el usuario ve de un vistazo qué falta y cuánto pesa.
|
// entre pestañas dentro de pestañas: el usuario ve de un vistazo qué falta y cuánto pesa.
|
||||||
const IR_ACCION = {
|
const IR_ACCION = {
|
||||||
plan: p => { irA('revisar'); if (PLAN && p) { PLAN_PASO = p; PLAN_EJ = 0; pintarPlan(); } subRev('plan'); if (!PLAN) $('#btnPlan').click(); },
|
plan: p => { if (p) PLAN_PASO = p; irA('revisar'); if (PLAN && p) { PLAN_EJ = 0; pintarPlan(); } subRev('plan'); if (!PLAN) $('#btnPlan').click(); },
|
||||||
|
tipo: (_, a) => {
|
||||||
|
const [actual, delCliente] = a.tipos || [];
|
||||||
|
modal('Cómo se ordena esta carpeta', `<p>${esc(a.que)}</p><div class="tipos" id="mTipo">${tarjetasTipo(delCliente || actual)}</div><p class="mut" style="font-size:12.5px">El plan se vuelve a calcular con el que elijas. No se mueve nada hasta que apliques.</p>`,
|
||||||
|
seguro(async () => {
|
||||||
|
const t = (document.querySelector('#mTipo input:checked') || {}).value; if (!t) return;
|
||||||
|
await api('/api/raiz/tipo', {raiz: raizActual(), tipo: t});
|
||||||
|
PLAN = null; MAPAE = null;
|
||||||
|
aviso(`Listo: la carpeta y el cliente se ordenan como «${(ESTADO.tipos[t] || t).split(' · ')[0]}».`, 'ok');
|
||||||
|
await cargarEstado(); cargarVista();
|
||||||
|
}));
|
||||||
|
$('#modalSi').textContent = 'Usar este';
|
||||||
|
},
|
||||||
nombrar: () => { irA('revisar'); subRev('nombrar'); },
|
nombrar: () => { irA('revisar'); subRev('nombrar'); },
|
||||||
stock: () => { irA('revisar'); subRev('stock'); if (!STOCK.length) $('#btnStock').click(); },
|
stock: () => { irA('revisar'); subRev('stock'); if (!STOCK.length) $('#btnStock').click(); },
|
||||||
vacias: () => { irA('revisar'); subRev('vacias'); if (!VACIAS || VACIAS.raiz !== raizActual()) $('#btnVacias').click(); },
|
vacias: () => { irA('revisar'); subRev('vacias'); if (!VACIAS || VACIAS.raiz !== raizActual()) $('#btnVacias').click(); },
|
||||||
@@ -1210,6 +1252,7 @@ async function pintarAcciones(){
|
|||||||
const d = await api('/api/carpeta/acciones?raiz=' + encodeURIComponent(raiz));
|
const d = await api('/api/carpeta/acciones?raiz=' + encodeURIComponent(raiz));
|
||||||
if (raizActual() !== raiz) return; // cambió de carpeta mientras llegaba
|
if (raizActual() !== raiz) return; // cambió de carpeta mientras llegaba
|
||||||
ACCIONES = d.acciones || [];
|
ACCIONES = d.acciones || [];
|
||||||
|
$('#acciones').dataset.raiz = raiz; // de qué carpeta es lo que se ve: así se sabe cuándo decir «leyendo»
|
||||||
const quedan = ACCIONES.filter(a => a.estado === 'toca').length;
|
const quedan = ACCIONES.filter(a => a.estado === 'toca').length;
|
||||||
const auto = d.automatico || [], asis = d.asistente;
|
const auto = d.automatico || [], asis = d.asistente;
|
||||||
const corriendo = asis && asis.activo;
|
const corriendo = asis && asis.activo;
|
||||||
@@ -1218,7 +1261,7 @@ async function pintarAcciones(){
|
|||||||
: `<div class="cabsec">${quedan ? `Lo que se puede hacer ahora · ${quedan}` : 'Esta carpeta está al día' + (d.ultimo ? ` · último cambio ${esc(fechaCorta(d.ultimo.ts))}` : '')}${auto.length > 1 ? '<span class="sep"></span><button class="p" id="btnHacerTodo" style="text-transform:none;letter-spacing:0">Hacer las ' + auto.length + ' de una vez</button>' : ''}</div>`;
|
: `<div class="cabsec">${quedan ? `Lo que se puede hacer ahora · ${quedan}` : 'Esta carpeta está al día' + (d.ultimo ? ` · último cambio ${esc(fechaCorta(d.ultimo.ts))}` : '')}${auto.length > 1 ? '<span class="sep"></span><button class="p" id="btnHacerTodo" style="text-transform:none;letter-spacing:0">Hacer las ' + auto.length + ' de una vez</button>' : ''}</div>`;
|
||||||
$('#acciones').innerHTML = cab +
|
$('#acciones').innerHTML = cab +
|
||||||
ACCIONES.map((a, i) => `<div class="ac ${a.estado}"><span class="ico">${a.calculando ? '<span class="spin"></span>' : a.estado === 'hecho' ? '✓' : a.estado === 'espera' ? '·' : i + 1}</span><div class="tx"><b>${esc(a.titulo)}</b><div class="q">${esc(a.que)}</div>${a.detalle ? `<div class="d">${esc(a.detalle)}</div>` : ''}</div><div class="der">${a.cifra ? `<span class="cifra">${esc(a.cifra)}</span>` : ''}${a.boton ? `<button class="${a.estado === 'espera' ? 'lnk' : 'p'}" data-a="${i}">${esc(a.boton)}</button>` : ''}</div></div>`).join('');
|
ACCIONES.map((a, i) => `<div class="ac ${a.estado}"><span class="ico">${a.calculando ? '<span class="spin"></span>' : a.estado === 'hecho' ? '✓' : a.estado === 'espera' ? '·' : i + 1}</span><div class="tx"><b>${esc(a.titulo)}</b><div class="q">${esc(a.que)}</div>${a.detalle ? `<div class="d">${esc(a.detalle)}</div>` : ''}</div><div class="der">${a.cifra ? `<span class="cifra">${esc(a.cifra)}</span>` : ''}${a.boton ? `<button class="${a.estado === 'espera' ? 'lnk' : 'p'}" data-a="${i}">${esc(a.boton)}</button>` : ''}</div></div>`).join('');
|
||||||
$$('#acciones [data-a]').forEach(b => b.onclick = () => { const a = ACCIONES[+b.dataset.a]; (IR_ACCION[a.ir] || (() => {}))(a.paso); });
|
$$('#acciones [data-a]').forEach(b => b.onclick = () => { const a = ACCIONES[+b.dataset.a]; (IR_ACCION[a.ir] || (() => {}))(a.paso, a); });
|
||||||
// se acaba de mover algo y el programa está comprobando qué queda: se vuelve a preguntar en un momento
|
// se acaba de mover algo y el programa está comprobando qué queda: se vuelve a preguntar en un momento
|
||||||
if (d.calculando) setTimeout(() => { if (raizActual() === raiz && vistaActual() === 'carpeta') cargarCarpeta().catch(() => {}); }, 1500);
|
if (d.calculando) setTimeout(() => { if (raizActual() === raiz && vistaActual() === 'carpeta') cargarCarpeta().catch(() => {}); }, 1500);
|
||||||
const bt = $('#btnHacerTodo');
|
const bt = $('#btnHacerTodo');
|
||||||
@@ -1316,7 +1359,12 @@ async function deshacer(lote){
|
|||||||
// ---------------- revisar: qué hay ----------------
|
// ---------------- revisar: qué hay ----------------
|
||||||
async function cargarMapa(){
|
async function cargarMapa(){
|
||||||
if (!raizActual()) { $('#mapaVacio').style.display = 'block'; $('#revisarLleno').style.display = 'none'; return; }
|
if (!raizActual()) { $('#mapaVacio').style.display = 'block'; $('#revisarLleno').style.display = 'none'; return; }
|
||||||
MAPA = await api('/api/mapa?raiz=' + encodeURIComponent(raizActual()));
|
const raiz = raizActual();
|
||||||
|
if (!MAPA || MAPA.raiz !== raiz) { $('#mapaVacio').style.display = 'none'; $('#revisarLleno').style.display = 'none'; $('#mapaCargando').style.display = 'block'; }
|
||||||
|
let m0;
|
||||||
|
try { m0 = await api('/api/mapa?raiz=' + encodeURIComponent(raiz)); } finally { $('#mapaCargando').style.display = 'none'; }
|
||||||
|
if (raizActual() !== raiz) return;
|
||||||
|
MAPA = m0;
|
||||||
$('#mapaVacio').style.display = MAPA.escaneado ? 'none' : 'block';
|
$('#mapaVacio').style.display = MAPA.escaneado ? 'none' : 'block';
|
||||||
$('#revisarLleno').style.display = MAPA.escaneado ? 'block' : 'none';
|
$('#revisarLleno').style.display = MAPA.escaneado ? 'block' : 'none';
|
||||||
cargarDiscos().catch(() => {});
|
cargarDiscos().catch(() => {});
|
||||||
@@ -1329,9 +1377,7 @@ async function cargarMapa(){
|
|||||||
[gb(m.duplicados.bytes), `repetidos (${m.duplicados.grupos} grupos)`], [gb(m.proxies.bytes), `proxies (${m.proxies.n})`],
|
[gb(m.duplicados.bytes), `repetidos (${m.duplicados.grupos} grupos)`], [gb(m.proxies.bytes), `proxies (${m.proxies.n})`],
|
||||||
[num(m.basura.n), `archivos basura · ${gb(m.basura.bytes)}`], [m.sueltos_raiz.n, `sueltos en la raíz · ${gb(m.sueltos_raiz.bytes)}`],
|
[num(m.basura.n), `archivos basura · ${gb(m.basura.bytes)}`], [m.sueltos_raiz.n, `sueltos en la raíz · ${gb(m.sueltos_raiz.bytes)}`],
|
||||||
].map(([b,s]) => `<div class="card"><b>${b}</b><small>${s}</small></div>`).join('');
|
].map(([b,s]) => `<div class="card"><b>${b}</b><small>${s}</small></div>`).join('');
|
||||||
treemap($('#treemap'), m.arbol.map(h => ({nombre:h.nombre, bytes:h.bytes, n:h.n, color: COLORES[h.cubeta] || COLORES[''], hijos:h.hijos, cubeta:h.cubeta})));
|
verCarpetaTm(TM_RUTA);
|
||||||
const usados = [...new Set(m.arbol.map(h => h.cubeta || ''))];
|
|
||||||
$('#legend').innerHTML = usados.map(c => `<span><i style="background:${COLORES[c]||COLORES['']}"></i>${NOMBRE_CUBETA[c] || c || 'no reconocida'}</span>`).join('');
|
|
||||||
const al = [];
|
const al = [];
|
||||||
if (m.basura.n) al.push(`<li><b>${num(m.basura.n)} archivos basura</b> (${gb(m.basura.bytes)}): .DS_Store, Thumbs.db, .tmp, autoguardados de Adobe. Al ordenar van a _Para borrar.</li>`);
|
if (m.basura.n) al.push(`<li><b>${num(m.basura.n)} archivos basura</b> (${gb(m.basura.bytes)}): .DS_Store, Thumbs.db, .tmp, autoguardados de Adobe. Al ordenar van a _Para borrar.</li>`);
|
||||||
if (m.proxies.n) al.push(`<li><b>${gb(m.proxies.bytes)} en proxies</b> (${m.proxies.n}). Se regeneran desde Resolve/Premiere: sobran en el disco.</li>`);
|
if (m.proxies.n) al.push(`<li><b>${gb(m.proxies.bytes)} en proxies</b> (${m.proxies.n}). Se regeneran desde Resolve/Premiere: sobran en el disco.</li>`);
|
||||||
@@ -1366,13 +1412,47 @@ $('#btnJuntar').onclick = () => { if (!DUPS || DUPS.raiz !== raizActual()) retur
|
|||||||
await cargarEstado(); cargarVista();
|
await cargarEstado(); cargarVista();
|
||||||
})); $('#modalSi').textContent = 'Juntar'; };
|
})); $('#modalSi').textContent = 'Juntar'; };
|
||||||
|
|
||||||
function treemap(el, items){
|
// «Qué pesa», nivel a nivel: se entra en cualquier carpeta y al lado dice qué guarda, de qué cámaras, de
|
||||||
|
// qué fechas y qué hace la app con ella. Antes solo había el primer nivel, y en Vía Sumapaz una carpeta de
|
||||||
|
// 3,6 TB era un bloque gris sin nada dentro: parecía que el escaneo no había entrado.
|
||||||
|
let TM_RUTA = null;
|
||||||
|
const mesCorto = ym => { const [a, m] = (ym || '').split('-'); return m ? `${MESES_UI[+m - 1].slice(0, 3).toLowerCase()} ${a}` : ''; };
|
||||||
|
const verCarpetaTm = seguro(async ruta => {
|
||||||
|
const raiz = raizActual(); if (!raiz) return;
|
||||||
|
const d = await api(`/api/mapa/carpeta?raiz=${encodeURIComponent(raiz)}${ruta ? '&ruta=' + encodeURIComponent(ruta) : ''}`);
|
||||||
|
if (raizActual() !== raiz) return;
|
||||||
|
TM_RUTA = d.rel ? d.ruta : null;
|
||||||
|
const items = d.hijos.map(h => ({nombre: h.nombre, bytes: h.bytes, n: h.n, color: COLORES[h.cubeta] || h.color || COLORES[''], cubeta: h.cubeta, ruta: h.ruta}));
|
||||||
|
if (d.sueltos.n && d.hijos.length) items.push({nombre: 'archivos sueltos aquí', bytes: d.sueltos.bytes, n: d.sueltos.n, color: '#9aa3ad', cubeta: ''});
|
||||||
|
treemap($('#treemap'), items, r => verCarpetaTm(r));
|
||||||
|
if (!d.hijos.length) $('#treemap').innerHTML = `<div class="tmvacio">Aquí ya no hay más carpetas: ${num(d.n)} archivo${d.n !== 1 ? 's' : ''} sueltos, ${gb(d.bytes)}.</div>`;
|
||||||
|
const usados = [...new Set(items.map(i => i.cubeta || ''))];
|
||||||
|
$('#legend').innerHTML = usados.map(c => `<span><i style="background:${COLORES[c] || COLORES['']}"></i>${NOMBRE_CUBETA[c] || c || 'no reconocida'}</span>`).join('');
|
||||||
|
let acum = d.raiz.replace(/[\\/]+$/, '');
|
||||||
|
const segs = d.rel ? d.rel.split(/[\\/]/) : [];
|
||||||
|
$('#tmMigas').innerHTML = [`<button class="lnk" data-r="">${esc((cfgRaiz() || {}).cliente || d.raiz)}</button>`]
|
||||||
|
.concat(segs.map(s => { acum += '\\' + s; return `<button class="lnk" data-r="${esc(acum)}">${esc(s)}</button>`; })).join('<span class="mut">›</span>');
|
||||||
|
$$('#tmMigas [data-r]').forEach(b => b.onclick = () => verCarpetaTm(b.dataset.r || null));
|
||||||
|
const fila = (a, b, c) => `<div class="ln"><span>${esc(a)}</span><b>${b}</b><span class="mut">${c}</span></div>`;
|
||||||
|
$('#tmDetalle').innerHTML = `<h4>${esc(d.rel ? d.nombre : ((cfgRaiz() || {}).cliente || d.nombre))}</h4>
|
||||||
|
<div><b>${gb(d.bytes)}</b> · ${num(d.n)} archivo${d.n !== 1 ? 's' : ''}${d.carpetas ? ` · ${num(d.carpetas)} carpetas dentro, hasta ${d.niveles} nivel${d.niveles !== 1 ? 'es' : ''} más abajo` : ''}</div>
|
||||||
|
${d.trato.texto ? `<p class="trato">${esc(d.trato.texto)}</p>` : ''}
|
||||||
|
${d.en_plan ? `<div class="mut">En el plan: ${d.en_plan.filas ? `${num(d.en_plan.filas)} cambio${d.en_plan.filas !== 1 ? 's' : ''} (${gb(d.en_plan.bytes)}), ${num(d.en_plan.marcadas)} marcado${d.en_plan.marcadas !== 1 ? 's' : ''}` : 'nada que cambiar'}.</div>` : ''}
|
||||||
|
<div class="tit">Qué guarda</div>${d.roles.slice(0, 6).map(r => fila(r.que, gb(r.bytes), num(r.n))).join('') || '<div class="mut">Nada.</div>'}
|
||||||
|
${d.fuentes.length ? '<div class="tit">De qué cámara o grabadora</div>' + d.fuentes.slice(0, 6).map(f => fila(f.que, gb(f.bytes), num(f.n))).join('') : ''}
|
||||||
|
${d.fechas.desde ? `<div class="tit">Cuándo se grabó</div><div>De ${mesCorto(d.fechas.desde)} a ${mesCorto(d.fechas.hasta)} · ${d.fechas.meses} mes${d.fechas.meses !== 1 ? 'es' : ''} distintos</div>` : ''}
|
||||||
|
${d.apartable.proxies || d.apartable.basura ? `<div class="mut" style="margin-top:6px">Se puede apartar: ${gb(d.apartable.proxies)} en proxies y ${gb(d.apartable.basura)} de basura.</div>` : ''}
|
||||||
|
<div class="acts"><button class="lnk" id="tmAbrir">Abrir en el Explorador</button>${d.rel ? '<button class="lnk" id="tmSubir">Subir un nivel</button>' : ''}</div>`;
|
||||||
|
$('#tmAbrir').onclick = seguro(async () => { await api('/api/abrir', {ruta: d.ruta}); });
|
||||||
|
const sb = $('#tmSubir'); if (sb) sb.onclick = () => { const p = d.ruta.replace(/[\\/][^\\/]+$/, ''); verCarpetaTm(p.toLowerCase() === d.raiz.toLowerCase() ? null : p); };
|
||||||
|
});
|
||||||
|
function treemap(el, items, alPulsar){
|
||||||
el.innerHTML = ''; const W = el.clientWidth || 600, H = el.clientHeight || 300;
|
el.innerHTML = ''; const W = el.clientWidth || 600, H = el.clientHeight || 300;
|
||||||
const total = items.reduce((s,i) => s + i.bytes, 0) || 1;
|
const total = items.reduce((s,i) => s + i.bytes, 0) || 1;
|
||||||
const nodos = items.filter(i => i.bytes > 0).sort((a,b) => b.bytes - a.bytes).map(i => ({...i, area: i.bytes/total*W*H}));
|
const nodos = items.filter(i => i.bytes > 0).sort((a,b) => b.bytes - a.bytes).map(i => ({...i, area: i.bytes/total*W*H}));
|
||||||
let x = 0, y = 0, w = W, h = H, fila = [], resto = nodos.slice();
|
let x = 0, y = 0, w = W, h = H, fila = [], resto = nodos.slice();
|
||||||
const peor = (fila, lado) => { const s = fila.reduce((a,b)=>a+b.area,0); const mx = Math.max(...fila.map(f=>f.area)), mn = Math.min(...fila.map(f=>f.area)); return Math.max(lado*lado*mx/(s*s), s*s/(lado*lado*mn)); };
|
const peor = (fila, lado) => { const s = fila.reduce((a,b)=>a+b.area,0); const mx = Math.max(...fila.map(f=>f.area)), mn = Math.min(...fila.map(f=>f.area)); return Math.max(lado*lado*mx/(s*s), s*s/(lado*lado*mn)); };
|
||||||
const pinta = (fila, lado, horizontal) => { const s = fila.reduce((a,b)=>a+b.area,0); const grosor = s/lado; let off = 0; fila.forEach(f => { const largo = f.area/grosor; const d = document.createElement('div'); const rx = horizontal ? x+off : x, ry = horizontal ? y : y+off, rw = horizontal ? largo : grosor, rh = horizontal ? grosor : largo; d.style.left = rx+'px'; d.style.top = ry+'px'; d.style.width = rw+'px'; d.style.height = rh+'px'; d.style.background = f.color; d.title = `${f.nombre}\n${gb(f.bytes)} · ${f.n} archivos${f.cubeta ? '\n' + (NOMBRE_CUBETA[f.cubeta] || f.cubeta) : ''}` + (f.hijos && f.hijos.length ? '\n\n' + f.hijos.slice(0,12).map(h => ` ${h.nombre}: ${gb(h.bytes)}`).join('\n') : ''); d.innerHTML = rw > 40 && rh > 18 ? `<b>${esc(f.nombre)}</b>${rh > 34 ? `<span>${gb(f.bytes)}</span>` : ''}` : ''; el.appendChild(d); off += largo; }); if (horizontal) { y += grosor; h -= grosor; } else { x += grosor; w -= grosor; } };
|
const pinta = (fila, lado, horizontal) => { const s = fila.reduce((a,b)=>a+b.area,0); const grosor = s/lado; let off = 0; fila.forEach(f => { const largo = f.area/grosor; const d = document.createElement('div'); const rx = horizontal ? x+off : x, ry = horizontal ? y : y+off, rw = horizontal ? largo : grosor, rh = horizontal ? grosor : largo; d.style.left = rx+'px'; d.style.top = ry+'px'; d.style.width = rw+'px'; d.style.height = rh+'px'; d.style.background = f.color; d.title = `${f.nombre}\n${gb(f.bytes)} · ${f.n} archivos${f.cubeta ? '\n' + (NOMBRE_CUBETA[f.cubeta] || f.cubeta) : ''}` + (f.hijos && f.hijos.length ? '\n\n' + f.hijos.slice(0,12).map(h => ` ${h.nombre}: ${gb(h.bytes)}`).join('\n') : ''); d.innerHTML = rw > 40 && rh > 18 ? `<b>${esc(f.nombre)}</b>${rh > 34 ? `<span>${gb(f.bytes)}</span>` : ''}` : ''; if (f.ruta && alPulsar) { d.classList.add('pick'); d.onclick = () => alPulsar(f.ruta); } el.appendChild(d); off += largo; }); if (horizontal) { y += grosor; h -= grosor; } else { x += grosor; w -= grosor; } };
|
||||||
while (resto.length) { const horizontal = w >= h; const lado = horizontal ? w : h; const n = resto.shift(); if (!fila.length || peor(fila.concat(n), lado) <= peor(fila, lado)) fila.push(n); else { pinta(fila, lado, !horizontal); fila = [n]; } }
|
while (resto.length) { const horizontal = w >= h; const lado = horizontal ? w : h; const n = resto.shift(); if (!fila.length || peor(fila.concat(n), lado) <= peor(fila, lado)) fila.push(n); else { pinta(fila, lado, !horizontal); fila = [n]; } }
|
||||||
if (fila.length) pinta(fila, w >= h ? w : h, !(w >= h));
|
if (fila.length) pinta(fila, w >= h ? w : h, !(w >= h));
|
||||||
}
|
}
|
||||||
@@ -1429,7 +1509,10 @@ const PASOS_PLAN = [
|
|||||||
{k: 'limpiar', t: 'Apartar basura y proxies', q: 'Archivos de sistema y proxies van a «_Para borrar». Nada se borra.', vacio: 'No hay basura ni proxies que apartar.'},
|
{k: 'limpiar', t: 'Apartar basura y proxies', q: 'Archivos de sistema y proxies van a «_Para borrar». Nada se borra.', vacio: 'No hay basura ni proxies que apartar.'},
|
||||||
{k: 'fechas', t: 'Ordenar por año y mes', q: 'Grabaciones y piezas, en la carpeta del mes en que se grabaron.', vacio: 'Las fechas ya están en orden.'},
|
{k: 'fechas', t: 'Ordenar por año y mes', q: 'Grabaciones y piezas, en la carpeta del mes en que se grabaron.', vacio: 'Las fechas ya están en orden.'},
|
||||||
{k: 'resto', t: 'Ordenar lo demás', q: 'Exportados, proyectos, música y marca, cada cosa a su carpeta.', vacio: 'Nada más que ordenar.'},
|
{k: 'resto', t: 'Ordenar lo demás', q: 'Exportados, proyectos, música y marca, cada cosa a su carpeta.', vacio: 'Nada más que ordenar.'},
|
||||||
|
{k: 'banco', t: 'Banco de material', q: 'Los temas se quedan; dentro de cada uno, las grabaciones por fecha de rodaje y cámara.', vacio: 'Las grabaciones del banco ya están por fecha y cámara.'},
|
||||||
];
|
];
|
||||||
|
// el banco de material solo sale si la carpeta tiene uno
|
||||||
|
const pasosVisibles = () => PASOS_PLAN.filter(p => p.k !== 'banco' || filasPaso('banco').length);
|
||||||
const SEG_TXT = {alta: 'seguro', media: 'probable', baja: 'dudoso'};
|
const SEG_TXT = {alta: 'seguro', media: 'probable', baja: 'dudoso'};
|
||||||
let PLAN_PASO = null, PLAN_EJ = 0, PLAN_VER_OPC = false, PLAN_POR_QUE = false, PLAN_AVISOS = false, ACCIONES = [];
|
let PLAN_PASO = null, PLAN_EJ = 0, PLAN_VER_OPC = false, PLAN_POR_QUE = false, PLAN_AVISOS = false, ACCIONES = [];
|
||||||
const pasoDe = r => r.paso || 'resto';
|
const pasoDe = r => r.paso || 'resto';
|
||||||
@@ -1450,7 +1533,10 @@ function estadoPlan(){
|
|||||||
}
|
}
|
||||||
async function calcularPlan(){
|
async function calcularPlan(){
|
||||||
$('#planIntro').innerHTML = '<span class="spin"></span> mirando la carpeta…';
|
$('#planIntro').innerHTML = '<span class="spin"></span> mirando la carpeta…';
|
||||||
PLAN = await api('/api/plan?raiz=' + encodeURIComponent(raizActual()));
|
const raiz = raizActual();
|
||||||
|
const p = await api('/api/plan?raiz=' + encodeURIComponent(raiz));
|
||||||
|
if (raizActual() !== raiz) return; // se cambió de carpeta mientras calculaba
|
||||||
|
PLAN = p;
|
||||||
PLAN_EJ = 0; PLAN_VER_OPC = false;
|
PLAN_EJ = 0; PLAN_VER_OPC = false;
|
||||||
pintarPlan();
|
pintarPlan();
|
||||||
}
|
}
|
||||||
@@ -1472,8 +1558,9 @@ function pasoPorDefecto(){
|
|||||||
}
|
}
|
||||||
// «43 cosméticos y 5 dudosos»: por qué hay cambios sin marcar
|
// «43 cosméticos y 5 dudosos»: por qué hay cambios sin marcar
|
||||||
function resumenOpc(rs){
|
function resumenOpc(rs){
|
||||||
const cos = rs.filter(r => r.cosmetico).length, dud = rs.filter(r => !r.cosmetico && r.confianza === 'baja').length, otros = rs.length - cos - dud;
|
const nt = rs.filter(r => r.no_tocar).length, cos = rs.filter(r => r.cosmetico && !r.no_tocar).length,
|
||||||
return [cos ? `${cos} cosmético${cos > 1 ? 's' : ''}` : '', dud ? `${dud} dudoso${dud > 1 ? 's' : ''}` : '', otros ? `${otros} que desmarcaste` : ''].filter(Boolean).join(', ').replace(/, ([^,]*)$/, ' y $1');
|
dud = rs.filter(r => !r.cosmetico && !r.no_tocar && r.confianza === 'baja').length, otros = rs.length - nt - cos - dud;
|
||||||
|
return [nt ? `${nt} dentro de «No tocar»` : '', cos ? `${cos} cosmético${cos > 1 ? 's' : ''}` : '', dud ? `${dud} dudoso${dud > 1 ? 's' : ''}` : '', otros ? `${otros} que desmarcaste` : ''].filter(Boolean).join(', ').replace(/, ([^,]*)$/, ' y $1');
|
||||||
}
|
}
|
||||||
function pintarPlan(){
|
function pintarPlan(){
|
||||||
if (!PLAN) { estadoPlan(); return; }
|
if (!PLAN) { estadoPlan(); return; }
|
||||||
@@ -1492,7 +1579,7 @@ function pintarCabPlan(){
|
|||||||
$('#planIntro').innerHTML = !tot ? `<b class="grande">La carpeta ya está en orden.</b> No hay nada que apartar ni mover.`
|
$('#planIntro').innerHTML = !tot ? `<b class="grande">La carpeta ya está en orden.</b> No hay nada que apartar ni mover.`
|
||||||
: !marc.length ? `<b class="grande">Lo seguro ya está hecho.</b> Quedan ${sin.length} cambio${sin.length > 1 ? 's' : ''} que la app no hace sola (${resumenOpc(sin)}). Están abajo, por si quieres marcarlos.`
|
: !marc.length ? `<b class="grande">Lo seguro ya está hecho.</b> Quedan ${sin.length} cambio${sin.length > 1 ? 's' : ''} que la app no hace sola (${resumenOpc(sin)}). Están abajo, por si quieres marcarlos.`
|
||||||
: `<b class="grande">La app va a hacer ${marc.length} cambio${marc.length > 1 ? 's' : ''}</b> (${gb(marc.reduce((a, r) => a + r.bytes, 0))}) dentro de <span class="path">${esc(PLAN.raiz)}</span>. Nada se borra, y se deshace desde Historial.${sin.length ? ` Otros ${sin.length} quedan sin marcar (${resumenOpc(sin)}): están abajo, por si los quieres.` : ''}`;
|
: `<b class="grande">La app va a hacer ${marc.length} cambio${marc.length > 1 ? 's' : ''}</b> (${gb(marc.reduce((a, r) => a + r.bytes, 0))}) dentro de <span class="path">${esc(PLAN.raiz)}</span>. Nada se borra, y se deshace desde Historial.${sin.length ? ` Otros ${sin.length} quedan sin marcar (${resumenOpc(sin)}): están abajo, por si los quieres.` : ''}`;
|
||||||
$('#planPasos').innerHTML = PASOS_PLAN.map((p, i) => {
|
$('#planPasos').innerHTML = pasosVisibles().map((p, i) => {
|
||||||
const rs = filasPaso(p.k), m = rs.filter(r => r.marcado), vacio = !rs.length, on = PLAN_PASO === p.k;
|
const rs = filasPaso(p.k), m = rs.filter(r => r.marcado), vacio = !rs.length, on = PLAN_PASO === p.k;
|
||||||
return `<div class="ficha ${vacio ? 'vacio' : ''} ${on ? 'on' : ''}" data-paso="${p.k}" ${vacio ? '' : `tabindex="0" role="button" aria-pressed="${on}"`}><span class="num">${vacio ? '✓' : i + 1}</span><div class="tx"><b>${p.t}</b><div class="q">${vacio ? p.vacio : p.q}</div>${vacio ? '' : `<div class="cifra">${m.length ? `<b>${m.length}</b> cambio${m.length > 1 ? 's' : ''} · ${gb(m.reduce((a, r) => a + r.bytes, 0))}` : '<b>0</b> marcados'}${rs.length > m.length ? ` <span class="mut">· ${rs.length - m.length} sin marcar</span>` : ''}</div>`}</div>${vacio || !m.length ? '' : `<button class="lnk" data-aplicar="${p.k}" title="Aplicar solo lo marcado de este paso">solo este paso</button>`}</div>`;
|
return `<div class="ficha ${vacio ? 'vacio' : ''} ${on ? 'on' : ''}" data-paso="${p.k}" ${vacio ? '' : `tabindex="0" role="button" aria-pressed="${on}"`}><span class="num">${vacio ? '✓' : i + 1}</span><div class="tx"><b>${p.t}</b><div class="q">${vacio ? p.vacio : p.q}</div>${vacio ? '' : `<div class="cifra">${m.length ? `<b>${m.length}</b> cambio${m.length > 1 ? 's' : ''} · ${gb(m.reduce((a, r) => a + r.bytes, 0))}` : '<b>0</b> marcados'}${rs.length > m.length ? ` <span class="mut">· ${rs.length - m.length} sin marcar</span>` : ''}</div>`}</div>${vacio || !m.length ? '' : `<button class="lnk" data-aplicar="${p.k}" title="Aplicar solo lo marcado de este paso">solo este paso</button>`}</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -1563,12 +1650,13 @@ function pintarLista(){
|
|||||||
const peso = {alta: 0, media: 1, baja: 2}, orden = (a, b) => (peso[a.confianza] - peso[b.confianza]) || b.bytes - a.bytes;
|
const peso = {alta: 0, media: 1, baja: 2}, orden = (a, b) => (peso[a.confianza] - peso[b.confianza]) || b.bytes - a.bytes;
|
||||||
const todas = filasPaso(PLAN_PASO).filter(pasa).sort(orden), marc = todas.filter(r => r.marcado), sin = todas.filter(r => !r.marcado), ej = ejemploActual();
|
const todas = filasPaso(PLAN_PASO).filter(pasa).sort(orden), marc = todas.filter(r => r.marcado), sin = todas.filter(r => !r.marcado), ej = ejemploActual();
|
||||||
const grupos = rs => { const g = {}; rs.forEach(r => (g[r.cubeta] = g[r.cubeta] || []).push(r)); return Object.keys(g).map(c => `<div class="grp">${esc(NOMBRE_CUBETA[c] || c)} · ${g[c].length}</div>` + g[c].map(r => filaHtml(r, ej)).join('')).join(''); };
|
const grupos = rs => { const g = {}; rs.forEach(r => (g[r.cubeta] = g[r.cubeta] || []).push(r)); return Object.keys(g).map(c => `<div class="grp">${esc(NOMBRE_CUBETA[c] || c)} · ${g[c].length}</div>` + g[c].map(r => filaHtml(r, ej)).join('')).join(''); };
|
||||||
const cos = sin.filter(r => r.cosmetico), dud = sin.filter(r => !r.cosmetico);
|
const nt = sin.filter(r => r.no_tocar), cos = sin.filter(r => r.cosmetico && !r.no_tocar), dud = sin.filter(r => !r.cosmetico && !r.no_tocar);
|
||||||
const explica = [cos.length ? `${cos.length === 1 ? 'uno solo cambia' : cos.length + ' solo cambian'} el nombre a la forma «NN- Título» (como «01 - Celular» → «01- Celular»): da igual hacerlo o no` : '', dud.length ? `${dud.length} ${dud.length === 1 ? 'es dudoso' : 'son dudosos'}: la app no está segura y no ${dud.length === 1 ? 'lo' : 'los'} mueve sin que lo marques tú` : ''].filter(Boolean).join('; ');
|
const explica = [nt.length ? `${nt.length === 1 ? 'uno está' : nt.length + ' están'} dentro de una carpeta que dice «No tocar»: no se mueve${nt.length === 1 ? '' : 'n'} hasta que ${nt.length === 1 ? 'lo' : 'los'} marques tú` : '', cos.length ? `${cos.length === 1 ? 'uno solo cambia' : cos.length + ' solo cambian'} el nombre a la forma «NN- Título» (como «01 - Celular» → «01- Celular»): da igual hacerlo o no` : '', dud.length ? `${dud.length} ${dud.length === 1 ? 'es dudoso' : 'son dudosos'}: la app no está segura y no ${dud.length === 1 ? 'lo' : 'los'} mueve sin que lo marques tú` : ''].filter(Boolean).join('; ');
|
||||||
caja.innerHTML = `<div class="lista ${PLAN_POR_QUE ? 'porque' : ''}"><div class="lcab"><b>${marc.length ? `Lo que va a hacer este paso · ${marc.length}` : f ? 'Nada marcado con ese filtro' : 'Este paso no tiene nada marcado'}</b><span class="sep"></span><label class="mut" style="font-size:12.5px"><input type="checkbox" id="chkPorQue" ${PLAN_POR_QUE ? 'checked' : ''}> ver por qué</label></div>${grupos(marc)}${sin.length ? `<div class="opc"><div class="lcab"><div><b>Sin marcar · ${sin.length}</b> <span class="mut">${explica}.</span></div><span class="sep"></span>${cos.length ? `<button class="lnk" id="btnMarcarCosm">marcar los cosméticos</button>` : ''}<button class="lnk" id="btnVerOpc">${PLAN_VER_OPC ? 'ocultar la lista' : 'ver la lista'}</button></div>${PLAN_VER_OPC ? grupos(sin) : ''}</div>` : ''}</div>`;
|
caja.innerHTML = `<div class="lista ${PLAN_POR_QUE ? 'porque' : ''}"><div class="lcab"><b>${marc.length ? `Lo que va a hacer este paso · ${marc.length}` : f ? 'Nada marcado con ese filtro' : 'Este paso no tiene nada marcado'}</b><span class="sep"></span><label class="mut" style="font-size:12.5px"><input type="checkbox" id="chkPorQue" ${PLAN_POR_QUE ? 'checked' : ''}> ver por qué</label></div>${grupos(marc)}${sin.length ? `<div class="opc"><div class="lcab"><div><b>Sin marcar · ${sin.length}</b> <span class="mut">${explica}.</span></div><span class="sep"></span>${cos.length ? `<button class="lnk" id="btnMarcarCosm">marcar los cosméticos</button>` : ''}${PLAN_PASO === 'banco' && sin.length > 1 ? `<button class="lnk" id="btnMarcarBanco">marcar los ${sin.length}</button>` : ''}<button class="lnk" id="btnVerOpc">${PLAN_VER_OPC ? 'ocultar la lista' : 'ver la lista'}</button></div>${PLAN_VER_OPC ? grupos(sin) : ''}</div>` : ''}</div>`;
|
||||||
$('#chkPorQue').onchange = e => { PLAN_POR_QUE = e.target.checked; caja.querySelector('.lista').classList.toggle('porque', PLAN_POR_QUE); };
|
$('#chkPorQue').onchange = e => { PLAN_POR_QUE = e.target.checked; caja.querySelector('.lista').classList.toggle('porque', PLAN_POR_QUE); };
|
||||||
const bo = $('#btnVerOpc'); if (bo) bo.onclick = () => { PLAN_VER_OPC = !PLAN_VER_OPC; pintarLista(); };
|
const bo = $('#btnVerOpc'); if (bo) bo.onclick = () => { PLAN_VER_OPC = !PLAN_VER_OPC; pintarLista(); };
|
||||||
const bc = $('#btnMarcarCosm'); if (bc) bc.onclick = () => { cos.forEach(r => r.marcado = true); pintarPlan(); };
|
const bc = $('#btnMarcarCosm'); if (bc) bc.onclick = () => { cos.forEach(r => r.marcado = true); pintarPlan(); };
|
||||||
|
const bb = $('#btnMarcarBanco'); if (bb) bb.onclick = () => { sin.forEach(r => r.marcado = true); pintarPlan(); };
|
||||||
// marcar o desmarcar una fila cambia las cifras, no la lista: así el foco se queda donde está
|
// marcar o desmarcar una fila cambia las cifras, no la lista: así el foco se queda donde está
|
||||||
$$('#planLista .fila').forEach(el => { const r = PLAN.filas.find(x => x.id === +el.dataset.id); el.querySelector('input[type=checkbox]').onchange = e => { r.marcado = e.target.checked; el.classList.toggle('sinmarcar', !r.marcado); pintarCabPlan(); estadoPlan(); }; });
|
$$('#planLista .fila').forEach(el => { const r = PLAN.filas.find(x => x.id === +el.dataset.id); el.querySelector('input[type=checkbox]').onchange = e => { r.marcado = e.target.checked; el.classList.toggle('sinmarcar', !r.marcado); pintarCabPlan(); estadoPlan(); }; });
|
||||||
$$('#planLista [data-ej]').forEach(b => b.onclick = () => { const i = ejemplosDe(PLAN_PASO).findIndex(x => x.id === +b.dataset.ej); if (i >= 0) { PLAN_EJ = i; pintarEjemplo(); pintarLista(); $('#ejCaja').scrollIntoView({behavior: 'smooth', block: 'nearest'}); } });
|
$$('#planLista [data-ej]').forEach(b => b.onclick = () => { const i = ejemplosDe(PLAN_PASO).findIndex(x => x.id === +b.dataset.ej); if (i >= 0) { PLAN_EJ = i; pintarEjemplo(); pintarLista(); $('#ejCaja').scrollIntoView({behavior: 'smooth', block: 'nearest'}); } });
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user