Cerrar lo para todo, continuar sigue por donde iba, convertir antes de ordenar, y lo grande a la vista

Munka: «que al cerrar cierre todos los procesos… que al continuar la cola siga por donde iba y vuelva a generar los
videos que hagan falta… que identifique bien el material… y convertir antes de mover». Cerrar con trabajo avisa y
lo para todo (cola, lectura, respaldo, traer), cierra lo que quede con lo que lanzó y borra lo que quedó a medias.
Lo cortado vuelve a esperar en su sitio (no «cancelado») y al continuar se hace entero; también vuelven los fallos
por un corte y los convertidos que ya no están. Actualizar con la cola en marcha la detiene y sigue sola al volver.
El Convertidor enseña lo grande de la carpeta y qué pasa con cada parte (exportados sin incluir, con un botón).
Convertir va antes de ordenar en los pasos y en Inicio; lo que se está convirtiendo no se mueve, y una carpeta con
algo abierto ya no se copia entera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-10-14 17:55:10 -05:00
padre 00b84b4aa7
commit 58197ffd67
Se han modificado 7 ficheros con 387 adiciones y 45 borrados

Ver fichero

@@ -446,6 +446,9 @@
.cmp .marco.con .lupa{display:block}
/* en vertical (móvil, cámara girada): los dos cuadros a lo alto, uno junto al otro, y la nota y lo demás en una
columna a su derecha. Antes el marco era siempre apaisado y el clip quedaba como una tira en medio */
.grandes{border:1px solid var(--line);border-radius:8px;padding:10px 14px;margin:0 0 12px;font-size:12.5px;display:flex;flex-wrap:wrap;gap:6px 18px;align-items:baseline}
.grandes b{font-size:13px}.grandes .t{color:var(--mut);font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;font-weight:600;flex-basis:100%}
.grandes button{font-size:12px;padding:2px 8px}
.cmpcuerpo.vert{display:grid;grid-template-columns:auto minmax(260px,1fr);gap:22px;align-items:start}
.cmpcuerpo.vert .cmp{grid-template-columns:auto auto;justify-content:start}
.cmpcuerpo.vert .cmp .lado{min-width:auto}
@@ -716,6 +719,7 @@
<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="convGrandes" class="grandes" style="display:none"></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>
@@ -1401,6 +1405,7 @@ function pasosDe(C, r){
: verif ? {e:'listo', t:'Convertir', d:`${verif} verificado${verif > 1 ? 's' : ''} por reemplazar`, corto: `${verif} por reemplazar`}
: reemp ? {e:'hecho', t:'Convertir', d:`${reemp} clip${reemp > 1 ? 's' : ''} ya convertido${reemp > 1 ? 's' : ''}`, corto: `${reemp} hechos`}
: {e:'pend', t:'Convertir', d:`${gb(cv.camara_bytes)} de cámara${cv.raw_bytes ? ' · ' + gb(cv.raw_bytes) + ' BRAW' : ''}`, corto: gb(cv.camara_bytes)});
{ const io = P.findIndex(x => x.t === 'Ordenar'), ic = P.findIndex(x => x.t === 'Convertir'); if (io >= 0 && ic > io) P.splice(io, 0, P.splice(ic, 1)[0]); } // convertir antes de mover
const rp = C.respaldo || {};
P.push(!esc ? {e:'bloq', t:'Respaldar', d:'Después de escanear'} : rp.estado === 'corriendo' ? {e:'curso', t:'Respaldar', d:'Copiando…', corto: 'copiando'}
: rp.plan ? {e:'listo', t:'Respaldar', d:`${num(rp.plan.n)} archivos · ${gb(rp.plan.bytes)} listos para copiar`, corto: gb(rp.plan.bytes)}
@@ -1813,6 +1818,7 @@ const MOTIVO_FALLO = {
en_uso: ['estaban abiertos en otro programa', 'Cierra Resolve, Premiere o la ventana del Explorador que los tenga abiertos, pulsa «Calcular de nuevo» y aplica: solo intentará lo que faltó.'],
permiso: ['Windows no dio permiso para moverlos', 'Suele ser una carpeta del NAS de solo lectura o de otro usuario. Revisa los permisos de esa carpeta compartida.'],
no_esta: ['ya no estaban donde decía el plan', 'Alguien los movió o los borró después de calcular. Pulsa «Calcular de nuevo».'],
convirtiendo: ['se estaban convirtiendo en ese momento', 'Un clip que se convierte no se puede mover, ni la carpeta que lo tiene: se quedaron donde estaban. Cuando acabe la cola, pulsa «Calcular de nuevo» y aplica: se moverán junto con su convertido.'],
copia: ['la copia entre discos no quedó completa', 'El original no se tocó. Vuelve a aplicar; si se repite, mira el espacio libre y la conexión con el NAS.'],
red: ['se cortó la conexión con el disco de red', 'Comprueba que la unidad sigue conectada y vuelve a aplicar: solo intentará lo que faltó.'],
otro: ['por otra razón', 'Mira la lista con «ver cuáles».'],
@@ -1954,9 +1960,32 @@ async function cargarCandidatos(){
const q = `raiz=${encodeURIComponent(raizActual())}&meses=${+$('#optMeses').value || 0}&perfil=${$('#optPerfil').value}&min_mb=${+$('#optMin').value || 0}&exportados=${$('#optExport').checked ? 1 : 0}&formato=${$('#optFormato').value}&ahorro=${ahorroPedido()}`;
const d = await api('/api/optimizar/candidatos?' + q); CAND = d.candidatos.map(c => ({...c, sel: !c.excluido && !c.en_cola}));
$('#anaEstado').textContent = '';
pintarGrandes();
if (!CAND.length) aviso('No se encontraron clips con esos criterios. Prueba a cambiar los meses o el tamaño mínimo.', '');
await cargarCola();
}
// Lo que pesa de la carpeta, de un vistazo: cuánto vídeo grande hay y qué pasa con cada parte. Munka, 14-oct-2026:
// «que identifique bien el material y haga la búsqueda de grandes archivos de video». En sus discos, 353 GB de
// exportados y masters no se ofrecían porque la casilla estaba apagada, y 757 GB ya convertidos esperaban «Reemplazar».
function pintarGrandes(){
const caja = $('#convGrandes'); if (!caja) return;
if (!CAND.length) { caja.style.display = 'none'; return; }
const g = {}; const suma = (k, c) => { const x = g[k] = g[k] || {n: 0, b: 0, e: 0}; x.n++; x.b += c.bytes || 0; x.e += c.estimado || 0; };
CAND.forEach(c => { const m = c.excluido || ''; suma(c.en_cola ? (['listo', 'verificado'].includes(c.en_cola) ? 'hechos' : 'cola') : !m ? 'ofrece' : m.startsWith('es un exportado') ? 'export' : m.startsWith('está apartado') ? 'repes' : m.startsWith('convertido ahorraría') ? 'poco' : m.startsWith('ya es una conversión') ? 'ya' : 'otros', c); });
const tot = CAND.reduce((s, c) => s + (c.bytes || 0), 0), v = k => g[k] || {n: 0, b: 0, e: 0};
const trozo = (k, txt, extra = '') => v(k).n ? `<span><b>${num(v(k).n)}</b> ${txt} · ${gb(v(k).b)}${extra}</span>` : '';
caja.innerHTML = `<span class="t">Vídeo de más de ${+$('#optMin').value || 0} MB en esta carpeta: ${num(CAND.length)} archivos · ${gb(tot)}</span>`
+ trozo('ofrece', 'se ofrecen', ` → ≈ ${gb(v('ofrece').e)}`)
+ trozo('cola', 'en la cola')
+ trozo('hechos', 'ya convertidos, por reemplazar', ' (se liberan al reemplazar)')
+ trozo('export', 'exportados y masters sin incluir', ` → ≈ ${gb(v('export').e)} <button id="grInclExp">incluirlos</button>`)
+ trozo('poco', 'ahorrarían poco')
+ trozo('repes', 'apartados como repetidos (para borrar)')
+ trozo('ya', 'convertidos por el programa')
+ trozo('otros', 'por otros motivos (alfa, stock, recientes…)');
caja.style.display = '';
const b = $('#grInclExp'); if (b) b.onclick = () => { $('#optExport').checked = true; cargarCandidatos().catch(e => aviso(e.message, 'bad')); };
}
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, ahorro_minimo: ahorroPedido(), perfil: $('#optPerfil').value, formato: $('#optFormato').value, chroma: $('#optChroma').value, color: $('#optColor').value}}); const r = await api('/api/optimizar/encolar', {raiz: raizActual(), rutas: s.map(c => c.ruta), perfil: $('#optPerfil').value, formato: $('#optFormato').value, chroma: $('#optChroma').value, color: $('#optColor').value, iniciar: true}); aviso(`${r.ids.length} clips en cola. El avance está en la barra de abajo.`, 'ok'); if (r.ids.length < s.length) aviso(`${s.length - r.ids.length} no se encolaron: ya estaban en cola o su salida ya existe (el mismo clip en MP4 y MOV, o una conversión anterior).`, ''); await cargarCandidatos(); await cargarEstado(); })); $('#modalSi').textContent = 'Convertir'; };
const EST_TXT = {pendiente:'en espera', corriendo:'convirtiendo', listo:'convertido', verificado:'verificado', reemplazado:'reemplazado', error:'falló', cancelado:'cancelado', descartado:'no compensa'};
@@ -1970,7 +1999,7 @@ async function cargarCola(){
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` : ''}${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';
const tg = $('#btnColaToggle'); tg.style.display = (d.cola.activa || pend) ? '' : 'none'; tg.textContent = d.cola.activa ? 'Detener' : 'Continuar';
$('#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';
@@ -2043,7 +2072,7 @@ async function cargarCola(){
const ahora = corrs.length > 1 ? `a la vez: ${corrs.map(x => `<b>${esc((x.ruta || '').split(/[\\/]/).pop())}</b> ${Math.round(x.pct || 0)} %${restoDe(x)}`).join(' · ')} · ${eta ? eta.en_espera : pend} en espera` : '';
mo = `<div class="momento corre"><div class="l1"><span class="spin"></span><b>Convirtiendo ${corrs.length > 1 ? `${Math.min(hechosT + 1, total)} y ${Math.min(hechosT + 2, total)}` : Math.min(hechosT + 1, total)} de ${total}</b><span class="mut">${eta && eta.seg ? `quedan unas <b>${tiempoLargo(eta.seg)}</b> · termina hacia las <b>${hora(eta.termina)}</b>${eta.medido ? ` · va a ${eta.vel.toFixed(1)}× tiempo real` : ' · estimado, hasta que acabe el primero'}` : 'calculando cuánto falta…'}</span><span class="sep"></span><button class="bad" id="moDetener">Detener</button></div><div class="bar grande"><i style="width:${pctTot}%"></i></div><div class="l3 mut">${ahora || (corr ? `ahora: <b>${esc((corr.ruta || '').split(/[\\/]/).pop())}</b> · ${Math.round(corr.pct || 0)} %${restoDe(corr)} de este clip · ${eta ? eta.en_espera : pend} en espera` : 'empezando…')}</div></div>`;
} else if (pend) {
mo = `<div class="momento"><div class="l1"><b>${pend} clip${pend > 1 ? 's' : ''} en espera, con la cola parada</b><span class="mut">lo que ya iba se retoma desde cero</span><span class="sep"></span><button class="p grande" id="moReanudar">Reanudar la cola</button></div></div>`;
mo = `<div class="momento"><div class="l1"><b>${pend} clip${pend > 1 ? 's' : ''} en espera, con la cola parada</b><span class="mut">sigue por donde iba; lo que quedó a medias se hace entero otra vez</span><span class="sep"></span><button class="p grande" id="moReanudar">Continuar la cola</button></div></div>`;
} else if (vers.length) {
const bIn = vers.reduce((s, t) => s + (t.bytes_in || 0), 0), bOut = vers.reduce((s, t) => s + (t.bytes_out || 0), 0);
mo = `<div class="momento listo"><div class="l1"><span class="ico">✓</span><span><b>${vers.length} clip${vers.length > 1 ? 's' : ''} convertido${vers.length > 1 ? 's' : ''} y verificado${vers.length > 1 ? 's' : ''}</b> · ${gb(bIn)} → ${gb(bOut)}${notaMedia ? ` · nota media ${notaMedia} / 100` : ''}</span><span class="sep"></span><button class="lnk" id="moComparar">comparar unos cuantos</button><button class="ok grande" id="moReemplazar">Reemplazar ${vers.length > 1 ? 'los ' + vers.length + ' originales' : 'el original'} · libera ${gb(Math.max(0, bIn - bOut))}</button></div><div class="l3 mut">Los originales van a _Para borrar/originales, no se borran, y se deshace desde Historial. Si quieres verlo antes, «comparar» enseña el mismo cuadro en los dos y mide la calidad.</div></div>`;
@@ -2686,8 +2715,8 @@ function pintarActualizacion(){
if (a.aplicando || a.estado === 'descargando' || a.estado === 'verificando' || a.estado === 'reiniciando') { if (!ACT_SIGUIENDO) seguirActualizacion(); return; }
if (!a.disponible) { caja.style.display = 'none'; return; }
caja.style.display = ''; caja.classList.add('actualizacion');
caja.innerHTML = `<h3>Hay una versión nueva: ${esc(a.disponible.version)} <span class="mut">(tienes la ${esc(ESTADO.version)})</span></h3><p class="mut">${esc(a.disponible.notes || '')}</p><div class="toolbar"><button id="btnActualizar" class="p">Actualizar ahora</button><span class="mut">${(a.disponible.size / 1e6).toFixed(0)} MB · el programa se cierra y vuelve a abrirse solo; tus datos no se tocan. No lo hagas con una conversión o copia en marcha.</span></div>` + (a.error ? `<p class="bad">${esc(a.error)}</p>` : '');
$('#btnActualizar').onclick = () => accion($('#btnActualizar'), async () => { if (huella(ESTADO).cola || huella(ESTADO).resp || huella(ESTADO).asis) throw new ApiError('Hay una tarea en marcha. Espera a que termine antes de actualizar.'); const r = await api('/api/actualizacion/instalar', {}); if (!r.lanzado) throw new ApiError(r.error || 'No se pudo lanzar la actualización'); seguirActualizacion(); });
caja.innerHTML = `<h3>Hay una versión nueva: ${esc(a.disponible.version)} <span class="mut">(tienes la ${esc(ESTADO.version)})</span></h3><p class="mut">${esc(a.disponible.notes || '')}</p><div class="toolbar"><button id="btnActualizar" class="p">Actualizar ahora</button><span class="mut">${(a.disponible.size / 1e6).toFixed(0)} MB · el programa se cierra y vuelve a abrirse solo; tus datos no se tocan. Si está convirtiendo, la cola se detiene y sigue sola al volver (el clip que iba se hace entero otra vez).</span></div>` + (a.error ? `<p class="bad">${esc(a.error)}</p>` : '');
$('#btnActualizar').onclick = () => accion($('#btnActualizar'), async () => { if (huella(ESTADO).resp || huella(ESTADO).asis) throw new ApiError('Hay una copia o el asistente en marcha. Espera a que termine antes de actualizar.'); const r = await api('/api/actualizacion/instalar', {}); if (!r.lanzado) throw new ApiError(r.error || 'No se pudo lanzar la actualización'); seguirActualizacion(); });
}
async function seguirActualizacion(){
ACT_SIGUIENDO = true; const caja = $('#actualizacionCaja'); caja.style.display = ''; irA('carpeta');