1.7.0: recorrido paso a paso, sugerencias en Por nombrar, stock por antigüedad, Convertidor con formatos y GPU/CPU, respaldo por lotes, barra de tareas corregida
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
23
README.md
23
README.md
@@ -279,6 +279,29 @@ con LIKE tardan menos de 10 ms; no hacía falta optimizar.
|
|||||||
carpetas de Windows, sin depender de soltar la carpeta ni de pegar la ruta. `POST /api/elegir-carpeta` lanza el
|
carpetas de Windows, sin depender de soltar la carpeta ni de pegar la ruta. `POST /api/elegir-carpeta` lanza el
|
||||||
diálogo en un PowerShell aparte para no bloquear el servidor y devuelve la ruta (o nulo si se cancela).
|
diálogo en un PowerShell aparte para no bloquear el servidor y devuelve la ruta (o nulo si se cancela).
|
||||||
|
|
||||||
|
1.7.0, recorrido paso a paso y convertidor:
|
||||||
|
- Arrastrar una carpeta a la ventana ya trae su ruta real: el puente WebView2 (`v.dom.document.events.drop`)
|
||||||
|
guarda lo soltado y la interfaz lo pide en `/api/soltado`; la búsqueda por nombre queda solo de reserva
|
||||||
|
(navegador externo).
|
||||||
|
- «Qué va a pasar» son ahora tres pasos en frases, cada uno con su botón Aplicar: 1 apartar basura y proxies,
|
||||||
|
2 ordenar por año y mes, 3 ordenar lo demás. Cada fila dice «X pasa a Y» y por qué en lenguaje llano; lo
|
||||||
|
dudoso queda sin marcar. Al aplicar un paso el plan se recalcula solo. Cada fila del plan trae `paso`.
|
||||||
|
- «Por nombrar» propone qué es cada carpeta mirando su contenido (clips de cámara, proyectos, música, fotos,
|
||||||
|
documentos, videos) y el parecido del nombre con el diccionario; botón «Sí, es …» por carpeta y «Aceptar las
|
||||||
|
sugerencias seguras» para todas las de confianza alta (≥ 90 % de un mismo tipo y ≥ 3 archivos).
|
||||||
|
- «Stock viejo» agrupa por antigüedad (más de 12, 6-12, 3-6, 1-3, menos de 1 mes) con casilla por tramo y
|
||||||
|
atajos 1/2/3/6/12 meses.
|
||||||
|
- «Deshacer» sale de la interfaz (confundía). Sigue existiendo por API/CLI para emergencias.
|
||||||
|
- «Desconectar» pasa a llamarse «Quitar de la lista» con explicación clara.
|
||||||
|
- «Convertidor» (antes «Convertir a 10 bits»): formatos MP4 H.265 10 bits, H.265 ligero, H.264, ProRes Proxy /
|
||||||
|
LT / HQ (`FORMATOS`), detección real de GPU NVIDIA (NVENC) y CPU en `/api/optimizar/motores` (se prueba
|
||||||
|
codificando 3 cuadros), cola tipo Media Encoder («Convirtiendo 2 de 3 · 47 %») y el mismo clip se puede
|
||||||
|
convertir a varios formatos. ProRes nunca reemplaza el original (es de edición). Columna `formato` en
|
||||||
|
`trabajos`.
|
||||||
|
- Respaldar por lotes: «solo lo de más de 3 / 6 / 12 meses» (`meses` en `/api/respaldo/plan`).
|
||||||
|
- Barra de tareas revisada: la conversión en curso llegaba como un id y la interfaz esperaba un objeto (salía
|
||||||
|
0 % sin nombre); ahora `estado.cola` trae `actual` (ruta, pct, raíz), `hechos` y `total`.
|
||||||
|
|
||||||
Probado el 9 sep 2026 sobre una copia de ensayo: plan → aplicar → deshacer → volver a aplicar deja el árbol
|
Probado el 9 sep 2026 sobre una copia de ensayo: plan → aplicar → deshacer → volver a aplicar deja el árbol
|
||||||
exacto; conversión ffmpeg verificada y reemplazo con original a `_Para borrar`. El puente de Resolve conectó
|
exacto; conversión ffmpeg verificada y reemplazo con original a `_Para borrar`. El puente de Resolve conectó
|
||||||
con Resolve Studio 21.1 pero no pudo abrir proyecto porque Resolve estaba en el Gestor de proyectos:
|
con Resolve Studio 21.1 pero no pudo abrir proyecto porque Resolve estaba en el Gestor de proyectos:
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"nombre": "OrganiLAB",
|
"nombre": "OrganiLAB",
|
||||||
"version": "1.6.3",
|
"version": "1.7.0",
|
||||||
"builtAt": "2026-09-16T00:50:31.000Z",
|
"builtAt": "2026-09-16T01:45:56.000Z",
|
||||||
"archivo": "OrganizadorLabstream-portatil-1.6.3.zip",
|
"archivo": "OrganizadorLabstream-portatil-1.7.0.zip",
|
||||||
"size": 198631521,
|
"size": 198673691,
|
||||||
"sha256": "f84af3a46e4284660acac3dd0fd7fdfee9437c680a899dbfcc3cbf85660a9962",
|
"sha256": "2dad6903c9484815406c4d0f3e2e3ebf65a916990fc93ee3ade07ac489bd7f9e",
|
||||||
"app": {
|
"app": {
|
||||||
"archivo": "OrganizadorLabstream-app-1.6.3.zip",
|
"archivo": "OrganizadorLabstream-app-1.7.0.zip",
|
||||||
"size": 14811784,
|
"size": 14853941,
|
||||||
"sha256": "8ec8fcacbbd865c4aca52d233667d4281f5510e3bf14044e8efd5173c2beeeb9",
|
"sha256": "faf85782a465f0d1cec062f6d7993e70e4e292adf2095f9864cbfafe4f33c935",
|
||||||
"url": "/api/herramientas/OrganizadorLabstream-app-1.6.3.zip"
|
"url": "/api/herramientas/OrganizadorLabstream-app-1.7.0.zip"
|
||||||
},
|
},
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"firma": {
|
"firma": {
|
||||||
"alg": "rsa2048-pkcs1v15-sha256",
|
"alg": "rsa2048-pkcs1v15-sha256",
|
||||||
"valor": "58afe4902d9cc98d02ffb98a3376de4dd9da56504c24a88ef1ff3175f8832cac22a20ae9439aca68cba6bbb2126441770b3abe190f5d99d6b313830dd4cc7c4000eede641e97a169f7cebdf541abd618a6f27e032d0c1931aa567849f91a63489b67f3d062a4ea3fd6478d5479a6da9c0cfd56242afbf133ecc2558c95448e443054fd8879a03ccf54c0e0e23057f9923a7ef0fac270ea8f512aab2caf1b1c506f9e8ea749604171a2364d9054901582b455681947d9a26b55459b5b880023d41d532730324daeec88175990a0883c96ad670f79925e35335c68658ccfc70326a7adead5b95c6f3b6d5f309dfc5214734f039397ff9a05b2348643bce27c54fa"
|
"valor": "bba9a7041615d6b3fc47ecdfee39e6835ed09d8f06bba6629b46e23d8e91a57c84ebf82ad0c9cda49632143a8f0b9baf56698393363c197604687a01f477cda7877cc7f4f6368460a85c08aaa5a17c3e2205ee5f3db598b02592b372f7ea479ca722a1498d2c0c13d6f95c02c1e8a0982ac280bf00b1cbcd71ee2d840a8a8a08038fc45fe7195cbf6ad1f2ab562d619c67060af21028bfab9f8213d9f77a6e974646e7c4c597356b094bfdc379950d2015a75ea131beaf34b29aab710934863e44b7e52055ee2b6d2108c53b2fe1c0249746f00537085a1d2ba051f653091d3b7fa9d523a3ba3d459f4c48ea3985a798d29214a985575e263cdebc4fac4cd493"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
185
organizador.py
185
organizador.py
@@ -33,7 +33,7 @@ import actualizador
|
|||||||
asistente.O = sys.modules[__name__]
|
asistente.O = sys.modules[__name__]
|
||||||
actualizador.O = sys.modules[__name__]
|
actualizador.O = sys.modules[__name__]
|
||||||
|
|
||||||
VERSION = "1.6.3"
|
VERSION = "1.7.0"
|
||||||
|
|
||||||
CONGELADO = getattr(sys, "frozen", False)
|
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
|
SIN_VENTANA = getattr(subprocess, "CREATE_NO_WINDOW", 0) # ffmpeg/ffprobe/net sin abrir consolas negras # empaquetado como .exe con PyInstaller
|
||||||
@@ -101,6 +101,7 @@ CONFIG_DEFECTO = {
|
|||||||
"meses_minimos": 3,
|
"meses_minimos": 3,
|
||||||
"min_mb": 300,
|
"min_mb": 300,
|
||||||
"perfil": "equilibrado",
|
"perfil": "equilibrado",
|
||||||
|
"formato": "h265_10bit",
|
||||||
"chroma": "420",
|
"chroma": "420",
|
||||||
"color": "rec709",
|
"color": "rec709",
|
||||||
"salida": "junto", # junto = al lado del original con sufijo _10bit ; espejo = _Archivo/<ruta>
|
"salida": "junto", # junto = al lado del original con sufijo _10bit ; espejo = _Archivo/<ruta>
|
||||||
@@ -117,6 +118,68 @@ PERFILES = {
|
|||||||
"maxima": {"bpp": 0.14, "cq": 21, "nombre": "Máxima calidad"},
|
"maxima": {"bpp": 0.14, "cq": 21, "nombre": "Máxima calidad"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Formatos de salida del convertidor. gpu/cpu = codificador de ffmpeg que lo hace; se usa la GPU si está
|
||||||
|
# disponible y si no la CPU. bpp_mult escala el bitrate objetivo del perfil (H.265 10 bits = 1.0).
|
||||||
|
# ProRes no va por bitrate: mbps_1080p30 es su peso aproximado a 1080p 30 fps para estimar.
|
||||||
|
FORMATOS = {
|
||||||
|
"h265_10bit": {"nombre": "MP4 H.265 10 bits · archivo (recomendado)", "ext": ".mp4", "codec": "hevc", "bits": 10, "sufijo": "_10bit",
|
||||||
|
"gpu": "hevc_nvenc", "cpu": "libx265", "bpp_mult": 1.0, "que": "unas 10 veces más ligero que el original, sin perder calidad visible; el que usa el respaldo"},
|
||||||
|
"h265_ligero": {"nombre": "MP4 H.265 8 bits · ligero para compartir", "ext": ".mp4", "codec": "hevc", "bits": 8, "sufijo": "_ligero",
|
||||||
|
"gpu": "hevc_nvenc", "cpu": "libx265", "bpp_mult": 0.55, "que": "para enviar por WhatsApp o Drive; pierde latitud de color"},
|
||||||
|
"h264": {"nombre": "MP4 H.264 8 bits · compatible con todo", "ext": ".mp4", "codec": "h264", "bits": 8, "sufijo": "_h264",
|
||||||
|
"gpu": "h264_nvenc", "cpu": "libx264", "bpp_mult": 1.5, "que": "se abre en cualquier equipo o televisor; pesa más que H.265"},
|
||||||
|
"prores_proxy": {"nombre": "MOV ProRes Proxy · edición ligera", "ext": ".mov", "codec": "prores", "bits": 10, "sufijo": "_proxy",
|
||||||
|
"cpu": "prores_ks", "prores": 0, "mbps_1080p30": 45, "que": "para editar suave en Resolve o Premiere; luego se reenlaza al original"},
|
||||||
|
"prores_lt": {"nombre": "MOV ProRes 422 LT · edición", "ext": ".mov", "codec": "prores", "bits": 10, "sufijo": "_LT",
|
||||||
|
"cpu": "prores_ks", "prores": 1, "mbps_1080p30": 102, "que": "calidad de edición; pesa bastante"},
|
||||||
|
"prores_hq": {"nombre": "MOV ProRes 422 HQ · máster", "ext": ".mov", "codec": "prores", "bits": 10, "sufijo": "_HQ",
|
||||||
|
"cpu": "prores_ks", "prores": 3, "mbps_1080p30": 220, "que": "entrega o máster; pesa mucho"},
|
||||||
|
}
|
||||||
|
_MOTORES = {"ts": 0, "datos": None}
|
||||||
|
|
||||||
|
|
||||||
|
def motores():
|
||||||
|
"""Qué codificadores hay: GPU NVIDIA (NVENC) y CPU. Se prueba de verdad (un clip de 3 cuadros), no solo
|
||||||
|
por la lista de ffmpeg, porque la lista incluye nvenc aunque no haya tarjeta NVIDIA."""
|
||||||
|
if _MOTORES["datos"] and time.time() - _MOTORES["ts"] < 600:
|
||||||
|
return _MOTORES["datos"]
|
||||||
|
ff = ruta_herramienta("ffmpeg")
|
||||||
|
out = {"gpu": {"nombre": "", "hevc": False, "h264": False}, "cpu": {"libx265": False, "libx264": False, "prores_ks": False}, "formatos": {}, "ffmpeg": bool(ff)}
|
||||||
|
disponibles = set()
|
||||||
|
if ff:
|
||||||
|
try:
|
||||||
|
r = subprocess.run([ff, "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=20, creationflags=SIN_VENTANA)
|
||||||
|
for linea in r.stdout.splitlines():
|
||||||
|
partes = linea.split()
|
||||||
|
if len(partes) >= 2 and partes[0][:1] in ("V", "A") and len(partes[0]) == 6:
|
||||||
|
disponibles.add(partes[1])
|
||||||
|
except Exception: # noqa
|
||||||
|
pass
|
||||||
|
for k in ("libx265", "libx264", "prores_ks"):
|
||||||
|
out["cpu"][k] = k in disponibles
|
||||||
|
for k, enc in (("hevc", "hevc_nvenc"), ("h264", "h264_nvenc")):
|
||||||
|
if enc in disponibles:
|
||||||
|
try:
|
||||||
|
r = subprocess.run([ff, "-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i", "testsrc=size=256x144:rate=30",
|
||||||
|
"-frames:v", "3", "-c:v", enc, "-f", "null", "-"], capture_output=True, text=True, timeout=40, creationflags=SIN_VENTANA)
|
||||||
|
out["gpu"][k] = r.returncode == 0
|
||||||
|
except Exception: # noqa
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], capture_output=True, text=True, timeout=10, creationflags=SIN_VENTANA)
|
||||||
|
out["gpu"]["nombre"] = (r.stdout or "").strip().splitlines()[0] if r.returncode == 0 and r.stdout.strip() else ""
|
||||||
|
except Exception: # noqa
|
||||||
|
pass
|
||||||
|
for k, f in FORMATOS.items():
|
||||||
|
m = None
|
||||||
|
if f.get("gpu") and out["gpu"].get(f["codec"]):
|
||||||
|
m = "gpu"
|
||||||
|
elif f.get("cpu") and out["cpu"].get(f["cpu"]):
|
||||||
|
m = "cpu"
|
||||||
|
out["formatos"][k] = m # None = no se puede con este ffmpeg
|
||||||
|
_MOTORES.update(ts=time.time(), datos=out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def cargar_json(ruta, defecto):
|
def cargar_json(ruta, defecto):
|
||||||
try:
|
try:
|
||||||
@@ -234,7 +297,7 @@ def init_db():
|
|||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
con.commit()
|
con.commit()
|
||||||
for alter in ("ALTER TABLE archivos ADD COLUMN es_stock INTEGER DEFAULT 0",):
|
for alter in ("ALTER TABLE archivos ADD COLUMN es_stock INTEGER DEFAULT 0", "ALTER TABLE trabajos ADD COLUMN formato TEXT DEFAULT 'h265_10bit'"):
|
||||||
try:
|
try:
|
||||||
con.execute(alter)
|
con.execute(alter)
|
||||||
con.commit()
|
con.commit()
|
||||||
@@ -1350,7 +1413,19 @@ def kbps_objetivo(ancho, alto, fps, perfil):
|
|||||||
return max(1500, int(ancho * alto * fps * bpp / 1000))
|
return max(1500, int(ancho * alto * fps * bpp / 1000))
|
||||||
|
|
||||||
|
|
||||||
def candidatos(raiz, meses, perfil, min_mb, incluir_exportados=False):
|
def estimar_bytes(f, perfil, formato):
|
||||||
|
"""peso aproximado de la salida para un archivo del inventario"""
|
||||||
|
fm = FORMATOS.get(formato, FORMATOS["h265_10bit"])
|
||||||
|
dur = f["dur"] or 0
|
||||||
|
if fm.get("mbps_1080p30"):
|
||||||
|
pix = (f["ancho"] or 1920) * (f["alto"] or 1080) * (f["fps"] or 30)
|
||||||
|
return int(fm["mbps_1080p30"] * 1e6 / 8 * dur * pix / (1920 * 1080 * 30))
|
||||||
|
obj = kbps_objetivo(f["ancho"] or 1920, f["alto"] or 1080, f["fps"], perfil) * fm.get("bpp_mult", 1.0)
|
||||||
|
return int(obj * 1000 / 8 * dur) + int(256000 / 8 * dur)
|
||||||
|
|
||||||
|
|
||||||
|
def candidatos(raiz, meses, perfil, min_mb, incluir_exportados=False, formato="h265_10bit"):
|
||||||
|
fm = FORMATOS.get(formato, FORMATOS["h265_10bit"])
|
||||||
con = db()
|
con = db()
|
||||||
limite = time.time() - int(meses) * 30.4 * 86400
|
limite = time.time() - int(meses) * 30.4 * 86400
|
||||||
filas = con.execute("SELECT * FROM archivos WHERE raiz = ? AND analizado = 1 AND es_proxy = 0 AND bytes >= ? ORDER BY bytes DESC",
|
filas = con.execute("SELECT * FROM archivos WHERE raiz = ? AND analizado = 1 AND es_proxy = 0 AND bytes >= ? ORDER BY bytes DESC",
|
||||||
@@ -1365,16 +1440,18 @@ def candidatos(raiz, meses, perfil, min_mb, incluir_exportados=False):
|
|||||||
motivo_no = "stock de Envato: mejor borrarlo (se vuelve a descargar)"
|
motivo_no = "stock de Envato: mejor borrarlo (se vuelve a descargar)"
|
||||||
elif f["alfa"]:
|
elif f["alfa"]:
|
||||||
motivo_no = "tiene canal alfa (HEVC lo perdería)"
|
motivo_no = "tiene canal alfa (HEVC lo perdería)"
|
||||||
elif f["codec"] in ("hevc", "av1") and f["kbps"] and f["kbps"] <= kbps_objetivo(f["ancho"], f["alto"], f["fps"], perfil) * 1.3:
|
elif fm["codec"] == "hevc" and f["codec"] in ("hevc", "av1") and f["kbps"] and f["kbps"] <= kbps_objetivo(f["ancho"], f["alto"], f["fps"], perfil) * fm.get("bpp_mult", 1.0) * 1.3:
|
||||||
motivo_no = "ya está en HEVC/AV1 eficiente"
|
motivo_no = "ya está en HEVC/AV1 eficiente"
|
||||||
|
elif motor == "resolve" and formato != "h265_10bit":
|
||||||
|
motivo_no = "los BRAW van por Resolve y solo salen a H.265 10 bits"
|
||||||
elif f["mtime"] > limite:
|
elif f["mtime"] > limite:
|
||||||
motivo_no = f"tiene menos de {meses} meses"
|
motivo_no = f"tiene menos de {meses} meses"
|
||||||
elif f["contexto"] == "EXPORTADOS" and not incluir_exportados:
|
elif f["contexto"] == "EXPORTADOS" and not incluir_exportados:
|
||||||
motivo_no = "es un exportado / master"
|
motivo_no = "es un exportado / master"
|
||||||
elif not f["ancho"] or not f["dur"]:
|
elif not f["ancho"] or not f["dur"]:
|
||||||
motivo_no = "no se pudo leer (ffprobe)"
|
motivo_no = "no se pudo leer (ffprobe)"
|
||||||
obj = kbps_objetivo(f["ancho"] or 1920, f["alto"] or 1080, f["fps"], perfil)
|
obj = int(kbps_objetivo(f["ancho"] or 1920, f["alto"] or 1080, f["fps"], perfil) * fm.get("bpp_mult", 1.0))
|
||||||
est = int(obj * 1000 / 8 * (f["dur"] or 0)) + int(256000 / 8 * (f["dur"] or 0))
|
est = estimar_bytes(f, perfil, formato)
|
||||||
out.append({
|
out.append({
|
||||||
"ruta": f["ruta"], "rel": os.path.relpath(f["ruta"], raiz), "bytes": f["bytes"], "codec": f["codec"], "perfil": f["perfil"],
|
"ruta": f["ruta"], "rel": os.path.relpath(f["ruta"], raiz), "bytes": f["bytes"], "codec": f["codec"], "perfil": f["perfil"],
|
||||||
"pix_fmt": f["pix_fmt"], "res": f"{f['ancho']}x{f['alto']}", "fps": f["fps"], "dur": f["dur"], "kbps": f["kbps"],
|
"pix_fmt": f["pix_fmt"], "res": f"{f['ancho']}x{f['alto']}", "fps": f["fps"], "dur": f["dur"], "kbps": f["kbps"],
|
||||||
@@ -1384,16 +1461,21 @@ def candidatos(raiz, meses, perfil, min_mb, incluir_exportados=False):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def salida_para(ruta, raiz):
|
def salida_para(ruta, raiz, formato="h265_10bit"):
|
||||||
o = CONFIG["optimizar"]
|
o = CONFIG["optimizar"]
|
||||||
|
fm = FORMATOS.get(formato, FORMATOS["h265_10bit"])
|
||||||
base, _ = os.path.splitext(ruta)
|
base, _ = os.path.splitext(ruta)
|
||||||
|
sufijo = o.get("sufijo", "_10bit") if formato == "h265_10bit" else fm["sufijo"]
|
||||||
if o.get("salida") == "espejo":
|
if o.get("salida") == "espejo":
|
||||||
rel = os.path.relpath(base, raiz)
|
rel = os.path.relpath(base, raiz)
|
||||||
return os.path.join(raiz, "_Archivo", rel + ".mp4")
|
return os.path.join(raiz, "_Archivo", rel + fm["ext"])
|
||||||
return base + o.get("sufijo", "_10bit") + ".mp4"
|
return base + sufijo + fm["ext"]
|
||||||
|
|
||||||
|
|
||||||
def encolar(rutas, perfil, chroma, color, raiz):
|
def encolar(rutas, perfil, chroma, color, raiz, formato="h265_10bit"):
|
||||||
|
if formato not in FORMATOS:
|
||||||
|
formato = "h265_10bit"
|
||||||
|
fm = FORMATOS[formato]
|
||||||
con = db()
|
con = db()
|
||||||
ids = []
|
ids = []
|
||||||
with con:
|
with con:
|
||||||
@@ -1401,18 +1483,20 @@ def encolar(rutas, perfil, chroma, color, raiz):
|
|||||||
f = con.execute("SELECT * FROM archivos WHERE ruta = ?", (r,)).fetchone()
|
f = con.execute("SELECT * FROM archivos WHERE ruta = ?", (r,)).fetchone()
|
||||||
if not f:
|
if not f:
|
||||||
continue
|
continue
|
||||||
ya = con.execute("SELECT id FROM trabajos WHERE ruta = ? AND estado IN ('pendiente','corriendo','listo','verificado')", (r,)).fetchone()
|
ya = con.execute("SELECT id FROM trabajos WHERE ruta = ? AND formato = ? AND estado IN ('pendiente','corriendo','listo','verificado')", (r, formato)).fetchone()
|
||||||
if ya:
|
if ya:
|
||||||
continue
|
continue
|
||||||
salida = salida_para(r, raiz)
|
motor = "resolve" if f["ext"] == ".braw" else "ffmpeg"
|
||||||
|
if motor == "resolve" and formato != "h265_10bit":
|
||||||
|
continue # los BRAW solo salen por Resolve a H.265 10 bits
|
||||||
|
salida = salida_para(r, raiz, formato)
|
||||||
# el mismo clip en MP4 y MOV daría la misma salida: el segundo pisaría al primero. No se encola.
|
# el mismo clip en MP4 y MOV daría la misma salida: el segundo pisaría al primero. No se encola.
|
||||||
otro = con.execute("SELECT id FROM trabajos WHERE salida = ? AND ruta != ? AND estado IN ('pendiente','corriendo','listo','verificado')", (salida, r)).fetchone()
|
otro = con.execute("SELECT id FROM trabajos WHERE salida = ? AND ruta != ? AND estado IN ('pendiente','corriendo','listo','verificado')", (salida, r)).fetchone()
|
||||||
if otro or os.path.exists(salida):
|
if otro or os.path.exists(salida):
|
||||||
continue
|
continue
|
||||||
motor = "resolve" if f["ext"] == ".braw" else "ffmpeg"
|
kb = int(kbps_objetivo(f["ancho"] or 1920, f["alto"] or 1080, f["fps"], perfil) * fm.get("bpp_mult", 1.0))
|
||||||
kb = kbps_objetivo(f["ancho"] or 1920, f["alto"] or 1080, f["fps"], perfil)
|
cur = con.execute("INSERT INTO trabajos (ruta, salida, motor, perfil, chroma, color, kbps, estado, bytes_in, creado, raiz, formato) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
cur = con.execute("INSERT INTO trabajos (ruta, salida, motor, perfil, chroma, color, kbps, estado, bytes_in, creado, raiz) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
(r, salida, motor, perfil, chroma, color, kb, "pendiente", f["bytes"], time.time(), raiz, formato))
|
||||||
(r, salida, motor, perfil, chroma, color, kb, "pendiente", f["bytes"], time.time(), raiz))
|
|
||||||
ids.append(cur.lastrowid)
|
ids.append(cur.lastrowid)
|
||||||
con.close()
|
con.close()
|
||||||
return ids
|
return ids
|
||||||
@@ -1440,18 +1524,45 @@ def correr_ffmpeg(t):
|
|||||||
cq = PERFILES.get(t["perfil"], PERFILES["equilibrado"])["cq"]
|
cq = PERFILES.get(t["perfil"], PERFILES["equilibrado"])["cq"]
|
||||||
maxrate = int(t["kbps"] * 1.8)
|
maxrate = int(t["kbps"] * 1.8)
|
||||||
salida = t["salida"]
|
salida = t["salida"]
|
||||||
|
formato = (t["formato"] if "formato" in t.keys() else None) or "h265_10bit"
|
||||||
|
fm = FORMATOS.get(formato, FORMATOS["h265_10bit"])
|
||||||
|
motor = motores()["formatos"].get(formato)
|
||||||
|
if not motor:
|
||||||
|
set_trabajo(t["id"], estado="error", log=f"este ffmpeg no puede producir {fm['nombre']} (ni por GPU ni por CPU)")
|
||||||
|
return
|
||||||
os.makedirs(os.path.dirname(salida), exist_ok=True)
|
os.makedirs(os.path.dirname(salida), exist_ok=True)
|
||||||
tmp = salida + ".parcial.mp4"
|
tmp = salida + ".parcial" + fm["ext"]
|
||||||
cmd = [ruta_herramienta("ffmpeg"), "-hide_banner", "-nostdin", "-y", "-i", t["ruta"], "-map", "0:v:0", "-map", "0:a?",
|
gop = str(int(round(fps * 2)))
|
||||||
"-c:v", "hevc_nvenc",
|
cmd = [ruta_herramienta("ffmpeg"), "-hide_banner", "-nostdin", "-y", "-i", t["ruta"], "-map", "0:v:0", "-map", "0:a?"]
|
||||||
"-pix_fmt", "p210le" if chroma == "422" else "p010le",
|
if fm["codec"] == "prores":
|
||||||
"-profile:v", "rext" if chroma == "422" else "main10",
|
cmd += ["-c:v", "prores_ks", "-profile:v", str(fm["prores"]), "-pix_fmt", "yuv422p10le", "-vendor", "apl0", "-qscale:v", "9"]
|
||||||
"-preset", "p6", "-tune", "hq", "-rc", "vbr", "-cq", str(cq), "-b:v", "0",
|
elif motor == "gpu":
|
||||||
"-maxrate", f"{maxrate}k", "-bufsize", f"{maxrate * 2}k",
|
diez = fm["bits"] == 10
|
||||||
"-spatial-aq", "1", "-temporal-aq", "1", "-aq-strength", "8", "-rc-lookahead", "32",
|
cmd += ["-c:v", fm["gpu"],
|
||||||
"-bf", "3", "-b_ref_mode", "middle", "-g", str(int(round(fps * 2))),
|
"-pix_fmt", ("p210le" if chroma == "422" else "p010le") if diez else "yuv420p",
|
||||||
"-tag:v", "hvc1", "-map_metadata", "0", "-movflags", "+faststart"]
|
"-preset", "p6", "-tune", "hq", "-rc", "vbr", "-cq", str(cq), "-b:v", "0",
|
||||||
if f and f["acodec"] == "aac":
|
"-maxrate", f"{maxrate}k", "-bufsize", f"{maxrate * 2}k",
|
||||||
|
"-spatial-aq", "1", "-temporal-aq", "1", "-aq-strength", "8", "-rc-lookahead", "32",
|
||||||
|
"-bf", "3", "-b_ref_mode", "middle", "-g", gop]
|
||||||
|
if fm["codec"] == "hevc":
|
||||||
|
cmd += ["-profile:v", ("rext" if chroma == "422" else "main10") if diez else "main", "-tag:v", "hvc1"]
|
||||||
|
else:
|
||||||
|
cmd += ["-profile:v", "high"]
|
||||||
|
else:
|
||||||
|
diez = fm["bits"] == 10
|
||||||
|
cmd += ["-c:v", fm["cpu"], "-preset", "medium", "-crf", str(cq - 2),
|
||||||
|
"-pix_fmt", ("yuv422p10le" if chroma == "422" else "yuv420p10le") if diez else "yuv420p",
|
||||||
|
"-maxrate", f"{maxrate}k", "-bufsize", f"{maxrate * 2}k", "-g", gop]
|
||||||
|
if fm["codec"] == "hevc":
|
||||||
|
cmd += ["-tag:v", "hvc1"]
|
||||||
|
if diez:
|
||||||
|
cmd += ["-profile:v", "main422-10" if chroma == "422" else "main10"]
|
||||||
|
cmd += ["-map_metadata", "0"]
|
||||||
|
if fm["ext"] == ".mp4":
|
||||||
|
cmd += ["-movflags", "+faststart"]
|
||||||
|
if fm["codec"] == "prores":
|
||||||
|
cmd += ["-c:a", "pcm_s16le"]
|
||||||
|
elif f and f["acodec"] == "aac":
|
||||||
cmd += ["-c:a", "copy"]
|
cmd += ["-c:a", "copy"]
|
||||||
else:
|
else:
|
||||||
cmd += ["-c:a", "aac", "-b:a", "320k"]
|
cmd += ["-c:a", "aac", "-b:a", "320k"]
|
||||||
@@ -1677,9 +1788,10 @@ def verificar(ids):
|
|||||||
problemas = []
|
problemas = []
|
||||||
if dur_src and abs(info["dur"] - dur_src) > tol:
|
if dur_src and abs(info["dur"] - dur_src) > tol:
|
||||||
problemas.append(f"duración {info['dur']:.2f}s vs original {dur_src:.2f}s")
|
problemas.append(f"duración {info['dur']:.2f}s vs original {dur_src:.2f}s")
|
||||||
if info["codec"] != "hevc":
|
fm = FORMATOS.get((t["formato"] if "formato" in t.keys() else None) or "h265_10bit", FORMATOS["h265_10bit"])
|
||||||
problemas.append(f"códec {info['codec']}")
|
if info["codec"] != fm["codec"]:
|
||||||
if "10" not in info["pix_fmt"]:
|
problemas.append(f"códec {info['codec']} (se esperaba {fm['codec']})")
|
||||||
|
if fm["bits"] == 10 and "10" not in info["pix_fmt"]:
|
||||||
problemas.append(f"no es 10 bits ({info['pix_fmt']})")
|
problemas.append(f"no es 10 bits ({info['pix_fmt']})")
|
||||||
if os.path.getsize(t["salida"]) < 100000:
|
if os.path.getsize(t["salida"]) < 100000:
|
||||||
problemas.append("archivo demasiado pequeño")
|
problemas.append("archivo demasiado pequeño")
|
||||||
@@ -1704,6 +1816,9 @@ def reemplazar(ids):
|
|||||||
if not t or t["estado"] != "verificado":
|
if not t or t["estado"] != "verificado":
|
||||||
res.append({"id": id_, "ok": False, "motivo": "solo se reemplazan trabajos verificados"})
|
res.append({"id": id_, "ok": False, "motivo": "solo se reemplazan trabajos verificados"})
|
||||||
continue
|
continue
|
||||||
|
if ((t["formato"] if "formato" in t.keys() else "") or "").startswith("prores"):
|
||||||
|
res.append({"id": id_, "ok": False, "motivo": "ProRes es un formato de edición: queda junto al original, no lo reemplaza"})
|
||||||
|
continue
|
||||||
if not os.path.exists(t["salida"]) or os.path.getsize(t["salida"]) < 1024:
|
if not os.path.exists(t["salida"]) or os.path.getsize(t["salida"]) < 1024:
|
||||||
res.append({"id": id_, "ok": False, "motivo": "el MP4 convertido ya no está donde se verificó; el original se queda"})
|
res.append({"id": id_, "ok": False, "motivo": "el MP4 convertido ya no está donde se verificó; el original se queda"})
|
||||||
set_trabajo(id_, estado="error", log="la salida verificada desapareció antes de reemplazar")
|
set_trabajo(id_, estado="error", log="la salida verificada desapareció antes de reemplazar")
|
||||||
@@ -2217,7 +2332,7 @@ def estado_general():
|
|||||||
"hechos": COLA["hechos"], "total": total_tanda, "inicio": COLA["inicio"]},
|
"hechos": COLA["hechos"], "total": total_tanda, "inicio": COLA["inicio"]},
|
||||||
"aplicando": APLICANDO, "asistente": asistente.ESTADO,
|
"aplicando": APLICANDO, "asistente": asistente.ESTADO,
|
||||||
"respaldo": {k: v for k, v in RESPALDO.items() if k != "errores"} | {"errores_n": len(RESPALDO.get("errores") or [])},
|
"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}
|
"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 estado_carpeta(ruta):
|
def estado_carpeta(ruta):
|
||||||
@@ -2387,7 +2502,9 @@ class Manejador(BaseHTTPRequestHandler):
|
|||||||
if ruta == "/api/optimizar/candidatos":
|
if ruta == "/api/optimizar/candidatos":
|
||||||
o = CONFIG["optimizar"]
|
o = CONFIG["optimizar"]
|
||||||
return self._json({"candidatos": candidatos(os.path.normpath(q["raiz"]), q.get("meses", o["meses_minimos"]), q.get("perfil", o["perfil"]),
|
return self._json({"candidatos": candidatos(os.path.normpath(q["raiz"]), q.get("meses", o["meses_minimos"]), q.get("perfil", o["perfil"]),
|
||||||
q.get("min_mb", o["min_mb"]), q.get("exportados") == "1")})
|
q.get("min_mb", o["min_mb"]), q.get("exportados") == "1", q.get("formato", o.get("formato", "h265_10bit")))})
|
||||||
|
if ruta == "/api/optimizar/motores":
|
||||||
|
return self._json(motores())
|
||||||
if ruta == "/api/optimizar/cola":
|
if ruta == "/api/optimizar/cola":
|
||||||
con = db()
|
con = db()
|
||||||
filas = [dict(r) for r in con.execute("SELECT * FROM trabajos ORDER BY id DESC LIMIT 500").fetchall()]
|
filas = [dict(r) for r in con.execute("SELECT * FROM trabajos ORDER BY id DESC LIMIT 500").fetchall()]
|
||||||
@@ -2451,7 +2568,7 @@ class Manejador(BaseHTTPRequestHandler):
|
|||||||
return self._json({"ok": True})
|
return self._json({"ok": True})
|
||||||
if ruta == "/api/config":
|
if ruta == "/api/config":
|
||||||
# solo ajustes de uso; las rutas de herramientas y la URL de actualización se cambian a mano en config.json
|
# solo ajustes de uso; las rutas de herramientas y la URL de actualización se cambian a mano en config.json
|
||||||
permitidas = {"optimizar": {"meses_minimos", "min_mb", "perfil", "chroma", "color", "salida", "sufijo"}, "respaldo": {"destino"}}
|
permitidas = {"optimizar": {"meses_minimos", "min_mb", "perfil", "formato", "chroma", "color", "salida", "sufijo"}, "respaldo": {"destino"}}
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
if k in permitidas and isinstance(v, dict):
|
if k in permitidas and isinstance(v, dict):
|
||||||
CONFIG.setdefault(k, {}).update({kk: vv for kk, vv in v.items() if kk in permitidas[k]})
|
CONFIG.setdefault(k, {}).update({kk: vv for kk, vv in v.items() if kk in permitidas[k]})
|
||||||
@@ -2527,7 +2644,7 @@ class Manejador(BaseHTTPRequestHandler):
|
|||||||
return self._json({"lanzado": True})
|
return self._json({"lanzado": True})
|
||||||
if ruta == "/api/optimizar/encolar":
|
if ruta == "/api/optimizar/encolar":
|
||||||
o = CONFIG["optimizar"]
|
o = CONFIG["optimizar"]
|
||||||
ids = encolar(d.get("rutas", []), d.get("perfil", o["perfil"]), d.get("chroma", o["chroma"]), d.get("color", o["color"]), os.path.normpath(d["raiz"]))
|
ids = encolar(d.get("rutas", []), d.get("perfil", o["perfil"]), d.get("chroma", o["chroma"]), d.get("color", o["color"]), os.path.normpath(d["raiz"]), d.get("formato", o.get("formato", "h265_10bit")))
|
||||||
if d.get("iniciar"):
|
if d.get("iniciar"):
|
||||||
iniciar_cola()
|
iniciar_cola()
|
||||||
return self._json({"ids": ids})
|
return self._json({"ids": ids})
|
||||||
|
|||||||
@@ -164,6 +164,10 @@
|
|||||||
.asis-paso h4{margin:0 0 4px;font-size:13.5px}
|
.asis-paso h4{margin:0 0 4px;font-size:13.5px}
|
||||||
.asis-paso label{display:block;margin:4px 0;cursor:pointer}
|
.asis-paso label{display:block;margin:4px 0;cursor:pointer}
|
||||||
.asis-frase{color:var(--acc)}
|
.asis-frase{color:var(--acc)}
|
||||||
|
.colabar{display:flex;gap:12px;align-items:center;padding:10px 12px;background:var(--acc2);border-radius:6px;margin-bottom:10px;font-size:13px}
|
||||||
|
.colabar .b{flex:1;height:8px;background:#fff;border-radius:99px;overflow:hidden}
|
||||||
|
.colabar .b i{display:block;height:100%;background:var(--acc)}
|
||||||
|
.colabar b{white-space:nowrap}
|
||||||
.est{display:inline-block;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600;letter-spacing:.02em;white-space:nowrap}
|
.est{display:inline-block;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600;letter-spacing:.02em;white-space:nowrap}
|
||||||
.est.pendiente,.est.cancelado{background:var(--panel2);color:var(--mut)}
|
.est.pendiente,.est.cancelado{background:var(--panel2);color:var(--mut)}
|
||||||
.est.corriendo{background:var(--acc2);color:var(--acc)}
|
.est.corriendo{background:var(--acc2);color:var(--acc)}
|
||||||
@@ -282,7 +286,7 @@
|
|||||||
<nav id="nav" aria-label="Vistas">
|
<nav id="nav" aria-label="Vistas">
|
||||||
<a data-t="carpeta" class="on" tabindex="0">Carpeta</a>
|
<a data-t="carpeta" class="on" tabindex="0">Carpeta</a>
|
||||||
<a data-t="revisar" tabindex="0">Revisar y ordenar</a>
|
<a data-t="revisar" tabindex="0">Revisar y ordenar</a>
|
||||||
<a data-t="convertir" tabindex="0">Convertir a 10 bits</a>
|
<a data-t="convertir" tabindex="0">Convertidor</a>
|
||||||
<a data-t="respaldar" tabindex="0">Respaldar</a>
|
<a data-t="respaldar" tabindex="0">Respaldar</a>
|
||||||
<a data-t="historial" tabindex="0">Historial</a>
|
<a data-t="historial" tabindex="0">Historial</a>
|
||||||
<a data-t="ajustes" class="eng" tabindex="0" title="Clientes, sus carpetas y plantillas">⚙ Ajustes</a>
|
<a data-t="ajustes" class="eng" tabindex="0" title="Clientes, sus carpetas y plantillas">⚙ Ajustes</a>
|
||||||
@@ -302,7 +306,7 @@
|
|||||||
<div><b>1 · Escanear</b>Lee nombres, tamaños y fechas. Solo lectura.</div>
|
<div><b>1 · Escanear</b>Lee nombres, tamaños y fechas. Solo lectura.</div>
|
||||||
<div><b>2 · Revisar</b>Qué pesa: BRAW, cámara, stock, proxies, basura, repetidos.</div>
|
<div><b>2 · Revisar</b>Qué pesa: BRAW, cámara, stock, proxies, basura, repetidos.</div>
|
||||||
<div><b>3 · Ordenar</b>Mueve dentro de la misma carpeta. Lo que sobra va a _Para borrar.</div>
|
<div><b>3 · Ordenar</b>Mueve dentro de la misma carpeta. Lo que sobra va a _Para borrar.</div>
|
||||||
<div><b>4 · Convertir</b>Clips ya entregados a MP4 10 bits, unas 10 veces más ligeros.</div>
|
<div><b>4 · Convertir</b>Clips ya entregados a un formato más ligero (H.265 10 bits, H.264, ProRes), en cola uno tras otro.</div>
|
||||||
<div><b>5 · Respaldar</b>Copia compacta a otro disco, con índice.</div>
|
<div><b>5 · Respaldar</b>Copia compacta a otro disco, con índice.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -377,7 +381,7 @@
|
|||||||
|
|
||||||
<!-- ============ CONVERTIR ============ -->
|
<!-- ============ CONVERTIR ============ -->
|
||||||
<section id="t-convertir">
|
<section id="t-convertir">
|
||||||
<div class="cab"><h2 class="v">Convertir a 10 bits</h2><span class="sub">clips de cámara <b id="convCriterio">con más de 3 meses y más de 300 MB</b> · <details class="ajc" id="convAjustes"><summary>cambiar</summary><div class="toolbar">
|
<div class="cab"><h2 class="v">Convertidor</h2><span class="sub">clips de cámara <b id="convCriterio">con más de 3 meses y más de 300 MB</b> · <details class="ajc" id="convAjustes"><summary>cambiar</summary><div class="toolbar">
|
||||||
<label>con más de <input type="number" id="optMeses" value="3" min="0" style="width:60px"> meses</label>
|
<label>con más de <input type="number" id="optMeses" value="3" min="0" style="width:60px"> meses</label>
|
||||||
<label>mínimo <input type="number" id="optMin" value="300" min="0" style="width:70px"> MB</label>
|
<label>mínimo <input type="number" id="optMin" value="300" min="0" style="width:70px"> MB</label>
|
||||||
<label>calidad <select id="optPerfil"></select></label>
|
<label>calidad <select id="optPerfil"></select></label>
|
||||||
@@ -385,7 +389,8 @@
|
|||||||
<label>BRAW en <select id="optColor"><option value="rec709">Rec.709 (se ve bien)</option><option value="log">Log de cámara (plano, más latitud)</option></select></label>
|
<label>BRAW en <select id="optColor"><option value="rec709">Rec.709 (se ve bien)</option><option value="log">Log de cámara (plano, más latitud)</option></select></label>
|
||||||
<label><input type="checkbox" id="optExport"> incluir exportados</label>
|
<label><input type="checkbox" id="optExport"> incluir exportados</label>
|
||||||
</div></details></span></div>
|
</div></details></span></div>
|
||||||
<p class="ayuda">Los clips de proyectos ya entregados pasan a <b>MP4 H.265 de 10 bits</b>, unas 10 veces más ligeros. <b>El original no se toca</b> hasta que reemplaces. <span class="q">?</span><span class="mas">Buscar lee los clips (códec, duración, resolución) y calcula cuánto pesarían. Convertir va en segundo plano, un clip tras otro; puedes cambiar de vista. Cada clip terminado se verifica solo (códec, 10 bits, duración). Al reemplazar, el original se aparta a <span class="path">_Para borrar/originales</span> y el MP4 queda en su lugar con el mismo nombre más «_10bit». Los BRAW van por DaVinci Resolve Studio (debe estar abierto); el resto por ffmpeg con la GPU. Nada se borra; se deshace desde Historial.</span></p>
|
<p class="ayuda">Convierte los clips de proyectos ya entregados a un formato más ligero. <b>El original no se toca</b> hasta que reemplaces. <span class="q">?</span><span class="mas">Buscar lee los clips (códec, duración, resolución) y calcula cuánto pesarían. La cola convierte un clip tras otro en segundo plano, como Media Encoder; puedes cambiar de vista. Cada clip terminado se verifica solo (códec, bits, duración). Al reemplazar, el original se aparta a <span class="path">_Para borrar/originales</span> y el archivo nuevo queda en su lugar con el mismo nombre más un sufijo. Los BRAW van por DaVinci Resolve Studio (debe estar abierto) y solo salen a H.265 10 bits; el resto por ffmpeg, con la GPU cuando la hay. Nada se borra.</span></p>
|
||||||
|
<div class="toolbar" style="margin-bottom:10px"><label>formato de salida <select id="optFormato"></select></label><span id="fmtQue" class="mut" style="font-size:12px"></span><span class="sep"></span><span id="motores" class="mut" style="font-size:12px"><span class="spin"></span> detectando GPU y CPU…</span></div>
|
||||||
<div class="cards c4" id="optCards"></div>
|
<div class="cards c4" id="optCards"></div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button id="btnBuscarClips" class="p">Buscar clips</button>
|
<button id="btnBuscarClips" class="p">Buscar clips</button>
|
||||||
@@ -538,6 +543,7 @@ async function cargarEstado(){
|
|||||||
sel.innerHTML = ESTADO.raices.map(r => `<option value="${esc(r.clave)}">${esc(r.cliente)}</option>`).join('');
|
sel.innerHTML = ESTADO.raices.map(r => `<option value="${esc(r.clave)}">${esc(r.cliente)}</option>`).join('');
|
||||||
if (antes && ESTADO.raices.some(r => r.clave === antes)) sel.value = antes;
|
if (antes && ESTADO.raices.some(r => r.clave === antes)) sel.value = antes;
|
||||||
pintarIndice();
|
pintarIndice();
|
||||||
|
const fmt = $('#optFormato'); if (!fmt.options.length && ESTADO.formatos) { fmt.innerHTML = Object.entries(ESTADO.formatos).map(([k, v]) => `<option value="${k}">${esc(v.nombre)}</option>`).join(''); fmt.value = ESTADO.config.optimizar.formato || 'h265_10bit'; pintarFormato(); cargarMotores(); }
|
||||||
const perf = $('#optPerfil'); if (!perf.options.length) { perf.innerHTML = Object.entries(ESTADO.perfiles).map(([k,v]) => `<option value="${k}">${esc(v.nombre)}</option>`).join(''); perf.value = ESTADO.config.optimizar.perfil; $('#optMeses').value = ESTADO.config.optimizar.meses_minimos; $('#optMin').value = ESTADO.config.optimizar.min_mb; $('#optChroma').value = ESTADO.config.optimizar.chroma; $('#optColor').value = ESTADO.config.optimizar.color; pintarCriterio(); }
|
const perf = $('#optPerfil'); if (!perf.options.length) { perf.innerHTML = Object.entries(ESTADO.perfiles).map(([k,v]) => `<option value="${k}">${esc(v.nombre)}</option>`).join(''); perf.value = ESTADO.config.optimizar.perfil; $('#optMeses').value = ESTADO.config.optimizar.meses_minimos; $('#optMin').value = ESTADO.config.optimizar.min_mb; $('#optChroma').value = ESTADO.config.optimizar.chroma; $('#optColor').value = ESTADO.config.optimizar.color; pintarCriterio(); }
|
||||||
const r = cfgRaiz(); const a = r && r.analisis;
|
const r = cfgRaiz(); const a = r && r.analisis;
|
||||||
if (a && a.estado === 'corriendo') $('#anaEstado').innerHTML = `<span class="spin"></span> leyendo clips: ${a.hechos} de ${a.total}`;
|
if (a && a.estado === 'corriendo') $('#anaEstado').innerHTML = `<span class="spin"></span> leyendo clips: ${a.hechos} de ${a.total}`;
|
||||||
@@ -561,7 +567,7 @@ function detectarCambios(){
|
|||||||
fin('cola', 'Conversión terminada.', {texto: 'Ver', ir: () => irA('convertir')});
|
fin('cola', 'Conversión terminada.', {texto: 'Ver', ir: () => irA('convertir')});
|
||||||
fin('asis', 'El asistente terminó.' + (ESTADO.asistente.resultado && ESTADO.asistente.resultado.errores && ESTADO.asistente.resultado.errores.length ? ` ${ESTADO.asistente.resultado.errores.length} avisos.` : ''), {texto: 'Ver historial', ir: () => irA('historial')});
|
fin('asis', 'El asistente terminó.' + (ESTADO.asistente.resultado && ESTADO.asistente.resultado.errores && ESTADO.asistente.resultado.errores.length ? ` ${ESTADO.asistente.resultado.errores.length} avisos.` : ''), {texto: 'Ver historial', ir: () => irA('historial')});
|
||||||
fin('resp', 'Copia de respaldo terminada.', {texto: 'Ver', ir: () => irA('respaldar')});
|
fin('resp', 'Copia de respaldo terminada.', {texto: 'Ver', ir: () => irA('respaldar')});
|
||||||
if (antes.cola !== ahora.cola && vistaActual() === 'convertir') cargarCola();
|
if ((antes.cola !== ahora.cola || ahora.cola) && vistaActual() === 'convertir' && !document.querySelector('#convLista input:focus')) cargarCola();
|
||||||
}
|
}
|
||||||
function huella(E){
|
function huella(E){
|
||||||
const h = {};
|
const h = {};
|
||||||
@@ -688,7 +694,7 @@ async function cargarCarpeta(){
|
|||||||
}
|
}
|
||||||
function queSeHizo(lote){
|
function queSeHizo(lote){
|
||||||
if (!lote) return 'Cambio';
|
if (!lote) return 'Cambio';
|
||||||
if (lote === 'optimizar' || lote.startsWith('optimizar-')) return 'Reemplazó originales por MP4 10 bits';
|
if (lote === 'optimizar' || lote.startsWith('optimizar-')) return 'Reemplazó originales por la versión convertida';
|
||||||
if (lote.startsWith('stock-')) return 'Apartó stock de Envato viejo';
|
if (lote.startsWith('stock-')) return 'Apartó stock de Envato viejo';
|
||||||
if (lote.startsWith('dup-')) return 'Juntó material repetido';
|
if (lote.startsWith('dup-')) return 'Juntó material repetido';
|
||||||
return 'Ordenó carpetas y archivos';
|
return 'Ordenó carpetas y archivos';
|
||||||
@@ -720,7 +726,7 @@ function pasosDe(C, r){
|
|||||||
: !cv.camara_bytes ? {e:'hecho', t:'Convertir', d:'No hay material de cámara', corto: 'nada'}
|
: !cv.camara_bytes ? {e:'hecho', t:'Convertir', d:'No hay material de cámara', corto: 'nada'}
|
||||||
: listos ? {e:'listo', t:'Convertir', d:`${listos} convertido${listos > 1 ? 's' : ''} por verificar`, corto: `${listos} sin verificar`}
|
: listos ? {e:'listo', t:'Convertir', d:`${listos} convertido${listos > 1 ? 's' : ''} por verificar`, corto: `${listos} sin verificar`}
|
||||||
: verif ? {e:'listo', t:'Convertir', d:`${verif} verificado${verif > 1 ? 's' : ''} por reemplazar`, corto: `${verif} por reemplazar`}
|
: verif ? {e:'listo', t:'Convertir', d:`${verif} verificado${verif > 1 ? 's' : ''} por reemplazar`, corto: `${verif} por reemplazar`}
|
||||||
: reemp ? {e:'hecho', t:'Convertir', d:`${reemp} clip${reemp > 1 ? 's' : ''} ya en 10 bits`, corto: `${reemp} hechos`}
|
: reemp ? {e:'hecho', t:'Convertir', d:`${reemp} clip${reemp > 1 ? 's' : ''} ya convertido${reemp > 1 ? 's' : ''}`, corto: `${reemp} hechos`}
|
||||||
: {e:'pend', t:'Convertir', d:`${gb(cv.camara_bytes)} de cámara${cv.raw_bytes ? ' · ' + gb(cv.raw_bytes) + ' BRAW' : ''}`, corto: gb(cv.camara_bytes)});
|
: {e:'pend', t:'Convertir', d:`${gb(cv.camara_bytes)} de cámara${cv.raw_bytes ? ' · ' + gb(cv.raw_bytes) + ' BRAW' : ''}`, corto: gb(cv.camara_bytes)});
|
||||||
const rp = C.respaldo || {};
|
const rp = C.respaldo || {};
|
||||||
P.push(!esc ? {e:'bloq', t:'Respaldar', d:'Después de escanear'} : rp.estado === 'corriendo' ? {e:'curso', t:'Respaldar', d:'Copiando…', corto: 'copiando'}
|
P.push(!esc ? {e:'bloq', t:'Respaldar', d:'Después de escanear'} : rp.estado === 'corriendo' ? {e:'curso', t:'Respaldar', d:'Copiando…', corto: 'copiando'}
|
||||||
@@ -739,7 +745,7 @@ function pintarSiguiente(P){
|
|||||||
else if (C.plan) { s.t = `Plan listo: ${C.plan.marcadas} de ${C.plan.filas} movimientos marcados`; s.p = `Todo se mueve dentro de la misma carpeta y nada se borra. ${C.plan.baja ? C.plan.baja + ' son dudosos y están sin marcar. ' : ''}Revisa la lista y aplica; se puede deshacer desde Historial.`; b('Revisar y aplicar', () => { irA('revisar'); subRev('plan'); }, true); }
|
else if (C.plan) { s.t = `Plan listo: ${C.plan.marcadas} de ${C.plan.filas} movimientos marcados`; s.p = `Todo se mueve dentro de la misma carpeta y nada se borra. ${C.plan.baja ? C.plan.baja + ' son dudosos y están sin marcar. ' : ''}Revisa la lista y aplica; se puede deshacer desde Historial.`; b('Revisar y aplicar', () => { irA('revisar'); subRev('plan'); }, true); }
|
||||||
else if (P[2].e === 'pend') { s.t = 'Siguiente: ordenar la carpeta'; s.p = 'Calcula qué va a pasar (basura y proxies a _Para borrar, grabaciones por mes y cámara, piezas numeradas) y aplícalo con un clic. O deja que el asistente te pregunte paso a paso.'; b('Calcular qué va a pasar', () => { irA('revisar'); subRev('plan'); $('#btnPlan').click(); }, true); b('Asistente paso a paso', asis); }
|
else if (P[2].e === 'pend') { s.t = 'Siguiente: ordenar la carpeta'; s.p = 'Calcula qué va a pasar (basura y proxies a _Para borrar, grabaciones por mes y cámara, piezas numeradas) y aplícalo con un clic. O deja que el asistente te pregunte paso a paso.'; b('Calcular qué va a pasar', () => { irA('revisar'); subRev('plan'); $('#btnPlan').click(); }, true); b('Asistente paso a paso', asis); }
|
||||||
else if (P[3].e === 'listo') { const d = P[3].d; s.t = 'Siguiente: ' + (d.includes('verificar') ? 'verificar los clips convertidos' : d.includes('reemplazar') ? 'reemplazar los originales' : 'retomar la conversión'); s.p = d.includes('verificar') ? 'Verificar comprueba códec, 10 bits y duración. Después puedes reemplazar los originales (quedan en _Para borrar/originales).' : d.includes('reemplazar') ? 'Los originales se apartan a _Para borrar/originales y el MP4 10 bits queda en su lugar. Nada se borra.' : 'Hay clips en cola con la conversión parada.'; b('Ir a Convertir', () => irA('convertir'), true); }
|
else if (P[3].e === 'listo') { const d = P[3].d; s.t = 'Siguiente: ' + (d.includes('verificar') ? 'verificar los clips convertidos' : d.includes('reemplazar') ? 'reemplazar los originales' : 'retomar la conversión'); s.p = d.includes('verificar') ? 'Verificar comprueba códec, 10 bits y duración. Después puedes reemplazar los originales (quedan en _Para borrar/originales).' : d.includes('reemplazar') ? 'Los originales se apartan a _Para borrar/originales y el MP4 10 bits queda en su lugar. Nada se borra.' : 'Hay clips en cola con la conversión parada.'; b('Ir a Convertir', () => irA('convertir'), true); }
|
||||||
else if (P[3].e === 'curso') { s.t = 'Convirtiendo clips a 10 bits'; s.p = 'Sigue en segundo plano. El avance está en la barra de abajo; cuando termine, toca reemplazar.'; b('Ver la cola', () => irA('convertir')); }
|
else if (P[3].e === 'curso') { s.t = 'Convirtiendo clips'; s.p = 'Sigue en segundo plano. El avance está en la barra de abajo; cuando termine, toca reemplazar.'; b('Ver la cola', () => irA('convertir')); }
|
||||||
else if (P[3].e === 'pend') { const cv = C.convertir; s.t = 'Siguiente: aligerar el material de cámara'; s.p = `Hay ${gb(cv.camara_bytes)} de clips de cámara. Los de proyectos ya entregados pueden pasar a MP4 10 bits, unas 10 veces más ligeros. ${cv.raw_bytes ? 'Los BRAW necesitan DaVinci Resolve Studio abierto. ' : ''}El original no se toca hasta que reemplaces.`; b('Buscar clips a convertir', () => { irA('convertir'); $('#btnBuscarClips').click(); }, true); b('Saltar a respaldar', () => irA('respaldar')); }
|
else if (P[3].e === 'pend') { const cv = C.convertir; s.t = 'Siguiente: aligerar el material de cámara'; s.p = `Hay ${gb(cv.camara_bytes)} de clips de cámara. Los de proyectos ya entregados pueden pasar a MP4 10 bits, unas 10 veces más ligeros. ${cv.raw_bytes ? 'Los BRAW necesitan DaVinci Resolve Studio abierto. ' : ''}El original no se toca hasta que reemplaces.`; b('Buscar clips a convertir', () => { irA('convertir'); $('#btnBuscarClips').click(); }, true); b('Saltar a respaldar', () => irA('respaldar')); }
|
||||||
else if (P[4].e === 'listo') { s.t = 'Siguiente: copiar el respaldo'; s.p = P[4].d + '. Es incremental: lo que ya esté igual se salta.'; b('Ir a Respaldar', () => irA('respaldar'), true); }
|
else if (P[4].e === 'listo') { s.t = 'Siguiente: copiar el respaldo'; s.p = P[4].d + '. Es incremental: lo que ya esté igual se salta.'; b('Ir a Respaldar', () => irA('respaldar'), true); }
|
||||||
else if (P[4].e === 'curso') { s.t = 'Copiando el respaldo'; s.p = 'Sigue en segundo plano. Puedes detenerlo desde la barra de abajo y retomarlo luego.'; }
|
else if (P[4].e === 'curso') { s.t = 'Copiando el respaldo'; s.p = 'Sigue en segundo plano. Puedes detenerlo desde la barra de abajo y retomarlo luego.'; }
|
||||||
@@ -964,15 +970,26 @@ $('#btnBuscarClips').onclick = () => accion($('#btnBuscarClips'), async () => {
|
|||||||
await cargarEstado();
|
await cargarEstado();
|
||||||
} else await cargarCandidatos();
|
} else await cargarCandidatos();
|
||||||
});
|
});
|
||||||
|
let MOTORES = null;
|
||||||
|
function pintarFormato(){ const f = (ESTADO.formatos || {})[$('#optFormato').value]; $('#fmtQue').textContent = f ? f.que : ''; }
|
||||||
|
$('#optFormato').onchange = () => { pintarFormato(); if (CAND.length) cargarCandidatos().catch(e => aviso(e.message, 'bad')); };
|
||||||
|
async function cargarMotores(){
|
||||||
|
try { MOTORES = await api('/api/optimizar/motores'); } catch (e) { $('#motores').textContent = 'no se pudo detectar GPU/CPU'; return; }
|
||||||
|
const g = MOTORES.gpu, c = MOTORES.cpu;
|
||||||
|
const gpuT = g.hevc || g.h264 ? `GPU ${esc(g.nombre || 'NVIDIA')}: ${[g.hevc && 'H.265', g.h264 && 'H.264'].filter(Boolean).join(' y ')} acelerados` : 'Sin GPU NVIDIA: todo por CPU (más lento)';
|
||||||
|
const cpuT = `CPU: ${[c.prores_ks && 'ProRes', c.libx265 && 'H.265', c.libx264 && 'H.264'].filter(Boolean).join(', ') || 'nada'}`;
|
||||||
|
$('#motores').innerHTML = MOTORES.ffmpeg ? `${gpuT} · ${cpuT}` : '<span class="bad">ffmpeg no encontrado: no se puede convertir</span>';
|
||||||
|
$$('#optFormato option').forEach(o => { const m = MOTORES.formatos[o.value]; o.disabled = !m; o.textContent = (ESTADO.formatos[o.value] || {}).nombre + (m === 'gpu' ? ' · GPU' : m === 'cpu' ? ' · CPU' : ' · no disponible'); });
|
||||||
|
}
|
||||||
async function cargarCandidatos(){
|
async function cargarCandidatos(){
|
||||||
const q = `raiz=${encodeURIComponent(raizActual())}&meses=${+$('#optMeses').value || 0}&perfil=${$('#optPerfil').value}&min_mb=${+$('#optMin').value || 0}&exportados=${$('#optExport').checked ? 1 : 0}`;
|
const q = `raiz=${encodeURIComponent(raizActual())}&meses=${+$('#optMeses').value || 0}&perfil=${$('#optPerfil').value}&min_mb=${+$('#optMin').value || 0}&exportados=${$('#optExport').checked ? 1 : 0}&formato=${$('#optFormato').value}`;
|
||||||
const d = await api('/api/optimizar/candidatos?' + q); CAND = d.candidatos.map(c => ({...c, sel: !c.excluido && !c.en_cola}));
|
const d = await api('/api/optimizar/candidatos?' + q); CAND = d.candidatos.map(c => ({...c, sel: !c.excluido && !c.en_cola}));
|
||||||
$('#anaEstado').textContent = '';
|
$('#anaEstado').textContent = '';
|
||||||
if (!CAND.length) aviso('No se encontraron clips con esos criterios. Prueba a cambiar los meses o el tamaño mínimo.', '');
|
if (!CAND.length) aviso('No se encontraron clips con esos criterios. Prueba a cambiar los meses o el tamaño mínimo.', '');
|
||||||
await cargarCola();
|
await cargarCola();
|
||||||
}
|
}
|
||||||
function resumenCand(){ const s = CAND.filter(c => c.sel); const validos = CAND.filter(c => !c.excluido && !c.en_cola); bloquear('btnEncolar', validos.length ? (s.length ? '' : 'x') : 'x'); $('#btnEncolar').textContent = s.length ? `Convertir ${s.length} marcado${s.length > 1 ? 's' : ''}` : 'Convertir'; }
|
function resumenCand(){ const s = CAND.filter(c => c.sel); const validos = CAND.filter(c => !c.excluido && !c.en_cola); bloquear('btnEncolar', validos.length ? (s.length ? '' : 'x') : 'x'); $('#btnEncolar').textContent = s.length ? `Convertir ${s.length} marcado${s.length > 1 ? 's' : ''}` : 'Convertir'; }
|
||||||
$('#btnEncolar').onclick = () => { const s = CAND.filter(c => c.sel); modal('Convertir a 10 bits', `<p><b>${s.length}</b> clips (${gb(s.reduce((a,c)=>a+c.bytes,0))}) se convierten a MP4 H.265 10 bits, uno tras otro, en segundo plano.</p><p><b>El original no se toca.</b> Cuando terminen y estén verificados, decides si reemplazar.</p>`, () => accion($('#btnEncolar'), async () => { await api('/api/config', {optimizar: {meses_minimos: +$('#optMeses').value || 0, min_mb: +$('#optMin').value || 0, perfil: $('#optPerfil').value, chroma: $('#optChroma').value, color: $('#optColor').value}}); const r = await api('/api/optimizar/encolar', {raiz: raizActual(), rutas: s.map(c => c.ruta), perfil: $('#optPerfil').value, chroma: $('#optChroma').value, color: $('#optColor').value, iniciar: true}); aviso(`${r.ids.length} clips en cola. El avance está en la barra de abajo.`, 'ok'); if (r.ids.length < s.length) aviso(`${s.length - r.ids.length} no se encolaron: ya estaban en cola o su salida ya existe (mismo clip en MP4 y MOV).`, ''); await cargarCandidatos(); await cargarEstado(); })); $('#modalSi').textContent = 'Convertir'; };
|
$('#btnEncolar').onclick = () => { const s = CAND.filter(c => c.sel); const fm = (ESTADO.formatos || {})[$('#optFormato').value] || {nombre: $('#optFormato').value}; modal('Convertir', `<p><b>${s.length}</b> clips (${gb(s.reduce((a,c)=>a+c.bytes,0))}) se convierten a <b>${esc(fm.nombre)}</b>, uno tras otro, en segundo plano. Pesarían ≈ ${gb(s.reduce((a,c)=>a+c.estimado,0))}.</p><p><b>El original no se toca.</b> Cuando terminen y estén verificados, decides si reemplazar.</p>`, () => accion($('#btnEncolar'), async () => { await api('/api/config', {optimizar: {meses_minimos: +$('#optMeses').value || 0, min_mb: +$('#optMin').value || 0, perfil: $('#optPerfil').value, formato: $('#optFormato').value, chroma: $('#optChroma').value, color: $('#optColor').value}}); const r = await api('/api/optimizar/encolar', {raiz: raizActual(), rutas: s.map(c => c.ruta), perfil: $('#optPerfil').value, formato: $('#optFormato').value, chroma: $('#optChroma').value, color: $('#optColor').value, iniciar: true}); aviso(`${r.ids.length} clips en cola. El avance está en la barra de abajo.`, 'ok'); if (r.ids.length < s.length) aviso(`${s.length - r.ids.length} no se encolaron: ya estaban en cola, su salida ya existe (mismo clip en MP4 y MOV) o son BRAW con un formato que no sea H.265 10 bits.`, ''); await cargarCandidatos(); await cargarEstado(); })); $('#modalSi').textContent = 'Convertir'; };
|
||||||
const EST_TXT = {pendiente:'en espera', corriendo:'convirtiendo', listo:'convertido', verificado:'verificado', reemplazado:'reemplazado', error:'falló', cancelado:'cancelado'};
|
const EST_TXT = {pendiente:'en espera', corriendo:'convirtiendo', listo:'convertido', verificado:'verificado', reemplazado:'reemplazado', error:'falló', cancelado:'cancelado'};
|
||||||
async function cargarCola(){
|
async function cargarCola(){
|
||||||
const d = await api('/api/optimizar/cola');
|
const d = await api('/api/optimizar/cola');
|
||||||
@@ -982,7 +999,7 @@ async function cargarCola(){
|
|||||||
ts.sort((x, y) => (orden[x.estado] ?? 9) - (orden[y.estado] ?? 9) || y.id - x.id);
|
ts.sort((x, y) => (orden[x.estado] ?? 9) - (orden[y.estado] ?? 9) || y.id - x.id);
|
||||||
COLA_TS = ts;
|
COLA_TS = ts;
|
||||||
const n = k => ts.filter(t => t.estado === k).length;
|
const n = k => ts.filter(t => t.estado === k).length;
|
||||||
const pend = n('pendiente'), listos = n('listo'), verif = n('verificado'), err = n('error') + n('cancelado');
|
const pend = n('pendiente'), listos = n('listo'), verif = ts.filter(t => t.estado === 'verificado' && !(t.formato || '').startsWith('prores')).length, err = n('error') + n('cancelado');
|
||||||
const est = $('#colaEstado'); est.className = 'est ' + (d.cola.activa ? 'corriendo' : err ? 'error' : verif ? 'verificado' : 'pendiente'); est.textContent = d.cola.activa ? `convirtiendo${d.cola.mensaje ? ' · ' + d.cola.mensaje : ''}` : pend ? `${pend} en espera, parada` : ts.length ? `${verif} verificados · ${n('reemplazado')} reemplazados${err ? ` · ${err} con error` : ''}` : 'sin actividad';
|
const est = $('#colaEstado'); est.className = 'est ' + (d.cola.activa ? 'corriendo' : err ? 'error' : verif ? 'verificado' : 'pendiente'); est.textContent = d.cola.activa ? `convirtiendo${d.cola.mensaje ? ' · ' + d.cola.mensaje : ''}` : pend ? `${pend} en espera, parada` : ts.length ? `${verif} verificados · ${n('reemplazado')} reemplazados${err ? ` · ${err} con error` : ''}` : 'sin actividad';
|
||||||
const tg = $('#btnColaToggle'); tg.style.display = (d.cola.activa || pend) ? '' : 'none'; tg.textContent = d.cola.activa ? 'Detener' : 'Reanudar';
|
const tg = $('#btnColaToggle'); tg.style.display = (d.cola.activa || pend) ? '' : 'none'; tg.textContent = d.cola.activa ? 'Detener' : 'Reanudar';
|
||||||
$('#btnReintentar').style.display = err ? '' : 'none'; $('#btnQuitar').style.display = ts.some(t => ['error','cancelado','reemplazado'].includes(t.estado)) ? '' : 'none'; $('#btnVerificar').style.display = listos ? '' : 'none';
|
$('#btnReintentar').style.display = err ? '' : 'none'; $('#btnQuitar').style.display = ts.some(t => ['error','cancelado','reemplazado'].includes(t.estado)) ? '' : 'none'; $('#btnVerificar').style.display = listos ? '' : 'none';
|
||||||
@@ -990,23 +1007,26 @@ async function cargarCola(){
|
|||||||
bloquear('btnReemplazar', verif ? '' : (listos ? 'hay convertidos sin verificar' : 'x')); $('#btnReemplazar').textContent = verif ? `Reemplazar ${verif} verificado${verif > 1 ? 's' : ''}` : 'Reemplazar';
|
bloquear('btnReemplazar', verif ? '' : (listos ? 'hay convertidos sin verificar' : 'x')); $('#btnReemplazar').textContent = verif ? `Reemplazar ${verif} verificado${verif > 1 ? 's' : ''}` : 'Reemplazar';
|
||||||
const validos = CAND.filter(c => !c.excluido), ahora = validos.reduce((s,c)=>s+c.bytes,0), despues = validos.reduce((s,c)=>s+c.estimado,0);
|
const validos = CAND.filter(c => !c.excluido), ahora = validos.reduce((s,c)=>s+c.bytes,0), despues = validos.reduce((s,c)=>s+c.estimado,0);
|
||||||
const hechosB = ts.filter(t => t.bytes_out).reduce((s,t) => s + (t.bytes_in - t.bytes_out), 0);
|
const hechosB = ts.filter(t => t.bytes_out).reduce((s,t) => s + (t.bytes_in - t.bytes_out), 0);
|
||||||
$('#optCards').innerHTML = (CAND.length || ts.length) ? [[validos.length, 'clips convertibles'], [gb(ahora), 'pesan hoy'], [gb(despues), 'pesarían (estimado)'], [hechosB ? '−' + gb(hechosB) : `${verif}`, hechosB ? `ya liberados · ${verif} verificados` : 'verificados']].map(([b,s]) => `<div class="card"><b>${b}</b><small>${s}</small></div>`).join('') : '';
|
$('#optCards').innerHTML = (CAND.length || ts.length) ? [[validos.length, 'clips convertibles'], [gb(ahora), 'pesan hoy'], [gb(despues), 'pesarían (estimado)'], [hechosB ? (hechosB > 0 ? '−' : '+') + gb(Math.abs(hechosB)) : `${verif}`, hechosB > 0 ? `ya liberados · ${verif} verificados` : hechosB < 0 ? `de más (formato de edición) · ${verif} verificados` : 'verificados']].map(([b,s]) => `<div class="card"><b>${b}</b><small>${s}</small></div>`).join('') : '';
|
||||||
resumenCand();
|
resumenCand();
|
||||||
let html = '';
|
let html = '';
|
||||||
if (CAND.length) {
|
if (CAND.length) {
|
||||||
html += `<div class="cabsec"><label style="display:flex;gap:6px;align-items:center;text-transform:none;letter-spacing:0;font-weight:500"><input type="checkbox" id="candTodos" ${validos.length && CAND.filter(c => c.sel).length === validos.filter(c => !c.en_cola).length ? 'checked' : ''}> marcar todos los válidos</label><span class="sep"></span>${CAND.length} candidatos</div>`;
|
html += `<div class="cabsec"><label style="display:flex;gap:6px;align-items:center;text-transform:none;letter-spacing:0;font-weight:500"><input type="checkbox" id="candTodos" ${validos.length && CAND.filter(c => c.sel).length === validos.filter(c => !c.en_cola).length ? 'checked' : ''}> marcar todos los válidos</label><span class="sep"></span>${CAND.length} candidatos</div>`;
|
||||||
html += CAND.slice(0, 800).map((c, i) => `<div class="cfila ${c.excluido ? 'exc' : ''}"><input type="checkbox" data-i="${i}" ${c.sel ? 'checked' : ''} ${c.excluido || c.en_cola ? 'disabled' : ''} aria-label="marcar"><span class="est candidato">${c.en_cola ? 'en cola' : c.excluido ? 'no' : 'candidato'}</span><span class="nom" title="${esc(c.codec)} ${esc(c.perfil)} ${esc(c.pix_fmt)} · ${esc(c.res)} · ${esc(c.fps)} fps · ${c.kbps} kb/s → ≤ ${c.kbps_objetivo} kb/s"><span class="clip">${esc(c.rel)}</span><span class="sub2">${esc(c.fecha)} · ${esc(c.codec)} ${esc(c.res)} · ${dur(c.dur)} · ${c.motor === 'resolve' ? 'Resolve' : 'ffmpeg'}${c.excluido || c.en_cola ? ' · ' + esc(c.excluido || c.en_cola) : ''}</span></span><span class="tam">${gb(c.bytes)} → ≈ ${gb(c.estimado)}</span><span class="ah">−${c.bytes ? Math.round((1-c.estimado/c.bytes)*100) : 0}%</span></div>`).join('');
|
html += CAND.slice(0, 800).map((c, i) => `<div class="cfila ${c.excluido ? 'exc' : ''}"><input type="checkbox" data-i="${i}" ${c.sel ? 'checked' : ''} ${c.excluido || c.en_cola ? 'disabled' : ''} aria-label="marcar"><span class="est candidato">${c.en_cola ? 'en cola' : c.excluido ? 'no' : 'candidato'}</span><span class="nom" title="${esc(c.codec)} ${esc(c.perfil)} ${esc(c.pix_fmt)} · ${esc(c.res)} · ${esc(c.fps)} fps · ${c.kbps} kb/s → ≤ ${c.kbps_objetivo} kb/s"><span class="clip">${esc(c.rel)}</span><span class="sub2">${esc(c.fecha)} · ${esc(c.codec)} ${esc(c.res)} · ${dur(c.dur)} · ${c.motor === 'resolve' ? 'Resolve' : 'ffmpeg'}${c.excluido || c.en_cola ? ' · ' + esc(c.excluido || c.en_cola) : ''}</span></span><span class="tam">${gb(c.bytes)} → ≈ ${gb(c.estimado)}</span><span class="ah">${c.bytes ? (c.estimado <= c.bytes ? '−' + Math.round((1-c.estimado/c.bytes)*100) : '+' + Math.round((c.estimado/c.bytes-1)*100)) : '0'}%</span></div>`).join('');
|
||||||
}
|
}
|
||||||
if (ts.length) {
|
if (ts.length) {
|
||||||
html += `<div class="cabsec">Conversiones<span class="sep"></span>${ts.length}</div>`;
|
const E = ESTADO && ESTADO.cola; const corr = ts.find(t => t.estado === 'corriendo');
|
||||||
html += ts.slice(0, 120).map(t => `<div class="cfila"><span></span><span class="est ${t.estado}">${EST_TXT[t.estado] || esc(t.estado)}${t.estado === 'corriendo' ? ` ${Math.round(t.pct || 0)} %` : ''}</span><span class="nom" title="${esc(t.ruta)}"><span class="clip">${esc((t.ruta || '').split(/[\\/]/).pop())} <span class="mut" style="font-weight:500;font-size:11px">· ${t.motor === 'resolve' ? 'Resolve' : 'ffmpeg · GPU'}</span></span><span class="sub2">${t.estado === 'error' ? `<span class="bad">${esc((t.log || '').slice(-110))}</span>` : t.estado === 'pendiente' && (t.log || '').startsWith('se interrumpió') ? `<span class="mut">${esc(t.log)}</span>` : esc((t.salida || '').split(/[\\/]/).slice(-2).join(' / '))}</span>${t.estado === 'corriendo' ? `<div class="bar"><i style="width:${Math.round(t.pct || 0)}%"></i></div>` : ''}</span><span class="tam">${gb(t.bytes_in)}${t.bytes_out ? ' → ' + gb(t.bytes_out) : ''}</span><span class="ah">${t.bytes_out ? '−' + Math.round((1 - t.bytes_out / t.bytes_in) * 100) + '%' : ''}</span></div>`).join('') + (ts.length > 120 ? `<div class="mut" style="padding:8px 0">… y ${ts.length - 120} más</div>` : '');
|
if (E && E.activa && E.total) { const pctTot = Math.round(((E.hechos || 0) + ((E.actual && E.actual.pct) || 0) / 100) / E.total * 100); html += `<div class="colabar"><b>Convirtiendo ${Math.min((E.hechos || 0) + 1, E.total)} de ${E.total}</b><span class="b"><i style="width:${pctTot}%"></i></span><b>${pctTot} %</b>${corr ? `<span class="mut">${esc((corr.ruta || '').split(/[\\/]/).pop())} · ${Math.round(corr.pct || 0)} %</span>` : ''}</div>`; }
|
||||||
|
else if (pend) html += `<div class="colabar" style="background:var(--panel2)"><b>${pend} en espera</b><span class="mut">la cola está parada: pulsa Reanudar</span></div>`;
|
||||||
|
html += `<div class="cabsec">Cola de conversión<span class="sep"></span>${ts.length}</div>`;
|
||||||
|
html += ts.slice(0, 120).map(t => `<div class="cfila"><span></span><span class="est ${t.estado}">${EST_TXT[t.estado] || esc(t.estado)}${t.estado === 'corriendo' ? ` ${Math.round(t.pct || 0)} %` : ''}</span><span class="nom" title="${esc(t.ruta)}"><span class="clip">${esc((t.ruta || '').split(/[\\/]/).pop())} <span class="mut" style="font-weight:500;font-size:11px">· ${t.motor === 'resolve' ? 'Resolve' : (MOTORES && MOTORES.formatos[t.formato || 'h265_10bit'] === 'gpu' ? 'GPU' : 'CPU')} · ${esc(((ESTADO.formatos || {})[t.formato || 'h265_10bit'] || {}).nombre || t.formato || '').split(' · ')[0]}</span></span><span class="sub2">${t.estado === 'error' ? `<span class="bad">${esc((t.log || '').slice(-110))}</span>` : t.estado === 'pendiente' && (t.log || '').startsWith('se interrumpió') ? `<span class="mut">${esc(t.log)}</span>` : esc((t.salida || '').split(/[\\/]/).slice(-2).join(' / '))}</span>${t.estado === 'corriendo' ? `<div class="bar"><i style="width:${Math.round(t.pct || 0)}%"></i></div>` : ''}</span><span class="tam">${gb(t.bytes_in)}${t.bytes_out ? ' → ' + gb(t.bytes_out) : ''}</span><span class="ah">${t.bytes_out ? (t.bytes_out <= t.bytes_in ? '−' + Math.round((1 - t.bytes_out / t.bytes_in) * 100) : '+' + Math.round((t.bytes_out / t.bytes_in - 1) * 100)) + '%' : ''}</span></div>`).join('') + (ts.length > 120 ? `<div class="mut" style="padding:8px 0">… y ${ts.length - 120} más</div>` : '');
|
||||||
}
|
}
|
||||||
$('#convLista').innerHTML = html || '<span class="mut">Pulsa «Buscar clips» para ver qué se puede convertir en esta carpeta.</span>';
|
$('#convLista').innerHTML = html || '<span class="mut">Pulsa «Buscar clips» para ver qué se puede convertir en esta carpeta.</span>';
|
||||||
$$('#convLista input[data-i]').forEach(i => i.onchange = () => { CAND[+i.dataset.i].sel = i.checked; resumenCand(); });
|
$$('#convLista input[data-i]').forEach(i => i.onchange = () => { CAND[+i.dataset.i].sel = i.checked; resumenCand(); });
|
||||||
const ct = $('#candTodos'); if (ct) ct.onchange = e => { CAND.forEach(c => c.sel = e.target.checked && !c.excluido && !c.en_cola); cargarCola(); };
|
const ct = $('#candTodos'); if (ct) ct.onchange = e => { CAND.forEach(c => c.sel = e.target.checked && !c.excluido && !c.en_cola); cargarCola(); };
|
||||||
}
|
}
|
||||||
$('#btnColaToggle').onclick = () => accion($('#btnColaToggle'), async () => { await api('/api/optimizar/control', {accion: $('#btnColaToggle').textContent === 'Detener' ? 'detener' : 'iniciar'}); await cargarEstado(); cargarCola(); });
|
$('#btnColaToggle').onclick = () => accion($('#btnColaToggle'), async () => { await api('/api/optimizar/control', {accion: $('#btnColaToggle').textContent === 'Detener' ? 'detener' : 'iniciar'}); await cargarEstado(); cargarCola(); });
|
||||||
const idsPorEstado = async (...est) => { const d = await api('/api/optimizar/cola'); const raiz = raizActual().toLowerCase(); return d.trabajos.filter(t => est.includes(t.estado) && (t.raiz || '').toLowerCase() === raiz).map(t => t.id); };
|
const idsPorEstado = async (...est) => { const d = await api('/api/optimizar/cola'); const raiz = raizActual().toLowerCase(); return d.trabajos.filter(t => est.includes(t.estado) && (t.raiz || '').toLowerCase() === raiz && !(est.includes('verificado') && (t.formato || '').startsWith('prores'))).map(t => t.id); };
|
||||||
$('#btnVerificar').onclick = () => accion($('#btnVerificar'), async () => { const ids = await idsPorEstado('listo'); const r = await api('/api/optimizar/verificar', {ids}); const ok = r.resultados.filter(x => x.ok).length; aviso(`${ok} de ${r.resultados.length} verificados.`, ok === r.resultados.length ? 'ok' : 'bad'); if (ok < r.resultados.length) modal('Verificación', `<pre>${esc(r.resultados.map(x => `#${x.id} ${x.ok ? 'OK' : 'FALLA'} · ${x.motivo}`).join('\n'))}</pre>`, null); cargarCola(); });
|
$('#btnVerificar').onclick = () => accion($('#btnVerificar'), async () => { const ids = await idsPorEstado('listo'); const r = await api('/api/optimizar/verificar', {ids}); const ok = r.resultados.filter(x => x.ok).length; aviso(`${ok} de ${r.resultados.length} verificados.`, ok === r.resultados.length ? 'ok' : 'bad'); if (ok < r.resultados.length) modal('Verificación', `<pre>${esc(r.resultados.map(x => `#${x.id} ${x.ok ? 'OK' : 'FALLA'} · ${x.motivo}`).join('\n'))}</pre>`, null); cargarCola(); });
|
||||||
$('#btnReemplazar').onclick = () => accion($('#btnReemplazar'), async () => { const ids = await idsPorEstado('verificado'); modal('Reemplazar originales', `<p><b>${ids.length}</b> originales se apartan a <span class="path">_Para borrar/originales</span> dentro de su carpeta (no se borran). El MP4 10 bits queda en su lugar.</p><p>Cuando estés seguro, una persona vacía _Para borrar a mano.</p>`, () => accion($('#btnReemplazar'), async () => { const r = await api('/api/optimizar/reemplazar', {ids}); const ok = r.resultados.filter(x => x.ok).length; aviso(`${ok} de ${r.resultados.length} reemplazados.`, ok === r.resultados.length ? 'ok' : 'bad', {texto: 'Ver historial', ir: () => irA('historial')}); if (ok < r.resultados.length) modal('Reemplazo', `<pre>${esc(r.resultados.map(x => `#${x.id} ${x.ok ? 'OK' : 'NO'} · ${x.motivo}`).join('\n'))}</pre>`, null); cargarCola(); })); $('#modalSi').textContent = 'Reemplazar'; });
|
$('#btnReemplazar').onclick = () => accion($('#btnReemplazar'), async () => { const ids = await idsPorEstado('verificado'); modal('Reemplazar originales', `<p><b>${ids.length}</b> originales se apartan a <span class="path">_Para borrar/originales</span> dentro de su carpeta (no se borran). El MP4 10 bits queda en su lugar.</p><p>Cuando estés seguro, una persona vacía _Para borrar a mano.</p>`, () => accion($('#btnReemplazar'), async () => { const r = await api('/api/optimizar/reemplazar', {ids}); const ok = r.resultados.filter(x => x.ok).length; aviso(`${ok} de ${r.resultados.length} reemplazados.`, ok === r.resultados.length ? 'ok' : 'bad', {texto: 'Ver historial', ir: () => irA('historial')}); if (ok < r.resultados.length) modal('Reemplazo', `<pre>${esc(r.resultados.map(x => `#${x.id} ${x.ok ? 'OK' : 'NO'} · ${x.motivo}`).join('\n'))}</pre>`, null); cargarCola(); })); $('#modalSi').textContent = 'Reemplazar'; });
|
||||||
$('#btnReintentar').onclick = () => accion($('#btnReintentar'), async () => { const ids = await idsPorEstado('error', 'cancelado'); await api('/api/optimizar/control', {accion: 'reintentar', ids}); await api('/api/optimizar/control', {accion: 'iniciar'}); cargarCola(); });
|
$('#btnReintentar').onclick = () => accion($('#btnReintentar'), async () => { const ids = await idsPorEstado('error', 'cancelado'); await api('/api/optimizar/control', {accion: 'reintentar', ids}); await api('/api/optimizar/control', {accion: 'iniciar'}); cargarCola(); });
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user