3.9.0: la cola más rápida: la tarjeta descomprime, dos clips a la vez, y las conversiones sueltas se cierran
Medido en la RTX 5080 del estudio con la cola de Vía Sumapaz (1.920 clips, unas 26 horas): el codificador iba al 46-74 % y el descompresor al 0 %. Con NVDEC un H.264 4K pasa de 0,54× a 0,86× tiempo real; dos a la vez salen a 1,29× entre los dos. Además: al arrancar se cierran las conversiones que quedaron sin su programa (una de las 10:04 seguía colgada a las 17:00), «Detener» cierra el ffmpeg del lector de BRAW, la cola se pide entera por carpeta (antes, los 500 últimos de todas), y lo encolado que ya no compensa con la regla del 30 % no se convierte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
25
README.md
25
README.md
@@ -915,6 +915,31 @@ Pruebas: `prueba_orden.py` 24 casos (5 nuevos: pieza, bandeja, tarjeta con una c
|
|||||||
que rechazar, versiones con ffprobe simulado, y la pasada automática con sus frenos); `prueba_funcional.py`
|
que rechazar, versiones con ffprobe simulado, y la pasada automática con sus frenos); `prueba_funcional.py`
|
||||||
29 de 29 (3 nuevas, por la interfaz de verdad del servidor); `probar.py` 19 y `prueba_ventana.py` 18.
|
29 de 29 (3 nuevas, por la interfaz de verdad del servidor); `probar.py` 19 y `prueba_ventana.py` 18.
|
||||||
|
|
||||||
|
3.9.0, la cola más rápida. Munka: «ayúdame a mejorar todo lo que se pueda en la app». Medido en su equipo el
|
||||||
|
14-oct-2026, con 1.920 clips de Vía Sumapaz en espera (31,8 horas de clip, 1,58 TB, unas 26 horas de cola): el
|
||||||
|
codificador de la RTX 5080 iba al 46-74 %, la tarjeta entera al 9-18 % y su descompresor al 0 %.
|
||||||
|
|
||||||
|
- **La tarjeta descomprime** los H.264, H.265, AV1, VP9 y MPEG-2 (NVDEC): hasta ahora lo hacía el procesador y la
|
||||||
|
tarjeta esperaba. Con dos clips 4K de la cola de Sumapaz, un H.264 pasó de 0,54× a 0,86× tiempo real y un H.265
|
||||||
|
de 0,70× a 0,85×, con el mismo archivo de salida. Si con ella un clip no sale, se repite con el procesador.
|
||||||
|
- **Dos clips a la vez** (la RTX 5080 tiene dos codificadores): 1,29× tiempo real entre los dos, frente a unos
|
||||||
|
0,77× de uno solo. Juntando las dos cosas, un H.264 de esos va más del doble de rápido. Se cambia en «cambiar» ›
|
||||||
|
«a la vez» (automático: dos con tarjeta NVIDIA, uno sin ella). La ventana, la barra de abajo y la de Windows
|
||||||
|
enseñan los dos, y el «termina hacia las…» cuenta con ellos.
|
||||||
|
- **Las conversiones sueltas se cierran al arrancar.** Una de las 10:04, de antes de actualizar a la 3.8.0 (el
|
||||||
|
lector de BRAW y su ffmpeg), seguía colgada a las 17:00 con una sesión del codificador y memoria de la tarjeta.
|
||||||
|
Ahora, al abrir, se cierran las que no tienen quien las espere (no las de la pasada de la noche) y se borra su
|
||||||
|
«.parcial»; y «Detener» cierra también el ffmpeg del lector de BRAW, que quedaba vivo.
|
||||||
|
- **La cola, entera.** Se pedían los 500 trabajos más recientes de todas las carpetas: con 1.920 en espera en Vía
|
||||||
|
Sumapaz, los de Línea Gene no salían (ni su «Reemplazar 12 verificados») y el «termina hacia las…» contaba 500
|
||||||
|
en espera, no 1.920 (decía 7 horas y eran 26). Ahora se pide la cola de la carpeta, entera, y el cálculo mira todo.
|
||||||
|
- **Lo que ya no compensa no se convierte**: lo que se encoló con la regla de antes y con la del 30 % ya no se
|
||||||
|
ofrecería (en Sumapaz, 21 H.265) sale de la cola al llegarle el turno, y se dice cuántos.
|
||||||
|
|
||||||
|
Pruebas: `prueba_orden.py` 46 casos (cinco nuevos: dos a la vez y «Detener», las conversiones sueltas, la tarjeta
|
||||||
|
descomprime y su vuelta al procesador si falla, lo que ya no compensa); `prueba_funcional.py` 33 de 33 (la
|
||||||
|
conversión de verdad ya descomprime con la tarjeta), `probar.py` 19 y `prueba_ventana.py` 18.
|
||||||
|
|
||||||
3.8.1, convertir todo lo que ahorre un 30 %. Munka: «quiero que si un archivo se puede optimizar así sea un 30 %
|
3.8.1, convertir todo lo que ahorre un 30 %. Munka: «quiero que si un archivo se puede optimizar así sea un 30 %
|
||||||
se convierta, la idea es aprovechar lo máximo el espacio».
|
se convierta, la idea es aprovechar lo máximo el espacio».
|
||||||
|
|
||||||
|
|||||||
355
organizador.py
355
organizador.py
@@ -42,7 +42,7 @@ import actualizador
|
|||||||
asistente.O = sys.modules[__name__]
|
asistente.O = sys.modules[__name__]
|
||||||
actualizador.O = sys.modules[__name__]
|
actualizador.O = sys.modules[__name__]
|
||||||
|
|
||||||
VERSION = "3.8.1"
|
VERSION = "3.9.0"
|
||||||
|
|
||||||
BASE = os.path.dirname(os.path.abspath(sys.executable if getattr(sys, "frozen", False) else __file__))
|
BASE = os.path.dirname(os.path.abspath(sys.executable if getattr(sys, "frozen", False) else __file__))
|
||||||
# Empaquetado: el programa corre con el motor que viaja en su propia carpeta. OrganizadorLabstream.exe es
|
# Empaquetado: el programa corre con el motor que viaja en su propia carpeta. OrganizadorLabstream.exe es
|
||||||
@@ -254,6 +254,11 @@ def motores():
|
|||||||
out["gpu"]["uhq"] = r.returncode == 0
|
out["gpu"]["uhq"] = r.returncode == 0
|
||||||
except Exception: # noqa
|
except Exception: # noqa
|
||||||
out["gpu"]["uhq"] = False
|
out["gpu"]["uhq"] = False
|
||||||
|
try: # ¿descomprime la tarjeta (NVDEC)? ver _decodificar_con_tarjeta
|
||||||
|
r = subprocess.run([ff, "-hide_banner", "-hwaccels"], capture_output=True, text=True, timeout=20, creationflags=SIN_VENTANA)
|
||||||
|
out["gpu"]["nvdec"] = bool(out["gpu"]["hevc"] or out["gpu"]["h264"]) and "cuda" in (r.stdout or "").split()
|
||||||
|
except Exception: # noqa
|
||||||
|
out["gpu"]["nvdec"] = False
|
||||||
try:
|
try:
|
||||||
r = subprocess.run(["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], capture_output=True, text=True, timeout=10, creationflags=SIN_VENTANA)
|
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 ""
|
out["gpu"]["nombre"] = (r.stdout or "").strip().splitlines()[0] if r.returncode == 0 and r.stdout.strip() else ""
|
||||||
@@ -3747,7 +3752,7 @@ def encolar(rutas, perfil, chroma, color, raiz, formato="h265_10bit"):
|
|||||||
# empezo: cuándo arrancó el clip que va; ritmo: (segundos de clip, segundos reales) de cada clip acabado en
|
# empezo: cuándo arrancó el clip que va; ritmo: (segundos de clip, segundos reales) de cada clip acabado en
|
||||||
# esta tanda, para decir cuánto falta
|
# esta tanda, para decir cuánto falta
|
||||||
COLA = {"activa": False, "actual": None, "cancelar": False, "hilo": None, "mensaje": "", "hechos": 0, "inicio": 0, "empezo": 0, "ritmo": [],
|
COLA = {"activa": False, "actual": None, "cancelar": False, "hilo": None, "mensaje": "", "hechos": 0, "inicio": 0, "empezo": 0, "ritmo": [],
|
||||||
"esperando": "", "hechos_ids": [], "ultima": None}
|
"esperando": "", "hechos_ids": [], "ultima": None, "actuales": {}, "saltados": 0}
|
||||||
|
|
||||||
|
|
||||||
def _opt(k, defecto=None):
|
def _opt(k, defecto=None):
|
||||||
@@ -4063,6 +4068,22 @@ def _quitar_parcial(tmp):
|
|||||||
time.sleep(0.25)
|
time.sleep(0.25)
|
||||||
|
|
||||||
|
|
||||||
|
# Los códecs que descomprime la tarjeta. ProRes, DNxHD y MJPEG no: esos los sigue leyendo el procesador.
|
||||||
|
NVDEC = {"h264", "hevc", "av1", "vp9", "mpeg2video"}
|
||||||
|
|
||||||
|
|
||||||
|
def _decodificar_con_tarjeta(f, motor):
|
||||||
|
"""["-hwaccel", "cuda"] si el clip lo puede descomprimir la tarjeta, o []. Hasta la 3.8 lo descomprimía el
|
||||||
|
procesador y la tarjeta esperaba: medido el 14-oct-2026 en la RTX 5080 del estudio, con la cola en marcha,
|
||||||
|
el codificador iba al 46-74 % y el descompresor de la tarjeta al 0 %. Con dos clips 4K de Vía Sumapaz, un
|
||||||
|
H.264 pasó de 0,54× a 0,86× tiempo real y un H.265 de 0,70× a 0,85×, con el mismo archivo de salida. Los
|
||||||
|
cuadros vuelven a la memoria (sin «-hwaccel_output_format cuda»): el formato de píxel y los filtros siguen
|
||||||
|
igual, y hacerlo todo en la tarjeta no ganó nada medible. Si con ella falla, se repite con el procesador."""
|
||||||
|
if motor != "gpu" or not f or (f["codec"] or "") not in NVDEC or _opt("sin_nvdec", False):
|
||||||
|
return []
|
||||||
|
return ["-hwaccel", "cuda"] if motores()["gpu"].get("nvdec") else []
|
||||||
|
|
||||||
|
|
||||||
def correr_ffmpeg(t):
|
def correr_ffmpeg(t):
|
||||||
con = db()
|
con = db()
|
||||||
f = con.execute("SELECT * FROM archivos WHERE ruta = ?", (t["ruta"],)).fetchone()
|
f = con.execute("SELECT * FROM archivos WHERE ruta = ?", (t["ruta"],)).fetchone()
|
||||||
@@ -4083,7 +4104,9 @@ def correr_ffmpeg(t):
|
|||||||
tmp = salida + ".parcial" + fm["ext"]
|
tmp = salida + ".parcial" + fm["ext"]
|
||||||
video, _ = _args_video(t, fm, motor, fps)
|
video, _ = _args_video(t, fm, motor, fps)
|
||||||
contenedor, audio = _args_salida(fm, f)
|
contenedor, audio = _args_salida(fm, f)
|
||||||
cmd = [ruta_herramienta("ffmpeg"), "-hide_banner", "-nostdin", "-y", "-i", t["ruta"], "-map", "0:v:0", "-map", "0:a?"]
|
con_tarjeta = _decodificar_con_tarjeta(f, motor)
|
||||||
|
for entrada in ([con_tarjeta, []] if con_tarjeta else [[]]):
|
||||||
|
cmd = [ruta_herramienta("ffmpeg"), "-hide_banner", "-nostdin", "-y", *entrada, "-i", t["ruta"], "-map", "0:v:0", "-map", "0:a?"]
|
||||||
cmd += video + ["-map_metadata", "0"] + contenedor + audio
|
cmd += video + ["-map_metadata", "0"] + contenedor + audio
|
||||||
cmd += ["-progress", "pipe:1", "-nostats", "-loglevel", "error", tmp]
|
cmd += ["-progress", "pipe:1", "-nostats", "-loglevel", "error", tmp]
|
||||||
set_trabajo(t["id"], log=" ".join(cmd)[:4000])
|
set_trabajo(t["id"], log=" ".join(cmd)[:4000])
|
||||||
@@ -4099,9 +4122,7 @@ def correr_ffmpeg(t):
|
|||||||
break
|
break
|
||||||
if linea.startswith("out_time_us=") or linea.startswith("out_time_ms="):
|
if linea.startswith("out_time_us=") or linea.startswith("out_time_ms="):
|
||||||
try:
|
try:
|
||||||
us = int(linea.split("=")[1])
|
us = int(linea.split("=")[1]) # ffmpeg reporta microsegundos en las dos claves
|
||||||
if linea.startswith("out_time_ms="):
|
|
||||||
us = us # ffmpeg reporta microsegundos en ambas claves
|
|
||||||
pct = min(99.0, us / 1e6 / dur * 100) if dur else 0
|
pct = min(99.0, us / 1e6 / dur * 100) if dur else 0
|
||||||
if time.time() - ultimo > 1:
|
if time.time() - ultimo > 1:
|
||||||
set_trabajo(t["id"], pct=pct)
|
set_trabajo(t["id"], pct=pct)
|
||||||
@@ -4114,10 +4135,13 @@ def correr_ffmpeg(t):
|
|||||||
set_trabajo(t["id"], estado="cancelado", pct=0)
|
set_trabajo(t["id"], estado="cancelado", pct=0)
|
||||||
_quitar_parcial(tmp)
|
_quitar_parcial(tmp)
|
||||||
return
|
return
|
||||||
if p.returncode != 0 or not os.path.exists(tmp):
|
if p.returncode == 0 and os.path.exists(tmp):
|
||||||
set_trabajo(t["id"], estado="error", log=(err or "")[-3000:])
|
break
|
||||||
_quitar_parcial(tmp) # si no, el .parcial queda y el siguiente escaneo lo inventaría como video
|
_quitar_parcial(tmp) # si no, el .parcial queda y el siguiente escaneo lo inventaría como video
|
||||||
|
if not entrada:
|
||||||
|
set_trabajo(t["id"], estado="error", log=(err or "")[-3000:])
|
||||||
return
|
return
|
||||||
|
# con la tarjeta descomprimiendo no salió: se repite con el procesador, como hasta la 3.8
|
||||||
os.replace(tmp, salida)
|
os.replace(tmp, salida)
|
||||||
try:
|
try:
|
||||||
st = os.stat(t["ruta"])
|
st = os.stat(t["ruta"])
|
||||||
@@ -4182,14 +4206,15 @@ def correr_braw(t):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def vigia():
|
def vigia():
|
||||||
# Detener: se le pide al lector que pare (él cierra ffmpeg y borra lo que llevaba); si no hace caso, se corta
|
# Detener: se le pide al lector que pare (él cierra ffmpeg y borra lo que llevaba); si no hace caso, se
|
||||||
|
# corta, y con él su ffmpeg (p.kill() solo cerraba el lector, y su ffmpeg seguía vivo)
|
||||||
while p.poll() is None:
|
while p.poll() is None:
|
||||||
if COLA["cancelar"]:
|
if COLA["cancelar"]:
|
||||||
try:
|
try:
|
||||||
open(bandera, "w").close()
|
open(bandera, "w").close()
|
||||||
p.wait(20)
|
p.wait(20)
|
||||||
except (OSError, subprocess.TimeoutExpired):
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
p.kill()
|
_matar_arbol(p.pid)
|
||||||
return
|
return
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
threading.Thread(target=vigia, daemon=True).start()
|
threading.Thread(target=vigia, daemon=True).start()
|
||||||
@@ -4342,61 +4367,146 @@ def _clip_presente(t):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def bucle_cola():
|
def simultaneas():
|
||||||
|
"""Cuántos clips convierte la cola a la vez: optimizar.simultaneas, o, si es 0, dos con tarjeta NVIDIA y uno
|
||||||
|
sin ella. Medido el 14-oct-2026 en la RTX 5080 del estudio, que tiene dos codificadores: con un clip a la vez
|
||||||
|
el codificador iba al 46-74 % y la tarjeta entera al 9-18 %; dos clips 4K a la vez salen a 1,29× tiempo real
|
||||||
|
entre los dos, frente a unos 0,77× de uno solo."""
|
||||||
try:
|
try:
|
||||||
while COLA["activa"]:
|
n = int(_opt("simultaneas", 0) or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
n = 0
|
||||||
|
if n <= 0:
|
||||||
|
g = motores()["gpu"]
|
||||||
|
n = 2 if (g.get("hevc") or g.get("h264")) else 1
|
||||||
|
return max(1, min(3, n))
|
||||||
|
|
||||||
|
|
||||||
|
_COLA_TOMA = threading.Lock() # para que dos trabajadores no cojan el mismo clip
|
||||||
|
|
||||||
|
|
||||||
|
def _fijar_actual():
|
||||||
|
"""«actual» es el primero de los que van (lo que enseñan la barra de abajo y la de Windows); «actuales», todos."""
|
||||||
|
ids = sorted(COLA["actuales"])
|
||||||
|
COLA["actual"] = ids[0] if ids else None
|
||||||
|
COLA["empezo"] = COLA["actuales"].get(ids[0], 0) if ids else 0
|
||||||
|
|
||||||
|
|
||||||
|
def _soltar(*ids):
|
||||||
|
with _COLA_TOMA:
|
||||||
|
for i in ids:
|
||||||
|
COLA["actuales"].pop(i, None)
|
||||||
|
_fijar_actual()
|
||||||
|
|
||||||
|
|
||||||
|
def _tomar(n):
|
||||||
|
"""El siguiente clip para el trabajador n, ya apartado para que no lo coja otro. Los BRAW que van por el guion
|
||||||
|
de Resolve (un lote de hasta 40) solo los coge el primero, y solo cuando no queda nada más."""
|
||||||
|
with _COLA_TOMA:
|
||||||
|
ocupados = list(COLA["actuales"])
|
||||||
|
filtro = f" AND id NOT IN ({','.join('?' * len(ocupados))})" if ocupados else ""
|
||||||
con = db()
|
con = db()
|
||||||
t = con.execute("SELECT * FROM trabajos WHERE estado = 'pendiente' AND motor IN ('ffmpeg', 'braw') ORDER BY orden DESC, id LIMIT 1").fetchone()
|
try:
|
||||||
lote_resolve = [dict(r) for r in con.execute("SELECT * FROM trabajos WHERE estado = 'pendiente' AND motor = 'resolve' ORDER BY id LIMIT 40").fetchall()]
|
t = con.execute("SELECT * FROM trabajos WHERE estado = 'pendiente' AND motor IN ('ffmpeg', 'braw')" + filtro +
|
||||||
|
" ORDER BY orden DESC, id LIMIT 1", ocupados).fetchone()
|
||||||
|
lote = [] if t or n else [dict(r) for r in con.execute(
|
||||||
|
"SELECT * FROM trabajos WHERE estado = 'pendiente' AND motor = 'resolve'" + filtro + " ORDER BY id LIMIT 40", ocupados).fetchall()]
|
||||||
|
finally:
|
||||||
con.close()
|
con.close()
|
||||||
if t and not _esperar_turno():
|
for i in ([t["id"]] if t else [r["id"] for r in lote]):
|
||||||
break
|
COLA["actuales"][i] = time.time()
|
||||||
if t:
|
_fijar_actual()
|
||||||
t = _clip_presente(dict(t))
|
return (dict(t) if t else None), lote
|
||||||
if not t:
|
|
||||||
continue
|
|
||||||
if not t and lote_resolve:
|
def _ya_no_compensa(t):
|
||||||
lote_resolve = [x for x in map(_clip_presente, lote_resolve) if x]
|
"""Una conversión en espera que con la regla de ahorro de ahora no se ofrecería (se encoló con la de antes): no
|
||||||
if not lote_resolve:
|
se convierte. Solo en los formatos que existen para pesar menos. Devuelve el porqué, o «»."""
|
||||||
|
if FORMATOS.get(formato_de(t), FORMATOS["h265_10bit"])["codec"] != "hevc" or t["motor"] != "ffmpeg":
|
||||||
|
return ""
|
||||||
|
con = db()
|
||||||
|
f = con.execute("SELECT * FROM archivos WHERE ruta = ?", (t["ruta"],)).fetchone()
|
||||||
|
con.close()
|
||||||
|
if not f or not f["dur"] or not f["kbps"]:
|
||||||
|
return ""
|
||||||
|
gana = round((1 - estimar_bytes(f, t["perfil"], formato_de(t)) / (f["kbps"] * 1000 / 8 * f["dur"])) * 100)
|
||||||
|
ahorro = ahorro_minimo()
|
||||||
|
return f"convertido ahorraría solo un {max(gana, 0)} %, y se convierte desde un {ahorro} %" if gana < ahorro else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _trabajador(n):
|
||||||
|
"""Uno de los que convierten a la vez: coge el siguiente clip, lo convierte y vuelve a por otro. Si no queda
|
||||||
|
nada pero otro aún trabaja, espera un poco por si se encola algo más (así no se pierde el «a la vez»)."""
|
||||||
|
while COLA["activa"] and not COLA["cancelar"]:
|
||||||
|
t, lote = _tomar(n)
|
||||||
|
if not t and not lote:
|
||||||
|
if any(COLA["actuales"]):
|
||||||
|
time.sleep(3)
|
||||||
continue
|
continue
|
||||||
|
return
|
||||||
|
ids = [t["id"]] if t else [r["id"] for r in lote]
|
||||||
|
if not _esperar_turno():
|
||||||
|
_soltar(*ids)
|
||||||
|
return
|
||||||
if t:
|
if t:
|
||||||
COLA["actual"] = t["id"]
|
t = _clip_presente(t)
|
||||||
COLA["empezo"] = time.time()
|
pega = _ya_no_compensa(t) if t else ""
|
||||||
|
if pega:
|
||||||
|
con = db()
|
||||||
|
with con:
|
||||||
|
con.execute("DELETE FROM trabajos WHERE id = ? AND estado = 'pendiente'", (t["id"],))
|
||||||
|
con.close()
|
||||||
|
COLA["saltados"] += 1
|
||||||
|
COLA["mensaje"] = f"{COLA['saltados']} sin convertir: con la regla de ahorro de ahora ya no compensaban ({pega})"
|
||||||
|
if not t or pega:
|
||||||
|
_soltar(*ids)
|
||||||
|
continue
|
||||||
|
COLA["actuales"][t["id"]] = empezo = time.time() # el tiempo cuenta desde que empieza de verdad
|
||||||
|
_fijar_actual()
|
||||||
set_trabajo(t["id"], estado="corriendo", pct=0)
|
set_trabajo(t["id"], estado="corriendo", pct=0)
|
||||||
try:
|
try:
|
||||||
(correr_braw if t["motor"] == "braw" else correr_ffmpeg)(dict(t))
|
(correr_braw if t["motor"] == "braw" else correr_ffmpeg)(t)
|
||||||
except Exception as ex: # noqa
|
except Exception as ex: # noqa
|
||||||
set_trabajo(t["id"], estado="error", log=f"fallo inesperado: {ex}")
|
set_trabajo(t["id"], estado="error", log=f"fallo inesperado: {ex}")
|
||||||
_verificar_listos([t["id"]])
|
_verificar_listos([t["id"]])
|
||||||
_anotar_ritmo(t["id"], time.time() - COLA["empezo"])
|
_anotar_ritmo(t["id"], time.time() - empezo)
|
||||||
if CONFIG["optimizar"].get("auto_reemplazar"):
|
if CONFIG["optimizar"].get("auto_reemplazar"):
|
||||||
try: # solo reemplaza lo verificado; lo demás lo deja como está
|
try: # solo reemplaza lo verificado; lo demás lo deja como está
|
||||||
reemplazar([t["id"]])
|
reemplazar([t["id"]])
|
||||||
except Exception as ex: # noqa
|
except Exception as ex: # noqa
|
||||||
COLA["mensaje"] = f"no se pudo reemplazar: {ex}"
|
COLA["mensaje"] = f"no se pudo reemplazar: {ex}"
|
||||||
COLA["hechos"] += 1
|
hechos = [t["id"]]
|
||||||
COLA["hechos_ids"].append(t["id"])
|
|
||||||
elif lote_resolve:
|
|
||||||
COLA["actual"] = lote_resolve[0]["id"]
|
|
||||||
try:
|
|
||||||
correr_resolve(lote_resolve)
|
|
||||||
except Exception as ex: # noqa
|
|
||||||
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)
|
|
||||||
COLA["hechos_ids"].extend(r["id"] for r in lote_resolve)
|
|
||||||
else:
|
else:
|
||||||
break
|
lote = [x for x in map(_clip_presente, lote) if x]
|
||||||
COLA["actual"] = None
|
try:
|
||||||
if COLA["cancelar"]:
|
if lote:
|
||||||
COLA["cancelar"] = False
|
correr_resolve(lote)
|
||||||
break
|
except Exception as ex: # noqa
|
||||||
|
for r in lote:
|
||||||
|
set_trabajo(r["id"], estado="error", log=f"fallo inesperado del puente: {ex}")
|
||||||
|
_verificar_listos([r["id"] for r in lote])
|
||||||
|
hechos = [r["id"] for r in lote]
|
||||||
|
with _COLA_TOMA:
|
||||||
|
COLA["hechos"] += len(hechos)
|
||||||
|
COLA["hechos_ids"].extend(hechos)
|
||||||
|
_soltar(*ids)
|
||||||
|
|
||||||
|
|
||||||
|
def bucle_cola():
|
||||||
|
"""La cola: arranca los trabajadores (ver simultaneas) y espera a que acaben todos."""
|
||||||
|
try:
|
||||||
|
hilos = [threading.Thread(target=_trabajador, args=(i,), daemon=True) for i in range(simultaneas())]
|
||||||
|
for h in hilos:
|
||||||
|
h.start()
|
||||||
|
for h in hilos:
|
||||||
|
h.join()
|
||||||
except Exception as ex: # noqa
|
except Exception as ex: # noqa
|
||||||
COLA["mensaje"] = f"la cola se detuvo por un error: {ex}"
|
COLA["mensaje"] = f"la cola se detuvo por un error: {ex}"
|
||||||
finally:
|
finally:
|
||||||
# pase lo que pase, la cola no queda «activa» para siempre (bloqueaba el cierre del programa)
|
# pase lo que pase, la cola no queda «activa» para siempre (bloqueaba el cierre del programa)
|
||||||
COLA["activa"] = False
|
COLA["activa"] = False
|
||||||
|
COLA["cancelar"] = False
|
||||||
|
COLA["actuales"] = {}
|
||||||
COLA["actual"] = None
|
COLA["actual"] = None
|
||||||
COLA["hilo"] = None
|
COLA["hilo"] = None
|
||||||
COLA["esperando"] = ""
|
COLA["esperando"] = ""
|
||||||
@@ -4420,42 +4530,50 @@ def _anotar_ritmo(id_, segundos):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def eta_cola(trabajos):
|
def eta_cola(trabajos=None):
|
||||||
"""Cuánto falta para que acabe la cola: segundos de clip pendientes partidos por el ritmo medido (clips ya
|
"""Cuánto falta para que acabe la cola: segundos de clip pendientes partidos por el ritmo medido (clips ya
|
||||||
hechos en esta tanda; si no hay ninguno, el avance del que va; si tampoco, a tiempo real los BRAW y a 2,5×
|
hechos en esta tanda; si no hay ninguno, el avance de los que van; si tampoco, a tiempo real los BRAW y a 2,5×
|
||||||
la tarjeta). Devuelve también el clip que va, con lo que le queda."""
|
la tarjeta), por cuántos van a la vez. Sin lista, mira toda la cola: hasta la 3.8 contaba solo los 500 últimos
|
||||||
pend = [t for t in trabajos if t["estado"] in ("pendiente", "corriendo")]
|
trabajos, y con 1.920 en espera en Vía Sumapaz decía que acababa en 7 horas y no en 26. Devuelve también los
|
||||||
|
clips que van, con lo que le queda a cada uno."""
|
||||||
|
con = db()
|
||||||
|
if trabajos is None:
|
||||||
|
pend = [dict(r) for r in con.execute("SELECT t.id, t.estado, t.pct, t.motor, a.dur AS dur FROM trabajos t "
|
||||||
|
"LEFT JOIN archivos a ON a.ruta = t.ruta WHERE t.estado IN ('pendiente','corriendo')")]
|
||||||
|
else:
|
||||||
|
pend = [dict(t) for t in trabajos if t["estado"] in ("pendiente", "corriendo")]
|
||||||
|
for t in pend:
|
||||||
|
if t.get("dur") is None:
|
||||||
|
f = con.execute("SELECT dur FROM archivos WHERE ruta = ?", (t["ruta"],)).fetchone()
|
||||||
|
t["dur"] = f["dur"] if f else 0
|
||||||
|
con.close()
|
||||||
if not pend:
|
if not pend:
|
||||||
return None
|
return None
|
||||||
con = db()
|
|
||||||
durs = {}
|
|
||||||
for t in pend:
|
|
||||||
f = con.execute("SELECT dur FROM archivos WHERE ruta = ?", (t["ruta"],)).fetchone()
|
|
||||||
durs[t["id"]] = float((f["dur"] if f else 0) or 0)
|
|
||||||
con.close()
|
|
||||||
ritmo = COLA.get("ritmo") or []
|
ritmo = COLA.get("ritmo") or []
|
||||||
clip_s, real_s = sum(d for d, _ in ritmo), sum(s for _, s in ritmo)
|
clip_s, real_s = sum(d for d, _ in ritmo), sum(s for _, s in ritmo)
|
||||||
vel, medido = (clip_s / real_s if clip_s > 0 and real_s > 0 else 0.0), "tanda" if ritmo else ""
|
vel, medido = (clip_s / real_s if clip_s > 0 and real_s > 0 else 0.0), "tanda" if ritmo else ""
|
||||||
falta, actual, ahora = 0.0, None, time.time()
|
falta, actuales, ahora = 0.0, [], time.time()
|
||||||
for t in pend:
|
for t in pend:
|
||||||
d = durs[t["id"]]
|
d = float(t.get("dur") or 0)
|
||||||
if t["estado"] == "corriendo":
|
if t["estado"] == "corriendo":
|
||||||
pct = float(t["pct"] or 0)
|
pct = float(t["pct"] or 0)
|
||||||
resto = d * max(0.0, 1 - pct / 100)
|
resto = d * max(0.0, 1 - pct / 100)
|
||||||
falta += resto
|
falta += resto
|
||||||
llevado = ahora - (COLA.get("empezo") or ahora)
|
llevado = ahora - (COLA["actuales"].get(t["id"]) or COLA.get("empezo") or ahora)
|
||||||
if not vel and pct >= 3 and llevado > 5:
|
if not vel and pct >= 3 and llevado > 5:
|
||||||
vel, medido = (d * pct / 100) / llevado, "clip"
|
vel, medido = (d * pct / 100) / llevado, "clip"
|
||||||
actual = {"id": t["id"], "dur": d, "resto": resto, "llevado": llevado}
|
actuales.append({"id": t["id"], "dur": d, "resto": resto, "llevado": llevado})
|
||||||
else:
|
else:
|
||||||
falta += d
|
falta += d
|
||||||
if not vel:
|
if not vel:
|
||||||
vel = 1.0 if any(t["motor"] == "braw" for t in pend) else 2.5
|
vel = 1.0 if any(t["motor"] == "braw" for t in pend) else 2.5
|
||||||
seg = falta / vel if falta else 0.0
|
paralelo = max(1, min(simultaneas() if COLA["activa"] else 1, len(pend)))
|
||||||
if actual:
|
seg = falta / (vel * paralelo) if falta else 0.0
|
||||||
actual["resto_seg"] = actual["resto"] / vel
|
for a in actuales:
|
||||||
return {"clip_seg": round(falta), "seg": round(seg), "vel": round(vel, 2), "medido": medido, "termina": ahora + seg if seg else 0,
|
a["resto_seg"] = a["resto"] / vel
|
||||||
"actual": actual, "en_espera": sum(1 for t in pend if t["estado"] == "pendiente"), "hechos": len(ritmo)}
|
return {"clip_seg": round(falta), "seg": round(seg), "vel": round(vel * paralelo, 2), "vel_clip": round(vel, 2), "paralelo": paralelo,
|
||||||
|
"medido": medido, "termina": ahora + seg if seg else 0, "actual": actuales[0] if actuales else None, "actuales": actuales,
|
||||||
|
"en_espera": sum(1 for t in pend if t["estado"] == "pendiente"), "hechos": len(ritmo)}
|
||||||
|
|
||||||
|
|
||||||
def _ruta_original(t):
|
def _ruta_original(t):
|
||||||
@@ -4581,6 +4699,78 @@ def calidad_de(id_, segundos=10, ancho=1920):
|
|||||||
return {"id": id_, "vmaf": nota, "minimo": minimo, "cuadros": cuadros, "desde_seg": round(desde / fps, 2), "seg": round(time.time() - t0, 1)}
|
return {"id": id_, "vmaf": nota, "minimo": minimo, "cuadros": cuadros, "desde_seg": round(desde / fps, 2), "seg": round(time.time() - t0, 1)}
|
||||||
|
|
||||||
|
|
||||||
|
def _matar_arbol(pid):
|
||||||
|
"""Cierra un proceso y lo que lanzó (el lector de BRAW lanza su propio ffmpeg)."""
|
||||||
|
try:
|
||||||
|
if os.name == "nt":
|
||||||
|
subprocess.run(["taskkill", "/PID", str(pid), "/T", "/F"], capture_output=True, timeout=30, creationflags=SIN_VENTANA)
|
||||||
|
else:
|
||||||
|
os.kill(pid, 9)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _conversores_sueltos(procesos, base, yo):
|
||||||
|
"""De una lista de procesos ({ProcessId, ParentProcessId, Name, CommandLine}), los de conversión de esta
|
||||||
|
instalación (un ffmpeg o un lector de BRAW) que ya no tienen quien los espere: su padre murió, o el número
|
||||||
|
lo reusó otro proceso. Los de un programa vivo (este, u otra instancia como la pasada de la noche) no."""
|
||||||
|
vivos = {p.get("ProcessId"): p for p in procesos}
|
||||||
|
base = base.lower()
|
||||||
|
|
||||||
|
def conversor(p):
|
||||||
|
cmd = (p.get("CommandLine") or "").lower()
|
||||||
|
return base in cmd and ((p.get("Name") or "").lower() == "ffmpeg.exe" or "braw.py" in cmd)
|
||||||
|
|
||||||
|
def programa(p):
|
||||||
|
cmd = (p.get("CommandLine") or "").lower()
|
||||||
|
return p.get("ProcessId") == yo or (not conversor(p) and (base in cmd or "organizador" in cmd or "ventana.py" in cmd))
|
||||||
|
sueltos = []
|
||||||
|
for p in procesos:
|
||||||
|
if not conversor(p):
|
||||||
|
continue
|
||||||
|
padre = vivos.get(p.get("ParentProcessId"))
|
||||||
|
if padre and (conversor(padre) or programa(padre)):
|
||||||
|
continue # el ffmpeg de un lector va con su lector; lo demás lo espera un programa vivo
|
||||||
|
sueltos.append(p)
|
||||||
|
return sueltos
|
||||||
|
|
||||||
|
|
||||||
|
def cerrar_conversores_sueltos():
|
||||||
|
"""Al arrancar, se cierran las conversiones que quedaron vivas sin el programa que las lanzó, y se borra lo que
|
||||||
|
dejaron a medio escribir. Medido el 14-oct-2026: el lector de BRAW y el ffmpeg de una conversión de las 10:04,
|
||||||
|
de antes de actualizar a la 3.8.0, seguían a las 17:00 colgados, con una sesión del codificador de la tarjeta
|
||||||
|
y su memoria, y su «.parcial.mp4» en la carpeta de Galeano."""
|
||||||
|
if os.name != "nt" or os.environ.get("ORGANILAB_ENSAYO"):
|
||||||
|
return []
|
||||||
|
filtro = "Name='ffmpeg.exe' or Name='OrganizadorLabstream.exe' or Name like 'python%'"
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-Command",
|
||||||
|
f'Get-CimInstance Win32_Process -Filter "{filtro}" | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress'],
|
||||||
|
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=60, creationflags=SIN_VENTANA)
|
||||||
|
procesos = json.loads(r.stdout or "[]")
|
||||||
|
except (OSError, subprocess.SubprocessError, ValueError):
|
||||||
|
return []
|
||||||
|
if isinstance(procesos, dict):
|
||||||
|
procesos = [procesos]
|
||||||
|
sueltos = _conversores_sueltos(procesos, BASE, os.getpid())
|
||||||
|
if not sueltos:
|
||||||
|
return []
|
||||||
|
cerrados = {p["ProcessId"] for p in sueltos}
|
||||||
|
for p in sueltos:
|
||||||
|
_matar_arbol(p["ProcessId"])
|
||||||
|
# lo que dejaron a medias: el último argumento de cada ffmpeg cerrado (el suyo o el de su lector)
|
||||||
|
parciales = []
|
||||||
|
for p in procesos:
|
||||||
|
if (p.get("Name") or "").lower() == "ffmpeg.exe" and (p.get("ProcessId") in cerrados or p.get("ParentProcessId") in cerrados):
|
||||||
|
m = re.search(r'(?:"([^"]*\.parcial\.\w+)"|(\S*\.parcial\.\w+))\s*$', p.get("CommandLine") or "")
|
||||||
|
if m:
|
||||||
|
parciales.append(m.group(1) or m.group(2))
|
||||||
|
for tmp in parciales:
|
||||||
|
_quitar_parcial(tmp)
|
||||||
|
bitacora({"op": "conversores_sueltos", "cerrados": sorted(cerrados), "parciales": parciales})
|
||||||
|
return sorted(cerrados)
|
||||||
|
|
||||||
|
|
||||||
def recuperar_huerfanas():
|
def recuperar_huerfanas():
|
||||||
"""Trabajos que quedaron «corriendo» sin una cola viva: el programa se cerró (o se actualizó) a mitad de
|
"""Trabajos que quedaron «corriendo» sin una cola viva: el programa se cerró (o se actualizó) a mitad de
|
||||||
una conversión y nadie volvía a tocarlos. Bloqueaban «Desconectar», no salían en «En curso» y el paso
|
una conversión y nadie volvía a tocarlos. Bloqueaban «Desconectar», no salían en «En curso» y el paso
|
||||||
@@ -4653,6 +4843,8 @@ def iniciar_cola():
|
|||||||
COLA["ritmo"] = []
|
COLA["ritmo"] = []
|
||||||
COLA["hechos_ids"] = []
|
COLA["hechos_ids"] = []
|
||||||
COLA["esperando"] = ""
|
COLA["esperando"] = ""
|
||||||
|
COLA["actuales"] = {}
|
||||||
|
COLA["saltados"] = 0
|
||||||
COLA["hilo"] = threading.Thread(target=bucle_cola, daemon=True)
|
COLA["hilo"] = threading.Thread(target=bucle_cola, daemon=True)
|
||||||
COLA["hilo"].start()
|
COLA["hilo"].start()
|
||||||
return True
|
return True
|
||||||
@@ -5567,14 +5759,13 @@ def estado_general():
|
|||||||
con = db()
|
con = db()
|
||||||
esc = {r["raiz"]: dict(r) for r in con.execute("SELECT * FROM escaneos").fetchall()}
|
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()}
|
cola_n = {r["estado"]: r["n"] for r in con.execute("SELECT estado, COUNT(*) n FROM trabajos GROUP BY estado").fetchall()}
|
||||||
actual = None
|
ids = sorted(COLA["actuales"])
|
||||||
if COLA["actual"]:
|
actuales = [dict(t) for t in con.execute(f"SELECT id, ruta, raiz, pct, bytes_in, motor, estado FROM trabajos WHERE id IN ({','.join('?' * len(ids))})",
|
||||||
t = con.execute("SELECT id, ruta, raiz, pct, bytes_in, motor, estado FROM trabajos WHERE id = ?", (COLA["actual"],)).fetchone()
|
ids)] if ids else []
|
||||||
if t:
|
actual = next((a for a in actuales if a["id"] == COLA["actual"]), actuales[0] if actuales else None)
|
||||||
actual = dict(t)
|
|
||||||
con.close()
|
con.close()
|
||||||
# «clip 3 de 12»: los hechos en esta tanda + el que va + los que esperan
|
# «clip 3 de 12»: los hechos en esta tanda + los que van + los que esperan
|
||||||
total_tanda = COLA["hechos"] + (1 if actual else 0) + cola_n.get("pendiente", 0)
|
total_tanda = COLA["hechos"] + len(actuales) + cola_n.get("pendiente", 0)
|
||||||
esc_ci = {k.lower(): v for k, v in esc.items()}
|
esc_ci = {k.lower(): v for k, v in esc.items()}
|
||||||
raices = []
|
raices = []
|
||||||
for r in CONFIG["raices"]:
|
for r in CONFIG["raices"]:
|
||||||
@@ -5582,7 +5773,7 @@ def estado_general():
|
|||||||
raices.append({**r, "clave": k, "escaneo": esc_ci.get(k.lower()), "progreso": PROGRESO.get(k), "analisis": ANALISIS.get(k)})
|
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(),
|
return {"config": CONFIG, "raices": raices, "herramientas": herramientas(),
|
||||||
"version": VERSION, "actualizacion": actualizador.ESTADO, "congelado": CONGELADO,
|
"version": VERSION, "actualizacion": actualizador.ESTADO, "congelado": CONGELADO,
|
||||||
"cola": {"activa": COLA["activa"], "actual": actual, "mensaje": COLA["mensaje"], "n": cola_n,
|
"cola": {"activa": COLA["activa"], "actual": actual, "actuales": actuales, "mensaje": COLA["mensaje"], "n": cola_n,
|
||||||
"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 [])},
|
||||||
@@ -7464,9 +7655,13 @@ class Manejador(BaseHTTPRequestHandler):
|
|||||||
return self._json(motores())
|
return self._json(motores())
|
||||||
if ruta == "/api/optimizar/cola":
|
if ruta == "/api/optimizar/cola":
|
||||||
con = db()
|
con = db()
|
||||||
# con la duración y los fps del clip, para decir cuánto va de cada uno y para «comparar»
|
# con la duración y los fps del clip, para decir cuánto va de cada uno y para «comparar». Con la
|
||||||
|
# carpeta, solo los suyos y todos: hasta la 3.8 eran los 500 últimos de todas, y con 1.920 en espera
|
||||||
|
# en Vía Sumapaz los de Línea Gene no salían (ni su «Reemplazar 12 verificados»)
|
||||||
|
k = (q.get("raiz") or "").strip()
|
||||||
|
filtro, args = ("WHERE lower(t.raiz) = ? ", [os.path.normpath(k).lower()]) if k else ("", [])
|
||||||
filas = [dict(r) for r in con.execute("SELECT t.*, a.dur AS dur, a.fps AS fps FROM trabajos t LEFT JOIN archivos a ON a.ruta = t.ruta "
|
filas = [dict(r) for r in con.execute("SELECT t.*, a.dur AS dur, a.fps AS fps FROM trabajos t LEFT JOIN archivos a ON a.ruta = t.ruta "
|
||||||
"ORDER BY t.id DESC LIMIT 500").fetchall()]
|
+ filtro + "ORDER BY t.id DESC LIMIT " + ("10000" if k else "500"), args).fetchall()]
|
||||||
for f in filas: # un original ya reemplazado vive en _Para borrar/originales: su ficha está ahí
|
for f in filas: # un original ya reemplazado vive en _Para borrar/originales: su ficha está ahí
|
||||||
if f.get("dur") is None and f["estado"] == "reemplazado":
|
if f.get("dur") is None and f["estado"] == "reemplazado":
|
||||||
otra = _ruta_original(f)
|
otra = _ruta_original(f)
|
||||||
@@ -7475,7 +7670,7 @@ class Manejador(BaseHTTPRequestHandler):
|
|||||||
f["dur"], f["fps"] = a["dur"], a["fps"]
|
f["dur"], f["fps"] = a["dur"], a["fps"]
|
||||||
con.close()
|
con.close()
|
||||||
try:
|
try:
|
||||||
eta = eta_cola(filas) if COLA["activa"] else None
|
eta = eta_cola() if COLA["activa"] else None
|
||||||
except Exception: # noqa
|
except Exception: # noqa
|
||||||
eta = None
|
eta = None
|
||||||
if COLA.get("ultima") is None:
|
if COLA.get("ultima") is None:
|
||||||
@@ -7627,7 +7822,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", "ahorro_minimo", "perfil", "formato", "chroma", "color", "salida", "sufijo", "auto_reemplazar",
|
permitidas = {"optimizar": {"meses_minimos", "min_mb", "ahorro_minimo", "simultaneas", "perfil", "formato", "chroma", "color", "salida", "sufijo", "auto_reemplazar",
|
||||||
"horario", "pausar_si_edito", "aligerar", "avisar"},
|
"horario", "pausar_si_edito", "aligerar", "avisar"},
|
||||||
"respaldo": {"destino"}, "actualizaciones": {"auto"}, "escaneo": {"profundo"}}
|
"respaldo": {"destino"}, "actualizaciones": {"auto"}, "escaneo": {"profundo"}}
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
@@ -8119,12 +8314,13 @@ def avance():
|
|||||||
try:
|
try:
|
||||||
con = db()
|
con = db()
|
||||||
espera = con.execute("SELECT COUNT(*) FROM trabajos WHERE estado = 'pendiente'").fetchone()[0]
|
espera = con.execute("SELECT COUNT(*) FROM trabajos WHERE estado = 'pendiente'").fetchone()[0]
|
||||||
t = con.execute("SELECT pct FROM trabajos WHERE id = ?", (COLA["actual"],)).fetchone() if COLA["actual"] else None
|
ids = list(COLA["actuales"])
|
||||||
|
pcts = [r[0] or 0 for r in con.execute(f"SELECT pct FROM trabajos WHERE id IN ({','.join('?' * len(ids))})", ids)] if ids else []
|
||||||
con.close()
|
con.close()
|
||||||
except sqlite3.Error:
|
except sqlite3.Error:
|
||||||
return -1
|
return -1
|
||||||
total = COLA["hechos"] + (1 if t else 0) + espera
|
total = COLA["hechos"] + len(pcts) + espera
|
||||||
return (COLA["hechos"] + ((t[0] or 0) / 100 if t else 0)) / total if total else -1
|
return (COLA["hechos"] + sum(pcts) / 100) / total if total else -1
|
||||||
if RESPALDO.get("estado") == "corriendo":
|
if RESPALDO.get("estado") == "corriendo":
|
||||||
return RESPALDO["bytes"] / RESPALDO["bytes_total"] if RESPALDO.get("bytes_total") else -1
|
return RESPALDO["bytes"] / RESPALDO["bytes_total"] if RESPALDO.get("bytes_total") else -1
|
||||||
if TRAER.get("estado") == "copiando":
|
if TRAER.get("estado") == "copiando":
|
||||||
@@ -8350,7 +8546,8 @@ def main():
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
huerfanas = recuperar_huerfanas()
|
huerfanas = recuperar_huerfanas()
|
||||||
threading.Thread(target=lambda: (reubicar_trabajos(), repasar_verificados(), braws_al_lector()), daemon=True).start()
|
threading.Thread(target=lambda: (cerrar_conversores_sueltos(), reubicar_trabajos(), repasar_verificados(), braws_al_lector()),
|
||||||
|
daemon=True).start()
|
||||||
if huerfanas:
|
if huerfanas:
|
||||||
print(f"{huerfanas} conversiones quedaron a medias la última vez: vuelven a la cola en espera (Reanudar).")
|
print(f"{huerfanas} conversiones quedaron a medias la última vez: vuelven a la cola en espera (Reanudar).")
|
||||||
try:
|
try:
|
||||||
|
|||||||
152
prueba_orden.py
152
prueba_orden.py
@@ -12,6 +12,7 @@ import time
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
|
import subprocess
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
AQUI = os.path.dirname(os.path.abspath(__file__))
|
AQUI = os.path.dirname(os.path.abspath(__file__))
|
||||||
@@ -1315,6 +1316,157 @@ class Convertidor(unittest.TestCase):
|
|||||||
self.assertTrue(r["ok"], r)
|
self.assertTrue(r["ok"], r)
|
||||||
self.assertTrue(os.path.isfile(os.path.join(nueva, os.path.basename(tb["salida"]))))
|
self.assertTrue(os.path.isfile(os.path.join(nueva, os.path.basename(tb["salida"]))))
|
||||||
|
|
||||||
|
def _cola_falsa(self, durante=0.4):
|
||||||
|
"""Sustituye la conversión por una que tarda `durante` segundos y cuenta cuántas van a la vez."""
|
||||||
|
cuenta = {"vivos": 0, "maximo": 0, "vistos": []}
|
||||||
|
candado = threading.Lock()
|
||||||
|
|
||||||
|
def falso(t):
|
||||||
|
with candado:
|
||||||
|
cuenta["vivos"] += 1
|
||||||
|
cuenta["maximo"] = max(cuenta["maximo"], cuenta["vivos"])
|
||||||
|
cuenta["vistos"].append(t["id"])
|
||||||
|
fin = time.time() + durante
|
||||||
|
while time.time() < fin and not O.COLA["cancelar"]:
|
||||||
|
time.sleep(0.02)
|
||||||
|
with candado:
|
||||||
|
cuenta["vivos"] -= 1
|
||||||
|
if O.COLA["cancelar"]:
|
||||||
|
O.set_trabajo(t["id"], estado="cancelado", pct=0)
|
||||||
|
return
|
||||||
|
with open(t["salida"], "wb") as fh:
|
||||||
|
fh.write(b"x" * 1000)
|
||||||
|
O.set_trabajo(t["id"], estado="listo", pct=100, bytes_out=1000, fin=time.time())
|
||||||
|
for k, v in {"correr_ffmpeg": falso, "_verificar_listos": lambda ids: None, "editando": lambda: "",
|
||||||
|
"_ya_no_compensa": lambda t: ""}.items():
|
||||||
|
self.addCleanup(setattr, O, k, getattr(O, k))
|
||||||
|
setattr(O, k, v)
|
||||||
|
self.addCleanup(O.CONFIG["optimizar"].pop, "simultaneas", None)
|
||||||
|
return cuenta
|
||||||
|
|
||||||
|
def test_dos_a_la_vez(self):
|
||||||
|
"""RTX 5080 del estudio, 14-oct-2026: con un clip a la vez el codificador iba al 46-74 %; dos a la vez salen
|
||||||
|
a 1,29× tiempo real entre los dos, frente a 0,77×. Nunca van más de los pedidos, ningún clip se convierte
|
||||||
|
dos veces, y «Detener» para a todos."""
|
||||||
|
raiz = cliente("ALaVez")
|
||||||
|
nr = os.path.normpath(raiz)
|
||||||
|
g = "2025/11- Noviembre/00_Grabacion/SONY/"
|
||||||
|
clips = [mk(raiz, g + f"C00{i:02d}.MP4", 2025, 11, 11, tam=300000 + i) for i in range(8)]
|
||||||
|
O.escanear(raiz)
|
||||||
|
self.leidos(raiz, {os.path.basename(c): {"codec": "h264", "perfil": "High", "kbps": 120000} for c in clips})
|
||||||
|
cuenta = self._cola_falsa()
|
||||||
|
O.CONFIG["optimizar"]["simultaneas"] = 2
|
||||||
|
# lo que otras pruebas dejaran esperando no entra en esta tanda (y se devuelve al acabar)
|
||||||
|
con = O.db()
|
||||||
|
otros = [r[0] for r in con.execute("SELECT id FROM trabajos WHERE estado = 'pendiente'")]
|
||||||
|
with con:
|
||||||
|
con.execute("UPDATE trabajos SET estado = 'cancelado' WHERE estado = 'pendiente'")
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
def devolver():
|
||||||
|
c = O.db()
|
||||||
|
with c:
|
||||||
|
c.executemany("UPDATE trabajos SET estado = 'pendiente' WHERE id = ?", [(i,) for i in otros])
|
||||||
|
c.close()
|
||||||
|
self.addCleanup(devolver)
|
||||||
|
ids = O.encolar(clips[:5], "equilibrado", "420", "rec709", nr)
|
||||||
|
self.assertEqual(len(ids), 5)
|
||||||
|
self.assertTrue(O.iniciar_cola())
|
||||||
|
time.sleep(0.2)
|
||||||
|
e = O.eta_cola()
|
||||||
|
self.assertEqual(e["paralelo"], 2)
|
||||||
|
self.assertEqual(len(e["actuales"]), 2)
|
||||||
|
O.COLA["hilo"].join(30)
|
||||||
|
self.assertEqual(cuenta["maximo"], 2)
|
||||||
|
self.assertEqual(sorted(cuenta["vistos"]), sorted(ids))
|
||||||
|
self.assertEqual({self.trabajo(i)["estado"] for i in ids}, {"listo"})
|
||||||
|
self.assertEqual((O.COLA["actuales"], O.COLA["activa"], O.COLA["actual"]), ({}, False, None))
|
||||||
|
# Detener: los dos que van se cortan, los que esperan se quedan esperando
|
||||||
|
cuenta.update(maximo=0, vistos=[])
|
||||||
|
ids = O.encolar(clips[5:], "equilibrado", "420", "rec709", nr)
|
||||||
|
self.assertTrue(O.iniciar_cola())
|
||||||
|
time.sleep(0.2)
|
||||||
|
O.COLA["cancelar"], O.COLA["activa"] = True, False # lo que hace «Detener»
|
||||||
|
O.COLA["hilo"].join(30)
|
||||||
|
estados = sorted(self.trabajo(i)["estado"] for i in ids)
|
||||||
|
self.assertEqual(estados, ["cancelado", "cancelado", "pendiente"])
|
||||||
|
self.assertFalse(O.COLA["cancelar"])
|
||||||
|
con = O.db()
|
||||||
|
with con:
|
||||||
|
con.execute(f"DELETE FROM trabajos WHERE id IN ({','.join('?' * len(ids))})", ids)
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
def test_conversiones_sueltas(self):
|
||||||
|
"""14-oct-2026: el lector de BRAW y el ffmpeg de una conversión de antes de actualizar seguían colgados siete
|
||||||
|
horas después. Al arrancar se cierran los que no tienen quien los espere, y no los de un programa vivo."""
|
||||||
|
base = r"C:\Programas\OrganiLAB"
|
||||||
|
P = lambda pid, ppid, nombre, cmd: {"ProcessId": pid, "ParentProcessId": ppid, "Name": nombre, "CommandLine": cmd}
|
||||||
|
ff = base + r"\ffmpeg\ffmpeg.exe"
|
||||||
|
procesos = [
|
||||||
|
P(100, 50, "OrganizadorLabstream.exe", f'"{base}\\OrganizadorLabstream.exe"'), # este programa
|
||||||
|
P(101, 100, "OrganizadorLabstream.exe", f"{base}\\OrganizadorLabstream.exe {base}\\braw.py convertir x.json"), # su lector
|
||||||
|
P(102, 101, "ffmpeg.exe", f'{ff} -i pipe:0 "V:\\G\\a_10bit.mp4.parcial.mp4"'), # el ffmpeg del lector
|
||||||
|
P(103, 100, "ffmpeg.exe", f"{ff} -i a.mp4 b.parcial.mp4"), # uno suyo
|
||||||
|
P(200, 65492, "OrganizadorLabstream.exe", f"{base}\\OrganizadorLabstream.exe {base}\\braw.py convertir y.json"), # lector suelto
|
||||||
|
P(201, 200, "ffmpeg.exe", f"{ff} -i pipe:0 V:\\G\\b.parcial.mp4"), # y su ffmpeg
|
||||||
|
P(300, 60, "OrganizadorLabstream.exe", f'"{base}\\OrganizadorLabstream.exe" --automatico'), # la pasada de la noche
|
||||||
|
P(301, 300, "ffmpeg.exe", f"{ff} -i c.mp4 c.parcial.mp4"), # y su ffmpeg
|
||||||
|
P(401, 4000, "ffmpeg.exe", f"{ff} -i d.mp4 d.parcial.mp4"), # su programa ya no está
|
||||||
|
P(500, 1, "ffmpeg.exe", r"C:\otro\ffmpeg.exe -i e.mp4 e.mp4"), # de otro programa
|
||||||
|
]
|
||||||
|
self.assertEqual(sorted(p["ProcessId"] for p in O._conversores_sueltos(procesos, base, 100)), [200, 401])
|
||||||
|
|
||||||
|
def test_la_tarjeta_descomprime(self):
|
||||||
|
"""Con la cola en marcha, el descompresor de la tarjeta estaba al 0 %: un H.264 4K pasa de 0,54× a 0,86× tiempo
|
||||||
|
real si lo descomprime ella. Lo que ella no lee (ProRes) sigue por el procesador, y si con ella falla, se repite."""
|
||||||
|
real = O.motores
|
||||||
|
self.addCleanup(setattr, O, "motores", real)
|
||||||
|
O.motores = lambda: {"gpu": {"nvdec": True, "hevc": True}}
|
||||||
|
self.assertEqual(O._decodificar_con_tarjeta({"codec": "h264"}, "gpu"), ["-hwaccel", "cuda"])
|
||||||
|
self.assertEqual(O._decodificar_con_tarjeta({"codec": "hevc"}, "gpu"), ["-hwaccel", "cuda"])
|
||||||
|
self.assertEqual(O._decodificar_con_tarjeta({"codec": "prores"}, "gpu"), [])
|
||||||
|
self.assertEqual(O._decodificar_con_tarjeta({"codec": "h264"}, "cpu"), [])
|
||||||
|
O.motores = lambda: {"gpu": {"nvdec": False}}
|
||||||
|
self.assertEqual(O._decodificar_con_tarjeta({"codec": "h264"}, "gpu"), [])
|
||||||
|
O.motores = real
|
||||||
|
ff = shutil.which("ffmpeg")
|
||||||
|
if not ff:
|
||||||
|
self.skipTest("sin ffmpeg")
|
||||||
|
raiz = cliente("Descomprime")
|
||||||
|
nr = os.path.normpath(raiz)
|
||||||
|
clip = os.path.join(nr, "2025", "11- Noviembre", "00_Grabacion", "SONY", "C0001.MP4")
|
||||||
|
os.makedirs(os.path.dirname(clip))
|
||||||
|
subprocess.run([ff, "-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i", "testsrc=size=320x180:rate=30:duration=1",
|
||||||
|
"-c:v", "libx264", "-pix_fmt", "yuv420p", clip], check=True)
|
||||||
|
O.escanear(raiz)
|
||||||
|
self.leidos(raiz, {"C0001.MP4": {"codec": "h264", "perfil": "High", "ancho": 320, "alto": 180, "fps": 30, "dur": 1, "kbps": 900}})
|
||||||
|
id_ = O.encolar([clip], "equilibrado", "420", "rec709", nr)[0]
|
||||||
|
# una descompresión que no arranca: se repite con el procesador y sale igual
|
||||||
|
self.addCleanup(setattr, O, "_decodificar_con_tarjeta", O._decodificar_con_tarjeta)
|
||||||
|
O._decodificar_con_tarjeta = lambda f, motor: ["-hwaccel", "no_existe"]
|
||||||
|
O.correr_ffmpeg(self.trabajo(id_))
|
||||||
|
t = self.trabajo(id_)
|
||||||
|
self.assertEqual(t["estado"], "listo", t["log"])
|
||||||
|
self.assertTrue(os.path.isfile(t["salida"]))
|
||||||
|
self.assertNotIn("-hwaccel", t["log"])
|
||||||
|
|
||||||
|
def test_lo_que_ya_no_compensa_no_se_convierte(self):
|
||||||
|
"""Se encolaron con la regla de antes: un H.265 de cámara que se quedaría en el 93 % no se convierte."""
|
||||||
|
raiz = cliente("YaNoCompensa")
|
||||||
|
nr = os.path.normpath(raiz)
|
||||||
|
g = "2025/11- Noviembre/00_Grabacion/SONY/"
|
||||||
|
a, b = mk(raiz, g + "C0001.MP4", 2025, 11, 11, tam=300000), mk(raiz, g + "C0002.MP4", 2025, 11, 11, tam=300001)
|
||||||
|
O.escanear(raiz)
|
||||||
|
self.leidos(raiz, {"C0001.MP4": {"codec": "hevc", "perfil": "Main 10", "pix_fmt": "yuv420p10le", "fps": 59.94, "kbps": 72000},
|
||||||
|
"C0002.MP4": {"codec": "h264", "perfil": "High", "kbps": 120000}})
|
||||||
|
ia, ib = O.encolar([a, b], "equilibrado", "420", "rec709", nr)
|
||||||
|
self.assertTrue(O._ya_no_compensa(self.trabajo(ia)).startswith("convertido ahorraría solo un 7 %"))
|
||||||
|
self.assertEqual(O._ya_no_compensa(self.trabajo(ib)), "")
|
||||||
|
con = O.db()
|
||||||
|
with con:
|
||||||
|
con.execute("DELETE FROM trabajos WHERE id IN (?, ?)", (ia, ib))
|
||||||
|
con.close()
|
||||||
|
|
||||||
def test_el_codificador_que_toca(self):
|
def test_el_codificador_que_toca(self):
|
||||||
fm = O.FORMATOS["h265_10bit"]
|
fm = O.FORMATOS["h265_10bit"]
|
||||||
t = {"chroma": "420", "perfil": "equilibrado", "kbps": 30000}
|
t = {"chroma": "420", "perfil": "equilibrado", "kbps": 30000}
|
||||||
|
|||||||
@@ -697,6 +697,7 @@
|
|||||||
<label>croma <select id="optChroma"><option value="420">4:2:0 10 bits (compatible)</option><option value="422">4:2:2 10 bits (solo ffmpeg, pesa más)</option></select></label>
|
<label>croma <select id="optChroma"><option value="420">4:2:0 10 bits (compatible)</option><option value="422">4:2:2 10 bits (solo ffmpeg, pesa más)</option></select></label>
|
||||||
<label>BRAW en <select id="optColor"><option value="rec709">Rec.709 (como se vio al grabar)</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 (como se vio al grabar)</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>
|
||||||
|
<label title="Cuántos clips convierte la cola a la vez. Automático: dos con tarjeta NVIDIA, uno sin ella. Cuenta desde la próxima vez que arranque la cola.">a la vez <select id="optSimult"><option value="0">automático</option><option value="1">1</option><option value="2">2</option><option value="3">3</option></select></label>
|
||||||
</div></details></span></div>
|
</div></details></span></div>
|
||||||
<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 se leen con el decodificador de Blackmagic que instala DaVinci Resolve, sin tener que abrirlo; todo se codifica con ffmpeg, con la tarjeta gráfica cuando la hay. Se ofrece todo lo que convertido pesaría al menos un 30 % menos (se cambia en «cambiar»); lo calcula por el códec de cada clip: un H.264 o un AV1 de cámara suelen quedar en la décima parte, y un H.265 de cámara casi no adelgaza. Si al terminar ahorra menos de lo pedido (con 10 puntos de margen), se descarta y se queda el original. Nada se borra.</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 se leen con el decodificador de Blackmagic que instala DaVinci Resolve, sin tener que abrirlo; todo se codifica con ffmpeg, con la tarjeta gráfica cuando la hay. Se ofrece todo lo que convertido pesaría al menos un 30 % menos (se cambia en «cambiar»); lo calcula por el códec de cada clip: un H.264 o un AV1 de cámara suelen quedar en la décima parte, y un H.265 de cámara casi no adelgaza. Si al terminar ahorra menos de lo pedido (con 10 puntos de margen), se descarta y se queda el original. 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="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>
|
||||||
@@ -994,7 +995,7 @@ async function cargarEstado(){
|
|||||||
sel.value = (antes && ESTADO.raices.some(r => r.clave === antes)) ? antes : '';
|
sel.value = (antes && ESTADO.raices.some(r => r.clave === antes)) ? 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 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; $('#optAhorro').value = ESTADO.config.optimizar.ahorro_minimo ?? 30; $('#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; $('#optAhorro').value = ESTADO.config.optimizar.ahorro_minimo ?? 30; $('#optSimult').value = String(ESTADO.config.optimizar.simultaneas ?? 0); $('#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} <button class="lnk" id="btnAnaDetener">Detener</button>`;
|
if (a && a.estado === 'corriendo') $('#anaEstado').innerHTML = `<span class="spin"></span> leyendo clips: ${a.hechos} de ${a.total} <button class="lnk" id="btnAnaDetener">Detener</button>`;
|
||||||
else if (a && a.error && vistaActual() === 'convertir') $('#anaEstado').innerHTML = `<span class="warn">${esc(a.error)}</span>`;
|
else if (a && a.error && vistaActual() === 'convertir') $('#anaEstado').innerHTML = `<span class="warn">${esc(a.error)}</span>`;
|
||||||
@@ -1164,7 +1165,7 @@ function tareasDe(E){
|
|||||||
if (r.progreso && r.progreso.estado === 'corriendo') { const p = r.progreso; const est = r.escaneo && r.escaneo.n ? Math.min(99, Math.round(p.n * 100 / r.escaneo.n)) : null; T.push({raiz: r.clave, q: 'Escaneando', d: `${r.cliente} · ${num(p.n)} archivos · ${gb(p.bytes)}`, pct: est}); }
|
if (r.progreso && r.progreso.estado === 'corriendo') { const p = r.progreso; const est = r.escaneo && r.escaneo.n ? Math.min(99, Math.round(p.n * 100 / r.escaneo.n)) : null; T.push({raiz: r.clave, q: 'Escaneando', d: `${r.cliente} · ${num(p.n)} archivos · ${gb(p.bytes)}`, pct: est}); }
|
||||||
if (r.analisis && r.analisis.estado === 'corriendo') { const a = r.analisis; T.push({raiz: r.clave, q: 'Leyendo clips', d: `${r.cliente} · ${a.hechos} de ${a.total}`, pct: a.total ? Math.round(a.hechos * 100 / a.total) : null}); }
|
if (r.analisis && r.analisis.estado === 'corriendo') { const a = r.analisis; T.push({raiz: r.clave, q: 'Leyendo clips', d: `${r.cliente} · ${a.hechos} de ${a.total}`, pct: a.total ? Math.round(a.hechos * 100 / a.total) : null}); }
|
||||||
});
|
});
|
||||||
if (E.cola.activa) { const n = E.cola.n || {}; const a = E.cola.actual; const tot = E.cola.total || 0, hechos = E.cola.hechos || 0; const pctClip = a ? (a.pct || 0) : 0; const pctTot = tot ? Math.round((hechos + pctClip / 100) / tot * 100) : null; T.push({raiz: a ? a.raiz : null, q: tot ? `Convirtiendo ${Math.min(hechos + 1, tot)} de ${tot}` : 'Convirtiendo', d: `${a ? (a.ruta || '').split(/[\\/]/).pop() + ' · ' + Math.round(pctClip) + ' % de este clip · ' : ''}${n.pendiente || 0} en espera${E.cola.mensaje ? ' · ' + E.cola.mensaje : ''}`, pct: pctTot, cancelar: () => api('/api/optimizar/control', {accion: 'detener'})}); }
|
if (E.cola.activa) { const n = E.cola.n || {}; const a = E.cola.actual; const vs = (E.cola.actuales && E.cola.actuales.length) ? E.cola.actuales : (a ? [a] : []); const tot = E.cola.total || 0, hechos = E.cola.hechos || 0; const pctVan = vs.reduce((s, x) => s + (x.pct || 0), 0); const pctTot = tot ? Math.round((hechos + pctVan / 100) / tot * 100) : null; const numeros = vs.length > 1 ? `${Math.min(hechos + 1, tot)} y ${Math.min(hechos + 2, tot)}` : `${Math.min(hechos + 1, tot)}`; T.push({raiz: a ? a.raiz : null, q: tot ? `Convirtiendo ${numeros} de ${tot}` : 'Convirtiendo', d: `${vs.map(x => (x.ruta || '').split(/[\\/]/).pop() + ' · ' + Math.round(x.pct || 0) + ' %').join(' · ')}${vs.length ? ' · ' : ''}${n.pendiente || 0} en espera${E.cola.mensaje ? ' · ' + E.cola.mensaje : ''}`, pct: pctTot, cancelar: () => api('/api/optimizar/control', {accion: 'detener'})}); }
|
||||||
if (E.asistente.activo) { const a = E.asistente; T.push({raiz: a.raiz, q: 'Asistente: ' + (NOMBRE_PASO[a.paso] || a.paso || '…'), d: `${porNombre(a.raiz)} · ${a.total ? num(a.hechas) + ' de ' + num(a.total) + ' · ' : ''}${(a.texto || '').slice(-60)}`, pct: a.total ? a.pct : null}); }
|
if (E.asistente.activo) { const a = E.asistente; T.push({raiz: a.raiz, q: 'Asistente: ' + (NOMBRE_PASO[a.paso] || a.paso || '…'), d: `${porNombre(a.raiz)} · ${a.total ? num(a.hechas) + ' de ' + num(a.total) + ' · ' : ''}${(a.texto || '').slice(-60)}`, pct: a.total ? a.pct : null}); }
|
||||||
else if (E.aplicando && E.aplicando.total && E.aplicando.hechas < E.aplicando.total) T.push({q: 'Moviendo', d: E.aplicando.actual || '', pct: Math.round(E.aplicando.unidades_total ? E.aplicando.unidades * 100 / E.aplicando.unidades_total : E.aplicando.hechas * 100 / E.aplicando.total)});
|
else if (E.aplicando && E.aplicando.total && E.aplicando.hechas < E.aplicando.total) T.push({q: 'Moviendo', d: E.aplicando.actual || '', pct: Math.round(E.aplicando.unidades_total ? E.aplicando.unidades * 100 / E.aplicando.unidades_total : E.aplicando.hechas * 100 / E.aplicando.total)});
|
||||||
if (E.respaldo.estado === 'corriendo') { const e = E.respaldo; T.push({q: 'Copiando respaldo', d: `${num(e.hechos)} de ${num(e.total)} · ${gb(e.bytes)} de ${gb(e.bytes_total)}`, pct: e.bytes_total ? Math.round(e.bytes / e.bytes_total * 100) : null, cancelar: () => api('/api/respaldo/detener', {})}); }
|
if (E.respaldo.estado === 'corriendo') { const e = E.respaldo; T.push({q: 'Copiando respaldo', d: `${num(e.hechos)} de ${num(e.total)} · ${gb(e.bytes)} de ${gb(e.bytes_total)}`, pct: e.bytes_total ? Math.round(e.bytes / e.bytes_total * 100) : null, cancelar: () => api('/api/respaldo/detener', {})}); }
|
||||||
@@ -1894,6 +1895,7 @@ $('#btnStockPapelera').onclick = () => { const s = STOCK.filter(g => g.sel && g.
|
|||||||
const ahorroPedido = () => { const v = $('#optAhorro').value; return v === '' ? 30 : Math.max(0, Math.min(90, Math.round(+v) || 0)); };
|
const ahorroPedido = () => { const v = $('#optAhorro').value; return v === '' ? 30 : Math.max(0, Math.min(90, Math.round(+v) || 0)); };
|
||||||
function pintarCriterio(){ $('#convCriterio').textContent = `con más de ${+$('#optMeses').value || 0} meses, más de ${+$('#optMin').value || 0} MB y que ahorren al menos un ${ahorroPedido()} %`; }
|
function pintarCriterio(){ $('#convCriterio').textContent = `con más de ${+$('#optMeses').value || 0} meses, más de ${+$('#optMin').value || 0} MB y que ahorren al menos un ${ahorroPedido()} %`; }
|
||||||
['optMeses','optMin','optAhorro'].forEach(id => $('#' + id).oninput = pintarCriterio);
|
['optMeses','optMin','optAhorro'].forEach(id => $('#' + id).oninput = pintarCriterio);
|
||||||
|
$('#optSimult').onchange = () => api('/api/config', {optimizar: {simultaneas: +$('#optSimult').value}}).then(() => aviso(`La cola convertirá ${+$('#optSimult').value || 'automáticamente uno o dos'}${+$('#optSimult').value === 1 ? ' clip' : (+$('#optSimult').value ? ' clips' : '')} a la vez desde la próxima vez que arranque.`, 'ok')).catch(e => aviso(e.message, 'bad'));
|
||||||
// el ahorro mínimo vale también para la pasada automática y para Inicio: se guarda al cambiarlo
|
// el ahorro mínimo vale también para la pasada automática y para Inicio: se guarda al cambiarlo
|
||||||
$('#optAhorro').onchange = () => api('/api/config', {optimizar: {ahorro_minimo: ahorroPedido()}}).then(() => { if (CAND.length) return cargarCandidatos(); }).catch(e => aviso(e.message, 'bad'));
|
$('#optAhorro').onchange = () => api('/api/config', {optimizar: {ahorro_minimo: ahorroPedido()}}).then(() => { if (CAND.length) return cargarCandidatos(); }).catch(e => aviso(e.message, 'bad'));
|
||||||
let COLA_TS = [], CONV_VER = 'todos', CONV_ORDEN = {campo: 'carpeta', inverso: false}, CONV_EXC = '', CONV_PLEGADAS = new Set(), CONV_BUSCA = '', CONV_CRITERIO = '';
|
let COLA_TS = [], CONV_VER = 'todos', CONV_ORDEN = {campo: 'carpeta', inverso: false}, CONV_EXC = '', CONV_PLEGADAS = new Set(), CONV_BUSCA = '', CONV_CRITERIO = '';
|
||||||
@@ -1959,8 +1961,8 @@ function resumenCand(){ const s = CAND.filter(c => c.sel); const validos = CAND.
|
|||||||
$('#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, ahorro_minimo: ahorroPedido(), 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 o su salida ya existe (el mismo clip en MP4 y MOV, o una conversión anterior).`, ''); 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, ahorro_minimo: ahorroPedido(), 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 o su salida ya existe (el mismo clip en MP4 y MOV, o una conversión anterior).`, ''); 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', descartado:'no compensa'};
|
const EST_TXT = {pendiente:'en espera', corriendo:'convirtiendo', listo:'convertido', verificado:'verificado', reemplazado:'reemplazado', error:'falló', cancelado:'cancelado', descartado:'no compensa'};
|
||||||
async function cargarCola(){
|
async function cargarCola(){
|
||||||
const d = await api('/api/optimizar/cola');
|
|
||||||
const raiz = raizActual();
|
const raiz = raizActual();
|
||||||
|
const d = await api('/api/optimizar/cola' + (raiz ? '?raiz=' + encodeURIComponent(raiz) : ''));
|
||||||
const ts = d.trabajos.filter(t => !raiz || (t.raiz || '').toLowerCase() === raiz.toLowerCase());
|
const ts = d.trabajos.filter(t => !raiz || (t.raiz || '').toLowerCase() === raiz.toLowerCase());
|
||||||
const orden = {corriendo:0, pendiente:1, listo:2, verificado:2, error:3, descartado:4, reemplazado:5, cancelado:6};
|
const orden = {corriendo:0, pendiente:1, listo:2, verificado:2, error:3, descartado:4, reemplazado:5, cancelado:6};
|
||||||
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);
|
||||||
@@ -2011,7 +2013,7 @@ async function cargarCola(){
|
|||||||
const corre = t && t.estado === 'corriendo', pct = corre ? Math.max(0, Math.min(100, Math.round(t.pct || 0))) : 0;
|
const corre = t && t.estado === 'corriendo', pct = corre ? Math.max(0, Math.min(100, Math.round(t.pct || 0))) : 0;
|
||||||
const est = corre ? `<span class="est prog ${pct >= 50 ? 'media' : ''}" style="--p:${pct}%" title="convirtiendo: ${pct} %">${pct} %</span>` : t ? `<span class="est ${t.estado}">${EST_TXT[t.estado] || esc(t.estado)}</span>` : `<span class="est candidato">${c.excluido ? 'no' : 'candidato'}</span>`;
|
const est = corre ? `<span class="est prog ${pct >= 50 ? 'media' : ''}" style="--p:${pct}%" title="convirtiendo: ${pct} %">${pct} %</span>` : t ? `<span class="est ${t.estado}">${EST_TXT[t.estado] || esc(t.estado)}</span>` : `<span class="est candidato">${c.excluido ? 'no' : 'candidato'}</span>`;
|
||||||
let sub = c ? `${esc(c.fecha)} · ${esc(c.codec)} ${esc(c.res)} · ${dur(c.dur)} · ${motorT}` : motorT;
|
let sub = c ? `${esc(c.fecha)} · ${esc(c.codec)} ${esc(c.res)} · ${dur(c.dur)} · ${motorT}` : motorT;
|
||||||
if (corre) { const a = eta && eta.actual && eta.actual.id === t.id ? eta.actual : null; sub = `<span class="acc">convirtiendo${a && a.dur ? ` · ${tiempoLargo(a.dur - a.resto)} de ${tiempoLargo(a.dur)} · quedan ${tiempoLargo(a.resto_seg)}` : ''}${E && E.mensaje ? ' · ' + esc(E.mensaje) : ''}</span> · ${motorT}`; }
|
if (corre) { const a = eta && ((eta.actuales || []).find(x => x.id === t.id) || (eta.actual && eta.actual.id === t.id ? eta.actual : null)); sub = `<span class="acc">convirtiendo${a && a.dur ? ` · ${tiempoLargo(a.dur - a.resto)} de ${tiempoLargo(a.dur)} · quedan ${tiempoLargo(a.resto_seg)}` : ''}${E && E.mensaje ? ' · ' + esc(E.mensaje) : ''}</span> · ${motorT}`; }
|
||||||
else if (t && t.estado === 'error') sub = `<span class="bad" title="${esc(t.log || '')}">${esc(((t.log || '').trim().split('\n').pop() || 'falló sin decir por qué').slice(0, 320))}</span>`;
|
else if (t && t.estado === 'error') sub = `<span class="bad" title="${esc(t.log || '')}">${esc(((t.log || '').trim().split('\n').pop() || 'falló sin decir por qué').slice(0, 320))}</span>`;
|
||||||
else if (t && t.estado === 'descartado') sub = `<span class="mut">${esc(t.log || '')}</span>`;
|
else if (t && t.estado === 'descartado') sub = `<span class="mut">${esc(t.log || '')}</span>`;
|
||||||
else if (t && t.estado === 'pendiente' && t.log) sub = `<span class="mut">${esc(t.log)}</span>`;
|
else if (t && t.estado === 'pendiente' && t.log) sub = `<span class="mut">${esc(t.log)}</span>`;
|
||||||
@@ -2034,9 +2036,12 @@ async function cargarCola(){
|
|||||||
if (d.cola.activa && d.cola.esperando) {
|
if (d.cola.activa && d.cola.esperando) {
|
||||||
mo = `<div class="momento"><div class="l1"><span class="spin"></span><b>La cola está en pausa</b><span class="mut">${esc(d.cola.esperando)} · ${pend} en espera · se cambia en «Cuándo trabaja la cola»</span><span class="sep"></span><button class="bad" id="moDetener">Detener</button></div></div>`;
|
mo = `<div class="momento"><div class="l1"><span class="spin"></span><b>La cola está en pausa</b><span class="mut">${esc(d.cola.esperando)} · ${pend} en espera · se cambia en «Cuándo trabaja la cola»</span><span class="sep"></span><button class="bad" id="moDetener">Detener</button></div></div>`;
|
||||||
} else if (d.cola.activa) {
|
} else if (d.cola.activa) {
|
||||||
const total = E && E.total ? E.total : (pend + (corr ? 1 : 0)), hechosT = E ? (E.hechos || 0) : 0;
|
const corrs = ts.filter(t => t.estado === 'corriendo');
|
||||||
const pctTot = total ? Math.round((hechosT + (corr ? (corr.pct || 0) / 100 : 0)) / total * 100) : 0;
|
const total = E && E.total ? E.total : (pend + (corrs.length || (corr ? 1 : 0))), hechosT = E ? (E.hechos || 0) : 0;
|
||||||
mo = `<div class="momento corre"><div class="l1"><span class="spin"></span><b>Convirtiendo ${Math.min(hechosT + 1, total)} de ${total}</b><span class="mut">${eta && eta.seg ? `quedan unas <b>${tiempoLargo(eta.seg)}</b> · termina hacia las <b>${hora(eta.termina)}</b>${eta.medido ? ` · va a ${eta.vel.toFixed(1)}× tiempo real` : ' · estimado, hasta que acabe el primero'}` : 'calculando cuánto falta…'}</span><span class="sep"></span><button class="bad" id="moDetener">Detener</button></div><div class="bar grande"><i style="width:${pctTot}%"></i></div><div class="l3 mut">${corr ? `ahora: <b>${esc((corr.ruta || '').split(/[\\/]/).pop())}</b> · ${Math.round(corr.pct || 0)} %${eta && eta.actual ? ` · quedan ${tiempoLargo(eta.actual.resto_seg)} de este clip` : ''} · ${eta ? eta.en_espera : pend} en espera` : 'empezando…'}</div></div>`;
|
const pctTot = total ? Math.round((hechosT + corrs.reduce((s, x) => s + (x.pct || 0), 0) / 100) / total * 100) : 0;
|
||||||
|
const restoDe = x => { const a = eta && (eta.actuales || []).find(y => y.id === x.id); return a ? ` · quedan ${tiempoLargo(a.resto_seg)}` : ''; };
|
||||||
|
const ahora = corrs.length > 1 ? `a la vez: ${corrs.map(x => `<b>${esc((x.ruta || '').split(/[\\/]/).pop())}</b> ${Math.round(x.pct || 0)} %${restoDe(x)}`).join(' · ')} · ${eta ? eta.en_espera : pend} en espera` : '';
|
||||||
|
mo = `<div class="momento corre"><div class="l1"><span class="spin"></span><b>Convirtiendo ${corrs.length > 1 ? `${Math.min(hechosT + 1, total)} y ${Math.min(hechosT + 2, total)}` : Math.min(hechosT + 1, total)} de ${total}</b><span class="mut">${eta && eta.seg ? `quedan unas <b>${tiempoLargo(eta.seg)}</b> · termina hacia las <b>${hora(eta.termina)}</b>${eta.medido ? ` · va a ${eta.vel.toFixed(1)}× tiempo real` : ' · estimado, hasta que acabe el primero'}` : 'calculando cuánto falta…'}</span><span class="sep"></span><button class="bad" id="moDetener">Detener</button></div><div class="bar grande"><i style="width:${pctTot}%"></i></div><div class="l3 mut">${ahora || (corr ? `ahora: <b>${esc((corr.ruta || '').split(/[\\/]/).pop())}</b> · ${Math.round(corr.pct || 0)} %${restoDe(corr)} de este clip · ${eta ? eta.en_espera : pend} en espera` : 'empezando…')}</div></div>`;
|
||||||
} else if (pend) {
|
} else if (pend) {
|
||||||
mo = `<div class="momento"><div class="l1"><b>${pend} clip${pend > 1 ? 's' : ''} en espera, con la cola parada</b><span class="mut">lo que ya iba se retoma desde cero</span><span class="sep"></span><button class="p grande" id="moReanudar">Reanudar la cola</button></div></div>`;
|
mo = `<div class="momento"><div class="l1"><b>${pend} clip${pend > 1 ? 's' : ''} en espera, con la cola parada</b><span class="mut">lo que ya iba se retoma desde cero</span><span class="sep"></span><button class="p grande" id="moReanudar">Reanudar la cola</button></div></div>`;
|
||||||
} else if (vers.length) {
|
} else if (vers.length) {
|
||||||
@@ -2196,7 +2201,7 @@ function comparar(id){
|
|||||||
const br = $('#cmpReemplazar'); if (br) br.onclick = () => accion(br, async () => { const r = await api('/api/optimizar/reemplazar', {ids: [id]}); const x = r.resultados[0] || {}; aviso(x.ok ? `Reemplazado: ${x.motivo}` : `No se reemplazó: ${x.motivo}`, x.ok ? 'ok' : 'bad', x.ok ? {texto: 'Ver historial', ir: () => irA('historial')} : null); $('#modal').classList.remove('on'); cargarCola(); });
|
const br = $('#cmpReemplazar'); if (br) br.onclick = () => accion(br, async () => { const r = await api('/api/optimizar/reemplazar', {ids: [id]}); const x = r.resultados[0] || {}; aviso(x.ok ? `Reemplazado: ${x.motivo}` : `No se reemplazó: ${x.motivo}`, x.ok ? 'ok' : 'bad', x.ok ? {texto: 'Ver historial', ir: () => irA('historial')} : null); $('#modal').classList.remove('on'); 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 && !(est.includes('verificado') && (t.formato || '').startsWith('prores'))).map(t => t.id); };
|
const idsPorEstado = async (...est) => { const d = await api('/api/optimizar/cola?raiz=' + encodeURIComponent(raizActual())); 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