Librería con listas, recomendaciones, descarga anticipada, app instalable con lectura offline y seguridad para exponerlo a internet

- Historial pasa a Librería: pestañas Leyendo, Siguiendo, Terminados, En el dispositivo y listas propias (Favoritos, Pendientes, Abandonados por defecto)
- Recomendaciones en el inicio según tus géneros más leídos (populares de MangaDex)
- Descarga anticipada de los N capítulos siguientes (opcionalmente en HD) al pasar la mitad de uno
- PWA: manifest, iconos y service worker; capítulos guardados en el dispositivo para leer sin conexión, progreso offline sincronizado después
- Seguridad: baneo de IP tras 6 intentos fallidos (configurable), IP real detrás de proxies de confianza, CSP y cabeceras, cookie Secure/HSTS en HTTPS, login sin diferencias de tiempo, registro cerrado por defecto, contraseñas de 8+, protección SSRF en el proxy de imágenes, errores 500 genéricos
- Ajustes: sección Seguridad con IPs bloqueadas, intentos recientes y la IP vista por el servidor
- LeerCapitulo: reintentos ante caídas (522/5xx), copia anterior si sigue caído y mensaje claro

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 07:52:13 -06:00
co-authored by Claude Opus 5.5
parent 9086896a89
commit d3e196a2f3
16 changed files with 931 additions and 59 deletions
+14 -2
View File
@@ -41,6 +41,8 @@ export function setupUpscale({ db, MANGA_DIR, HttpError, json, readBody, MANGA_I
CREATE INDEX IF NOT EXISTS idx_upscale_manga ON upscale_jobs(manga_id);
`)
db.prepare("UPDATE upscale_jobs SET status = 'queued' WHERE status = 'running'").run() // reinicio a mitad
// Capítulos aún sin descargar que deben reescalarse en cuanto lleguen (descarga anticipada con HD).
db.exec('CREATE TABLE IF NOT EXISTS upscale_wanted (chapter_id TEXT PRIMARY KEY, created_at INTEGER NOT NULL)')
db.exec('CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT)')
// Segundos por página medidos (media móvil), para estimar cuánto tardará una cola.
let secPerPage = Number(db.prepare("SELECT v FROM kv WHERE k = 'hd_sec_per_page'").get()?.v) || 13
@@ -203,7 +205,8 @@ export function setupUpscale({ db, MANGA_DIR, HttpError, json, readBody, MANGA_I
// 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])
const wanted = db.prepare('DELETE FROM upscale_wanted WHERE chapter_id = ?').run(chapterId).changes
if (wanted || db.prepare('SELECT hd_auto FROM downloads WHERE manga_id = ?').get(mangaId)?.hd_auto) enqueue([chapterId])
}
async function route(p, method, req, res) {
@@ -250,5 +253,14 @@ 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, queue }
// Reescalar estos capítulos: los descargados ya, el resto al terminar su descarga.
function request(chapterIds) {
const have = db.prepare('SELECT 1 FROM downloaded_chapters WHERE chapter_id = ?')
const later = db.prepare('INSERT OR IGNORE INTO upscale_wanted (chapter_id, created_at) VALUES (?, ?)')
const now = []
for (const id of chapterIds) have.get(id) ? now.push(id) : later.run(id, Date.now())
if (now.length) enqueue(now)
}
return { info, hdFileOf, onChapterDownloaded, cancel, route, pump, available, queue, request }
}