2.0.0: una sola lista de acciones por carpeta
La pantalla «Carpeta» pasa a ser la lista de todo lo que se puede hacer ahora mismo, en orden, con la cifra real de cada cosa y su botón. Era la simplificación de fondo de la auditoría: antes había que bajar tres niveles de navegación para saber qué faltaba en una carpeta. - acciones() y GET /api/carpeta/acciones: todas las cifras salen de consultas baratas (0,04-0,23 s en las carpetas reales); el plan solo se usa si ya estaba calculado. - Se conecta la detección de repetidos, que funcionaba pero no tenía botón. - _gb da terabytes; el peso de «por nombrar» deja de contarse dos veces al anidarse (salían totales mayores que el propio disco). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Este commit está contenido en:
155
organizador.py
155
organizador.py
@@ -34,7 +34,7 @@ import actualizador
|
||||
asistente.O = sys.modules[__name__]
|
||||
actualizador.O = sys.modules[__name__]
|
||||
|
||||
VERSION = "1.9.0"
|
||||
VERSION = "2.0.0"
|
||||
|
||||
CONGELADO = getattr(sys, "frozen", False)
|
||||
SIN_VENTANA = getattr(subprocess, "CREATE_NO_WINDOW", 0) # ffmpeg/ffprobe/net sin abrir consolas negras # empaquetado como .exe con PyInstaller
|
||||
@@ -2699,6 +2699,152 @@ def estado_general():
|
||||
"cubetas": R.CUBETAS, "tipos": R.TIPOS_CLIENTE, "perfiles": PERFILES, "formatos": {k: {"nombre": v["nombre"], "que": v["que"], "ext": v["ext"]} for k, v in FORMATOS.items()}, "plantillas": PLANTILLAS}
|
||||
|
||||
|
||||
def _plural(n, uno, varios):
|
||||
return f"{n:,}".replace(",", ".") + " " + (uno if n == 1 else varios)
|
||||
|
||||
|
||||
def acciones(raiz):
|
||||
"""Todo lo que se puede hacer AHORA MISMO en esta carpeta, en orden, cada cosa con su cifra.
|
||||
|
||||
Es la vista principal: en lugar de repartir el trabajo entre pestañas dentro de pestañas, una sola
|
||||
lista dice qué falta y cuánto pesa. Todo sale de consultas baratas sobre el inventario; lo único
|
||||
que puede costar es el plan de orden, que solo se usa si ya estaba calculado."""
|
||||
raiz = _clave(raiz)
|
||||
r = raiz_cfg(raiz)
|
||||
if not r:
|
||||
return {"error": "carpeta no registrada"}
|
||||
con = db()
|
||||
esc = con.execute("SELECT * FROM escaneos WHERE raiz = ?", (raiz,)).fetchone()
|
||||
out = []
|
||||
|
||||
def añadir(**kw):
|
||||
kw.setdefault("estado", "toca") # toca | hecho | espera
|
||||
out.append(kw)
|
||||
|
||||
if not esc:
|
||||
con.close()
|
||||
añadir(clave="escanear", titulo="Escanear la carpeta", estado="toca", boton="Escanear",
|
||||
que="Lee nombres, tamaños y fechas de todos los archivos. No mueve nada.",
|
||||
cifra="", detalle="En una carpeta grande del NAS tarda unos minutos.")
|
||||
return {"raiz": raiz, "cliente": r["cliente"], "acciones": out, "escaneo": None}
|
||||
|
||||
en_papelera = " AND " + " AND ".join(
|
||||
["ruta NOT LIKE ? ESCAPE '!'"] * len(R.PAPELERAS))
|
||||
args_pap = [esc_like(os.path.join(raiz, pp)) + "%" for pp in R.PAPELERAS]
|
||||
|
||||
# --- 1. basura y proxies que siguen fuera de la papelera ---
|
||||
fila = con.execute("SELECT COUNT(*) n, COALESCE(SUM(bytes),0) b FROM archivos WHERE raiz = ? "
|
||||
"AND (es_basura = 1 OR es_proxy = 1)" + en_papelera, [raiz] + args_pap).fetchone()
|
||||
if fila["n"]:
|
||||
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.",
|
||||
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.",
|
||||
ir="plan", paso="limpiar")
|
||||
else:
|
||||
añadir(clave="limpiar", titulo="Basura y proxies", estado="hecho",
|
||||
que="No queda nada suelto que apartar.", cifra="", detalle="")
|
||||
|
||||
# --- 2. carpetas que la app no reconoce ---
|
||||
voc = vocabulario(raiz)
|
||||
sin = [t for t in voc if not t.get("cubeta") and not t.get("dentro_pieza")]
|
||||
seguras = [t for t in sin if t.get("sug_conf") == "alta"]
|
||||
if sin:
|
||||
# sin sumar bytes: una carpeta sin reconocer dentro de otra sin reconocer contaría dos veces
|
||||
# el mismo material, y salían totales mayores que el propio disco
|
||||
con_sug = [t for t in sin if t.get("sugerencia")]
|
||||
añadir(clave="nombrar", titulo="Decir qué son " + _plural(len(sin), "carpeta", "carpetas"), boton="Nombrarlas" if len(sin) > 1 else "Nombrarla",
|
||||
que="Sin esto la app no las mueve: se quedan donde están.",
|
||||
cifra=_plural(len(sin), "carpeta", "carpetas") + (f" · {len(seguras)} de un clic" if seguras else ""),
|
||||
detalle=(f"La app propone qué son {len(con_sug)} de ellas mirando lo que tienen dentro"
|
||||
+ (f", y {len(seguras)} con bastante seguridad como para aceptarlas todas juntas." if seguras else ".")
|
||||
if con_sug else "Puedes dejarlas sin nombrar: simplemente no se moverán."),
|
||||
ir="nombrar")
|
||||
else:
|
||||
añadir(clave="nombrar", titulo="Carpetas reconocidas", estado="hecho",
|
||||
que="La app sabe qué es cada carpeta.", cifra="", detalle="")
|
||||
|
||||
# --- 3 y 4. ordenar (usa el plan solo si ya estaba calculado) ---
|
||||
plan = PLANES.get(raiz)
|
||||
for clave, titulo, que in (("fechas", "Ordenar por año y mes",
|
||||
"Meses con nombre completo, grabaciones repartidas por mes y cámara, piezas numeradas."),
|
||||
("resto", "Ordenar lo demás",
|
||||
"Exportados, proyectos, música y marca a su carpeta.")):
|
||||
if plan:
|
||||
fs = [f for f in plan["filas"] if f.get("paso") == clave]
|
||||
marc = [f for f in fs if f.get("marcado")]
|
||||
if fs:
|
||||
añadir(clave=clave, titulo=titulo, boton="Ver y aplicar", que=que,
|
||||
cifra=f"{_plural(len(fs), 'cambio', 'cambios')} · {_gb(sum(f['bytes'] for f in fs))}",
|
||||
detalle=(f"{len(marc)} están marcados; el resto son dudosos y no se mueven salvo que los marques."
|
||||
if len(marc) < len(fs) else "Todo se mueve dentro de la misma carpeta."),
|
||||
ir="plan", paso=clave)
|
||||
else:
|
||||
añadir(clave=clave, titulo=titulo, estado="hecho", que="Nada que mover.", cifra="", detalle="")
|
||||
else:
|
||||
añadir(clave=clave, titulo=titulo, boton="Calcular", que=que, cifra="",
|
||||
detalle="Hay que calcular el plan para saber cuánto es.", ir="plan", paso=clave)
|
||||
|
||||
# --- 5. stock de Envato viejo ---
|
||||
tope = limite_meses(3)
|
||||
viejo = [f for f in con.execute(
|
||||
"SELECT bytes, fecha, anio_ruta, mes_ruta FROM archivos WHERE raiz = ? AND es_stock = 1 AND es_basura = 0"
|
||||
+ en_papelera, [raiz] + args_pap).fetchall() if es_viejo(f, tope)]
|
||||
if viejo:
|
||||
añadir(clave="stock", titulo="Apartar stock de Envato viejo", boton="Ver y apartar",
|
||||
que="De proyectos ya publicados: se vuelve a descargar con la suscripción.",
|
||||
cifra=f"{_plural(len(viejo), 'archivo', 'archivos')} · {_gb(sum(f['bytes'] for f in viejo))}",
|
||||
detalle="Son los de proyectos de más de 3 meses. Puedes ajustar el corte.",
|
||||
ir="stock")
|
||||
|
||||
# --- 6. material repetido ---
|
||||
rep_fila = con.execute(
|
||||
"SELECT COALESCE(SUM(n - 1), 0) n, COALESCE(SUM((n - 1) * bytes), 0) b FROM "
|
||||
"(SELECT nombre, bytes, COUNT(*) n FROM archivos WHERE raiz = ? AND es_basura = 0 AND es_proxy = 0 "
|
||||
"AND bytes > 5000000" + en_papelera + " GROUP BY nombre, bytes HAVING COUNT(*) > 1)",
|
||||
[raiz] + args_pap).fetchone()
|
||||
if rep_fila["n"]:
|
||||
añadir(clave="repetidos", titulo="Juntar material repetido", boton="Ver repetidos",
|
||||
que="Mismo nombre y mismo peso en varias carpetas.",
|
||||
cifra=f"{_plural(rep_fila['n'], 'copia de más', 'copias de más')} · {_gb(rep_fila['b'])}",
|
||||
detalle="Se comprueba el contenido antes de tocar nada; la copia buena se queda donde está.",
|
||||
ir="repetidos")
|
||||
|
||||
# --- 7. convertir clips viejos ---
|
||||
o = CONFIG["optimizar"]
|
||||
cam = con.execute(
|
||||
"SELECT COUNT(*) n, COALESCE(SUM(bytes),0) b, SUM(analizado = 1) ana FROM archivos WHERE raiz = ? "
|
||||
"AND rol IN ('camara_raw','camara_video') AND es_proxy = 0 AND es_basura = 0 AND bytes >= ?" + en_papelera,
|
||||
[raiz, int(o.get("min_mb", 300)) * 1024 * 1024] + args_pap).fetchone()
|
||||
hechos = con.execute("SELECT COUNT(*) n FROM trabajos WHERE lower(raiz) = ? AND estado = 'reemplazado'",
|
||||
(raiz.lower(),)).fetchone()["n"]
|
||||
if cam["n"]:
|
||||
if not (cam["ana"] or 0):
|
||||
añadir(clave="convertir", titulo="Aligerar los clips de cámara", boton="Leer los clips",
|
||||
que="Pasan a un formato unas 10 veces más ligero. El original no se toca hasta que reemplaces.",
|
||||
cifra=f"{_plural(cam['n'], 'clip', 'clips')} · {_gb(cam['b'])}",
|
||||
detalle="Primero hay que leerlos para saber cuáles conviene convertir. Va en segundo plano.",
|
||||
ir="convertir")
|
||||
else:
|
||||
añadir(clave="convertir", titulo="Aligerar los clips de cámara", boton="Ver clips",
|
||||
que="Pasan a un formato unas 10 veces más ligero. El original no se toca hasta que reemplaces.",
|
||||
cifra=f"{_plural(cam['n'], 'clip', 'clips')} · {_gb(cam['b'])}",
|
||||
detalle=f"{cam['ana']} ya están leídos" + (f" y {hechos} reemplazados." if hechos else "."),
|
||||
ir="convertir")
|
||||
|
||||
# --- 8. respaldo, al final ---
|
||||
dest = CONFIG.get("respaldo", {}).get("destino", "")
|
||||
añadir(clave="respaldar", titulo="Copiar el respaldo a otro disco",
|
||||
boton="Preparar respaldo", que="Una copia compacta con los clips ya convertidos, y un índice.",
|
||||
cifra="", estado="espera" if any(a["estado"] == "toca" for a in out) else "toca",
|
||||
detalle=(f"Destino: {dest}" if dest else "Falta indicar el disco de destino.") +
|
||||
" Conviene hacerlo cuando lo demás esté hecho.",
|
||||
ir="respaldar")
|
||||
|
||||
con.close()
|
||||
return {"raiz": raiz, "cliente": r["cliente"], "acciones": out, "escaneo": dict(esc)}
|
||||
|
||||
|
||||
def estado_carpeta(ruta):
|
||||
"""El recorrido de una carpeta: qué paso está hecho, cuál toca y qué lo bloquea."""
|
||||
raiz = _clave(ruta)
|
||||
@@ -2815,6 +2961,8 @@ class Manejador(BaseHTTPRequestHandler):
|
||||
if ruta == "/api/soltado":
|
||||
r = soltado(q.get("nombre", ""))
|
||||
return self._json({"ruta": r, "es_archivo": bool(r) and os.path.isfile(r)})
|
||||
if ruta == "/api/carpeta/acciones":
|
||||
return self._json(acciones(q["raiz"]))
|
||||
if ruta == "/api/carpeta/estado":
|
||||
return self._json(estado_carpeta(q["raiz"]))
|
||||
if ruta == "/api/mapa":
|
||||
@@ -3104,7 +3252,10 @@ La app NUNCA borra: lo que sobra va a _Para borrar dentro de la misma carpeta (l
|
||||
|
||||
|
||||
def _gb(b):
|
||||
return f"{(b or 0) / 1e9:.1f} GB" if (b or 0) >= 1e8 else f"{(b or 0) / 1e6:.0f} MB"
|
||||
b = b or 0
|
||||
if b >= 1e12:
|
||||
return f"{b / 1e12:.2f} TB"
|
||||
return f"{b / 1e9:.1f} GB" if b >= 1e8 else f"{b / 1e6:.0f} MB"
|
||||
|
||||
|
||||
def _opcion(args, nombre, defecto=None):
|
||||
|
||||
Referencia en una nueva incidencia
Block a user