3.4.0: los BRAW se convierten sin Resolve, y lo que no compensa no se reconvierte
Munka no podía reconvertir archivos que antes sí. Auditado sobre la carpeta de Andrea Galeano: - Los BRAW nunca habían salido: 87 clips (496 GB), tres tandas por el guion de Resolve y ni un clip convertido. Ahora los lee el decodificador de Blackmagic que instala Resolve (braw.py, en un proceso aparte), sin abrir Resolve, y ffmpeg los codifica como cualquier otro clip. Probado con clips del NAS: 15,6 GB → 394 MB con sus 7.004 cuadros, VMAF 96,0 contra el BRAW revelado sin pérdida. Las conversiones de BRAW que fallaron por Resolve pasan solas al lector. - Lo que «antes podía» eran reconversiones de lo ya convertido: ahorraban un 5 % con pérdida. Siguen sin ofrecerse, y lo que ya viene comprimido de cámara solo se ofrece si compensa. - La tarjeta codifica con «uhq»: entre un 19 y un 55 % menos de peso con la misma calidad medida, siempre H.265 de 10 bits. - Una conversión que no aligera (más del 80 % del clip, o reconversión de una conversión) no reemplaza al original: queda «no compensa» y lo que salió va a _Para borrar\descartados. De paso: los MP4 guardan las etiquetas de la cámara; un intento que no pasó la verificación se puede reintentar; un H.264 de más de 4.096 px de lado va por CPU; el motivo de un fallo se lee entero; el guion de Resolve llega con tildes y borra su ficha. Pruebas: prueba_orden.py 33 (una con un BRAW de verdad), probar.py 19, prueba_funcional.py 30, prueba_ventana.py 18 desde el código y 17 con el paquete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
@@ -218,7 +218,7 @@
|
||||
.colabar .b i{display:block;height:100%;background:var(--acc)}
|
||||
.colabar b{white-space:nowrap}
|
||||
.est{display:inline-block;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600;letter-spacing:.02em;white-space:nowrap}
|
||||
.est.pendiente,.est.cancelado{background:var(--panel2);color:var(--mut)}
|
||||
.est.pendiente,.est.cancelado,.est.descartado{background:var(--panel2);color:var(--mut)}
|
||||
.est.corriendo{background:var(--acc2);color:var(--acc)}
|
||||
.est.listo,.est.verificado,.est.reemplazado{background:var(--ok2);color:var(--ok)}
|
||||
.est.error{background:var(--bad2);color:var(--bad)}
|
||||
@@ -280,6 +280,7 @@
|
||||
.cfila .nom{min-width:0}
|
||||
.cfila .nom .clip{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
|
||||
.cfila .nom .sub2{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:11px;color:var(--mut);margin-top:1px}
|
||||
.cfila .nom .sub2.err{white-space:normal}
|
||||
.cfila .tam{color:var(--mut);font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap}
|
||||
.cfila .ah{font-variant-numeric:tabular-nums;text-align:right;font-weight:600;color:var(--ok);white-space:nowrap}
|
||||
.cfila .bar{margin-top:4px;height:5px}
|
||||
@@ -493,10 +494,10 @@
|
||||
<label>mínimo <input type="number" id="optMin" value="300" min="0" style="width:70px"> MB</label>
|
||||
<label>calidad <select id="optPerfil"></select></label>
|
||||
<label>croma <select id="optChroma"><option value="420">4:2:0 10 bits (compatible)</option><option value="422">4:2:2 10 bits (solo ffmpeg, pesa más)</option></select></label>
|
||||
<label>BRAW en <select id="optColor"><option value="rec709">Rec.709 (se ve bien)</option><option value="log">Log de cámara (plano, más latitud)</option></select></label>
|
||||
<label>BRAW en <select id="optColor"><option value="rec709">Rec.709 (como se vio al grabar)</option><option value="log">Log de cámara (plano, más latitud)</option></select></label>
|
||||
<label><input type="checkbox" id="optExport"> incluir exportados</label>
|
||||
</div></details></span></div>
|
||||
<p class="ayuda">Convierte los clips de proyectos ya entregados a un formato más ligero. <b>El original no se toca</b> hasta que reemplaces. <span class="q">?</span><span class="mas">Buscar lee los clips (códec, duración, resolución) y calcula cuánto pesarían. La cola convierte un clip tras otro en segundo plano, como Media Encoder; puedes cambiar de vista. Cada clip terminado se verifica solo (códec, bits, duración). Al reemplazar, el original se aparta a <span class="path">_Para borrar/originales</span> y el archivo nuevo queda en su lugar con el mismo nombre más un sufijo. Los BRAW van por DaVinci Resolve Studio (debe estar abierto) y solo salen a H.265 10 bits; el resto por ffmpeg, con la GPU cuando la hay. Nada se borra.</span></p>
|
||||
<p class="ayuda">Convierte los clips de proyectos ya entregados a un formato más ligero. <b>El original no se toca</b> hasta que reemplaces. <span class="q">?</span><span class="mas">Buscar lee los clips (códec, duración, resolución) y calcula cuánto pesarían. La cola convierte un clip tras otro en segundo plano, como Media Encoder; puedes cambiar de vista. Cada clip terminado se verifica solo (códec, bits, duración). Al reemplazar, el original se aparta a <span class="path">_Para borrar/originales</span> y el archivo nuevo queda en su lugar con el mismo nombre más un sufijo. Los BRAW se leen con el decodificador de Blackmagic que instala DaVinci Resolve, sin tener que abrirlo; todo se codifica con ffmpeg, con la tarjeta gráfica cuando la hay. Lo que ya viene comprimido de cámara (H.264 o H.265 de pocos megabits) no se ofrece: ahorraría poco y perdería calidad. Nada se borra.</span></p>
|
||||
<div class="toolbar" style="margin-bottom:10px"><label>formato de salida <select id="optFormato"></select></label><span id="fmtQue" class="mut" style="font-size:12px"></span><span class="sep"></span><span id="motores" class="mut" style="font-size:12px"><span class="spin"></span> detectando GPU y CPU…</span></div>
|
||||
<div class="cards c4" id="optCards"></div>
|
||||
<div class="toolbar">
|
||||
@@ -601,7 +602,7 @@
|
||||
<dl>
|
||||
<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>Convertir</dt><dd>Mete los clips en una cola que va uno tras otro en segundo plano. Se puede detener, reanudar, reintentar y vaciar. <i>Cómo: con ffmpeg, por tarjeta gráfica si la hay y si no por procesador; los BRAW pasan por DaVinci Resolve Studio, que tiene que estar abierto en un proyecto.</i></dd>
|
||||
<dt>Convertir</dt><dd>Mete los clips en una cola que va uno tras otro en segundo plano. Se puede detener, reanudar, reintentar y vaciar. <i>Cómo: con ffmpeg, por tarjeta gráfica si la hay y si no por procesador; los BRAW los lee el decodificador de Blackmagic que viene con DaVinci Resolve, sin abrirlo. No se ofrece lo que ya viene comprimido de cámara ni lo que ya convirtió el programa: reconvertirlo ahorra poco y pierde calidad.</i></dd>
|
||||
<dt>Verificar</dt><dd>Cada clip se comprueba solo al terminar: duración, códec y los 10 bits. Lo que no cuadra no se da por bueno.</dd>
|
||||
<dt>Reemplazar</dt><dd>El convertido ocupa el sitio del original, con su misma fecha, y el original se aparta a <span class="path">_Para borrar\originales</span>.</dd>
|
||||
</dl>
|
||||
@@ -773,7 +774,7 @@ async function cargarEstado(){
|
||||
const bad = $('#btnAnaDetener'); if (bad) bad.onclick = () => accion(bad, async () => { await api('/api/optimizar/analizar/detener', {raiz: raizActual()}); });
|
||||
const h = ESTADO.herramientas || {}; const faltas = [];
|
||||
if (h.ffmpeg && !h.ffmpeg.ok) faltas.push('ffmpeg no encontrado: sin él no se puede leer ni convertir (pon ffmpeg.exe y ffprobe.exe en la carpeta ffmpeg\\ junto al programa)');
|
||||
if (h.resolve && !h.resolve.ok) faltas.push('DaVinci Resolve no está instalado: los BRAW no se podrán convertir');
|
||||
if (h.braw && !h.braw.ok) faltas.push('Falta el decodificador de Blackmagic RAW (se instala con DaVinci Resolve, vale el gratuito): los BRAW no se podrán convertir');
|
||||
$('#reloj').innerHTML = (faltas.length ? `<span class="warn" title="${esc(faltas.join('\n'))}">⚠ ${faltas.length} aviso${faltas.length > 1 ? 's' : ''}</span> · ` : '') + `v${esc(ESTADO.version || '')}`;
|
||||
pintarTareas();
|
||||
pintarActualizacion();
|
||||
@@ -1070,6 +1071,7 @@ 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('dup-')) return 'Juntó material repetido';
|
||||
if (lote.startsWith('vacias-')) return 'Borró carpetas vacías';
|
||||
if (lote.startsWith('pieza-')) return 'Creó una pieza nueva';
|
||||
@@ -1084,7 +1086,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'));
|
||||
const lotes = (C.lotes || []).filter(l => !l.lote.startsWith('stock-') && !l.lote.startsWith('optimizar') && !l.lote.startsWith('descartados-'));
|
||||
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.`}
|
||||
@@ -1122,7 +1124,7 @@ function pintarSiguiente(P){
|
||||
else if (P[2].e === 'pend') { s.t = 'Siguiente: ordenar la carpeta'; s.p = 'Calcula qué va a pasar (basura y proxies a _Para borrar, grabaciones por mes y cámara, piezas numeradas) y aplícalo con un clic. O deja que el asistente te pregunte paso a paso.'; b('Calcular qué va a pasar', () => { irA('revisar'); subRev('plan'); $('#btnPlan').click(); }, true); }
|
||||
else if (P[3].e === 'listo') { const d = P[3].d; s.t = 'Siguiente: ' + (d.includes('verificar') ? 'verificar los clips convertidos' : d.includes('reemplazar') ? 'reemplazar los originales' : 'retomar la conversión'); s.p = d.includes('verificar') ? 'Verificar comprueba códec, 10 bits y duración. Después puedes reemplazar los originales (quedan en _Para borrar/originales).' : d.includes('reemplazar') ? 'Los originales se apartan a _Para borrar/originales y el MP4 10 bits queda en su lugar. Nada se borra.' : 'Hay clips en cola con la conversión parada.'; b('Ir a Convertir', () => irA('convertir'), true); }
|
||||
else if (P[3].e === 'curso') { s.t = 'Convirtiendo clips'; s.p = 'Sigue en segundo plano. El avance está en la barra de abajo; cuando termine, toca reemplazar.'; b('Ver la cola', () => irA('convertir')); }
|
||||
else if (P[3].e === 'pend') { const cv = C.convertir; s.t = 'Siguiente: aligerar el material de cámara'; s.p = `Hay ${gb(cv.camara_bytes)} de clips de cámara. Los de proyectos ya entregados pueden pasar a MP4 10 bits, unas 10 veces más ligeros. ${cv.raw_bytes ? 'Los BRAW necesitan DaVinci Resolve Studio abierto. ' : ''}El original no se toca hasta que reemplaces.`; b('Buscar clips a convertir', () => { irA('convertir'); $('#btnBuscarClips').click(); }, true); b('Saltar a respaldar', () => irA('respaldar')); }
|
||||
else if (P[3].e === 'pend') { const cv = C.convertir; s.t = 'Siguiente: aligerar el material de cámara'; s.p = `Hay ${gb(cv.camara_bytes)} de clips de cámara. Los de proyectos ya entregados pueden pasar a MP4 10 bits, unas 10 veces más ligeros. El original no se toca hasta que reemplaces.`; b('Buscar clips a convertir', () => { irA('convertir'); $('#btnBuscarClips').click(); }, true); b('Saltar a respaldar', () => irA('respaldar')); }
|
||||
else if (P[4].e === 'listo') { s.t = 'Siguiente: copiar el respaldo'; s.p = P[4].d + '. Es incremental: lo que ya esté igual se salta.'; b('Ir a Respaldar', () => irA('respaldar'), true); }
|
||||
else if (P[4].e === 'curso') { s.t = 'Copiando el respaldo'; s.p = 'Sigue en segundo plano. Puedes detenerlo desde la barra de abajo y retomarlo luego.'; }
|
||||
else { s.t = 'Siguiente: respaldo compacto en otro disco'; s.p = 'Copia organizada por carpeta y tipo, con los clips ya convertidos en vez de los originales y un índice. ' + (C.respaldo.raw_pendientes ? `${C.respaldo.raw_pendientes} originales de cámara sin convertir no se copiarían.` : ''); b('Calcular el respaldo', () => irA('respaldar'), true); }
|
||||
@@ -1390,7 +1392,9 @@ async function cargarMotores(){
|
||||
const g = MOTORES.gpu, c = MOTORES.cpu;
|
||||
const gpuT = g.hevc || g.h264 ? `GPU ${esc(g.nombre || 'NVIDIA')}: ${[g.hevc && 'H.265', g.h264 && 'H.264'].filter(Boolean).join(' y ')} acelerados` : 'Sin GPU NVIDIA: todo por CPU (más lento)';
|
||||
const cpuT = `CPU: ${[c.prores_ks && 'ProRes', c.libx265 && 'H.265', c.libx264 && 'H.264'].filter(Boolean).join(', ') || 'nada'}`;
|
||||
$('#motores').innerHTML = MOTORES.ffmpeg ? `${gpuT} · ${cpuT}` : '<span class="bad">ffmpeg no encontrado: no se puede convertir</span>';
|
||||
const lb = MOTORES.braw || {};
|
||||
const brawT = lb.ok ? 'BRAW: se leen sin abrir Resolve' : `<span class="bad" title="${esc(lb.motivo || '')}">BRAW: falta el decodificador de Blackmagic</span>`;
|
||||
$('#motores').innerHTML = MOTORES.ffmpeg ? `${gpuT} · ${cpuT} · ${brawT}` : '<span class="bad">ffmpeg no encontrado: no se puede convertir</span>';
|
||||
const ch = $('#optChroma'); const op422 = ch && ch.querySelector('option[value="422"]');
|
||||
if (op422) { const hay = MOTORES.chroma422 !== false; op422.disabled = !hay; op422.textContent = '4:2:2 10 bits' + (hay ? (MOTORES.gpu.chroma422 ? ' (pesa más)' : ' (por CPU, más lento)') : ' (este equipo no puede)'); if (!hay && ch.value === '422') ch.value = '420'; }
|
||||
$$('#optFormato option').forEach(o => { const m = MOTORES.formatos[o.value]; o.disabled = !m; o.textContent = (ESTADO.formatos[o.value] || {}).nombre + (m === 'gpu' ? ' · GPU' : m === 'cpu' ? ' · CPU' : ' · no disponible'); });
|
||||
@@ -1403,20 +1407,20 @@ async function cargarCandidatos(){
|
||||
await cargarCola();
|
||||
}
|
||||
function resumenCand(){ const s = CAND.filter(c => c.sel); const validos = CAND.filter(c => !c.excluido && !c.en_cola); bloquear('btnEncolar', validos.length ? (s.length ? '' : 'x') : 'x'); $('#btnEncolar').textContent = s.length ? `Convertir ${s.length} marcado${s.length > 1 ? 's' : ''}` : 'Convertir'; }
|
||||
$('#btnEncolar').onclick = () => { const s = CAND.filter(c => c.sel); const fm = (ESTADO.formatos || {})[$('#optFormato').value] || {nombre: $('#optFormato').value}; modal('Convertir', `<p><b>${s.length}</b> clips (${gb(s.reduce((a,c)=>a+c.bytes,0))}) se convierten a <b>${esc(fm.nombre)}</b>, uno tras otro, en segundo plano. Pesarían ≈ ${gb(s.reduce((a,c)=>a+c.estimado,0))}.</p><p><b>El original no se toca.</b> Cuando terminen y estén verificados, decides si reemplazar.</p>`, () => accion($('#btnEncolar'), async () => { await api('/api/config', {optimizar: {meses_minimos: +$('#optMeses').value || 0, min_mb: +$('#optMin').value || 0, perfil: $('#optPerfil').value, formato: $('#optFormato').value, chroma: $('#optChroma').value, color: $('#optColor').value}}); const r = await api('/api/optimizar/encolar', {raiz: raizActual(), rutas: s.map(c => c.ruta), perfil: $('#optPerfil').value, formato: $('#optFormato').value, chroma: $('#optChroma').value, color: $('#optColor').value, iniciar: true}); aviso(`${r.ids.length} clips en cola. El avance está en la barra de abajo.`, 'ok'); if (r.ids.length < s.length) aviso(`${s.length - r.ids.length} no se encolaron: ya estaban en cola, su salida ya existe (mismo clip en MP4 y MOV) o son BRAW con un formato que no sea H.265 10 bits.`, ''); await cargarCandidatos(); await cargarEstado(); })); $('#modalSi').textContent = 'Convertir'; };
|
||||
const EST_TXT = {pendiente:'en espera', corriendo:'convirtiendo', listo:'convertido', verificado:'verificado', reemplazado:'reemplazado', error:'falló', cancelado:'cancelado'};
|
||||
$('#btnEncolar').onclick = () => { const s = CAND.filter(c => c.sel); const fm = (ESTADO.formatos || {})[$('#optFormato').value] || {nombre: $('#optFormato').value}; modal('Convertir', `<p><b>${s.length}</b> clips (${gb(s.reduce((a,c)=>a+c.bytes,0))}) se convierten a <b>${esc(fm.nombre)}</b>, uno tras otro, en segundo plano. Pesarían ≈ ${gb(s.reduce((a,c)=>a+c.estimado,0))}.</p><p><b>El original no se toca.</b> Cuando terminen y estén verificados, decides si reemplazar.</p>`, () => accion($('#btnEncolar'), async () => { await api('/api/config', {optimizar: {meses_minimos: +$('#optMeses').value || 0, min_mb: +$('#optMin').value || 0, perfil: $('#optPerfil').value, formato: $('#optFormato').value, chroma: $('#optChroma').value, color: $('#optColor').value}}); const r = await api('/api/optimizar/encolar', {raiz: raizActual(), rutas: s.map(c => c.ruta), perfil: $('#optPerfil').value, formato: $('#optFormato').value, chroma: $('#optChroma').value, color: $('#optColor').value, iniciar: true}); aviso(`${r.ids.length} clips en cola. El avance está en la barra de abajo.`, 'ok'); if (r.ids.length < s.length) aviso(`${s.length - r.ids.length} no se encolaron: ya estaban en cola o su salida ya existe (el mismo clip en MP4 y MOV, o una conversión anterior).`, ''); await cargarCandidatos(); await cargarEstado(); })); $('#modalSi').textContent = 'Convertir'; };
|
||||
const EST_TXT = {pendiente:'en espera', corriendo:'convirtiendo', listo:'convertido', verificado:'verificado', reemplazado:'reemplazado', error:'falló', cancelado:'cancelado', descartado:'no compensa'};
|
||||
async function cargarCola(){
|
||||
const d = await api('/api/optimizar/cola');
|
||||
const raiz = raizActual();
|
||||
const ts = d.trabajos.filter(t => !raiz || (t.raiz || '').toLowerCase() === raiz.toLowerCase());
|
||||
const orden = {corriendo:0, pendiente:1, listo:2, verificado:2, error:3, reemplazado:4, cancelado:5};
|
||||
const orden = {corriendo:0, pendiente:1, listo:2, verificado:2, error:3, descartado:4, reemplazado:5, cancelado:6};
|
||||
ts.sort((x, y) => (orden[x.estado] ?? 9) - (orden[y.estado] ?? 9) || y.id - x.id);
|
||||
COLA_TS = ts;
|
||||
const n = k => ts.filter(t => t.estado === k).length;
|
||||
const pend = n('pendiente'), listos = n('listo'), verif = ts.filter(t => t.estado === 'verificado' && !(t.formato || '').startsWith('prores')).length, err = n('error') + n('cancelado');
|
||||
const est = $('#colaEstado'); est.className = 'est ' + (d.cola.activa ? 'corriendo' : err ? 'error' : verif ? 'verificado' : 'pendiente'); est.textContent = d.cola.activa ? `convirtiendo${d.cola.mensaje ? ' · ' + d.cola.mensaje : ''}` : pend ? `${pend} en espera, parada` : ts.length ? `${verif} verificados · ${n('reemplazado')} reemplazados${err ? ` · ${err} con error` : ''}` : 'sin actividad';
|
||||
const est = $('#colaEstado'); est.className = 'est ' + (d.cola.activa ? 'corriendo' : err ? 'error' : verif ? 'verificado' : 'pendiente'); est.textContent = d.cola.activa ? `convirtiendo${d.cola.mensaje ? ' · ' + d.cola.mensaje : ''}` : pend ? `${pend} en espera, parada` : ts.length ? `${verif} verificados · ${n('reemplazado')} reemplazados${err ? ` · ${err} con error` : ''}${n('descartado') ? ` · ${n('descartado')} que no compensan` : ''}` : 'sin actividad';
|
||||
const tg = $('#btnColaToggle'); tg.style.display = (d.cola.activa || pend) ? '' : 'none'; tg.textContent = d.cola.activa ? 'Detener' : 'Reanudar';
|
||||
$('#btnReintentar').style.display = err ? '' : 'none'; $('#btnQuitar').style.display = ts.some(t => ['error','cancelado','reemplazado'].includes(t.estado)) ? '' : 'none'; $('#btnVerificar').style.display = listos ? '' : 'none';
|
||||
$('#btnReintentar').style.display = err ? '' : 'none'; $('#btnQuitar').style.display = ts.some(t => ['error','cancelado','reemplazado','descartado'].includes(t.estado)) ? '' : 'none'; $('#btnVerificar').style.display = listos ? '' : 'none';
|
||||
$('#btnQuitarEspera').style.display = (!d.cola.activa && pend) ? '' : 'none';
|
||||
bloquear('btnReemplazar', verif ? '' : (listos ? 'hay convertidos sin verificar' : 'x')); $('#btnReemplazar').textContent = verif ? `Reemplazar ${verif} verificado${verif > 1 ? 's' : ''}` : 'Reemplazar';
|
||||
const validos = CAND.filter(c => !c.excluido), ahora = validos.reduce((s,c)=>s+c.bytes,0), despues = validos.reduce((s,c)=>s+c.estimado,0);
|
||||
@@ -1426,14 +1430,14 @@ async function cargarCola(){
|
||||
let html = '';
|
||||
if (CAND.length) {
|
||||
html += `<div class="cabsec"><label style="display:flex;gap:6px;align-items:center;text-transform:none;letter-spacing:0;font-weight:500"><input type="checkbox" id="candTodos" ${validos.length && CAND.filter(c => c.sel).length === validos.filter(c => !c.en_cola).length ? 'checked' : ''}> marcar todos los válidos</label><span class="sep"></span>${CAND.length} candidatos</div>`;
|
||||
html += CAND.slice(0, 800).map((c, i) => `<div class="cfila ${c.excluido ? 'exc' : ''}"><input type="checkbox" data-i="${i}" ${c.sel ? 'checked' : ''} ${c.excluido || c.en_cola ? 'disabled' : ''} aria-label="marcar"><span class="est candidato">${c.en_cola ? 'en cola' : c.excluido ? 'no' : 'candidato'}</span><span class="nom" title="${esc(c.codec)} ${esc(c.perfil)} ${esc(c.pix_fmt)} · ${esc(c.res)} · ${esc(c.fps)} fps · ${c.kbps} kb/s → ≤ ${c.kbps_objetivo} kb/s"><span class="clip">${esc(c.rel)}</span><span class="sub2">${esc(c.fecha)} · ${esc(c.codec)} ${esc(c.res)} · ${dur(c.dur)} · ${c.motor === 'resolve' ? 'Resolve' : 'ffmpeg'}${c.excluido || c.en_cola ? ' · ' + esc(c.excluido || c.en_cola) : ''}</span></span><span class="tam">${gb(c.bytes)} → ≈ ${gb(c.estimado)}</span><span class="ah">${c.bytes ? (c.estimado <= c.bytes ? '−' + Math.round((1-c.estimado/c.bytes)*100) : '+' + Math.round((c.estimado/c.bytes-1)*100)) : '0'}%</span></div>`).join('');
|
||||
html += CAND.slice(0, 800).map((c, i) => `<div class="cfila ${c.excluido ? 'exc' : ''}"><input type="checkbox" data-i="${i}" ${c.sel ? 'checked' : ''} ${c.excluido || c.en_cola ? 'disabled' : ''} aria-label="marcar"><span class="est candidato">${c.en_cola ? 'en cola' : c.excluido ? 'no' : 'candidato'}</span><span class="nom" title="${esc(c.codec)} ${esc(c.perfil)} ${esc(c.pix_fmt)} · ${esc(c.res)} · ${esc(c.fps)} fps · ${c.kbps} kb/s → ≤ ${c.kbps_objetivo} kb/s"><span class="clip">${esc(c.rel)}</span><span class="sub2">${esc(c.fecha)} · ${esc(c.codec)} ${esc(c.res)} · ${dur(c.dur)} · ${c.motor === 'braw' ? 'lector de BRAW' : c.motor === 'resolve' ? 'Resolve' : 'ffmpeg'}${c.excluido || c.en_cola ? ' · ' + esc(c.excluido || c.en_cola) : ''}</span></span><span class="tam">${gb(c.bytes)} → ≈ ${gb(c.estimado)}</span><span class="ah">${c.bytes ? (c.estimado <= c.bytes ? '−' + Math.round((1-c.estimado/c.bytes)*100) : '+' + Math.round((c.estimado/c.bytes-1)*100)) : '0'}%</span></div>`).join('');
|
||||
}
|
||||
if (ts.length) {
|
||||
const E = ESTADO && ESTADO.cola; const corr = ts.find(t => t.estado === 'corriendo');
|
||||
if (E && E.activa && E.total) { const pctTot = Math.round(((E.hechos || 0) + ((E.actual && E.actual.pct) || 0) / 100) / E.total * 100); html += `<div class="colabar"><b>Convirtiendo ${Math.min((E.hechos || 0) + 1, E.total)} de ${E.total}</b><span class="b"><i style="width:${pctTot}%"></i></span><b>${pctTot} %</b>${corr ? `<span class="mut">${esc((corr.ruta || '').split(/[\\/]/).pop())} · ${Math.round(corr.pct || 0)} %</span>` : ''}</div>`; }
|
||||
else if (pend) html += `<div class="colabar" style="background:var(--panel2)"><b>${pend} en espera</b><span class="mut">la cola está parada: pulsa Reanudar</span></div>`;
|
||||
html += `<div class="cabsec">Cola de conversión<span class="sep"></span>${ts.length}</div>`;
|
||||
html += ts.slice(0, 120).map(t => `<div class="cfila"><span></span><span class="est ${t.estado}">${EST_TXT[t.estado] || esc(t.estado)}${t.estado === 'corriendo' ? ` ${Math.round(t.pct || 0)} %` : ''}</span><span class="nom" title="${esc(t.ruta)}"><span class="clip">${esc((t.ruta || '').split(/[\\/]/).pop())} <span class="mut" style="font-weight:500;font-size:11px">· ${t.motor === 'resolve' ? 'Resolve' : (MOTORES && MOTORES.formatos[t.formato || 'h265_10bit'] === 'gpu' ? 'GPU' : 'CPU')} · ${esc(((ESTADO.formatos || {})[t.formato || 'h265_10bit'] || {}).nombre || t.formato || '').split(' · ')[0]}</span></span><span class="sub2">${t.estado === 'error' ? `<span class="bad">${esc((t.log || '').slice(-110))}</span>` : t.estado === 'pendiente' && (t.log || '').startsWith('se interrumpió') ? `<span class="mut">${esc(t.log)}</span>` : esc((t.salida || '').split(/[\\/]/).slice(-2).join(' / '))}</span>${t.estado === 'corriendo' ? `<div class="bar"><i style="width:${Math.round(t.pct || 0)}%"></i></div>` : ''}</span><span class="tam">${gb(t.bytes_in)}${t.bytes_out ? ' → ' + gb(t.bytes_out) : ''}</span><span class="ah">${t.bytes_out ? (t.bytes_out <= t.bytes_in ? '−' + Math.round((1 - t.bytes_out / t.bytes_in) * 100) : '+' + Math.round((t.bytes_out / t.bytes_in - 1) * 100)) + '%' : ''}</span></div>`).join('') + (ts.length > 120 ? `<div class="mut" style="padding:8px 0">… y ${ts.length - 120} más</div>` : '');
|
||||
html += ts.slice(0, 120).map(t => `<div class="cfila"><span></span><span class="est ${t.estado}">${EST_TXT[t.estado] || esc(t.estado)}${t.estado === 'corriendo' ? ` ${Math.round(t.pct || 0)} %` : ''}</span><span class="nom" title="${esc(t.ruta)}"><span class="clip">${esc((t.ruta || '').split(/[\\/]/).pop())} <span class="mut" style="font-weight:500;font-size:11px">· ${t.motor === 'resolve' ? 'Resolve' : (t.motor === 'braw' ? 'BRAW · ' : '') + (MOTORES && MOTORES.formatos[t.formato || 'h265_10bit'] === 'gpu' ? 'GPU' : 'CPU')} · ${esc(((ESTADO.formatos || {})[t.formato || 'h265_10bit'] || {}).nombre || t.formato || '').split(' · ')[0]}</span></span><span class="sub2${t.estado === 'error' || t.estado === 'descartado' ? ' err' : ''}">${t.estado === 'descartado' ? `<span class="mut">${esc(t.log || '')}</span>` : t.estado === 'error' ? `<span class="bad" title="${esc(t.log || '')}">${esc(((t.log || '').trim().split('\n').pop() || 'falló sin decir por qué').slice(0, 320))}</span>` : t.estado === 'pendiente' && (t.log || '').startsWith('se interrumpió') ? `<span class="mut">${esc(t.log)}</span>` : esc((t.salida || '').split(/[\\/]/).slice(-2).join(' / '))}</span>${t.estado === 'corriendo' ? `<div class="bar"><i style="width:${Math.round(t.pct || 0)}%"></i></div>` : ''}</span><span class="tam">${gb(t.bytes_in)}${t.bytes_out ? ' → ' + gb(t.bytes_out) : ''}</span><span class="ah">${t.bytes_out ? (t.bytes_out <= t.bytes_in ? '−' + Math.round((1 - t.bytes_out / t.bytes_in) * 100) : '+' + Math.round((t.bytes_out / t.bytes_in - 1) * 100)) + '%' : ''}</span></div>`).join('') + (ts.length > 120 ? `<div class="mut" style="padding:8px 0">… y ${ts.length - 120} más</div>` : '');
|
||||
}
|
||||
$('#convLista').innerHTML = html || '<span class="mut">Pulsa «Buscar clips» para ver qué se puede convertir en esta carpeta.</span>';
|
||||
$$('#convLista input[data-i]').forEach(i => i.onchange = () => { CAND[+i.dataset.i].sel = i.checked; resumenCand(); });
|
||||
@@ -1444,7 +1448,7 @@ const idsPorEstado = async (...est) => { const d = await api('/api/optimizar/col
|
||||
$('#btnVerificar').onclick = () => accion($('#btnVerificar'), async () => { const ids = await idsPorEstado('listo'); const r = await api('/api/optimizar/verificar', {ids}); const ok = r.resultados.filter(x => x.ok).length; aviso(`${ok} de ${r.resultados.length} verificados.`, ok === r.resultados.length ? 'ok' : 'bad'); if (ok < r.resultados.length) modal('Verificación', `<pre>${esc(r.resultados.map(x => `#${x.id} ${x.ok ? 'OK' : 'FALLA'} · ${x.motivo}`).join('\n'))}</pre>`, null); cargarCola(); });
|
||||
$('#btnReemplazar').onclick = () => accion($('#btnReemplazar'), async () => { const ids = await idsPorEstado('verificado'); modal('Reemplazar originales', `<p><b>${ids.length}</b> originales se apartan a <span class="path">_Para borrar/originales</span> dentro de su carpeta (no se borran). El MP4 10 bits queda en su lugar.</p><p>Cuando estés seguro, una persona vacía _Para borrar a mano.</p>`, () => accion($('#btnReemplazar'), async () => { const r = await api('/api/optimizar/reemplazar', {ids}); const ok = r.resultados.filter(x => x.ok).length; aviso(`${ok} de ${r.resultados.length} reemplazados.`, ok === r.resultados.length ? 'ok' : 'bad', {texto: 'Ver historial', ir: () => irA('historial')}); if (ok < r.resultados.length) modal('Reemplazo', `<pre>${esc(r.resultados.map(x => `#${x.id} ${x.ok ? 'OK' : 'NO'} · ${x.motivo}`).join('\n'))}</pre>`, null); cargarCola(); })); $('#modalSi').textContent = 'Reemplazar'; });
|
||||
$('#btnReintentar').onclick = () => accion($('#btnReintentar'), async () => { const ids = await idsPorEstado('error', 'cancelado'); await api('/api/optimizar/control', {accion: 'reintentar', ids}); await api('/api/optimizar/control', {accion: 'iniciar'}); cargarCola(); });
|
||||
$('#btnQuitar').onclick = () => accion($('#btnQuitar'), async () => { const ids = await idsPorEstado('error', 'cancelado', 'reemplazado'); await api('/api/optimizar/control', {accion: 'quitar', ids}); cargarCola(); });
|
||||
$('#btnQuitar').onclick = () => accion($('#btnQuitar'), async () => { const ids = await idsPorEstado('error', 'cancelado', 'reemplazado', 'descartado'); const r = await api('/api/optimizar/control', {accion: 'quitar', ids}); const ap = r.apartados || {}; if (ap.movidos) aviso(`${ap.movidos} archivo${ap.movidos > 1 ? 's' : ''} de conversiones que no compensaban, apartado${ap.movidos > 1 ? 's' : ''} a _Para borrar\\descartados. Se deshace desde Historial.`, 'ok', {texto: 'Ver historial', ir: () => irA('historial')}); if ((ap.errores || []).length) aviso(`No se pudieron apartar ${ap.errores.length}: ${ap.errores[0]}`, 'bad'); cargarCola(); });
|
||||
// Con la cola parada, las que esperan también se pueden quitar (antes no había forma desde la interfaz, y
|
||||
// bloqueaban Desconectar). No toca archivos: solo las saca de la cola.
|
||||
$('#btnQuitarEspera').onclick = () => accion($('#btnQuitarEspera'), async () => { const ids = await idsPorEstado('pendiente'); if (!ids.length) throw new ApiError('No hay conversiones en espera de esta carpeta.'); modal('Quitar las conversiones en espera', `<p><b>${ids.length}</b> conversión${ids.length > 1 ? 'es' : ''} en espera se quita${ids.length > 1 ? 'n' : ''} de la cola. No se toca ningún archivo: los originales siguen donde están y puedes volver a encolarlas desde «Buscar clips».</p>`, () => accion($('#btnQuitarEspera'), async () => { await api('/api/optimizar/control', {accion: 'quitar', ids}); await cargarEstado(); cargarCola(); })); $('#modalSi').textContent = 'Quitar'; });
|
||||
|
||||
Referencia en una nueva incidencia
Block a user