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
+8 -4
View File
@@ -41,7 +41,7 @@ const MODES = ['paged', 'double', 'vertical']
const FITS = ['height', 'width']
const DIRS = ['ltr', 'rtl']
export function setupFeatures({ db, fetchMangaFull, enqueueDownload, downloadInfo, HttpError, json, readBody, MANGA_ID }) {
export function setupFeatures({ db, fetchMangaFull, enqueueDownload, downloadInfo, HttpError, json, readBody, MANGA_ID, net }) {
db.exec(`
CREATE TABLE IF NOT EXISTS follows (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@@ -182,14 +182,17 @@ export function setupFeatures({ db, fetchMangaFull, enqueueDownload, downloadInf
return anyNew
}
let skippedOffline = false
function checkFollows() {
if (state.checking) return state.checking
// Sin internet no se puede comprobar nada: se hace en cuanto vuelva la conexión.
if (!net.online()) { skippedOffline = true; return Promise.resolve() }
state.checking = (async () => {
const groups = db.prepare('SELECT manga_id, lang FROM follows WHERE active = 1 GROUP BY manga_id, lang').all()
let found = 0
for (const g of groups) {
try { found += await checkManga(g.manga_id, g.lang) } catch (e) { console.warn('Comprobación', g.manga_id, e.message) }
await sleep(g.manga_id.startsWith('lc~') ? 1500 : 2500)
await sleep(/^(lc|mi)~/.test(g.manga_id) ? 1500 : 2500)
}
state.lastCheckAt = Date.now()
if (groups.length) console.log(`Comprobados ${groups.length} mangas seguidos: ${found} capítulo(s) nuevo(s)`)
@@ -199,6 +202,7 @@ export function setupFeatures({ db, fetchMangaFull, enqueueDownload, downloadInf
})
return state.checking
}
net.whenOnline(() => { if (skippedOffline) { skippedOffline = false; checkFollows() } })
state.nextCheckAt = Date.now() + 90e3
setTimeout(() => {
checkFollows()
@@ -217,7 +221,7 @@ export function setupFeatures({ db, fetchMangaFull, enqueueDownload, downloadInf
// Seguir automáticamente lo que se empieza a leer (salvo que se haya dejado de seguir).
db.prepare('INSERT OR IGNORE INTO follows (user_id, manga_id, title, cover, lang, created_at) VALUES (?, ?, ?, ?, ?, ?)')
.run(userId, b.mangaId, String(b.mangaTitle || '').slice(0, 300), b.cover ? String(b.cover).slice(0, 1000) : null,
b.mangaId.startsWith('lc~') ? 'es' : b.lang || null, Date.now())
/^(lc|mi)~/.test(b.mangaId) ? 'es' : b.lang || null, Date.now())
}
// Datos extra de la ficha; marca como vistos los capítulos nuevos.
@@ -321,7 +325,7 @@ export function setupFeatures({ db, fetchMangaFull, enqueueDownload, downloadInf
}
if ((m = /^\/api\/follow\/([^/]+)$/.exec(p)) && MANGA_ID.test(m[1]) && method === 'POST') {
const b = await readBody(req)
const lang = m[1].startsWith('lc~') ? 'es' : /^[a-z]{2}(-[a-z]{2})?$/.test(b.lang || '') ? b.lang : user.lang
const lang = /^(lc|mi)~/.test(m[1]) ? 'es' : /^[a-z]{2}(-[a-z]{2})?$/.test(b.lang || '') ? b.lang : user.lang
db.prepare(`INSERT INTO follows (user_id, manga_id, title, cover, lang, active, created_at) VALUES (?, ?, ?, ?, ?, 1, ?)
ON CONFLICT (user_id, manga_id) DO UPDATE SET active = 1, lang = excluded.lang, title = excluded.title, cover = excluded.cover,
known = CASE WHEN follows.lang IS excluded.lang THEN follows.known ELSE NULL END`)