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:
@@ -374,6 +374,32 @@
|
||||
.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)}
|
||||
.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 .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}
|
||||
@@ -636,6 +662,8 @@
|
||||
<button class="lnk" id="btnQuitarEspera" style="display:none">Quitar los que esperan</button>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
@@ -723,6 +751,9 @@
|
||||
|
||||
<h3>Convertidor</h3>
|
||||
<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>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>
|
||||
@@ -874,7 +905,7 @@ const cargarVista = seguro(async t => {
|
||||
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 === '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 === 'historial') { await cargarBit(); }
|
||||
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>` : '');
|
||||
}
|
||||
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));
|
||||
}
|
||||
// ---------------- lo que se puede hacer en esta carpeta ----------------
|
||||
@@ -1191,11 +1222,17 @@ async function 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){
|
||||
if (!lote) return 'Cambio';
|
||||
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('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('vacias-')) return 'Borró carpetas vacías';
|
||||
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: ''});
|
||||
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'});
|
||||
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;
|
||||
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.`}
|
||||
@@ -1605,7 +1642,19 @@ $('#btnStockPapelera').onclick = () => { const s = STOCK.filter(g => g.sel && g.
|
||||
// ---------------- convertir ----------------
|
||||
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);
|
||||
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' : ''}`; };
|
||||
// por qué no se ofrece un clip, en grupos: 502 filas grises con un «no» no explican nada
|
||||
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 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 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();
|
||||
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;
|
||||
@@ -1726,7 +1776,9 @@ async function cargarCola(){
|
||||
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) : '';
|
||||
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 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>`;
|
||||
@@ -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>`;
|
||||
}
|
||||
$('#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; };
|
||||
liga('moDetener', () => $('#btnColaToggle').click()); liga('moReanudar', () => $('#btnColaToggle').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 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']];
|
||||
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>`;
|
||||
const tope = lista.slice(0, 800);
|
||||
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>`;
|
||||
}
|
||||
$('#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(); };
|
||||
$$('#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(); });
|
||||
@@ -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-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-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
|
||||
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.
|
||||
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('') : '';
|
||||
$('#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));
|
||||
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