Portadas HD de lo seguido o leído; sección Colas con descargas, reescalado y biblioteca en tarjetas

- Portadas: se reescalan (×4/×2) solo las de mangas en historial o seguidos; caché en data/covers/.
- Colas (antes Descargas): cola de descargas y de reescalado HD con progreso, tiempo restante,
  reintentar/detener, recortes y portadas pendientes; biblioteca descargada en tarjetas verticales.
- Refresco de Colas sin parpadeo: solo se actualiza lo que cambia.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 16:31:18 -06:00
co-authored by Claude Opus 5.5
parent 36ff53cad4
commit 9086896a89
8 changed files with 352 additions and 51 deletions
+27 -1
View File
@@ -175,6 +175,32 @@ export function setupUpscale({ db, MANGA_DIR, HttpError, json, readBody, MANGA_I
}
}
// Cola global (página "Colas"): por manga, en el orden en que se procesan.
function queue() {
const rows = db.prepare(`SELECT j.chapter_id, j.manga_id, j.status, j.error, j.created_at, c.num, c.pages, d.title
FROM upscale_jobs j JOIN downloaded_chapters c ON c.chapter_id = j.chapter_id JOIN downloads d ON d.manga_id = j.manga_id
ORDER BY j.created_at, j.rowid`).all()
const groups = new Map()
for (const r of rows) {
let g = groups.get(r.manga_id)
if (!g) groups.set(r.manga_id, g = { mangaId: r.manga_id, title: r.title, queued: 0, pages: 0, errors: [], next: [] })
if (r.status === 'error') g.errors.push({ chapterId: r.chapter_id, num: r.num, error: r.error })
else {
g.queued++
g.pages += r.pages || 0
if (g.next.length < 5 && r.chapter_id !== current?.chapterId) g.next.push(r.num)
}
}
const cur = current && { mangaId: current.mangaId, chapterId: current.chapterId, num: current.num, pages: current.pages, total: current.total }
// Páginas que faltan (la del capítulo en curso cuenta solo lo que le queda).
for (const g of groups.values()) {
if (cur?.mangaId === g.mangaId) g.pages = Math.max(0, g.pages - (rows.find(r => r.chapter_id === cur.chapterId)?.pages || 0) + (cur.total - cur.pages))
g.eta = Math.round(g.pages * secPerPage)
}
const list = [...groups.values()].sort((a, b) => (b.mangaId === cur?.mangaId) - (a.mangaId === cur?.mangaId))
return { available: available(), current: cur, groups: list, secPerPage: Math.round(secPerPage * 10) / 10, eta: list.reduce((n, g) => n + g.eta, 0) }
}
// Llamado al terminar de descargar un capítulo: si el manga está marcado como HD, encolarlo.
function onChapterDownloaded(mangaId, chapterId) {
if (db.prepare('SELECT hd_auto FROM downloads WHERE manga_id = ?').get(mangaId)?.hd_auto) enqueue([chapterId])
@@ -224,5 +250,5 @@ export function setupUpscale({ db, MANGA_DIR, HttpError, json, readBody, MANGA_I
if (!fs.existsSync(BIN)) console.log('HD: Real-ESRGAN no instalado, reescalado desactivado')
else if (!fs.existsSync('/dev/dri')) console.log('HD: sin /dev/dri en el contenedor, reescalado desactivado')
return { info, hdFileOf, onChapterDownloaded, cancel, route, pump, available }
return { info, hdFileOf, onChapterDownloaded, cancel, route, pump, available, queue }
}