Fuente M440, modo sin conexión (servidor sin internet y app sin servidor), cola del dispositivo en Colas y arreglo del reescalado de tiras altas

- M440 (m440.in, español): búsqueda, populares, actualizados paginados, ficha, capítulos, descargas y seguimiento. La lista de capítulos viene cifrada: la clave se saca de un script ofuscado del sitio ejecutándolo en un proceso aislado con permisos mínimos (m440-key.mjs) y se renueva sola.
- Servidor sin internet (connectivity.js): se detecta en segundos; las fuentes fallan al instante, la portada muestra lo descargado en el servidor con buscador, portadas locales de respaldo, descargas y comprobaciones en pausa y reanudación automática al volver la conexión.
- App sin conexión con el servidor: el service worker usa la copia guardada si el servidor no contesta en 3 s, las peticiones comprueban si el servidor sigue vivo en vez de quedarse colgadas, aviso arriba e inicio con lo guardado en el dispositivo; al volver, se reconecta sola y envía el progreso pendiente.
- Colas: sección «En este dispositivo» con el guardado en curso en directo y cancelación (sin dejar capítulos a medias); también visible sin servidor.
- Imágenes: el proxy y el guardado en el dispositivo reintentan con tiempo límite cuando el CDN de la fuente falla.
- Reescalado HD: las páginas que superan 16383 px al reescalar (tiras de webtoon) se reducen al límite de WebP en vez de hacer fallar el capítulo.
- Entidades HTML con nombre en las sinopsis; pestañas de fuentes cortas en móvil; portadas rotas con icono de respaldo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 09:52:19 -06:00
co-authored by Claude Opus 5.5
parent d3e196a2f3
commit 86999b5725
11 changed files with 712 additions and 78 deletions
+5 -4
View File
@@ -6,7 +6,7 @@ const REC_TTL = 6 * 3600e3
import { genreName } from './features.js'
const norm = s => String(s).trim().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
export function setupLibrary({ db, HttpError, json, readBody, MANGA_ID, fetchMangaFull, enqueueDownload, upscale, stats, searchManga, mdCategories }) {
export function setupLibrary({ db, HttpError, json, readBody, MANGA_ID, fetchMangaFull, enqueueDownload, upscale, stats, searchManga, mdCategories, net }) {
db.exec(`
CREATE TABLE IF NOT EXISTS lists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -54,12 +54,12 @@ export function setupLibrary({ db, HttpError, json, readBody, MANGA_ID, fetchMan
// Al pasar la mitad de un capítulo, encola los N siguientes (y, si se pidió, su versión HD).
function onProgress(userId, b, page, total) {
const cfg = prefetchCfg(userId)
if (!cfg.n || !total || page < Math.floor(total / 2)) return
if (!cfg.n || !total || page < Math.floor(total / 2) || !net.online()) return
const key = `${b.mangaId}|${b.chapterId}|${cfg.n}|${cfg.hd}`
if (Date.now() - (recent.get(key) || 0) < 30 * 60e3) return
recent.set(key, Date.now())
if (recent.size > 2000) recent.delete(recent.keys().next().value)
const lang = b.mangaId.startsWith('lc~') ? 'es' : b.lang
const lang = /^(lc|mi)~/.test(b.mangaId) ? 'es' : b.lang
fetchMangaFull(b.mangaId, lang).then(({ manga, chapters }) => {
const i = chapters.findIndex(c => c.id === b.chapterId)
if (i < 0) return
@@ -82,6 +82,7 @@ export function setupLibrary({ db, HttpError, json, readBody, MANGA_ID, fetchMan
async function recommendations(userId, refresh) {
const hit = recCache.get(userId)
if (hit && !refresh && Date.now() - hit.at < REC_TTL) return hit.data
if (!net.online()) return hit?.data ?? { genres: [], items: [] }
const lang = db.prepare('SELECT lang FROM users WHERE id = ?').get(userId)?.lang || 'es-la'
const genres = stats(userId).genres.slice(0, 5)
const cats = (await mdCategories().catch(() => [])).filter(c => c.group === 'Géneros' || c.group === 'Temáticas')
@@ -105,7 +106,7 @@ export function setupLibrary({ db, HttpError, json, readBody, MANGA_ID, fetchMan
const items = [...score.values()].sort((a, b) => b.pts - a.pts).slice(0, 18)
.map(({ m, why }) => ({ ...m, sub: `Por ${why.slice(0, 2).join(' y ')}` }))
const data = { genres: picked.map(x => ({ name: x.g.name, id: x.cat.id, source: 'md' })), items }
recCache.set(userId, { at: Date.now(), data })
if (items.length) recCache.set(userId, { at: Date.now(), data }) // vacío (fuente caída): no guardarlo 6 h
return data
}