Las cuatro funciones que eligió Munka tras la 3.2.0: - Nueva pieza: crea «NN- Título» en su mes con el número que toca y sus subcarpetas; si la pieza ya existe, lo dice. Se deshace mientras siga vacía. - Bandeja de entrada: lo que se deje en _Entrada se reparte por mes, día de rodaje y cámara. «Traer material» copia una tarjeta comprobando cada archivo (se vuelve a leer y se compara la huella), conserva la fecha del rodaje y no toca el origen. - Versiones del mismo clip: dos clips de cámara que se llaman igual, duran lo mismo y tienen la misma forma son el mismo clip; se queda el de más imagen y el otro va a Duplicados. - Orden automático, apagado de fábrica: una tarea del Programador de tareas de Windows lanza «automatico», que lee todas las carpetas y hace solo lo seguro. No toca una carpeta que no se pueda leer entera ni una cuyo plan mueva más de 3.000 archivos, y si el programa ya está abierto se lo pide a él. Desinstalar quita la tarea. De paso: una ruta que empieza por guion bajo ya no pierde el guion al ir a la papelera. Pruebas: prueba_orden.py 24, probar.py 19, prueba_funcional.py 29, prueba_ventana.py 18 desde el código y 17 con el paquete. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
424 líneas
22 KiB
Python
424 líneas
22 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Asistente: al añadir una carpeta la escanea, da un diagnóstico corto por tipo de archivo y
|
|
pregunta, paso a paso, qué hacer con cada tipo. Sirve tanto a la consola (arrastrar la carpeta
|
|
sobre el .exe) como a la interfaz web (/api/diagnostico, /api/asistente).
|
|
|
|
`O` es el módulo organizador (lo fija él mismo al arrancar) para compartir CONFIG, base de datos,
|
|
Plan, aplicar, cola de conversión, etc. sin importarlo dos veces.
|
|
"""
|
|
import os
|
|
import sys
|
|
import time
|
|
import threading
|
|
|
|
O = None
|
|
|
|
# ---------------------------------------------------------------- diagnóstico
|
|
GRUPOS = [
|
|
# clave, título corto, qué es (una frase)
|
|
("camara_raw", "Clips BRAW (cámara URSA / Blackmagic)", "material original de cámara en 12 bits; lo que más pesa"),
|
|
("camara_video", "Clips de cámara MP4/MOV (Sony, Osmo, DJI, Atomos)", "material original de cámara, normalmente 10 bits"),
|
|
("audio", "Audio grabado (DJI Mic, Tascam) y música", "pistas de voz, ambiente y música"),
|
|
("celular", "Celular y WhatsApp", "videos y fotos del teléfono"),
|
|
("fotos", "Fotos de cámara", "JPG/RAW de cámara fotográfica"),
|
|
("stock_viejo", "Stock de Envato en proyectos viejos", "tomas descargadas de Envato de piezas ya entregadas"),
|
|
("stock_reciente", "Stock de Envato reciente", "tomas de Envato de piezas en curso"),
|
|
("proxies", "Proxies", "copias ligeras que Resolve/Premiere regeneran solas"),
|
|
("basura", "Basura de sistema y autoguardados", ".DS_Store, Thumbs.db, cachés, autosaves de After Effects"),
|
|
("exportados", "Exportados / masters", "videos finales renderizados"),
|
|
("video_otro", "Otros videos", "videos que no son de cámara ni exportados (descargas, referencias, IA)"),
|
|
("proyectos", "Proyectos de edición y diseño", ".drp, .prproj, .aep, .psd, .ai…"),
|
|
("imagenes", "Imágenes, diseño e IA", "PNG/JPG sueltos, capturas, imágenes generadas"),
|
|
("documentos", "Documentos", "guiones, PDF, hojas de cálculo"),
|
|
("otros", "Otros archivos", "todo lo demás"),
|
|
]
|
|
TITULO = {k: t for k, t, _ in GRUPOS}
|
|
QUE_ES = {k: q for k, _, q in GRUPOS}
|
|
|
|
|
|
def _grupo(f, limite_stock):
|
|
if f["es_basura"] or f["rol"] == "basura":
|
|
return "basura"
|
|
if f["es_proxy"] or f["rol"] == "proxy":
|
|
return "proxies"
|
|
if f["es_stock"] or f["rol"] == "stock":
|
|
return "stock_viejo" if f["mtime"] < limite_stock else "stock_reciente"
|
|
rol = f["rol"]
|
|
if rol == "camara_raw":
|
|
return "camara_raw"
|
|
if rol in ("camara_video", "camara_sidecar"):
|
|
return "camara_video"
|
|
if rol in ("audio", "audio_grabado"):
|
|
return "audio"
|
|
if rol in ("celular_video", "celular_foto"):
|
|
return "celular"
|
|
if rol == "foto_camara":
|
|
return "fotos"
|
|
if rol == "video":
|
|
return "exportados" if (f["contexto"] or "") == "EXPORTADOS" else "video_otro"
|
|
if rol in ("proyecto", "diseno", "fuente"):
|
|
return "proyectos"
|
|
if rol in ("imagen", "ia", "captura"):
|
|
return "imagenes"
|
|
if rol == "documento":
|
|
return "documentos"
|
|
return "otros"
|
|
|
|
|
|
def diagnostico(raiz):
|
|
"""Resumen corto y sencillo de lo que hay en la carpeta, por tipo de archivo."""
|
|
raiz = os.path.normpath(raiz)
|
|
con = O.db()
|
|
filas = con.execute("SELECT rol, bytes, mtime, es_basura, es_proxy, es_stock, contexto, fecha FROM archivos WHERE raiz = ?",
|
|
(raiz,)).fetchall()
|
|
esc = con.execute("SELECT * FROM escaneos WHERE raiz = ?", (raiz,)).fetchone()
|
|
con.close()
|
|
meses = int(O.CONFIG["optimizar"].get("meses_minimos", 3))
|
|
limite = time.time() - meses * 30.4 * 86400
|
|
acc = {k: {"clave": k, "titulo": TITULO[k], "que_es": QUE_ES[k], "n": 0, "bytes": 0, "desde": None, "hasta": None} for k, _, _ in GRUPOS}
|
|
total_b = 0
|
|
for f in filas:
|
|
g = acc[_grupo(f, limite)]
|
|
g["n"] += 1
|
|
g["bytes"] += f["bytes"] or 0
|
|
total_b += f["bytes"] or 0
|
|
fe = (f["fecha"] or "")[:7]
|
|
if fe:
|
|
g["desde"] = fe if not g["desde"] or fe < g["desde"] else g["desde"]
|
|
g["hasta"] = fe if not g["hasta"] or fe > g["hasta"] else g["hasta"]
|
|
grupos = [g for g in acc.values() if g["n"]]
|
|
grupos.sort(key=lambda g: -g["bytes"])
|
|
for g in grupos:
|
|
g["pct"] = round(g["bytes"] * 100 / total_b, 1) if total_b else 0
|
|
# frases de diagnóstico (lo importante, en una línea cada una)
|
|
frases = []
|
|
b = lambda k: acc[k]["bytes"] # noqa
|
|
if b("camara_raw") + b("camara_video"):
|
|
frases.append(f"El material de cámara ocupa {O._gb(b('camara_raw') + b('camara_video'))}"
|
|
f" ({round((b('camara_raw') + b('camara_video')) * 100 / total_b) if total_b else 0} % del total).")
|
|
if b("stock_viejo"):
|
|
frases.append(f"Hay {O._gb(b('stock_viejo'))} de stock de Envato en proyectos de hace más de {meses} meses: se puede borrar sin perder nada.")
|
|
if b("proxies"):
|
|
frases.append(f"Los proxies suman {O._gb(b('proxies'))}: se regeneran solos, no hace falta guardarlos.")
|
|
if acc["basura"]["n"]:
|
|
frases.append(f"{acc['basura']['n']:,} archivos de basura de sistema o autoguardado ({O._gb(b('basura'))}).")
|
|
dup = O.duplicados(raiz)
|
|
if dup["grupos"]:
|
|
frases.append(f"Hay {dup['n']} archivos repetidos ({O._gb(dup['bytes'])}) en {len(dup['grupos'])} grupos: se pueden juntar en subcarpetas Duplicados para revisarlos a mano.")
|
|
if not frases:
|
|
frases.append("Nada llamativo: la carpeta no tiene grandes bolsas de peso que recortar.")
|
|
return {"raiz": raiz, "n": len(filas), "bytes": total_b, "grupos": grupos, "frases": frases, "meses_stock": meses,
|
|
"duplicados": {"grupos": len(dup["grupos"]), "n": dup["n"], "bytes": dup["bytes"], "identicos": dup["identicos"], "versiones": dup["versiones"],
|
|
"por_comparar": dup.get("por_comparar", 0)},
|
|
"escaneo": dict(esc) if esc else None}
|
|
|
|
|
|
# ---------------------------------------------------------------- pasos del asistente
|
|
def pasos(diag, plan_filas):
|
|
"""Los pasos que se le preguntan al usuario, solo los que aplican a esta carpeta."""
|
|
g = {x["clave"]: x for x in diag["grupos"]}
|
|
por_cubeta = {}
|
|
for f in plan_filas:
|
|
c = por_cubeta.setdefault(f["cubeta"], {"n": 0, "bytes": 0, "filas": 0})
|
|
c["filas"] += 1
|
|
c["n"] += f["n"]
|
|
c["bytes"] += f["bytes"]
|
|
out = []
|
|
|
|
def paso(clave, titulo, resumen, opciones, defecto):
|
|
out.append({"clave": clave, "titulo": titulo, "resumen": resumen, "opciones": opciones, "defecto": defecto})
|
|
|
|
if "basura" in g or "proxies" in g:
|
|
partes = [f"{g[k]['n']:,} {TITULO[k].split(' ')[0].lower()} ({O._gb(g[k]['bytes'])})" for k in ("basura", "proxies") if k in g]
|
|
paso("limpieza", "Basura y proxies", " y ".join(partes),
|
|
[("papelera", "Apartarlos a _Para borrar (recomendado; nada se borra, se pueden recuperar)"), ("dejar", "Dejarlos donde están")], "papelera")
|
|
cam = [k for k in ("camara_raw", "camara_video", "audio", "celular", "fotos") if k in g]
|
|
if cam:
|
|
resumen = "; ".join(f"{TITULO[k]}: {g[k]['n']:,} archivos, {O._gb(g[k]['bytes'])}" for k in cam)
|
|
paso("material", "Material de cámara, audio, celular y fotos", resumen,
|
|
[("ordenar", "Ordenarlo por mes y por fuente en 00_Grabacion (recomendado)"), ("dejar", "No tocarlo")], "ordenar")
|
|
if "camara_raw" in g or "camara_video" in g:
|
|
h = O.herramientas()
|
|
aviso = ""
|
|
if "camara_raw" in g and not (h.get("resolve") or {}).get("ok"):
|
|
aviso = " Los BRAW necesitan DaVinci Resolve Studio abierto en un proyecto."
|
|
paso("convertir", "Clips de cámara de proyectos ya entregados",
|
|
f"Convertir a MP4 H.265 de 10 bits los clips con más de {diag['meses_stock']} meses: pesan ~10 veces menos y conservan margen de color.{aviso}",
|
|
[("no", "Por ahora no (se puede hacer luego desde la pestaña Optimizar)"),
|
|
("si", "Sí, convertir y reemplazar los originales (los originales quedan en _Para borrar/originales)")], "no")
|
|
dup = diag.get("duplicados") or {}
|
|
if dup.get("grupos") or dup.get("por_comparar"):
|
|
det = []
|
|
if dup.get("identicos"):
|
|
det.append(f"{dup['identicos']} idénticos (mismo nombre y peso)")
|
|
if dup.get("versiones"):
|
|
det.append(f"{dup['versiones']} en dos formatos (MP4 y MOV del mismo clip)")
|
|
if dup.get("por_comparar"):
|
|
det.append(f"{dup['por_comparar']} clips que se llaman igual y pesan distinto (se comparan: si duran lo mismo, se queda el de más calidad)")
|
|
paso("duplicados", "Material repetido",
|
|
f"{dup['n']} copias en {dup['grupos']} grupos ({O._gb(dup['bytes'])}): {' y '.join(det)}. No se borra nada: cada copia se junta en una subcarpeta Duplicados al lado del que se queda, para revisarla a mano.",
|
|
[("apartar", "Juntarlas en Duplicados junto al original (recomendado)"), ("dejar", "Dejarlas donde están")], "apartar")
|
|
if "stock_viejo" in g:
|
|
paso("stock", "Stock de Envato viejo",
|
|
f"{g['stock_viejo']['n']:,} tomas de Envato ({O._gb(g['stock_viejo']['bytes'])}) en proyectos de hace más de {diag['meses_stock']} meses",
|
|
[("apartar", "Apartarlas a _Para borrar/stock (recomendado; luego una persona las borra a mano)"), ("dejar", "Dejarlas")], "apartar")
|
|
carpetas = {c: v for c, v in por_cubeta.items() if c not in ("BASURA", "PROXY", "GRABACION", "CELULAR", "FOTOS")}
|
|
if carpetas:
|
|
resumen = ", ".join(f"{c.lower()} {v['filas']}" for c, v in sorted(carpetas.items(), key=lambda x: -x[1]["bytes"]))
|
|
paso("carpetas", "Carpetas: meses, piezas, marca, exportados, proyectos",
|
|
f"{sum(v['filas'] for v in carpetas.values())} movimientos propuestos ({resumen})",
|
|
[("seguras", "Aplicar las de confianza alta y media (recomendado)"), ("alta", "Solo las de confianza alta"),
|
|
("todas", "Todas, incluidas las dudosas y los renombrados cosméticos"), ("dejar", "Ninguna")], "seguras")
|
|
paso("respaldo", "Respaldo compacto en otro disco",
|
|
"Copia organizada por carpeta y tipo, con los clips ya convertidos en vez de los originales",
|
|
[("no", "Ahora no"), ("si", f"Sí, a {O.CONFIG.get('respaldo', {}).get('destino', 'T:\\Archivo Labstream')}")], "no")
|
|
return out
|
|
|
|
|
|
def filas_elegidas(plan_filas, dec):
|
|
"""Traduce las decisiones a las filas del plan que se aplican."""
|
|
out = []
|
|
for f in plan_filas:
|
|
c = f["cubeta"]
|
|
if c in ("BASURA", "PROXY"):
|
|
if dec.get("limpieza", "papelera") == "papelera":
|
|
out.append(f)
|
|
elif c in ("GRABACION", "CELULAR", "FOTOS"):
|
|
# solo lo marcado: antes entraba todo el material aunque el plan lo diera por dudoso (el mes
|
|
# sacado de la fecha de modificación de un archivo que no es de cámara), y «hacer lo seguro»
|
|
# movía justo lo que el plan decía que había que mirar
|
|
if dec.get("material", "ordenar") == "ordenar" and f["marcado"]:
|
|
out.append(f)
|
|
else:
|
|
m = dec.get("carpetas", "seguras")
|
|
if m == "todas" or (m == "seguras" and f["marcado"]) or (m == "alta" and f["confianza"] == "alta"):
|
|
out.append(f)
|
|
return out
|
|
|
|
|
|
# ---------------------------------------------------------------- ejecución con progreso
|
|
PLAN_CACHE = {}
|
|
ESTADO = {"activo": False, "raiz": "", "paso": "", "pct": 0, "texto": "", "hechas": 0, "total": 0, "resultado": None, "error": ""}
|
|
|
|
|
|
def _prog(paso, hechas, total, texto=""):
|
|
ESTADO.update(paso=paso, hechas=hechas, total=total, pct=int(hechas * 100 / total) if total else 0, texto=texto)
|
|
|
|
|
|
def ejecutar(raiz, dec, plan_filas=None, en_hilo=False):
|
|
"""Aplica las decisiones. Actualiza ESTADO para las barras de progreso."""
|
|
if en_hilo:
|
|
threading.Thread(target=ejecutar, args=(raiz, dec, plan_filas, False), daemon=True).start()
|
|
return
|
|
raiz = os.path.normpath(raiz)
|
|
r = O.raiz_cfg(raiz)
|
|
ESTADO.update(activo=True, raiz=raiz, resultado=None, error="")
|
|
res = {"lotes": [], "movimientos": 0, "errores": [], "stock": 0, "convertidos": 0, "respaldo": None}
|
|
try:
|
|
if plan_filas is None:
|
|
_prog("plan", 0, 0, "pensando el plan…")
|
|
plan_filas = O.Plan(r).correr()["filas"]
|
|
if dec.get("stock") == "apartar":
|
|
# primero el stock viejo, para que en _Para borrar/stock quede con su ruta original
|
|
_prog("stock", 0, 0, "apartando stock de Envato viejo")
|
|
s = O.stock_viejo(raiz, O.CONFIG["optimizar"]["meses_minimos"], ["Video", "Audio", "Imagenes"])
|
|
rutas = [x for g in s["grupos"] for x in g["rutas"]]
|
|
if rutas:
|
|
st = O.stock_a_papelera(raiz, rutas)
|
|
res["lotes"].append(st.get("lote"))
|
|
res["stock"] = st.get("movimientos", 0)
|
|
# las filas de stock cuyo material acaba de irse entero ya no tienen qué mover; las demás
|
|
# siguen valiendo (antes se quitaban todas, y una carpeta «Recursos» que no era de Envato
|
|
# se quedaba sin ordenar hasta la siguiente pasada)
|
|
plan_filas = [f for f in plan_filas if f["cubeta"] != "STOCK" or os.path.exists(f["origen"])]
|
|
elegidas = filas_elegidas(plan_filas, dec)
|
|
if elegidas:
|
|
_prog("ordenar", 0, len(elegidas), "moviendo carpetas y archivos")
|
|
hilo = threading.Thread(target=lambda: res.update(_ap=O.aplicar(raiz, elegidas)), daemon=True)
|
|
hilo.start()
|
|
while hilo.is_alive():
|
|
a = O.APLICANDO
|
|
_prog("ordenar", a["hechas"], a["total"] or len(elegidas), a["actual"])
|
|
time.sleep(0.3)
|
|
ap = res.pop("_ap", None) or {}
|
|
res["lotes"].append(ap.get("lote"))
|
|
res["movimientos"] += ap.get("movimientos", 0)
|
|
res["errores"] += ap.get("errores", [])
|
|
if dec.get("duplicados") == "apartar":
|
|
_prog("duplicados", 0, 0, "juntando los duplicados en subcarpetas Duplicados")
|
|
dd = O.apartar_duplicados(raiz)
|
|
res["lotes"].append(dd.get("lote"))
|
|
res["duplicados"] = dd.get("movimientos", 0)
|
|
res["movimientos"] += dd.get("movimientos", 0)
|
|
res["errores"] += dd.get("errores", [])
|
|
if dec.get("convertir") == "si":
|
|
o = O.CONFIG["optimizar"]
|
|
_prog("analizar", 0, 0, "leyendo los clips con ffprobe")
|
|
hilo = threading.Thread(target=O.analizar, args=(raiz, o["min_mb"]), daemon=True)
|
|
hilo.start()
|
|
while hilo.is_alive():
|
|
a = O.ANALISIS.get(raiz) or {}
|
|
_prog("analizar", a.get("hechos", 0), a.get("total", 0), "leyendo los clips con ffprobe")
|
|
time.sleep(0.5)
|
|
cands = [c for c in O.candidatos(raiz, o["meses_minimos"], o["perfil"], o["min_mb"]) if not c["excluido"] and not c["en_cola"]]
|
|
ids = O.encolar([c["ruta"] for c in cands], o["perfil"], o["chroma"], o["color"], raiz)
|
|
if ids:
|
|
O.iniciar_cola()
|
|
total = len(ids)
|
|
while O.COLA["activa"]:
|
|
con = O.db()
|
|
hechos = con.execute("SELECT COUNT(*) FROM trabajos WHERE id IN (%s) AND estado NOT IN ('pendiente','corriendo')" % ",".join(map(str, ids))).fetchone()[0]
|
|
act = con.execute("SELECT ruta, pct FROM trabajos WHERE estado = 'corriendo' LIMIT 1").fetchone()
|
|
con.close()
|
|
_prog("convertir", hechos, total, f"{os.path.basename(act['ruta'])} {act['pct'] or 0}%" if act else O.COLA["mensaje"])
|
|
time.sleep(1)
|
|
vf = O.verificar(ids)
|
|
ok = [v["id"] for v in vf if v.get("ok")]
|
|
if ok:
|
|
O.reemplazar(ok)
|
|
res["convertidos"] = len(ok)
|
|
res["conversion_fallida"] = len(ids) - len(ok)
|
|
if dec.get("respaldo") == "si":
|
|
destino = O.CONFIG.get("respaldo", {}).get("destino") or "T:\\Archivo Labstream"
|
|
plan = O.plan_respaldo(raiz, destino, True, False, True)
|
|
_prog("respaldo", 0, plan["n"], f"copiando a {plan['base']}")
|
|
hilo = threading.Thread(target=O.copiar_respaldo, args=(raiz, plan), daemon=True)
|
|
hilo.start()
|
|
while hilo.is_alive():
|
|
rb = O.RESPALDO
|
|
_prog("respaldo", rb.get("hechos", 0), rb.get("total", plan["n"]), rb.get("actual", ""))
|
|
time.sleep(0.5)
|
|
res["respaldo"] = {"base": plan["base"], "copiados": O.RESPALDO.get("copiados", 0), "saltados": O.RESPALDO.get("saltados", 0)}
|
|
res["lotes"] = [x for x in res["lotes"] if x]
|
|
ESTADO["resultado"] = res
|
|
_prog("fin", 1, 1, "listo")
|
|
except Exception as ex: # noqa
|
|
ESTADO["error"] = str(ex)
|
|
ESTADO["resultado"] = res
|
|
finally:
|
|
ESTADO["activo"] = False
|
|
return res
|
|
|
|
|
|
# ---------------------------------------------------------------- consola
|
|
ANCHO = 34
|
|
|
|
|
|
def barra(hechas, total, texto="", final=False):
|
|
if total:
|
|
pct = min(100, int(hechas * 100 / total))
|
|
llenos = int(ANCHO * pct / 100)
|
|
b = "█" * llenos + "░" * (ANCHO - llenos)
|
|
linea = f"\r [{b}] {pct:3d}% {hechas:,}/{total:,} {texto[-40:]:<40}"
|
|
else:
|
|
giro = "|/-\\"[int(time.time() * 4) % 4]
|
|
linea = f"\r [{giro}] {hechas:,} {texto[-52:]:<52}"
|
|
sys.stdout.write(linea[:120])
|
|
if final:
|
|
sys.stdout.write("\n")
|
|
sys.stdout.flush()
|
|
|
|
|
|
def escanear_con_barra(raiz):
|
|
raiz = os.path.normpath(raiz)
|
|
con = O.db()
|
|
prev = con.execute("SELECT n FROM escaneos WHERE raiz = ?", (raiz,)).fetchone()
|
|
con.close()
|
|
estimado = prev["n"] if prev else 0
|
|
print(f"\nEscaneando {raiz}")
|
|
hilo = threading.Thread(target=O.escanear, args=(raiz,), daemon=True)
|
|
hilo.start()
|
|
while hilo.is_alive():
|
|
p = O.PROGRESO.get(raiz) or {}
|
|
barra(p.get("n", 0), estimado if estimado and p.get("n", 0) <= estimado else 0, os.path.basename(p.get("actual") or ""))
|
|
time.sleep(0.25)
|
|
p = O.PROGRESO[raiz]
|
|
if p["estado"] != "listo":
|
|
print("\nERROR al escanear:", p.get("error"))
|
|
return False
|
|
barra(p["n"], p["n"], f"{O._gb(p['bytes'])} en {p.get('fin', time.time()) - p['inicio']:.0f} s", final=True)
|
|
return True
|
|
|
|
|
|
def _preguntar(paso, auto):
|
|
print(f"\n{paso['titulo']}\n {paso['resumen']}")
|
|
for i, (clave, texto) in enumerate(paso["opciones"], 1):
|
|
marca = " (Enter)" if clave == paso["defecto"] else ""
|
|
print(f" {i}. {texto}{marca}")
|
|
if auto:
|
|
return paso["defecto"]
|
|
while True:
|
|
try:
|
|
t = input(" Elige: ").strip()
|
|
except EOFError:
|
|
return paso["defecto"]
|
|
if not t:
|
|
return paso["defecto"]
|
|
if t.isdigit() and 1 <= int(t) <= len(paso["opciones"]):
|
|
return paso["opciones"][int(t) - 1][0]
|
|
for clave, _ in paso["opciones"]:
|
|
if t.lower() == clave:
|
|
return clave
|
|
print(" Escribe el número de la opción.")
|
|
|
|
|
|
def cli(ruta, args):
|
|
r = O.cli_raiz(ruta, args)
|
|
if not r:
|
|
return 1
|
|
raiz = r["ruta"]
|
|
if not escanear_con_barra(raiz):
|
|
return 1
|
|
d = diagnostico(raiz)
|
|
print(f"\nDIAGNÓSTICO · {r['cliente']} · {d['n']:,} archivos · {O._gb(d['bytes'])}")
|
|
for g in d["grupos"]:
|
|
rango = f"{g['desde']} → {g['hasta']}" if g["desde"] and g["desde"] != g["hasta"] else (g["desde"] or "")
|
|
print(f" {O._gb(g['bytes']):>9} {g['pct']:5.1f} % {g['n']:>7,} {g['titulo']} {rango}")
|
|
for fr in d["frases"]:
|
|
print(" •", fr)
|
|
print("\nPensando el plan…", end="", flush=True)
|
|
plan_filas = O.Plan(r).correr()["filas"]
|
|
print(f" {len(plan_filas)} movimientos posibles.")
|
|
auto = "--si" in args
|
|
dec = {}
|
|
for paso in pasos(d, plan_filas):
|
|
dec[paso["clave"]] = _preguntar(paso, auto)
|
|
elegidas = filas_elegidas(plan_filas, dec)
|
|
print(f"\nRESUMEN: se moverán {len(elegidas)} carpetas/grupos ({sum(f['n'] for f in elegidas):,} archivos, {O._gb(sum(f['bytes'] for f in elegidas))})"
|
|
+ (", se apartará el stock viejo" if dec.get("stock") == "apartar" else "")
|
|
+ (", se convertirán los clips viejos a 10 bits" if dec.get("convertir") == "si" else "")
|
|
+ (", y se hará el respaldo" if dec.get("respaldo") == "si" else "") + ".")
|
|
if "--simular" in args:
|
|
print("Simulación: no se movió nada.")
|
|
return 0
|
|
if not O._confirmar("¿Empezamos? (nada se borra; lo sobrante va a _Para borrar y todo se puede deshacer)", auto):
|
|
print("Cancelado. No se movió nada.")
|
|
return 0
|
|
hilo = threading.Thread(target=ejecutar, args=(raiz, dec, plan_filas), daemon=True)
|
|
hilo.start()
|
|
paso_ant = None
|
|
while hilo.is_alive():
|
|
e = ESTADO
|
|
if e["paso"] != paso_ant:
|
|
if paso_ant:
|
|
barra(e["total"] or 1, e["total"] or 1, "", final=True)
|
|
paso_ant = e["paso"]
|
|
print({"plan": "Plan", "ordenar": "Ordenando", "stock": "Stock viejo", "analizar": "Analizando clips", "convertir": "Convirtiendo a 10 bits",
|
|
"respaldo": "Respaldo", "fin": "Fin"}.get(e["paso"], e["paso"]))
|
|
barra(e["hechas"], e["total"], e["texto"])
|
|
time.sleep(0.3)
|
|
barra(1, 1, "", final=True)
|
|
res = ESTADO["resultado"] or {}
|
|
if ESTADO["error"]:
|
|
print("ERROR:", ESTADO["error"])
|
|
print(f"\nHecho: {res.get('movimientos', 0)} movimientos"
|
|
+ (f", {res['stock']} tomas de stock apartadas" if res.get("stock") else "")
|
|
+ (f", {res['convertidos']} clips convertidos" if res.get("convertidos") else "")
|
|
+ (f", respaldo en {res['respaldo']['base']} ({res['respaldo']['copiados']} copiados)" if res.get("respaldo") else "") + ".")
|
|
for e in res.get("errores", [])[:20]:
|
|
print(" ERROR:", e)
|
|
for lote in res.get("lotes", []):
|
|
print(f"Para deshacer: OrganizadorLabstream.exe deshacer {lote}")
|
|
return 0 if not res.get("errores") else 2
|