Plan por pasos en frases, sugerencias en Por nombrar, stock por antigüedad, respaldo por lotes, arrastre con ruta real, barra de tareas
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
123
organizador.py
123
organizador.py
@@ -525,6 +525,10 @@ class Plan:
|
||||
"archivos": [a[0] for a in archivos] if archivos else None,
|
||||
"marcado": (confianza in ("alta", "media")) if marcado is None else marcado,
|
||||
"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,
|
||||
# piezas), 3 el resto (exportados, proyectos, marca, música, dudosos)
|
||||
"paso": "limpiar" if accion == "papelera" or cubeta in ("BASURA", "PROXY") else
|
||||
"fechas" if cubeta in ("MES", "ANIO", "GRABACION", "PIEZA", "CELULAR") else "resto",
|
||||
})
|
||||
|
||||
def suelto(self, fila, cubeta, destino_rel, motivo, confianza):
|
||||
@@ -1081,10 +1085,65 @@ def vocabulario(raiz):
|
||||
t["bytes"] += c["bytes"] or 0
|
||||
if len(t["rutas"]) < 4:
|
||||
t["rutas"].append(rel)
|
||||
if tokens:
|
||||
con = db()
|
||||
for t in tokens.values():
|
||||
if not t["cubeta"]:
|
||||
_sugerir(con, raiz, t)
|
||||
con.close()
|
||||
out = sorted(tokens.values(), key=lambda t: (-t["bytes"], t["token"]))
|
||||
return out
|
||||
|
||||
|
||||
_ROL_FRASE = {"camara": "clips de cámara", "proyecto": "proyectos de edición / diseño", "audio": "audio",
|
||||
"imagen": "imágenes", "documento": "documentos", "video": "videos", "otro": "otros archivos"}
|
||||
|
||||
|
||||
def _sugerir(con, raiz, t):
|
||||
"""Qué es probablemente esta carpeta, mirando lo que tiene dentro y a qué se parece su nombre.
|
||||
Deja t["sugerencia"], t["sug_conf"], t["por_que"] y t["contenido"] (resumen legible)."""
|
||||
import difflib
|
||||
conteo = defaultdict(lambda: [0, 0])
|
||||
total = [0, 0]
|
||||
for rel in t["rutas"][:3]:
|
||||
for f in archivos_bajo(con, os.path.join(raiz, rel)):
|
||||
if f["es_basura"] or f["es_proxy"]:
|
||||
continue
|
||||
rol = f["rol"]
|
||||
k = ("camara" if rol in ("camara_raw", "camara_video", "camara_sidecar", "audio_grabado") else
|
||||
"proyecto" if rol in ("proyecto", "diseno") else
|
||||
"audio" if rol in ("audio",) else
|
||||
"imagen" if rol in ("imagen", "foto_camara", "celular_foto") else
|
||||
"documento" if rol == "documento" else
|
||||
"video" if rol in ("video", "celular_video") else "otro")
|
||||
conteo[k][0] += 1
|
||||
conteo[k][1] += f["bytes"]
|
||||
total[0] += 1
|
||||
total[1] += f["bytes"]
|
||||
orden = sorted(conteo.items(), key=lambda kv: -kv[1][1])
|
||||
t["contenido"] = " · ".join(f"{n} {_ROL_FRASE[k]}" for k, (n, b) in orden[:3]) if orden else "vacía o solo subcarpetas"
|
||||
sug, conf, por_que = "", "", ""
|
||||
# a) por el nombre: se parece a una palabra del diccionario
|
||||
dicc = DICC["carpetas"]
|
||||
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"):
|
||||
sug, conf, por_que = dicc[cerca[0]], "media", f"el nombre se parece a «{cerca[0]}»"
|
||||
# b) por el contenido (manda si es contundente)
|
||||
if total[0] >= 3 and orden:
|
||||
k, (n, b) = orden[0]
|
||||
frac = b / total[1] if total[1] else n / total[0]
|
||||
if frac >= 0.7:
|
||||
por_cont = {"camara": "GRABACION", "proyecto": "PROYECTOS", "audio": "MUSICA", "imagen": "FOTOS",
|
||||
"documento": "DOCUMENTOS", "video": "EXPORTADOS"}.get(k)
|
||||
if por_cont:
|
||||
if sug and sug != por_cont:
|
||||
por_que += f"; pero dentro casi todo son {_ROL_FRASE[k]}"
|
||||
conf = "baja"
|
||||
else:
|
||||
sug, conf, por_que = por_cont, ("alta" if frac >= 0.9 and n >= 3 else "media"), f"dentro casi todo son {_ROL_FRASE[k]} ({round(frac * 100)} %)"
|
||||
t["sugerencia"], t["sug_conf"], t["por_que"] = sug, conf, por_que
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# Mapa
|
||||
# ==========================================================================
|
||||
@@ -1359,7 +1418,7 @@ def encolar(rutas, perfil, chroma, color, raiz):
|
||||
return ids
|
||||
|
||||
|
||||
COLA = {"activa": False, "actual": None, "cancelar": False, "hilo": None, "mensaje": ""}
|
||||
COLA = {"activa": False, "actual": None, "cancelar": False, "hilo": None, "mensaje": "", "hechos": 0, "inicio": 0}
|
||||
|
||||
|
||||
def set_trabajo(id_, **kw):
|
||||
@@ -1537,6 +1596,7 @@ def bucle_cola():
|
||||
except Exception as ex: # noqa
|
||||
set_trabajo(t["id"], estado="error", log=f"fallo inesperado: {ex}")
|
||||
_verificar_listos([t["id"]])
|
||||
COLA["hechos"] += 1
|
||||
elif lote_resolve:
|
||||
COLA["actual"] = lote_resolve[0]["id"]
|
||||
try:
|
||||
@@ -1545,6 +1605,7 @@ def bucle_cola():
|
||||
for r in lote_resolve:
|
||||
set_trabajo(r["id"], estado="error", log=f"fallo inesperado del puente: {ex}")
|
||||
_verificar_listos([r["id"] for r in lote_resolve])
|
||||
COLA["hechos"] += len(lote_resolve)
|
||||
else:
|
||||
break
|
||||
COLA["actual"] = None
|
||||
@@ -1590,6 +1651,8 @@ def iniciar_cola():
|
||||
recuperar_huerfanas()
|
||||
COLA["activa"] = True
|
||||
COLA["cancelar"] = False
|
||||
COLA["hechos"] = 0
|
||||
COLA["inicio"] = time.time()
|
||||
COLA["hilo"] = threading.Thread(target=bucle_cola, daemon=True)
|
||||
COLA["hilo"].start()
|
||||
return True
|
||||
@@ -1905,7 +1968,7 @@ def apartar_duplicados(raiz, grupos=None):
|
||||
RESPALDO = {"estado": "", "hechos": 0, "total": 0, "bytes": 0, "bytes_total": 0, "actual": "", "errores": [], "destino": "", "cancelar": False, "inicio": 0, "copiados": 0, "saltados": 0}
|
||||
|
||||
|
||||
def plan_respaldo(raiz, destino, por_tipo=True, incluir_raw=False, sin_stock=True):
|
||||
def plan_respaldo(raiz, destino, por_tipo=True, incluir_raw=False, sin_stock=True, meses=0):
|
||||
con = db()
|
||||
rc = raiz_cfg(raiz)
|
||||
cliente = rc["cliente"] if rc else os.path.basename(raiz.rstrip("\\/")) or "Raiz"
|
||||
@@ -1920,6 +1983,8 @@ def plan_respaldo(raiz, destino, por_tipo=True, incluir_raw=False, sin_stock=Tru
|
||||
marca_a = os.sep + "_Archivo" + os.sep
|
||||
items, vistos = [], set()
|
||||
omit_raw, omit_stock_b, omit_stock_n, convertidos = [], 0, 0, 0
|
||||
tope = limite_meses(meses) if int(meses or 0) > 0 else None # solo lo anterior a este (año, mes)
|
||||
omit_reciente_n, omit_reciente_b = 0, 0
|
||||
|
||||
def agregar(ruta, rol, ext, bytes_, mtime):
|
||||
k = os.path.normpath(ruta).lower()
|
||||
@@ -1938,6 +2003,10 @@ def plan_respaldo(raiz, destino, por_tipo=True, incluir_raw=False, sin_stock=Tru
|
||||
omit_stock_b += f["bytes"]
|
||||
omit_stock_n += 1
|
||||
continue
|
||||
if tope and not es_viejo(f, tope):
|
||||
omit_reciente_n += 1
|
||||
omit_reciente_b += f["bytes"]
|
||||
continue
|
||||
k = os.path.normpath(f["ruta"]).lower()
|
||||
if k in conv:
|
||||
sal = conv[k]
|
||||
@@ -1979,6 +2048,7 @@ def plan_respaldo(raiz, destino, por_tipo=True, incluir_raw=False, sin_stock=Tru
|
||||
"por_tipo": [{"tipo": k, **v} for k, v in sorted(por_tipo_res.items(), key=lambda kv: -kv[1]["bytes"])],
|
||||
"convertidos": convertidos, "raw_omitidos": {"n": len(omit_raw), "bytes": sum(x["bytes"] for x in omit_raw), "ejemplos": omit_raw[:15]},
|
||||
"stock_omitido": {"n": omit_stock_n, "bytes": omit_stock_b},
|
||||
"reciente_omitido": {"n": omit_reciente_n, "bytes": omit_reciente_b, "meses": int(meses or 0)},
|
||||
"muestra": [{"de": i["rel"], "a": os.path.relpath(i["destino"], destino)} for i in items[:25]],
|
||||
"items": items,
|
||||
}
|
||||
@@ -2128,7 +2198,14 @@ def estado_general():
|
||||
con = db()
|
||||
esc = {r["raiz"]: dict(r) for r in con.execute("SELECT * FROM escaneos").fetchall()}
|
||||
cola_n = {r["estado"]: r["n"] for r in con.execute("SELECT estado, COUNT(*) n FROM trabajos GROUP BY estado").fetchall()}
|
||||
actual = None
|
||||
if COLA["actual"]:
|
||||
t = con.execute("SELECT id, ruta, raiz, pct, bytes_in, motor, estado FROM trabajos WHERE id = ?", (COLA["actual"],)).fetchone()
|
||||
if t:
|
||||
actual = dict(t)
|
||||
con.close()
|
||||
# «clip 3 de 12»: los hechos en esta tanda + el que va + los que esperan
|
||||
total_tanda = COLA["hechos"] + (1 if actual else 0) + cola_n.get("pendiente", 0)
|
||||
esc_ci = {k.lower(): v for k, v in esc.items()}
|
||||
raices = []
|
||||
for r in CONFIG["raices"]:
|
||||
@@ -2136,7 +2213,8 @@ def estado_general():
|
||||
raices.append({**r, "clave": k, "escaneo": esc_ci.get(k.lower()), "progreso": PROGRESO.get(k), "analisis": ANALISIS.get(k)})
|
||||
return {"config": CONFIG, "raices": raices, "herramientas": herramientas(),
|
||||
"version": VERSION, "actualizacion": actualizador.ESTADO, "congelado": CONGELADO,
|
||||
"cola": {"activa": COLA["activa"], "actual": COLA["actual"], "mensaje": COLA["mensaje"], "n": cola_n},
|
||||
"cola": {"activa": COLA["activa"], "actual": actual, "mensaje": COLA["mensaje"], "n": cola_n,
|
||||
"hechos": COLA["hechos"], "total": total_tanda, "inicio": COLA["inicio"]},
|
||||
"aplicando": APLICANDO, "asistente": asistente.ESTADO,
|
||||
"respaldo": {k: v for k, v in RESPALDO.items() if k != "errores"} | {"errores_n": len(RESPALDO.get("errores") or [])},
|
||||
"cubetas": R.CUBETAS, "tipos": R.TIPOS_CLIENTE, "perfiles": PERFILES, "plantillas": PLANTILLAS}
|
||||
@@ -2253,6 +2331,9 @@ class Manejador(BaseHTTPRequestHandler):
|
||||
return
|
||||
if ruta == "/api/estado":
|
||||
return self._json(estado_general())
|
||||
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/estado":
|
||||
return self._json(estado_carpeta(q["raiz"]))
|
||||
if ruta == "/api/mapa":
|
||||
@@ -2324,7 +2405,7 @@ class Manejador(BaseHTTPRequestHandler):
|
||||
destino = q.get("destino") or CONFIG.get("respaldo", {}).get("destino", "")
|
||||
if not destino:
|
||||
return self._json({"error": "indica la carpeta de destino del respaldo"}, 400)
|
||||
plan = plan_respaldo(raiz, destino, q.get("por_tipo", "1") == "1", q.get("incluir_raw") == "1", q.get("sin_stock", "1") == "1")
|
||||
plan = plan_respaldo(raiz, destino, q.get("por_tipo", "1") == "1", q.get("incluir_raw") == "1", q.get("sin_stock", "1") == "1", int(q.get("meses", "0") or 0))
|
||||
PLANES_RESPALDO[raiz] = plan
|
||||
CONFIG.setdefault("respaldo", {})["destino"] = destino
|
||||
guardar_json(CONFIG_PATH, CONFIG)
|
||||
@@ -2724,6 +2805,31 @@ def _asegurar_consola():
|
||||
pass
|
||||
|
||||
|
||||
SOLTADOS = {"ts": 0, "rutas": []}
|
||||
|
||||
|
||||
def _al_soltar(e):
|
||||
try:
|
||||
rutas = [f.get("pywebviewFullPath") for f in (e.get("dataTransfer") or {}).get("files", []) if f.get("pywebviewFullPath")]
|
||||
except Exception: # noqa
|
||||
rutas = []
|
||||
if rutas:
|
||||
SOLTADOS.update(ts=time.time(), rutas=[os.path.normpath(r) for r in rutas])
|
||||
|
||||
|
||||
def soltado(nombre, espera=2.5):
|
||||
"""Ruta completa de lo que se acaba de soltar en la ventana (llega por el puente WebView2). Espera un poco
|
||||
porque el drop del navegador y el aviso del puente no llegan en el mismo orden."""
|
||||
fin = time.time() + espera
|
||||
while time.time() < fin:
|
||||
if time.time() - SOLTADOS["ts"] < 8:
|
||||
for r in SOLTADOS["rutas"]:
|
||||
if not nombre or os.path.basename(r.rstrip("\\/")).lower() == nombre.lower():
|
||||
return r
|
||||
time.sleep(0.1)
|
||||
return None
|
||||
|
||||
|
||||
def _ventana(url):
|
||||
"""Ventana propia con WebView2 (Edge). Si no está disponible, se abre en el navegador."""
|
||||
try:
|
||||
@@ -2733,6 +2839,15 @@ def _ventana(url):
|
||||
return False
|
||||
try:
|
||||
v = webview.create_window("OrganiLAB", url, width=1280, height=820, min_size=(960, 640), background_color="#ffffff")
|
||||
|
||||
def _al_cargar():
|
||||
# WebView2 entrega la ruta completa de lo que se suelta solo por este puente (el navegador no la da):
|
||||
# se guarda y la interfaz la pide en /api/soltado en cuanto ve el drop.
|
||||
try:
|
||||
v.dom.document.events.drop += _al_soltar
|
||||
except Exception as ex: # noqa
|
||||
print("sin puente de arrastre:", ex)
|
||||
v.events.loaded += _al_cargar
|
||||
webview.start(gui="edgechromium", private_mode=False, storage_path=os.path.join(DATOS, "ventana"))
|
||||
return True
|
||||
except Exception as ex: # noqa
|
||||
|
||||
Referencia en una nueva incidencia
Block a user