Convertir: horario y pausa si edito, aligerar solo lo viejo, aviso de Windows, resumen de la tanda con CSV, y marcar con criterio
Las tres propuestas que faltaban del lienzo: - La cola respeta un horario (espera entre clip y clip) y espera si Resolve, Premiere o After Effects están usando la tarjeta (nvidia-smi pmon). La pasada automática puede meter en cola lo de más de N meses hasta un tope de GB. Al acabar, un aviso de Windows con la identidad del programa. - Cada tanda deja un resumen en la bitácora (op tanda): clips, pesos, tiempo, nota, fallos, por carpeta. Sale en Convertir con «Exportar CSV» y «Abrir la carpeta», y en Historial cuenta clips. - Barra de marcar con criterio (tamaño, cámara, mes, antigüedad) y buscador. Pruebas: probar 19, prueba_orden 33, prueba_funcional 32 (nueva: resumen de la tanda), prueba_ventana 18. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
26
README.md
26
README.md
@@ -913,8 +913,7 @@ También del 10 de octubre:
|
|||||||
- **El menú lateral se queda fijo al bajar.** Munka lo vio irse con el resto de la página (en la 3.3.0 y la
|
- **El menú lateral se queda fijo al bajar.** Munka lo vio irse con el resto de la página (en la 3.3.0 y la
|
||||||
3.4.0): el cuerpo tenía `height:100%`, el alto de la ventana, así que su rejilla medía una pantalla y el
|
3.4.0): el cuerpo tenía `height:100%`, el alto de la ventana, así que su rejilla medía una pantalla y el
|
||||||
lateral «sticky» no tenía recorrido. Ahora el cuerpo crece con el contenido y el lateral no se mueve.
|
lateral «sticky» no tenía recorrido. Ahora el cuerpo crece con el contenido y el lateral no se mueve.
|
||||||
- **Convertir, rehecho** (Munka eligió tres de seis propuestas dibujadas en un lienzo; quedan para después
|
- **Convertir, rehecho** (seis propuestas dibujadas en un lienzo, las seis hechas):
|
||||||
el horario nocturno, el resumen al acabar y la barra de marcar con criterio):
|
|
||||||
- *Una sola lista, por carpetas.* Antes había dos: los candidatos (con el estado que tenían al pulsar
|
- *Una sola lista, por carpetas.* Antes había dos: los candidatos (con el estado que tenían al pulsar
|
||||||
«Buscar», que se quedaba en «corriendo» en un clip ya acabado) y, 700 filas más abajo, la cola con el
|
«Buscar», que se quedaba en «corriendo» en un clip ya acabado) y, 700 filas más abajo, la cola con el
|
||||||
avance. Ahora cada clip sale una vez con su estado de verdad, agrupado por carpeta (la ruta una sola vez,
|
avance. Ahora cada clip sale una vez con su estado de verdad, agrupado por carpeta (la ruta una sola vez,
|
||||||
@@ -934,6 +933,22 @@ También del 10 de octubre:
|
|||||||
≈ B» (con «y reemplazar solo cuando estén verificados», ajuste `optimizar.auto_reemplazar`: al acabar cada
|
≈ B» (con «y reemplazar solo cuando estén verificados», ajuste `optimizar.auto_reemplazar`: al acabar cada
|
||||||
clip, si verificó, reemplaza), la tanda con su barra y «Detener», «Reanudar la cola» si está parada, o,
|
clip, si verificó, reemplaza), la tanda con su barra y «Detener», «Reanudar la cola» si está parada, o,
|
||||||
con todo verificado, «Reemplazar los N originales · libera X» en verde con «comparar unos cuantos».
|
con todo verificado, «Reemplazar los N originales · libera X» en verde con «comparar unos cuantos».
|
||||||
|
- *Cuándo trabaja la cola y qué mete sola* (un desplegable en Convertir, ajustes `optimizar.horario`,
|
||||||
|
`pausar_si_edito`, `aligerar`, `avisar`): un horario (de 20:00 a 07:00, puede pasar por medianoche) fuera
|
||||||
|
del cual la cola espera entre clip y clip, sin perder lo que iba; esperar si DaVinci Resolve, Premiere o
|
||||||
|
After Effects están usando la tarjeta (lo dice `nvidia-smi pmon`, uso de cómputo o codificador por proceso;
|
||||||
|
sin nvidia-smi, basta con que estén abiertos), siempre entre clips, nunca a mitad de uno; «aligerar solo lo
|
||||||
|
viejo», que en la pasada automática mete en cola lo de más de N meses, de más pesado a menos, hasta un tope
|
||||||
|
de GB por pasada, en todas las carpetas conectadas (convierte y verifica; no reemplaza salvo
|
||||||
|
`auto_reemplazar`; si la pasada corre sin ventana, la cola va en ese mismo proceso); y «avisar al terminar»,
|
||||||
|
un aviso de Windows con la identidad del programa cuando acaba la tanda o si falla.
|
||||||
|
- *Resumen al acabar.* Cada tanda deja en la bitácora (op `tanda`) cuántos clips, pesos antes y después,
|
||||||
|
tiempo, nota media si se midió, fallos y «no compensan», y el reparto por carpeta. En Convertir sale la
|
||||||
|
ficha de la última tanda (con «Exportar CSV», `/api/optimizar/tanda.csv`, y «Abrir la carpeta») hasta que
|
||||||
|
se oculta o empieza otra; en Historial e Inicio cuenta clips, no movimientos, y no se deshace.
|
||||||
|
- *Marcar con criterio.* Sobre la lista, una barra de criterios con su cuenta: todos los válidos, los de más
|
||||||
|
de 5 GB, por cámara (BRAW, H.264…), por mes, de más de un año; «ninguno»; y un buscador por nombre o carpeta
|
||||||
|
que filtra la lista sin perder el foco. Debajo, «87 marcados · 496 GB → ≈ 23 GB».
|
||||||
- *Comparar antes de reemplazar.* En cualquier clip convertido, «comparar» abre el mismo cuadro del original
|
- *Comparar antes de reemplazar.* En cualquier clip convertido, «comparar» abre el mismo cuadro del original
|
||||||
y del convertido, lado a lado, en tres instantes del clip, con una lupa que amplía el mismo recorte en los
|
y del convertido, lado a lado, en tres instantes del clip, con una lupa que amplía el mismo recorte en los
|
||||||
dos; «Medir la calidad» da la nota VMAF (0 a 100) sobre unos 10 s del medio del clip, con el cuadro peor,
|
dos; «Medir la calidad» da la nota VMAF (0 a 100) sobre unos 10 s del medio del clip, con el cuadro peor,
|
||||||
@@ -946,9 +961,10 @@ También del 10 de octubre:
|
|||||||
cómo queda es un árbol de carpetas, no una lista de rutas completas.
|
cómo queda es un árbol de carpetas, no una lista de rutas completas.
|
||||||
|
|
||||||
Pruebas: `prueba_funcional.py` comprueba además que el avance de «Aplicar» acaba en «tantas de tantas» con
|
Pruebas: `prueba_funcional.py` comprueba además que el avance de «Aplicar» acaba en «tantas de tantas» con
|
||||||
los movimientos de la respuesta, y una nueva, «Comparar original y convertido», saca los dos cuadros (el
|
los movimientos de la respuesta, y dos nuevas: «Comparar original y convertido» saca los dos cuadros (el
|
||||||
original ya apartado en `_Para borrar/originales`) y mide la nota VMAF; `probar.py` 19, `prueba_orden.py` 33,
|
original ya apartado en `_Para borrar/originales`) y mide la nota VMAF, y «Resumen de la tanda» comprueba el
|
||||||
`prueba_funcional.py` 31 y `prueba_ventana.py` 18.
|
resumen, su CSV, su entrada en Historial y que el horario se guarda; `probar.py` 19, `prueba_orden.py` 33,
|
||||||
|
`prueba_funcional.py` 32 y `prueba_ventana.py` 18.
|
||||||
|
|
||||||
3.4.0, el convertidor: los BRAW salen, y lo que ya viene comprimido no se toca. Munka avisó de que había
|
3.4.0, el convertidor: los BRAW salen, y lo que ya viene comprimido no se toca. Munka avisó de que había
|
||||||
archivos que no podía reconvertir y que antes sí, y de lo que quiere: 10 bits de color, buena calidad y mucho
|
archivos que no podía reconvertir y que antes sí, y de lo que quiere: 10 bits de color, buena calidad y mucho
|
||||||
|
|||||||
275
organizador.py
275
organizador.py
@@ -2850,7 +2850,197 @@ 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}
|
||||||
|
|
||||||
|
|
||||||
|
def _opt(k, defecto=None):
|
||||||
|
v = (CONFIG.get("optimizar") or {}).get(k)
|
||||||
|
return defecto if v is None else v
|
||||||
|
|
||||||
|
|
||||||
|
def en_horario(ahora=None):
|
||||||
|
"""Si la cola tiene horario, ¿estamos dentro? (True, "") o (False, "hasta las 20:00"). El horario puede
|
||||||
|
pasar por medianoche (20:00 a 07:00)."""
|
||||||
|
h = _opt("horario") or {}
|
||||||
|
if not h.get("activo"):
|
||||||
|
return True, ""
|
||||||
|
try:
|
||||||
|
d1, m1 = map(int, (h.get("desde") or "20:00").split(":"))
|
||||||
|
d2, m2 = map(int, (h.get("hasta") or "07:00").split(":"))
|
||||||
|
except ValueError:
|
||||||
|
return True, ""
|
||||||
|
t = time.localtime(ahora or time.time())
|
||||||
|
x, a, b = t.tm_hour * 60 + t.tm_min, d1 * 60 + m1, d2 * 60 + m2
|
||||||
|
dentro = (a <= x < b) if a < b else (x >= a or x < b)
|
||||||
|
return (True, "") if dentro else (False, f"hasta las {h.get('desde') or '20:00'}")
|
||||||
|
|
||||||
|
|
||||||
|
APPS_EDICION = {"resolve.exe": "DaVinci Resolve", "adobe premiere pro.exe": "Premiere Pro", "afterfx.exe": "After Effects"}
|
||||||
|
|
||||||
|
|
||||||
|
def editando():
|
||||||
|
"""Qué programa de edición está usando la tarjeta ahora mismo, o "". Lo dice nvidia-smi pmon (uso de
|
||||||
|
cómputo, codificador y decodificador por proceso); si no hay nvidia-smi, basta con que el programa esté
|
||||||
|
abierto. Solo cuenta si está activado optimizar.pausar_si_edito."""
|
||||||
|
if not _opt("pausar_si_edito", True):
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["nvidia-smi", "pmon", "-c", "1", "-s", "u"], capture_output=True, text=True, timeout=15, creationflags=SIN_VENTANA)
|
||||||
|
if r.returncode == 0 and r.stdout:
|
||||||
|
for linea in r.stdout.splitlines():
|
||||||
|
partes = linea.split(None, 9)
|
||||||
|
if linea.startswith("#") or len(partes) < 10:
|
||||||
|
continue
|
||||||
|
nombre = partes[9].strip().lower()
|
||||||
|
uso = [float(u) for u in partes[3:7] if u.replace(".", "", 1).isdigit()]
|
||||||
|
for exe, bonito in APPS_EDICION.items():
|
||||||
|
if nombre and exe.startswith(nombre[:10]) and any(u > 5 for u in uso): # pmon recorta el nombre
|
||||||
|
return bonito
|
||||||
|
return ""
|
||||||
|
except (OSError, subprocess.SubprocessError, ValueError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["tasklist", "/FO", "CSV", "/NH"], capture_output=True, text=True, timeout=15, creationflags=SIN_VENTANA)
|
||||||
|
bajo = (r.stdout or "").lower()
|
||||||
|
for exe, bonito in APPS_EDICION.items():
|
||||||
|
if f'"{exe}"' in bajo:
|
||||||
|
return bonito
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _esperar_turno():
|
||||||
|
"""Antes de cada clip: si hay horario y estamos fuera, o si un programa de edición está usando la
|
||||||
|
tarjeta, la cola espera (sigue activa) y lo dice. Nunca corta un clip a medias. False si la detuvieron."""
|
||||||
|
while COLA["activa"] and not COLA["cancelar"]:
|
||||||
|
dentro, hasta = en_horario()
|
||||||
|
motivo = "" if dentro else f"espera {hasta}, por el horario de la cola"
|
||||||
|
if not motivo:
|
||||||
|
app = editando()
|
||||||
|
motivo = f"espera a que {app} deje la tarjeta" if app else ""
|
||||||
|
COLA["esperando"] = motivo
|
||||||
|
if not motivo:
|
||||||
|
return True
|
||||||
|
COLA["mensaje"] = motivo
|
||||||
|
time.sleep(20)
|
||||||
|
COLA["esperando"] = ""
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def notificar(titulo, texto):
|
||||||
|
"""Un aviso de Windows (la burbuja de la esquina), con la identidad del programa. Va en un PowerShell
|
||||||
|
aparte y sin esperar: si falla, no pasa nada."""
|
||||||
|
if os.name != "nt" or os.environ.get("ORGANILAB_ENSAYO"):
|
||||||
|
return False
|
||||||
|
esc_ = lambda s: str(s).replace("&", "&").replace("<", "<").replace(">", ">").replace("'", "'")
|
||||||
|
ps = ("[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null\n"
|
||||||
|
"[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null\n"
|
||||||
|
"$xml = New-Object Windows.Data.Xml.Dom.XmlDocument\n"
|
||||||
|
f"$xml.LoadXml('<toast><visual><binding template=\"ToastGeneric\"><text>{esc_(titulo)}</text><text>{esc_(texto)}</text></binding></visual></toast>')\n"
|
||||||
|
"$t = New-Object Windows.UI.Notifications.ToastNotification $xml\n"
|
||||||
|
"try { [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Labstream.OrganiLAB').Show($t) }\n"
|
||||||
|
"catch { [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe').Show($t) }\n")
|
||||||
|
try:
|
||||||
|
ruta = os.path.join(DATOS, "aviso.ps1")
|
||||||
|
with open(ruta, "w", encoding="utf-8-sig") as fh:
|
||||||
|
fh.write(ps.replace("\\n", "\n"))
|
||||||
|
subprocess.Popen(["powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", ruta],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=SIN_VENTANA)
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _resumen_tanda():
|
||||||
|
"""Al acabar la cola: qué se hizo en esta tanda (clips, pesos, tiempo, nota media, fallos, por carpeta).
|
||||||
|
Queda en la bitácora (op «tanda»), se recuerda para la interfaz y, si está activado, se avisa."""
|
||||||
|
ids = list(COLA.get("hechos_ids") or [])
|
||||||
|
if not ids:
|
||||||
|
return None
|
||||||
|
con = db()
|
||||||
|
ts = [dict(r) for r in con.execute(f"SELECT t.*, a.dur AS dur FROM trabajos t LEFT JOIN archivos a ON a.ruta = t.ruta "
|
||||||
|
f"WHERE t.id IN ({','.join('?' * len(ids))})", ids).fetchall()]
|
||||||
|
con.close()
|
||||||
|
bien = [t for t in ts if t["estado"] in ("listo", "verificado", "reemplazado")]
|
||||||
|
notas = [t["nota"] for t in bien if t.get("nota")]
|
||||||
|
carpetas = {}
|
||||||
|
for t in bien:
|
||||||
|
try:
|
||||||
|
rel = os.path.dirname(os.path.relpath(t["ruta"], t["raiz"]))
|
||||||
|
except ValueError:
|
||||||
|
rel = os.path.dirname(t["ruta"])
|
||||||
|
c = carpetas.setdefault(rel, {"carpeta": rel, "clips": 0, "bytes_in": 0, "bytes_out": 0})
|
||||||
|
c["clips"] += 1
|
||||||
|
c["bytes_in"] += t["bytes_in"] or 0
|
||||||
|
c["bytes_out"] += t["bytes_out"] or 0
|
||||||
|
raices = sorted({t["raiz"] for t in ts if t.get("raiz")})
|
||||||
|
seg = round(time.time() - (COLA["inicio"] or time.time()))
|
||||||
|
r = {"lote": "tanda-" + time.strftime("%Y%m%d-%H%M%S"), "inicio": COLA["inicio"], "seg": seg, "clips": len(bien), "pedidos": len(ts),
|
||||||
|
"bytes_in": sum(t["bytes_in"] or 0 for t in bien), "bytes_out": sum(t["bytes_out"] or 0 for t in bien),
|
||||||
|
"fallos": sum(1 for t in ts if t["estado"] in ("error", "cancelado")), "descartados": sum(1 for t in ts if t["estado"] == "descartado"),
|
||||||
|
"reemplazados": sum(1 for t in ts if t["estado"] == "reemplazado"), "verificados": sum(1 for t in ts if t["estado"] == "verificado"),
|
||||||
|
"nota": round(sum(notas) / len(notas), 1) if notas else None, "nota_min": min(notas) if notas else None,
|
||||||
|
"raiz": raices[0] if len(raices) == 1 else "", "raices": raices,
|
||||||
|
"cliente": ", ".join((raiz_cfg(x) or {}).get("cliente") or os.path.basename(x) for x in raices),
|
||||||
|
"carpetas": sorted(carpetas.values(), key=lambda c: -c["bytes_in"]), "ids": ids}
|
||||||
|
bitacora({"op": "tanda", **r})
|
||||||
|
COLA["ultima"] = dict(r, ts=time.time())
|
||||||
|
if _opt("avisar", True) and (r["clips"] or r["fallos"]):
|
||||||
|
que = f"{r['clips']} clip{'s' if r['clips'] != 1 else ''} convertido{'s' if r['clips'] != 1 else ''}"
|
||||||
|
if r["clips"]:
|
||||||
|
que += f" y verificado{'s' if r['clips'] != 1 else ''} en {_tiempo_llano(seg)}. " + ("Ya reemplazados." if r["reemplazados"] == r["clips"] else "Falta reemplazar.")
|
||||||
|
if r["fallos"]:
|
||||||
|
que += f" {r['fallos']} fallaron."
|
||||||
|
notificar("OrganiLAB terminó" if r["clips"] else "OrganiLAB: la tanda no salió", que)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def _tiempo_llano(seg):
|
||||||
|
seg = int(seg or 0)
|
||||||
|
if seg < 60:
|
||||||
|
return f"{seg} s"
|
||||||
|
if seg < 3600:
|
||||||
|
return f"{seg // 60} min"
|
||||||
|
return f"{seg // 3600} h {round(seg % 3600 / 60)} min"
|
||||||
|
|
||||||
|
|
||||||
|
def _ultima_tanda_guardada():
|
||||||
|
for e in reversed(leer_bitacora(3000)):
|
||||||
|
if e.get("op") == "tanda":
|
||||||
|
return e
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def tanda_csv(lote):
|
||||||
|
"""Los clips de una tanda, en CSV (para el cliente o para mirar en Excel)."""
|
||||||
|
import csv, io
|
||||||
|
e = next((x for x in reversed(leer_bitacora(3000)) if x.get("op") == "tanda" and x.get("lote") == lote), None)
|
||||||
|
if not e:
|
||||||
|
return None
|
||||||
|
ids = e.get("ids") or []
|
||||||
|
con = db()
|
||||||
|
ts = [dict(r) for r in con.execute(f"SELECT t.*, a.dur AS dur FROM trabajos t LEFT JOIN archivos a ON a.ruta = t.ruta "
|
||||||
|
f"WHERE t.id IN ({','.join('?' * len(ids))}) ORDER BY t.id", ids).fetchall()] if ids else []
|
||||||
|
con.close()
|
||||||
|
s = io.StringIO()
|
||||||
|
w = csv.writer(s, delimiter=";", lineterminator="\n")
|
||||||
|
w.writerow(["clip", "carpeta", "estado", "antes (MB)", "después (MB)", "ahorro %", "duración (s)", "nota de calidad", "formato", "salida"])
|
||||||
|
for t in ts:
|
||||||
|
try:
|
||||||
|
rel = os.path.dirname(os.path.relpath(t["ruta"], t["raiz"]))
|
||||||
|
except ValueError:
|
||||||
|
rel = os.path.dirname(t["ruta"])
|
||||||
|
bi, bo = t["bytes_in"] or 0, t["bytes_out"] or 0
|
||||||
|
w.writerow([os.path.basename(t["ruta"]), rel, EST_CSV.get(t["estado"], t["estado"]), round(bi / 1e6, 1), round(bo / 1e6, 1) if bo else "",
|
||||||
|
round((1 - bo / bi) * 100) if bi and bo else "", round(t["dur"] or 0, 1) if t.get("dur") else "", t.get("nota") or "",
|
||||||
|
formato_de(t), t["salida"] or ""])
|
||||||
|
return s.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
EST_CSV = {"pendiente": "en espera", "corriendo": "convirtiendo", "listo": "convertido", "verificado": "verificado", "reemplazado": "reemplazado",
|
||||||
|
"error": "falló", "cancelado": "cancelado", "descartado": "no compensa"}
|
||||||
|
|
||||||
|
|
||||||
def set_trabajo(id_, **kw):
|
def set_trabajo(id_, **kw):
|
||||||
@@ -3242,6 +3432,8 @@ def bucle_cola():
|
|||||||
t = con.execute("SELECT * FROM trabajos WHERE estado = 'pendiente' AND motor IN ('ffmpeg', 'braw') ORDER BY orden DESC, id LIMIT 1").fetchone()
|
t = con.execute("SELECT * FROM trabajos WHERE estado = 'pendiente' AND motor IN ('ffmpeg', 'braw') ORDER BY orden DESC, id LIMIT 1").fetchone()
|
||||||
lote_resolve = [dict(r) for r in con.execute("SELECT * FROM trabajos WHERE estado = 'pendiente' AND motor = 'resolve' ORDER BY id LIMIT 40").fetchall()]
|
lote_resolve = [dict(r) for r in con.execute("SELECT * FROM trabajos WHERE estado = 'pendiente' AND motor = 'resolve' ORDER BY id LIMIT 40").fetchall()]
|
||||||
con.close()
|
con.close()
|
||||||
|
if t and not _esperar_turno():
|
||||||
|
break
|
||||||
if t:
|
if t:
|
||||||
COLA["actual"] = t["id"]
|
COLA["actual"] = t["id"]
|
||||||
COLA["empezo"] = time.time()
|
COLA["empezo"] = time.time()
|
||||||
@@ -3258,6 +3450,7 @@ def bucle_cola():
|
|||||||
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
|
COLA["hechos"] += 1
|
||||||
|
COLA["hechos_ids"].append(t["id"])
|
||||||
elif lote_resolve:
|
elif lote_resolve:
|
||||||
COLA["actual"] = lote_resolve[0]["id"]
|
COLA["actual"] = lote_resolve[0]["id"]
|
||||||
try:
|
try:
|
||||||
@@ -3267,6 +3460,7 @@ def bucle_cola():
|
|||||||
set_trabajo(r["id"], estado="error", log=f"fallo inesperado del puente: {ex}")
|
set_trabajo(r["id"], estado="error", log=f"fallo inesperado del puente: {ex}")
|
||||||
_verificar_listos([r["id"] for r in lote_resolve])
|
_verificar_listos([r["id"] for r in lote_resolve])
|
||||||
COLA["hechos"] += len(lote_resolve)
|
COLA["hechos"] += len(lote_resolve)
|
||||||
|
COLA["hechos_ids"].extend(r["id"] for r in lote_resolve)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
COLA["actual"] = None
|
COLA["actual"] = None
|
||||||
@@ -3280,6 +3474,12 @@ def bucle_cola():
|
|||||||
COLA["activa"] = False
|
COLA["activa"] = False
|
||||||
COLA["actual"] = None
|
COLA["actual"] = None
|
||||||
COLA["hilo"] = None
|
COLA["hilo"] = None
|
||||||
|
COLA["esperando"] = ""
|
||||||
|
try:
|
||||||
|
_resumen_tanda()
|
||||||
|
except Exception as ex: # noqa
|
||||||
|
COLA["mensaje"] = f"no se pudo apuntar el resumen de la tanda: {ex}"
|
||||||
|
COLA["hechos_ids"] = []
|
||||||
|
|
||||||
|
|
||||||
def _anotar_ritmo(id_, segundos):
|
def _anotar_ritmo(id_, segundos):
|
||||||
@@ -3526,6 +3726,8 @@ def iniciar_cola():
|
|||||||
COLA["hechos"] = 0
|
COLA["hechos"] = 0
|
||||||
COLA["inicio"] = time.time()
|
COLA["inicio"] = time.time()
|
||||||
COLA["ritmo"] = []
|
COLA["ritmo"] = []
|
||||||
|
COLA["hechos_ids"] = []
|
||||||
|
COLA["esperando"] = ""
|
||||||
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
|
||||||
@@ -5337,12 +5539,12 @@ def automatico(en_hilo=False, origen="a mano"):
|
|||||||
raices = [_clave(r["ruta"]) for r in CONFIG.get("raices", [])]
|
raices = [_clave(r["ruta"]) for r in CONFIG.get("raices", [])]
|
||||||
AUTO.update(activo=True, raiz="", hechas=0, total=len(raices), inicio=time.time())
|
AUTO.update(activo=True, raiz="", hechas=0, total=len(raices), inicio=time.time())
|
||||||
if en_hilo:
|
if en_hilo:
|
||||||
threading.Thread(target=_automatico, args=(raices, origen), daemon=True).start()
|
threading.Thread(target=_automatico, args=(raices, origen, False), daemon=True).start()
|
||||||
return {"lanzado": True, "carpetas": len(raices)}
|
return {"lanzado": True, "carpetas": len(raices)}
|
||||||
return _automatico(raices, origen)
|
return _automatico(raices, origen, True)
|
||||||
|
|
||||||
|
|
||||||
def _automatico(raices, origen):
|
def _automatico(raices, origen, sincrono=False):
|
||||||
limite = int((CONFIG.get("automatico") or {}).get("limite") or 3000)
|
limite = int((CONFIG.get("automatico") or {}).get("limite") or 3000)
|
||||||
resumen = {"ts": time.time(), "origen": origen, "version": VERSION, "carpetas": []}
|
resumen = {"ts": time.time(), "origen": origen, "version": VERSION, "carpetas": []}
|
||||||
try:
|
try:
|
||||||
@@ -5355,6 +5557,10 @@ def _automatico(raices, origen):
|
|||||||
"estado": "con errores", "detalle": repr(ex), "movimientos": 0, "lotes": [],
|
"estado": "con errores", "detalle": repr(ex), "movimientos": 0, "lotes": [],
|
||||||
"dudosos": 0, "avisos": [], "errores": [repr(ex)]})
|
"dudosos": 0, "avisos": [], "errores": [repr(ex)]})
|
||||||
AUTO["hechas"] += 1
|
AUTO["hechas"] += 1
|
||||||
|
try:
|
||||||
|
aligerar_automatico(resumen, sincrono=sincrono)
|
||||||
|
except Exception as ex: # noqa
|
||||||
|
resumen["aligerar"] = {"error": repr(ex)}
|
||||||
resumen["seg"] = round(time.time() - resumen["ts"], 1)
|
resumen["seg"] = round(time.time() - resumen["ts"], 1)
|
||||||
guardar_json(AUTOMATICO_PATH, resumen)
|
guardar_json(AUTOMATICO_PATH, resumen)
|
||||||
finally:
|
finally:
|
||||||
@@ -5362,6 +5568,42 @@ def _automatico(raices, origen):
|
|||||||
return resumen
|
return resumen
|
||||||
|
|
||||||
|
|
||||||
|
def aligerar_automatico(resumen, sincrono=False):
|
||||||
|
"""Con optimizar.aligerar activado, la pasada mete en cola lo de más de N meses, de más pesado a menos,
|
||||||
|
hasta el tope de GB por noche, en todas las carpetas conectadas. Convierte y verifica; no reemplaza
|
||||||
|
(salvo que esté puesto auto_reemplazar). Si el programa corre sin ventana (la tarea de Windows), la cola
|
||||||
|
va aquí mismo, en este proceso; si no, arranca la cola de siempre."""
|
||||||
|
a = _opt("aligerar") or {}
|
||||||
|
if not a.get("activo"):
|
||||||
|
return
|
||||||
|
o = CONFIG["optimizar"]
|
||||||
|
meses, tope = int(a.get("meses") or 6), float(a.get("gb_noche") or 100) * 1e9
|
||||||
|
total_b, por_raiz = 0, {}
|
||||||
|
for r in CONFIG.get("raices", []):
|
||||||
|
raiz = _clave(r["ruta"])
|
||||||
|
if not os.path.isdir(raiz):
|
||||||
|
continue
|
||||||
|
AUTO["raiz"] = raiz
|
||||||
|
analizar(raiz, o["min_mb"]) # lee con ffprobe lo que falte, aquí mismo
|
||||||
|
for c in sorted(candidatos(raiz, meses, o["perfil"], o["min_mb"], False, o.get("formato", "h265_10bit")), key=lambda c: -c["bytes"]):
|
||||||
|
if c["excluido"] or c["en_cola"] or total_b + c["bytes"] > tope:
|
||||||
|
continue
|
||||||
|
total_b += c["bytes"]
|
||||||
|
por_raiz.setdefault(raiz, []).append(c["ruta"])
|
||||||
|
n = 0
|
||||||
|
for raiz, rutas in por_raiz.items():
|
||||||
|
n += len(encolar(rutas, o["perfil"], o.get("chroma", "420"), o.get("color", "rec709"), raiz, o.get("formato", "h265_10bit")) or [])
|
||||||
|
resumen["aligerar"] = {"clips": n, "bytes": total_b, "meses": meses, "tope_gb": a.get("gb_noche") or 100}
|
||||||
|
if not n:
|
||||||
|
return
|
||||||
|
if sincrono:
|
||||||
|
with _COLA_LOCK:
|
||||||
|
COLA.update(activa=True, cancelar=False, hechos=0, inicio=time.time(), ritmo=[], hechos_ids=[], esperando="")
|
||||||
|
bucle_cola()
|
||||||
|
else:
|
||||||
|
iniciar_cola()
|
||||||
|
|
||||||
|
|
||||||
def estado_automatico():
|
def estado_automatico():
|
||||||
c = CONFIG.get("automatico") or {}
|
c = CONFIG.get("automatico") or {}
|
||||||
return {"activo": bool(c.get("activo")), "hora": c.get("hora") or "02:00", "limite": int(c.get("limite") or 3000),
|
return {"activo": bool(c.get("activo")), "hora": c.get("hora") or "02:00", "limite": int(c.get("limite") or 3000),
|
||||||
@@ -5835,6 +6077,11 @@ class Manejador(BaseHTTPRequestHandler):
|
|||||||
lo["ejemplos"].append({"de": e.get("de"), "a": e.get("a")})
|
lo["ejemplos"].append({"de": e.get("de"), "a": e.get("a")})
|
||||||
if e.get("op") == "mkdir": # una pieza nueva: cuentan las carpetas creadas
|
if e.get("op") == "mkdir": # una pieza nueva: cuentan las carpetas creadas
|
||||||
lo["movimientos"] += 1
|
lo["movimientos"] += 1
|
||||||
|
if e.get("op") == "tanda": # una tanda del convertidor: su resumen
|
||||||
|
lo["raiz"] = e.get("raiz", "") or ", ".join(e.get("raices") or [])
|
||||||
|
lo["movimientos"] = e.get("clips", 0)
|
||||||
|
lo["errores"] = e.get("fallos", 0)
|
||||||
|
lo["tanda"] = {k: e.get(k) for k in ("clips", "bytes_in", "bytes_out", "seg", "nota", "reemplazados", "descartados", "cliente")}
|
||||||
if e.get("op") == "fin":
|
if e.get("op") == "fin":
|
||||||
lo["raiz"] = e.get("raiz", "")
|
lo["raiz"] = e.get("raiz", "")
|
||||||
lo["errores"] = len(e.get("errores") or [])
|
lo["errores"] = len(e.get("errores") or [])
|
||||||
@@ -5867,7 +6114,22 @@ class Manejador(BaseHTTPRequestHandler):
|
|||||||
eta = eta_cola(filas) if COLA["activa"] else None
|
eta = eta_cola(filas) if COLA["activa"] else None
|
||||||
except Exception: # noqa
|
except Exception: # noqa
|
||||||
eta = None
|
eta = None
|
||||||
return self._json({"trabajos": filas, "cola": {"activa": COLA["activa"], "actual": COLA["actual"], "mensaje": COLA["mensaje"], "eta": eta}})
|
if COLA.get("ultima") is None:
|
||||||
|
COLA["ultima"] = _ultima_tanda_guardada()
|
||||||
|
return self._json({"trabajos": filas, "cola": {"activa": COLA["activa"], "actual": COLA["actual"], "mensaje": COLA["mensaje"], "eta": eta,
|
||||||
|
"esperando": COLA.get("esperando") or "", "ultima": COLA.get("ultima") or None}})
|
||||||
|
if ruta == "/api/optimizar/tanda.csv": # la tanda, en CSV
|
||||||
|
csv_ = tanda_csv(q.get("lote") or "")
|
||||||
|
if csv_ is None:
|
||||||
|
return self._json({"error": "no hay una tanda con ese nombre"}, 404)
|
||||||
|
cuerpo = csv_.encode("utf-8-sig")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "text/csv; charset=utf-8")
|
||||||
|
self.send_header("Content-Disposition", f"attachment; filename=\"OrganiLAB-{q.get('lote') or 'tanda'}.csv\"")
|
||||||
|
self.send_header("Content-Length", str(len(cuerpo)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(cuerpo)
|
||||||
|
return
|
||||||
if ruta == "/api/optimizar/cuadro": # un cuadro del original o del convertido, para «comparar»
|
if ruta == "/api/optimizar/cuadro": # un cuadro del original o del convertido, para «comparar»
|
||||||
try:
|
try:
|
||||||
jpg = cuadro_de(int(q.get("id") or 0), q.get("cual") or "convertido", float(q.get("t") or 0))
|
jpg = cuadro_de(int(q.get("id") or 0), q.get("cual") or "convertido", float(q.get("t") or 0))
|
||||||
@@ -5971,7 +6233,8 @@ 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", "formato", "chroma", "color", "salida", "sufijo", "auto_reemplazar"},
|
permitidas = {"optimizar": {"meses_minimos", "min_mb", "perfil", "formato", "chroma", "color", "salida", "sufijo", "auto_reemplazar",
|
||||||
|
"horario", "pausar_si_edito", "aligerar", "avisar"},
|
||||||
"respaldo": {"destino"}, "actualizaciones": {"auto"}}
|
"respaldo": {"destino"}, "actualizaciones": {"auto"}}
|
||||||
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):
|
||||||
|
|||||||
@@ -383,6 +383,25 @@ def p_comparar():
|
|||||||
return 'dos cuadros JPEG y nota VMAF %s sobre %d cuadros (%.0f s)' % (r['vmaf'], r['cuadros'], r['seg'])
|
return 'dos cuadros JPEG y nota VMAF %s sobre %d cuadros (%.0f s)' % (r['vmaf'], r['cuadros'], r['seg'])
|
||||||
|
|
||||||
|
|
||||||
|
def p_tanda():
|
||||||
|
# al acabar la cola queda el resumen de la tanda: clips, pesos, tiempo; y se exporta en CSV
|
||||||
|
u = get('/api/optimizar/cola')['cola']['ultima']
|
||||||
|
assert u and u['clips'] >= 1 and u['bytes_in'] > u['bytes_out'] > 0 and u['lote'].startswith('tanda-'), u
|
||||||
|
assert IDS['trabajo'] in u['ids'] and u['raiz'].lower() == RAIZ.lower(), u
|
||||||
|
r = urllib.request.urlopen(BASE + '/api/optimizar/tanda.csv' + q(lote=u['lote']), timeout=60)
|
||||||
|
csv_ = r.read().decode('utf-8-sig')
|
||||||
|
assert r.headers.get('Content-Type', '').startswith('text/csv') and csv_.startswith('clip;carpeta;estado') and 'reemplazado' in csv_, csv_[:200]
|
||||||
|
lotes = get('/api/bitacora')['lotes']
|
||||||
|
l = next((x for x in lotes if x['lote'] == u['lote']), None)
|
||||||
|
assert l and l['tanda']['clips'] == u['clips'], l
|
||||||
|
# el horario: fuera de él la cola espera (se comprueba la regla, no esperando de verdad)
|
||||||
|
c, r2 = post('/api/config', {'optimizar': {'horario': {'activo': True, 'desde': '20:00', 'hasta': '07:00'}, 'pausar_si_edito': False, 'avisar': False}})
|
||||||
|
assert c == 200 and r2['config']['optimizar']['horario']['desde'] == '20:00'
|
||||||
|
c, r3 = post('/api/config', {'optimizar': {'horario': {'activo': False, 'desde': '20:00', 'hasta': '07:00'}}})
|
||||||
|
assert c == 200 and not r3['config']['optimizar']['horario']['activo']
|
||||||
|
return 'resumen de %d clip(s), %s, CSV con %d líneas, y en Historial' % (u['clips'], u['lote'], csv_.count('\n'))
|
||||||
|
|
||||||
|
|
||||||
def p_braw():
|
def p_braw():
|
||||||
m = get('/api/optimizar/motores')['braw']
|
m = get('/api/optimizar/motores')['braw']
|
||||||
if not BRAW:
|
if not BRAW:
|
||||||
@@ -552,7 +571,7 @@ for nombre, fn in (('Interfaz sin nada externo', p_estatico), ('Registrar carpet
|
|||||||
('Historial y papelera', p_historial), ('Repetidos', p_repetidos), ('Stock viejo', p_stock),
|
('Historial y papelera', p_historial), ('Repetidos', p_repetidos), ('Stock viejo', p_stock),
|
||||||
('Carpetas vacías', p_vacias), ('Mapa interactivo', p_mapa), ('Colores en Windows', p_colores),
|
('Carpetas vacías', p_vacias), ('Mapa interactivo', p_mapa), ('Colores en Windows', p_colores),
|
||||||
('Motores de conversión', p_motores), ('Leer clips (ffprobe)', p_analizar),
|
('Motores de conversión', p_motores), ('Leer clips (ffprobe)', p_analizar),
|
||||||
('Convertir, verificar, reemplazar', p_convertir), ('Comparar original y convertido', p_comparar), ('Convertir un BRAW', p_braw), ('Control de la cola', p_cola_control),
|
('Convertir, verificar, reemplazar', p_convertir), ('Comparar original y convertido', p_comparar), ('Resumen de la tanda', p_tanda), ('Convertir un BRAW', p_braw), ('Control de la cola', p_cola_control),
|
||||||
('Respaldar', p_respaldo), ('Asistente / hacer de una vez', p_asistente),
|
('Respaldar', p_respaldo), ('Asistente / hacer de una vez', p_asistente),
|
||||||
('Pieza nueva', p_pieza), ('Bandeja de entrada y tarjeta', p_entrada), ('Orden automático', p_automatico),
|
('Pieza nueva', p_pieza), ('Bandeja de entrada y tarjeta', p_entrada), ('Orden automático', p_automatico),
|
||||||
('Arrastrar y buscar carpeta', p_soltar_y_buscar), ('Actualizaciones', p_actualizacion_offline),
|
('Arrastrar y buscar carpeta', p_soltar_y_buscar), ('Actualizaciones', p_actualizacion_offline),
|
||||||
|
|||||||
@@ -374,6 +374,32 @@
|
|||||||
.antes .pista i.libre{display:block;height:100%;background:var(--ok2);border-right:2px solid var(--ok)}
|
.antes .pista i.libre{display:block;height:100%;background:var(--ok2);border-right:2px solid var(--ok)}
|
||||||
.antes .sw{display:inline-block;width:10px;height:10px;border-radius:2px;vertical-align:-1px;margin-right:4px}.antes .sw.conv{background:var(--acc)}.antes .sw.libre{background:var(--ok2);border:1px solid var(--ok)}
|
.antes .sw{display:inline-block;width:10px;height:10px;border-radius:2px;vertical-align:-1px;margin-right:4px}.antes .sw.conv{background:var(--acc)}.antes .sw.libre{background:var(--ok2);border:1px solid var(--ok)}
|
||||||
.modal.ancho .box{max-width:1200px}
|
.modal.ancho .box{max-width:1200px}
|
||||||
|
.ajcola{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:10px}
|
||||||
|
@media(max-width:900px){.ajcola{grid-template-columns:1fr}}
|
||||||
|
.ajcola .caja{border:1px solid var(--line);border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;background:var(--panel)}
|
||||||
|
.ajcola .caja .t{display:flex;gap:8px;align-items:center;font-weight:700;font-size:14px}
|
||||||
|
.ajcola .caja .d{font-size:12.5px;color:var(--mut)}
|
||||||
|
.ajcola .caja .f{display:flex;gap:8px;align-items:center;flex-wrap:wrap;font-size:12.5px;color:var(--mut)}
|
||||||
|
.ajcola input[type=time],.ajcola input[type=number]{font:inherit;padding:4px 6px;border:1px solid var(--line);border-radius:5px}
|
||||||
|
.ajcola .dia{display:flex;gap:2px;height:12px;border-radius:4px;overflow:hidden}
|
||||||
|
.ajcola .dia i{display:block;height:100%}
|
||||||
|
.ajcola .apps span{background:var(--acc2);color:var(--acc);border-radius:99px;padding:1px 9px;font-weight:600;font-size:12px;margin-right:4px}
|
||||||
|
.tanda{background:var(--panel);border:1px solid var(--ok);border-radius:8px;padding:14px 16px;margin-bottom:12px;display:flex;flex-direction:column;gap:12px}
|
||||||
|
.tanda .l1{display:flex;gap:10px;align-items:center;flex-wrap:wrap;font-size:13.5px}
|
||||||
|
.tanda .cifras{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px}
|
||||||
|
@media(max-width:900px){.tanda .cifras{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||||
|
.tanda .cifras div{border-bottom:1px solid var(--line);padding:2px 0 8px}
|
||||||
|
.tanda .cifras b{font-size:20px;display:block;white-space:nowrap}
|
||||||
|
.tanda .cifras small{font-size:12px;color:var(--mut)}
|
||||||
|
.tanda .carp{display:flex;gap:10px;align-items:center;font-size:12.5px}
|
||||||
|
.tanda .carp .n{width:260px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.tanda .carp .b{flex:1;height:8px;background:var(--panel2);border-radius:99px;overflow:hidden}.tanda .carp .b i{display:block;height:100%;background:var(--acc)}
|
||||||
|
.tanda .carp .c{width:190px;text-align:right;color:var(--mut);white-space:nowrap}
|
||||||
|
.marcar{display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:8px 0;border-bottom:1px solid var(--line);font-size:12.5px}
|
||||||
|
.marcar>span{color:var(--mut);margin-right:2px}
|
||||||
|
.marcar button{background:#fff;border:1px solid var(--line);border-radius:99px;padding:2px 10px;font-size:12px;font-weight:600;color:var(--tx);cursor:pointer}
|
||||||
|
.marcar button.on{background:var(--acc2);border-color:var(--acc);color:var(--acc)}
|
||||||
|
.marcar input[type=search]{font:inherit;padding:4px 8px;border:1px solid var(--line);border-radius:5px;min-width:220px;margin-left:auto}
|
||||||
.cmp{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
.cmp{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||||||
.cmp .lado{display:flex;flex-direction:column;gap:6px;min-width:0}
|
.cmp .lado{display:flex;flex-direction:column;gap:6px;min-width:0}
|
||||||
.cmp .marco{position:relative;background:#d9dcdf;border-radius:8px;overflow:hidden;aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;color:#4d5157;font-size:12.5px}
|
.cmp .marco{position:relative;background:#d9dcdf;border-radius:8px;overflow:hidden;aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;color:#4d5157;font-size:12.5px}
|
||||||
@@ -636,6 +662,8 @@
|
|||||||
<button class="lnk" id="btnQuitarEspera" style="display:none">Quitar los que esperan</button>
|
<button class="lnk" id="btnQuitarEspera" style="display:none">Quitar los que esperan</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="anaEstado" class="mut" style="margin:-6px 0 10px"></div>
|
<div id="anaEstado" class="mut" style="margin:-6px 0 10px"></div>
|
||||||
|
<div id="tanda"></div>
|
||||||
|
<details class="mas" id="colaAjustes" style="margin-bottom:12px"><summary>Cuándo trabaja la cola y qué mete sola</summary><div class="ajcola" id="colaAjustesCuerpo"></div></details>
|
||||||
<div class="box" id="convCaja"><div id="convLista"><span class="mut">Pulsa «Buscar clips» para ver qué se puede convertir en esta carpeta.</span></div></div>
|
<div class="box" id="convCaja"><div id="convLista"><span class="mut">Pulsa «Buscar clips» para ver qué se puede convertir en esta carpeta.</span></div></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -723,6 +751,9 @@
|
|||||||
|
|
||||||
<h3>Convertidor</h3>
|
<h3>Convertidor</h3>
|
||||||
<dl>
|
<dl>
|
||||||
|
<dt>Cuándo trabaja la cola</dt><dd>Un desplegable en Convertir: un horario fuera del cual la cola espera entre clip y clip; esperar si Resolve, Premiere o After Effects están usando la tarjeta (nunca corta un clip a medias); «aligerar solo lo viejo», que en la pasada automática de cada noche mete en cola lo de más de N meses hasta un tope de GB (convierte y verifica, no reemplaza); y un aviso de Windows al terminar. <i>Cómo: la cola mira el horario y los programas de edición antes de cada clip; si tiene que esperar, lo dice en el panel de arriba.</i></dd>
|
||||||
|
<dt>Resumen de la tanda</dt><dd>Al acabar la cola queda una ficha: clips, peso antes y después, lo que se libera, la nota media si se midió, los fallos, y el reparto por carpeta, con «Exportar CSV» y «Abrir la carpeta». Sale en Convertir hasta que la ocultes o empiece otra, y queda en Historial.</dd>
|
||||||
|
<dt>Marcar con criterio</dt><dd>Sobre la lista del convertidor: todos los válidos, los de más de 5 GB, por cámara, por mes, de más de un año, ninguno; y un buscador por nombre o carpeta.</dd>
|
||||||
<dt>Comparar</dt><dd>En un clip convertido, enseña el mismo cuadro del original y del convertido, lado a lado, en tres instantes del clip; al pasar el ratón, una lupa amplía el mismo recorte en los dos. «Medir la calidad» da la nota de 0 a 100 (VMAF) sobre unos diez segundos del medio del clip; de 93 arriba no se nota a simple vista. Desde ahí se reemplaza ese original solo. <i>Cómo: un BRAW se revela con la misma curva con que se convirtió; si el original ya se reemplazó, se busca en _Para borrar/originales. La nota se guarda en el trabajo y sale en la lista.</i></dd>
|
<dt>Comparar</dt><dd>En un clip convertido, enseña el mismo cuadro del original y del convertido, lado a lado, en tres instantes del clip; al pasar el ratón, una lupa amplía el mismo recorte en los dos. «Medir la calidad» da la nota de 0 a 100 (VMAF) sobre unos diez segundos del medio del clip; de 93 arriba no se nota a simple vista. Desde ahí se reemplaza ese original solo. <i>Cómo: un BRAW se revela con la misma curva con que se convirtió; si el original ya se reemplazó, se busca en _Para borrar/originales. La nota se guarda en el trabajo y sale en la lista.</i></dd>
|
||||||
<dt>Buscar clips</dt><dd>Encuentra los clips de cámara viejos y pesados (de fábrica, de más de 3 meses y 300 MB) y calcula cuánto pesarían convertidos. <i>Cómo: lee códec, duración y resolución con ffprobe; se salta lo que ya es ligero, los exportados y lo que ya convirtió este programa, para no pasar un clip dos veces.</i></dd>
|
<dt>Buscar clips</dt><dd>Encuentra los clips de cámara viejos y pesados (de fábrica, de más de 3 meses y 300 MB) y calcula cuánto pesarían convertidos. <i>Cómo: lee códec, duración y resolución con ffprobe; se salta lo que ya es ligero, los exportados y lo que ya convirtió este programa, para no pasar un clip dos veces.</i></dd>
|
||||||
<dt>Formato y calidad</dt><dd>H.265 10 bits (el recomendado, unas diez veces más ligero), H.265 ligero, H.264 o ProRes proxy, LT y HQ; calidad de ahorro, equilibrada o máxima. «Motores» te dice qué acelera tu tarjeta gráfica.</dd>
|
<dt>Formato y calidad</dt><dd>H.265 10 bits (el recomendado, unas diez veces más ligero), H.265 ligero, H.264 o ProRes proxy, LT y HQ; calidad de ahorro, equilibrada o máxima. «Motores» te dice qué acelera tu tarjeta gráfica.</dd>
|
||||||
@@ -874,7 +905,7 @@ const cargarVista = seguro(async t => {
|
|||||||
if (t === 'carpeta') await cargarCarpeta();
|
if (t === 'carpeta') await cargarCarpeta();
|
||||||
if (t === 'revisar') { await cargarMapa(); await cargarVoc(); if (PLAN) pintarPlan(); if (STOCK.length && STOCK[0].raiz !== raizActual()) { STOCK = []; $('#stockCaja').style.display = 'none'; } if (VACIAS && VACIAS.raiz !== raizActual()) { VACIAS = null; pintarVacias(); } }
|
if (t === 'revisar') { await cargarMapa(); await cargarVoc(); if (PLAN) pintarPlan(); if (STOCK.length && STOCK[0].raiz !== raizActual()) { STOCK = []; $('#stockCaja').style.display = 'none'; } if (VACIAS && VACIAS.raiz !== raizActual()) { VACIAS = null; pintarVacias(); } }
|
||||||
if (t === 'mapa') await cargarMapaEst();
|
if (t === 'mapa') await cargarMapaEst();
|
||||||
if (t === 'convertir') { await cargarCola(); }
|
if (t === 'convertir') { pintarAjustesCola(); await cargarCola(); }
|
||||||
if (t === 'respaldar') { if (!$('#respDestino').value) $('#respDestino').value = (ESTADO.config.respaldo || {}).destino || ''; pintarRespaldo(); }
|
if (t === 'respaldar') { if (!$('#respDestino').value) $('#respDestino').value = (ESTADO.config.respaldo || {}).destino || ''; pintarRespaldo(); }
|
||||||
if (t === 'historial') { await cargarBit(); }
|
if (t === 'historial') { await cargarBit(); }
|
||||||
if (t === 'ajustes') pintarAjustes();
|
if (t === 'ajustes') pintarAjustes();
|
||||||
@@ -1140,7 +1171,7 @@ async function cargarCarpeta(){
|
|||||||
$('#inicioDiag').innerHTML = d.frases.slice(0, 3).map(f => `<div>${esc(f)}</div>`).join('') + (d.frases.length > 3 ? `<div class="mut">Más en Revisar y ordenar.</div>` : '');
|
$('#inicioDiag').innerHTML = d.frases.slice(0, 3).map(f => `<div>${esc(f)}</div>`).join('') + (d.frases.length > 3 ? `<div class="mut">Más en Revisar y ordenar.</div>` : '');
|
||||||
}
|
}
|
||||||
const lotes = CARPETA.lotes || [];
|
const lotes = CARPETA.lotes || [];
|
||||||
$('#inicioLotes').innerHTML = lotes.length ? lotes.map(l => `<div class="ev"><span class="h">${esc(fechaCorta(l.ts))}</span><span class="q"><b>${esc(queSeHizo(l.lote))}</b> · ${l.movimientos} mov.${l.errores ? ` <span class="bad">· ${l.errores} avisos</span>` : ''}</span>${l.lote && (l.lote.startsWith('optimizar') || l.lote.startsWith('entrada-')) ? '' : `<button data-l="${esc(l.lote)}">Deshacer</button>`}</div>`).join('') : '<span class="mut">Todavía no se ha movido nada en esta carpeta.</span>';
|
$('#inicioLotes').innerHTML = lotes.length ? lotes.map(l => `<div class="ev"><span class="h">${esc(fechaCorta(l.ts))}</span><span class="q"><b>${esc(queSeHizo(l.lote))}</b> · ${cuentaLote(l, true)}${l.errores ? ` <span class="bad">· ${l.errores} avisos</span>` : ''}</span>${l.lote && (l.lote.startsWith('optimizar') || l.lote.startsWith('entrada-') || l.lote.startsWith('tanda-')) ? '' : `<button data-l="${esc(l.lote)}">Deshacer</button>`}</div>`).join('') : '<span class="mut">Todavía no se ha movido nada en esta carpeta.</span>';
|
||||||
$$('#inicioLotes button[data-l]').forEach(bt => bt.onclick = () => deshacer(bt.dataset.l));
|
$$('#inicioLotes button[data-l]').forEach(bt => bt.onclick = () => deshacer(bt.dataset.l));
|
||||||
}
|
}
|
||||||
// ---------------- lo que se puede hacer en esta carpeta ----------------
|
// ---------------- lo que se puede hacer en esta carpeta ----------------
|
||||||
@@ -1191,11 +1222,17 @@ async function pintarAcciones(){
|
|||||||
await cargarEstado(); await pintarAcciones();
|
await cargarEstado(); await pintarAcciones();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
// «212 clips · 676 GB → 24 GB · nota 96» para una tanda del convertidor; «N movimientos» para lo demás
|
||||||
|
function cuentaLote(l, corto){
|
||||||
|
if (l.tanda) return `${l.tanda.clips} clip${l.tanda.clips !== 1 ? 's' : ''} · ${gb(l.tanda.bytes_in)} → ${gb(l.tanda.bytes_out)}${l.tanda.nota ? ` · nota ${l.tanda.nota}` : ''}${l.tanda.reemplazados ? ` · ${l.tanda.reemplazados} reemplazado${l.tanda.reemplazados !== 1 ? 's' : ''}` : ''}`;
|
||||||
|
return corto ? `${l.movimientos} mov.` : `${l.movimientos} movimiento${l.movimientos !== 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
function queSeHizo(lote){
|
function queSeHizo(lote){
|
||||||
if (!lote) return 'Cambio';
|
if (!lote) return 'Cambio';
|
||||||
if (lote === 'optimizar' || lote.startsWith('optimizar-')) return 'Reemplazó originales por la versión convertida';
|
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('descartados-')) return 'Apartó conversiones que no compensaban';
|
if (lote.startsWith('descartados-')) return 'Apartó conversiones que no compensaban';
|
||||||
|
if (lote.startsWith('tanda-')) return 'Convirtió una tanda de clips';
|
||||||
if (lote.startsWith('dup-')) return 'Juntó material repetido';
|
if (lote.startsWith('dup-')) return 'Juntó material repetido';
|
||||||
if (lote.startsWith('vacias-')) return 'Borró carpetas vacías';
|
if (lote.startsWith('vacias-')) return 'Borró carpetas vacías';
|
||||||
if (lote.startsWith('pieza-')) return 'Creó una pieza nueva';
|
if (lote.startsWith('pieza-')) return 'Creó una pieza nueva';
|
||||||
@@ -1210,7 +1247,7 @@ function pasosDe(C, r){
|
|||||||
: esc ? {e:'hecho', t:'Escanear', d:`${fecha(C.escaneo.ts)} · ${num(C.escaneo.n)} archivos`, corto: fechaCorta(C.escaneo.ts)} : {e:'pend', t:'Escanear', d:'Lee nombres y tamaños. Solo lectura.', corto: ''});
|
: esc ? {e:'hecho', t:'Escanear', d:`${fecha(C.escaneo.ts)} · ${num(C.escaneo.n)} archivos`, corto: fechaCorta(C.escaneo.ts)} : {e:'pend', t:'Escanear', d:'Lee nombres y tamaños. Solo lectura.', corto: ''});
|
||||||
const sn = C.sin_nombre_n || 0;
|
const sn = C.sin_nombre_n || 0;
|
||||||
P.push(!esc ? {e:'bloq', t:'Revisar', d:'Después de escanear'} : sn ? {e:'listo', t:'Revisar', d:`${sn} carpeta${sn > 1 ? 's' : ''} por nombrar`, corto: `${sn} por nombrar`} : {e:'hecho', t:'Revisar', d:'Todas las carpetas reconocidas', corto: 'ok'});
|
P.push(!esc ? {e:'bloq', t:'Revisar', d:'Después de escanear'} : sn ? {e:'listo', t:'Revisar', d:`${sn} carpeta${sn > 1 ? 's' : ''} por nombrar`, corto: `${sn} por nombrar`} : {e:'hecho', t:'Revisar', d:'Todas las carpetas reconocidas', corto: 'ok'});
|
||||||
const lotes = (C.lotes || []).filter(l => !l.lote.startsWith('stock-') && !l.lote.startsWith('optimizar') && !l.lote.startsWith('descartados-'));
|
const lotes = (C.lotes || []).filter(l => !l.lote.startsWith('stock-') && !l.lote.startsWith('optimizar') && !l.lote.startsWith('descartados-') && !l.lote.startsWith('tanda-'));
|
||||||
const asis = C.asistente && C.asistente.activo;
|
const asis = C.asistente && C.asistente.activo;
|
||||||
P.push(!esc ? {e:'bloq', t:'Ordenar', d:'Después de escanear'} : asis ? {e:'curso', t:'Ordenar', d: (NOMBRE_PASO[C.asistente.paso] || '') + '…', corto: 'en marcha'}
|
P.push(!esc ? {e:'bloq', t:'Ordenar', d:'Después de escanear'} : asis ? {e:'curso', t:'Ordenar', d: (NOMBRE_PASO[C.asistente.paso] || '') + '…', corto: 'en marcha'}
|
||||||
: C.plan ? {e:'listo', t:'Ordenar', d:`Plan listo: ${C.plan.marcadas} de ${C.plan.filas} movimientos marcados`, corto: `${C.plan.marcadas} mov.`}
|
: C.plan ? {e:'listo', t:'Ordenar', d:`Plan listo: ${C.plan.marcadas} de ${C.plan.filas} movimientos marcados`, corto: `${C.plan.marcadas} mov.`}
|
||||||
@@ -1605,7 +1642,19 @@ $('#btnStockPapelera').onclick = () => { const s = STOCK.filter(g => g.sel && g.
|
|||||||
// ---------------- convertir ----------------
|
// ---------------- convertir ----------------
|
||||||
function pintarCriterio(){ $('#convCriterio').textContent = `con más de ${+$('#optMeses').value || 0} meses y más de ${+$('#optMin').value || 0} MB`; }
|
function pintarCriterio(){ $('#convCriterio').textContent = `con más de ${+$('#optMeses').value || 0} meses y más de ${+$('#optMin').value || 0} MB`; }
|
||||||
['optMeses','optMin'].forEach(id => $('#' + id).oninput = pintarCriterio);
|
['optMeses','optMin'].forEach(id => $('#' + id).oninput = pintarCriterio);
|
||||||
let COLA_TS = [], CONV_VER = 'todos', CONV_ORDEN = {campo: 'carpeta', inverso: false}, CONV_EXC = '', CONV_PLEGADAS = new Set();
|
let COLA_TS = [], CONV_VER = 'todos', CONV_ORDEN = {campo: 'carpeta', inverso: false}, CONV_EXC = '', CONV_PLEGADAS = new Set(), CONV_BUSCA = '', CONV_CRITERIO = '';
|
||||||
|
const NOMBRE_CAM = {braw: 'BRAW', h264: 'H.264', hevc: 'H.265', h265: 'H.265', prores: 'ProRes', mpeg4: 'MPEG-4', mpeg2video: 'MPEG-2', dnxhd: 'DNxHD', vp9: 'VP9', av1: 'AV1'};
|
||||||
|
const MES_LARGO = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
|
||||||
|
// los criterios de «marcar»: cada uno sabe decir cuántos clips coge
|
||||||
|
function criteriosDe(marcables){
|
||||||
|
const hoy = Date.now(), anio = 365 * 86400000, cods = {}, meses = {};
|
||||||
|
marcables.forEach(c => { const k = NOMBRE_CAM[(c.codec || '').toLowerCase()] || (c.codec || '').toUpperCase(); cods[k] = (cods[k] || 0) + 1; const m = (c.fecha || '').slice(0, 7); if (m) meses[m] = (meses[m] || 0) + 1; });
|
||||||
|
const cr = [['todos', 'todos los válidos', c => true], ['grandes', 'los de más de 5 GB', c => c.bytes > 5e9]];
|
||||||
|
Object.keys(cods).sort((a, b) => cods[b] - cods[a]).forEach(k => cr.push(['cam:' + k, 'solo ' + k, c => (NOMBRE_CAM[(c.codec || '').toLowerCase()] || (c.codec || '').toUpperCase()) === k]));
|
||||||
|
Object.keys(meses).sort().slice(-6).forEach(m => cr.push(['mes:' + m, `${MES_LARGO[+m.slice(5, 7) - 1] || m} ${m.slice(0, 4)}`, c => (c.fecha || '').slice(0, 7) === m]));
|
||||||
|
cr.push(['viejos', 'de más de un año', c => c.fecha && hoy - new Date(c.fecha).getTime() > anio]);
|
||||||
|
return cr.map(([k, t, p]) => ({k, t, p, n: marcables.filter(p).length})).filter(x => x.n);
|
||||||
|
}
|
||||||
const tiempoLargo = s => { s = Math.max(0, Math.round(s || 0)); if (s < 60) return `${s} s`; if (s < 3600) return `${Math.floor(s / 60)} min${s % 60 ? ' ' + (s % 60) + ' s' : ''}`; return `${Math.floor(s / 3600)} h${Math.round(s % 3600 / 60) ? ' ' + Math.round(s % 3600 / 60) + ' min' : ''}`; };
|
const tiempoLargo = s => { s = Math.max(0, Math.round(s || 0)); if (s < 60) return `${s} s`; if (s < 3600) return `${Math.floor(s / 60)} min${s % 60 ? ' ' + (s % 60) + ' s' : ''}`; return `${Math.floor(s / 3600)} h${Math.round(s % 3600 / 60) ? ' ' + Math.round(s % 3600 / 60) + ' min' : ''}`; };
|
||||||
// por qué no se ofrece un clip, en grupos: 502 filas grises con un «no» no explican nada
|
// por qué no se ofrece un clip, en grupos: 502 filas grises con un «no» no explican nada
|
||||||
function claveExc(m){
|
function claveExc(m){
|
||||||
@@ -1694,7 +1743,8 @@ async function cargarCola(){
|
|||||||
const rango = f => f.t ? ({corriendo: 0, pendiente: 1, listo: 3, verificado: 3, error: 4, descartado: 5, reemplazado: 6, cancelado: 6}[f.t.estado] ?? 7) : f.c.excluido ? 8 : 2;
|
const rango = f => f.t ? ({corriendo: 0, pendiente: 1, listo: 3, verificado: 3, error: 4, descartado: 5, reemplazado: 6, cancelado: 6}[f.t.estado] ?? 7) : f.c.excluido ? 8 : 2;
|
||||||
const grupo = f => f.t && ['pendiente', 'corriendo'].includes(f.t.estado) ? 'cola' : f.t && ['listo', 'verificado', 'reemplazado'].includes(f.t.estado) ? 'hechos' : f.t && ['error', 'cancelado', 'descartado'].includes(f.t.estado) ? 'fallos' : f.c && f.c.excluido ? 'no' : 'libres';
|
const grupo = f => f.t && ['pendiente', 'corriendo'].includes(f.t.estado) ? 'cola' : f.t && ['listo', 'verificado', 'reemplazado'].includes(f.t.estado) ? 'hechos' : f.t && ['error', 'cancelado', 'descartado'].includes(f.t.estado) ? 'fallos' : f.c && f.c.excluido ? 'no' : 'libres';
|
||||||
const CLAVES = {carpeta: (a, b) => carpeta(a).localeCompare(carpeta(b), 'es') || rango(a) - rango(b) || bytes(b) - bytes(a), estado: (a, b) => rango(a) - rango(b) || bytes(b) - bytes(a), nombre: (a, b) => nombre(a).localeCompare(nombre(b), 'es'), fecha: (a, b) => ((b.c || {}).fecha || '').localeCompare((a.c || {}).fecha || '') || bytes(b) - bytes(a), tamano: (a, b) => bytes(b) - bytes(a), ahorro: (a, b) => ahorro(b) - ahorro(a)};
|
const CLAVES = {carpeta: (a, b) => carpeta(a).localeCompare(carpeta(b), 'es') || rango(a) - rango(b) || bytes(b) - bytes(a), estado: (a, b) => rango(a) - rango(b) || bytes(b) - bytes(a), nombre: (a, b) => nombre(a).localeCompare(nombre(b), 'es'), fecha: (a, b) => ((b.c || {}).fecha || '').localeCompare((a.c || {}).fecha || '') || bytes(b) - bytes(a), tamano: (a, b) => bytes(b) - bytes(a), ahorro: (a, b) => ahorro(b) - ahorro(a)};
|
||||||
const lista = filas.filter(f => (CONV_VER === 'todos' || grupo(f) === CONV_VER) && (!CONV_EXC || (f.c && claveExc(f.c.excluido) === CONV_EXC))).sort(CLAVES[CONV_ORDEN.campo] || CLAVES.carpeta);
|
const busca = CONV_BUSCA.trim().toLowerCase();
|
||||||
|
const lista = filas.filter(f => (CONV_VER === 'todos' || grupo(f) === CONV_VER) && (!CONV_EXC || (f.c && claveExc(f.c.excluido) === CONV_EXC)) && (!busca || relDe(f).toLowerCase().includes(busca))).sort(CLAVES[CONV_ORDEN.campo] || CLAVES.carpeta);
|
||||||
if (CONV_ORDEN.inverso) lista.reverse();
|
if (CONV_ORDEN.inverso) lista.reverse();
|
||||||
const cuenta = {}; filas.forEach(f => cuenta[grupo(f)] = (cuenta[grupo(f)] || 0) + 1);
|
const cuenta = {}; filas.forEach(f => cuenta[grupo(f)] = (cuenta[grupo(f)] || 0) + 1);
|
||||||
const E = ESTADO && ESTADO.cola, corr = ts.find(t => t.estado === 'corriendo'), eta = d.cola.eta;
|
const E = ESTADO && ESTADO.cola, corr = ts.find(t => t.estado === 'corriendo'), eta = d.cola.eta;
|
||||||
@@ -1726,7 +1776,9 @@ async function cargarCola(){
|
|||||||
const vers = ts.filter(t => t.estado === 'verificado' && !(t.formato || '').startsWith('prores'));
|
const vers = ts.filter(t => t.estado === 'verificado' && !(t.formato || '').startsWith('prores'));
|
||||||
const notas = vers.filter(t => t.nota), notaMedia = notas.length ? (notas.reduce((s, t) => s + t.nota, 0) / notas.length).toFixed(0) : '';
|
const notas = vers.filter(t => t.nota), notaMedia = notas.length ? (notas.reduce((s, t) => s + t.nota, 0) / notas.length).toFixed(0) : '';
|
||||||
let mo = '';
|
let mo = '';
|
||||||
if (d.cola.activa) {
|
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>`;
|
||||||
|
} else if (d.cola.activa) {
|
||||||
const total = E && E.total ? E.total : (pend + (corr ? 1 : 0)), hechosT = E ? (E.hechos || 0) : 0;
|
const total = E && E.total ? E.total : (pend + (corr ? 1 : 0)), hechosT = E ? (E.hechos || 0) : 0;
|
||||||
const pctTot = total ? Math.round((hechosT + (corr ? (corr.pct || 0) / 100 : 0)) / total * 100) : 0;
|
const pctTot = total ? Math.round((hechosT + (corr ? (corr.pct || 0) / 100 : 0)) / total * 100) : 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>`;
|
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>`;
|
||||||
@@ -1744,6 +1796,16 @@ async function cargarCola(){
|
|||||||
mo = `<div class="momento"><div class="l1"><button class="p grande" id="moBuscar">Buscar clips que se puedan aligerar</button><span class="mut">lee los clips de más de ${+$('#optMeses').value || 0} meses y ${+$('#optMin').value || 0} MB, y calcula cuánto pesarían. No convierte nada todavía.</span></div></div>`;
|
mo = `<div class="momento"><div class="l1"><button class="p grande" id="moBuscar">Buscar clips que se puedan aligerar</button><span class="mut">lee los clips de más de ${+$('#optMeses').value || 0} meses y ${+$('#optMin').value || 0} MB, y calcula cuánto pesarían. No convierte nada todavía.</span></div></div>`;
|
||||||
}
|
}
|
||||||
$('#momento').innerHTML = mo;
|
$('#momento').innerHTML = mo;
|
||||||
|
// la última tanda, con su resumen, hasta que se oculte o empiece otra
|
||||||
|
const ul = d.cola.ultima, vista = (() => { try { return localStorage.getItem('organilab.tanda.vista'); } catch (e) { return ''; } })();
|
||||||
|
if (!d.cola.activa && ul && ul.lote && ul.clips && ul.lote !== vista && (!raizActual() || !ul.raices || !ul.raices.length || ul.raices.some(x => x.toLowerCase() === raizActual().toLowerCase()))) {
|
||||||
|
const carps = (ul.carpetas || []).slice(0, 3), maxB = Math.max(1, ...carps.map(c => c.bytes_in));
|
||||||
|
$('#tanda').innerHTML = `<div class="tanda"><div class="l1"><span class="ico" style="width:22px;height:22px;border-radius:50%;background:var(--ok2);color:var(--ok);display:inline-flex;align-items:center;justify-content:center;font-weight:700">✓</span><b>Tanda terminada${ul.cliente ? ' · ' + esc(ul.cliente) : ''}</b><span class="mut">${ul.inicio ? `empezó el ${new Date(ul.inicio * 1000).toLocaleDateString('es-CO', {day: 'numeric', month: 'long'})} a las ${hora(ul.inicio)} · ` : ''}acabó a las ${hora(ul.ts || (ul.inicio + ul.seg))} · ${tiempoLargo(ul.seg)}</span><span class="sep"></span><a class="lnk" href="/api/optimizar/tanda.csv?lote=${encodeURIComponent(ul.lote)}" download style="padding:5px 6px;font-weight:600">Exportar CSV</a>${ul.raiz ? '<button id="taAbrir">Abrir la carpeta</button>' : ''}<button class="lnk" id="taOcultar">ocultar</button></div>
|
||||||
|
<div class="cifras"><div><b>${ul.clips}</b><small>clip${ul.clips !== 1 ? 's' : ''} convertido${ul.clips !== 1 ? 's' : ''}${ul.verificados || ul.reemplazados ? ' y verificado' + (ul.clips !== 1 ? 's' : '') : ''}${ul.pedidos > ul.clips ? ` de ${ul.pedidos}` : ''}</small></div><div><b>${gb(ul.bytes_in)} → ${gb(ul.bytes_out)}</b><small>${ul.bytes_out ? Math.round(ul.bytes_in / ul.bytes_out) + ' veces menos' : ''}</small></div><div><b class="ok">${gb(Math.max(0, ul.bytes_in - ul.bytes_out))}</b><small>${ul.reemplazados === ul.clips ? 'liberados' : 'se liberan al reemplazar'}</small></div><div><b>${ul.nota ? ul.nota + ' / 100' : '—'}</b><small>${ul.nota ? `nota media · la peor, ${ul.nota_min}` : 'sin medir: «comparar» mide'}</small></div><div><b class="${ul.fallos || ul.descartados ? 'warn' : ''}">${(ul.fallos || 0) + (ul.descartados || 0)}</b><small>${ul.fallos ? ul.fallos + ' fallaron' : ''}${ul.fallos && ul.descartados ? ' · ' : ''}${ul.descartados ? ul.descartados + ' no compensaban' : ''}${!ul.fallos && !ul.descartados ? 'sin fallos' : ''}</small></div></div>
|
||||||
|
${carps.length ? `<div style="display:flex;flex-direction:column;gap:4px"><span class="cabsec" style="margin:0">Por carpeta</span>${carps.map(c => `<div class="carp"><span class="n" title="${esc(c.carpeta)}">${esc((c.carpeta || '').split(/[\\/]/).filter(Boolean).join(' › ') || '(raíz)')}</span><div class="b"><i style="width:${Math.round(c.bytes_in / maxB * 100)}%"></i></div><span class="c">${c.clips} clip${c.clips !== 1 ? 's' : ''} · ${gb(c.bytes_in)} → ${gb(c.bytes_out)}</span></div>`).join('')}${(ul.carpetas || []).length > 3 ? `<span class="mut" style="font-size:12px">y ${ul.carpetas.length - 3} carpeta${ul.carpetas.length - 3 > 1 ? 's' : ''} más: están en el CSV</span>` : ''}</div>` : ''}</div>`;
|
||||||
|
const ta = $('#taAbrir'); if (ta) ta.onclick = () => api('/api/abrir', {ruta: ul.raiz}).catch(e => aviso(e.message, 'bad'));
|
||||||
|
$('#taOcultar').onclick = () => { try { localStorage.setItem('organilab.tanda.vista', ul.lote); } catch (e) {} $('#tanda').innerHTML = ''; };
|
||||||
|
} else $('#tanda').innerHTML = '';
|
||||||
const liga = (id, a) => { const b = $('#' + id); if (b) b.onclick = a; };
|
const liga = (id, a) => { const b = $('#' + id); if (b) b.onclick = a; };
|
||||||
liga('moDetener', () => $('#btnColaToggle').click()); liga('moReanudar', () => $('#btnColaToggle').click());
|
liga('moDetener', () => $('#btnColaToggle').click()); liga('moReanudar', () => $('#btnColaToggle').click());
|
||||||
liga('moReemplazar', () => $('#btnReemplazar').click()); liga('moConvertir', () => $('#btnEncolar').click()); liga('moBuscar', () => $('#btnBuscarClips').click());
|
liga('moReemplazar', () => $('#btnReemplazar').click()); liga('moConvertir', () => $('#btnEncolar').click()); liga('moBuscar', () => $('#btnBuscarClips').click());
|
||||||
@@ -1755,6 +1817,12 @@ async function cargarCola(){
|
|||||||
const marcables = CAND.filter(c => !c.excluido && !c.en_cola);
|
const marcables = CAND.filter(c => !c.excluido && !c.en_cola);
|
||||||
const VER = [['todos', 'todos', filas.length], ['libres', 'por convertir', cuenta.libres], ['cola', 'en cola', cuenta.cola], ['hechos', 'terminados', cuenta.hechos], ['fallos', 'con fallo', cuenta.fallos], ['no', 'excluidos', cuenta.no]];
|
const VER = [['todos', 'todos', filas.length], ['libres', 'por convertir', cuenta.libres], ['cola', 'en cola', cuenta.cola], ['hechos', 'terminados', cuenta.hechos], ['fallos', 'con fallo', cuenta.fallos], ['no', 'excluidos', cuenta.no]];
|
||||||
const ORD = [['carpeta', 'carpeta'], ['estado', 'estado'], ['nombre', 'nombre'], ['fecha', 'fecha'], ['tamano', 'tamaño'], ['ahorro', 'ahorro']];
|
const ORD = [['carpeta', 'carpeta'], ['estado', 'estado'], ['nombre', 'nombre'], ['fecha', 'fecha'], ['tamano', 'tamaño'], ['ahorro', 'ahorro']];
|
||||||
|
if (marcables.length) {
|
||||||
|
// marcar con criterio: por tamaño, por cámara, por mes, por antigüedad; y un buscador
|
||||||
|
const cr = criteriosDe(marcables), nSel = marcables.filter(c => c.sel).length;
|
||||||
|
html += `<div class="marcar"><span>marcar</span>${cr.map(x => `<button data-criterio="${esc(x.k)}" class="${CONV_CRITERIO === x.k ? 'on' : ''}">${esc(x.t)} · ${x.n}</button>`).join('')}<button class="lnk" data-criterio="ninguno" style="border:0;background:none">ninguno</button><input type="search" id="convBusca" placeholder="buscar por nombre o carpeta…" value="${esc(CONV_BUSCA)}" aria-label="buscar clip"></div>`;
|
||||||
|
if (nSel) html += `<div class="mut" style="padding:6px 0 2px;font-size:12.5px"><b style="color:var(--tx)">${nSel} marcado${nSel > 1 ? 's' : ''}</b> · ${gb(marcables.filter(c => c.sel).reduce((s, c) => s + c.bytes, 0))} → ≈ ${gb(marcables.filter(c => c.sel).reduce((s, c) => s + c.estimado, 0))}</div>`;
|
||||||
|
} else if (busca || !CAND.length) html += `<div class="marcar"><input type="search" id="convBusca" placeholder="buscar por nombre o carpeta…" value="${esc(CONV_BUSCA)}" aria-label="buscar clip"></div>`;
|
||||||
html += `<div class="convbar">${CAND.length ? `<label style="display:flex;gap:6px;align-items:center;font-weight:500"><input type="checkbox" id="candTodos" ${marcables.length && CAND.filter(c => c.sel).length === marcables.length ? 'checked' : ''} ${marcables.length ? '' : 'disabled'}> marcar todos los válidos</label>` : ''}<span class="sep"></span><span class="mut">ver</span>${VER.filter(x => x[2]).map(([k, t, n]) => `<button class="lnk ${CONV_VER === k ? 'on' : ''}" data-ver="${k}">${t} · ${n}</button>`).join('')}<span class="mut">ordenar por</span>${ORD.map(([k, t]) => `<button class="lnk ${CONV_ORDEN.campo === k ? 'on' : ''}" data-orden="${k}" title="${CONV_ORDEN.campo === k ? 'otra vez para invertir el orden' : ''}">${t}${CONV_ORDEN.campo === k ? (CONV_ORDEN.inverso ? ' ↑' : ' ↓') : ''}</button>`).join('')}</div>`;
|
html += `<div class="convbar">${CAND.length ? `<label style="display:flex;gap:6px;align-items:center;font-weight:500"><input type="checkbox" id="candTodos" ${marcables.length && CAND.filter(c => c.sel).length === marcables.length ? 'checked' : ''} ${marcables.length ? '' : 'disabled'}> marcar todos los válidos</label>` : ''}<span class="sep"></span><span class="mut">ver</span>${VER.filter(x => x[2]).map(([k, t, n]) => `<button class="lnk ${CONV_VER === k ? 'on' : ''}" data-ver="${k}">${t} · ${n}</button>`).join('')}<span class="mut">ordenar por</span>${ORD.map(([k, t]) => `<button class="lnk ${CONV_ORDEN.campo === k ? 'on' : ''}" data-orden="${k}" title="${CONV_ORDEN.campo === k ? 'otra vez para invertir el orden' : ''}">${t}${CONV_ORDEN.campo === k ? (CONV_ORDEN.inverso ? ' ↑' : ' ↓') : ''}</button>`).join('')}</div>`;
|
||||||
const tope = lista.slice(0, 800);
|
const tope = lista.slice(0, 800);
|
||||||
if (CONV_ORDEN.campo === 'carpeta') {
|
if (CONV_ORDEN.campo === 'carpeta') {
|
||||||
@@ -1778,7 +1846,7 @@ async function cargarCola(){
|
|||||||
if (ks.length) html += `<div class="excl"><b style="color:var(--tx)">Excluidos · ${cuenta.no || 0}</b><span>no se ofrecen, y por qué:</span>${ks.map(k => `<button data-exc="${esc(k)}" class="${CONV_EXC === k ? 'on' : ''} ${k === 'no se pudieron leer' ? 'ojo' : ''}">${exc[k]} ${esc(k)}</button>`).join('')}${CONV_EXC ? '<button class="lnk" data-exc="" style="background:none;border:0">ver todos</button>' : ''}</div>`;
|
if (ks.length) html += `<div class="excl"><b style="color:var(--tx)">Excluidos · ${cuenta.no || 0}</b><span>no se ofrecen, y por qué:</span>${ks.map(k => `<button data-exc="${esc(k)}" class="${CONV_EXC === k ? 'on' : ''} ${k === 'no se pudieron leer' ? 'ojo' : ''}">${exc[k]} ${esc(k)}</button>`).join('')}${CONV_EXC ? '<button class="lnk" data-exc="" style="background:none;border:0">ver todos</button>' : ''}</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; CONV_CRITERIO = ''; 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(); };
|
||||||
$$('#convLista [data-grupo]').forEach(g => g.onchange = e => { CAND.forEach(c => { if (padreDe(c.rel) === g.dataset.grupo && !c.excluido && !c.en_cola) c.sel = e.target.checked; }); cargarCola(); });
|
$$('#convLista [data-grupo]').forEach(g => g.onchange = e => { CAND.forEach(c => { if (padreDe(c.rel) === g.dataset.grupo && !c.excluido && !c.en_cola) c.sel = e.target.checked; }); cargarCola(); });
|
||||||
$$('#convLista [data-plegar]').forEach(b => b.onclick = () => { const k = b.dataset.plegar; if (CONV_PLEGADAS.has(k)) CONV_PLEGADAS.delete(k); else CONV_PLEGADAS.add(k); cargarCola(); });
|
$$('#convLista [data-plegar]').forEach(b => b.onclick = () => { const k = b.dataset.plegar; if (CONV_PLEGADAS.has(k)) CONV_PLEGADAS.delete(k); else CONV_PLEGADAS.add(k); cargarCola(); });
|
||||||
@@ -1787,6 +1855,38 @@ async function cargarCola(){
|
|||||||
$$('#convLista [data-orden]').forEach(b => b.onclick = () => { if (CONV_ORDEN.campo === b.dataset.orden) CONV_ORDEN.inverso = !CONV_ORDEN.inverso; else CONV_ORDEN = {campo: b.dataset.orden, inverso: false}; cargarCola(); });
|
$$('#convLista [data-orden]').forEach(b => b.onclick = () => { if (CONV_ORDEN.campo === b.dataset.orden) CONV_ORDEN.inverso = !CONV_ORDEN.inverso; else CONV_ORDEN = {campo: b.dataset.orden, inverso: false}; cargarCola(); });
|
||||||
$$('#convLista [data-primero]').forEach(b => b.onclick = () => accion(b, async () => { await api('/api/optimizar/control', {accion: 'primero', id: +b.dataset.primero}); await cargarCola(); }));
|
$$('#convLista [data-primero]').forEach(b => b.onclick = () => accion(b, async () => { await api('/api/optimizar/control', {accion: 'primero', id: +b.dataset.primero}); await cargarCola(); }));
|
||||||
$$('#convLista [data-comparar]').forEach(b => b.onclick = () => comparar(+b.dataset.comparar));
|
$$('#convLista [data-comparar]').forEach(b => b.onclick = () => comparar(+b.dataset.comparar));
|
||||||
|
$$('#convLista [data-criterio]').forEach(b => b.onclick = () => { const k = b.dataset.criterio, marc = CAND.filter(c => !c.excluido && !c.en_cola), cr = criteriosDe(marc).find(x => x.k === k); CONV_CRITERIO = k === 'ninguno' ? '' : k; marc.forEach(c => c.sel = cr ? cr.p(c) : false); cargarCola(); });
|
||||||
|
const bq = $('#convBusca'); if (bq) { bq.oninput = () => { CONV_BUSCA = bq.value; clearTimeout(bq._t); bq._t = setTimeout(() => { const pos = bq.selectionStart; cargarCola().then(() => { const b2 = $('#convBusca'); if (b2) { b2.focus(); b2.setSelectionRange(pos, pos); } }); }, 350); }; }
|
||||||
|
}
|
||||||
|
// ---- cuándo trabaja la cola y qué mete sola (se guarda en optimizar.*)
|
||||||
|
function pintarAjustesCola(){
|
||||||
|
const o = (ESTADO.config && ESTADO.config.optimizar) || {}, h = o.horario || {}, a = o.aligerar || {};
|
||||||
|
const desde = h.desde || '20:00', hasta = h.hasta || '07:00';
|
||||||
|
const min = s => { const [hh, mm] = s.split(':').map(Number); return hh * 60 + mm; };
|
||||||
|
const d1 = min(desde), d2 = min(hasta), seg = d1 < d2 ? [[0, d1, 0], [d1, d2, 1], [d2, 1440, 0]] : [[0, d2, 1], [d2, d1, 0], [d1, 1440, 1]];
|
||||||
|
$('#colaAjustesCuerpo').innerHTML = `
|
||||||
|
<div class="caja"><label class="t"><input type="checkbox" id="ajHorario" ${h.activo ? 'checked' : ''}> Convertir solo en un horario</label>
|
||||||
|
<div class="f"><span>de</span><input type="time" id="ajDesde" value="${esc(desde)}" aria-label="desde"><span>a</span><input type="time" id="ajHasta" value="${esc(hasta)}" aria-label="hasta"><span>· fuera de ese horario la cola espera entre clip y clip, sin perder lo que iba</span></div>
|
||||||
|
<div class="dia" aria-hidden="true">${seg.map(([x, y, on]) => `<i style="flex:${Math.max(1, y - x)};background:${on ? 'var(--acc)' : 'var(--panel2)'}"></i>`).join('')}</div>
|
||||||
|
<div class="f" style="justify-content:space-between"><span>0 h</span><span>12 h</span><span>24 h</span></div></div>
|
||||||
|
<div class="caja"><label class="t"><input type="checkbox" id="ajPausa" ${o.pausar_si_edito === false ? '' : 'checked'}> Esperar si estoy editando</label>
|
||||||
|
<div class="d">Si DaVinci Resolve, Premiere o After Effects están usando la tarjeta, la cola no empieza el siguiente clip hasta que la dejen. El que va se termina.</div>
|
||||||
|
<div class="apps"><span>DaVinci Resolve</span><span>Premiere Pro</span><span>After Effects</span></div></div>
|
||||||
|
<div class="caja"><label class="t"><input type="checkbox" id="ajAligerar" ${a.activo ? 'checked' : ''}> Aligerar solo lo viejo</label>
|
||||||
|
<div class="f"><span>en la pasada automática, mete en cola lo de más de</span><input type="number" id="ajMeses" value="${+a.meses || 6}" min="1" style="width:56px" aria-label="meses"><span>meses, hasta</span><input type="number" id="ajGb" value="${+a.gb_noche || 100}" min="1" style="width:70px" aria-label="GB por pasada"><span>GB por pasada</span></div>
|
||||||
|
<div class="d">Va con el orden automático de la pestaña Carpeta (la tarea de Windows de cada noche). Convierte y verifica; <b style="color:var(--tx)">no reemplaza</b>, salvo que marques «y reemplazar solo cuando estén verificados».</div></div>
|
||||||
|
<div class="caja"><label class="t"><input type="checkbox" id="ajAvisar" ${o.avisar === false ? '' : 'checked'}> Avisar al terminar</label>
|
||||||
|
<div class="d">Un aviso de Windows cuando acabe la tanda, o si falla, aunque la ventana esté cerrada.</div>
|
||||||
|
<div style="border:1px solid var(--line);border-radius:8px;padding:8px 12px;display:flex;gap:10px;align-items:center;background:var(--panel2);max-width:420px;font-size:12.5px"><span style="width:26px;height:26px;border-radius:7px;background:var(--acc);flex:none"></span><span><b>OrganiLAB terminó</b><br>212 clips convertidos y verificados en 7 h 12 min. Falta reemplazar.</span></div></div>`;
|
||||||
|
const guardar = seguro(async () => {
|
||||||
|
const parcial = {horario: {activo: $('#ajHorario').checked, desde: $('#ajDesde').value || '20:00', hasta: $('#ajHasta').value || '07:00'},
|
||||||
|
pausar_si_edito: $('#ajPausa').checked, aligerar: {activo: $('#ajAligerar').checked, meses: +$('#ajMeses').value || 6, gb_noche: +$('#ajGb').value || 100},
|
||||||
|
avisar: $('#ajAvisar').checked};
|
||||||
|
await api('/api/config', {optimizar: parcial});
|
||||||
|
Object.assign(ESTADO.config.optimizar, parcial);
|
||||||
|
pintarAjustesCola();
|
||||||
|
});
|
||||||
|
$$('#colaAjustesCuerpo input').forEach(i => i.onchange = guardar);
|
||||||
}
|
}
|
||||||
// ---- comparar: el mismo cuadro en el original y en el convertido, con lupa, y la nota de calidad
|
// ---- comparar: el mismo cuadro en el original y en el convertido, con lupa, y la nota de calidad
|
||||||
function comparar(id){
|
function comparar(id){
|
||||||
@@ -1866,7 +1966,7 @@ async function cargarBit(){
|
|||||||
// Avisos de error recientes (los globos rojos), con su detalle: para saber qué pasó después de que desaparecieron.
|
// Avisos de error recientes (los globos rojos), con su detalle: para saber qué pasó después de que desaparecieron.
|
||||||
const av = avisosGuardados().filter(a => !solo || !raiz || (a.raiz || '').toLowerCase() === raiz);
|
const av = avisosGuardados().filter(a => !solo || !raiz || (a.raiz || '').toLowerCase() === raiz);
|
||||||
const avHtml = av.length ? `<div class="dia">Avisos recientes <button class="lnk" id="btnBorrarAvisos" style="margin-left:8px;font-size:12px">borrar</button></div>` + av.slice(0, 20).map(a => `<div class="ev"><span class="h">${hora(a.ts)}</span><span class="q"><b>${esc(a.texto)}</b>${!solo && a.raiz ? ` · <span class="mut">${esc(a.raiz.split(/[\\/]/).filter(Boolean).pop())}</span>` : ''}${a.detalle ? `<details><summary>detalle</summary><pre class="cell-small" style="white-space:pre-wrap">${esc(a.detalle)}</pre></details>` : ''}</span><span></span></div>`).join('') : '';
|
const avHtml = av.length ? `<div class="dia">Avisos recientes <button class="lnk" id="btnBorrarAvisos" style="margin-left:8px;font-size:12px">borrar</button></div>` + av.slice(0, 20).map(a => `<div class="ev"><span class="h">${hora(a.ts)}</span><span class="q"><b>${esc(a.texto)}</b>${!solo && a.raiz ? ` · <span class="mut">${esc(a.raiz.split(/[\\/]/).filter(Boolean).pop())}</span>` : ''}${a.detalle ? `<details><summary>detalle</summary><pre class="cell-small" style="white-space:pre-wrap">${esc(a.detalle)}</pre></details>` : ''}</span><span></span></div>`).join('') : '';
|
||||||
$('#historial').innerHTML = avHtml + (Object.keys(dias).map(k => `<div class="dia">${esc(titulo(k))}</div>` + dias[k].map(l => `<div class="ev"><span class="h">${hora(l.ts)}</span><span class="q"><b>${esc(queSeHizo(l.lote))}</b> · ${l.movimientos} movimiento${l.movimientos !== 1 ? 's' : ''}${!solo ? ` · <span class="mut">${esc((l.raiz || '').split(/[\\/]/).filter(Boolean).pop() || l.raiz)}</span>` : ''}${l.deshecho ? ' <span class="tag">deshecho</span>' : ''}${l.errores ? ` <span class="tag baja">${l.errores} avisos</span>` : ''}${l.ejemplos.length ? `<details><summary>ejemplos</summary><div class="cell-small">${l.ejemplos.map(e => esc(e.de.split(/[\\/]/).slice(-2).join('/')) + ' → ' + esc(e.a.split(/[\\/]/).slice(-3).join('/'))).join('<br>')}</div></details>` : ''}</span>${l.deshecho || (l.lote || '').startsWith('entrada-') ? '<span></span>' : `<button data-l="${esc(l.lote)}">Deshacer</button>`}</div>`).join('')).join('') || '<div class="mut">Todavía no se ha movido nada.</div>');
|
$('#historial').innerHTML = avHtml + (Object.keys(dias).map(k => `<div class="dia">${esc(titulo(k))}</div>` + dias[k].map(l => `<div class="ev"><span class="h">${hora(l.ts)}</span><span class="q"><b>${esc(queSeHizo(l.lote))}</b> · ${cuentaLote(l)}${!solo ? ` · <span class="mut">${esc((l.raiz || '').split(/[\\/]/).filter(Boolean).pop() || l.raiz)}</span>` : ''}${l.deshecho ? ' <span class="tag">deshecho</span>' : ''}${l.errores ? ` <span class="tag baja">${l.errores} avisos</span>` : ''}${l.ejemplos.length ? `<details><summary>ejemplos</summary><div class="cell-small">${l.ejemplos.map(e => esc(e.de.split(/[\\/]/).slice(-2).join('/')) + ' → ' + esc(e.a.split(/[\\/]/).slice(-3).join('/'))).join('<br>')}</div></details>` : ''}</span>${l.deshecho || (l.lote || '').startsWith('entrada-') || (l.lote || '').startsWith('tanda-') ? '<span></span>' : `<button data-l="${esc(l.lote)}">Deshacer</button>`}</div>`).join('')).join('') || '<div class="mut">Todavía no se ha movido nada.</div>');
|
||||||
$$('#historial button[data-l]').forEach(b => b.onclick = () => deshacer(b.dataset.l));
|
$$('#historial button[data-l]').forEach(b => b.onclick = () => deshacer(b.dataset.l));
|
||||||
const ba = $('#btnBorrarAvisos'); if (ba) ba.onclick = () => { try { localStorage.removeItem(AVISOS_CLAVE); } catch (e) {} cargarBit().catch(e => aviso(e.message, 'bad')); };
|
const ba = $('#btnBorrarAvisos'); if (ba) ba.onclick = () => { try { localStorage.removeItem(AVISOS_CLAVE); } catch (e) {} cargarBit().catch(e => aviso(e.message, 'bad')); };
|
||||||
}
|
}
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user