Reescalado HD, ficha reorganizada y mejoras del lector
- Reescalado HD con Real-ESRGAN (ncnn-vulkan, GPU por /dev/dri): cola persistente, todo lo pendiente, un rango o capítulos elegidos en la lista, con estimación de tiempo. La versión HD va a <carpeta>/.hd/ sin tocar el original; marca HD en la lista de capítulos y casilla "Versión HD" en el lector. - Imagen Debian slim (glibc + Mesa Vulkan + cwebp); compose con /dev/dri y RENDER_GID. - Ficha: acciones principales + menú ⋯, panel "Biblioteca local" (copia local y HD), idioma junto a la lista de capítulos. - Lector: la barra de progreso avanza de derecha a izquierda en der → izq; botón rápido páginas ⇄ vertical; en vertical "Ajustar alto" y "Separar páginas" para mangas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+19
-9
@@ -7,6 +7,7 @@ import crypto from 'node:crypto'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { writeCbz, readCbzIndex, sendCbzEntry, imageType } from './cbz.js'
|
||||
import { setupFeatures } from './features.js'
|
||||
import { setupUpscale } from './upscale.js'
|
||||
|
||||
const PORT = Number(process.env.PORT || 3352)
|
||||
const DATA_DIR = process.env.DATA_DIR || '/app/data'
|
||||
@@ -487,6 +488,7 @@ async function downloadChapter(dl, manga, c, dir) {
|
||||
}
|
||||
db.prepare(`INSERT OR REPLACE INTO downloaded_chapters (chapter_id, manga_id, num, title, file, pages, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
||||
.run(c.id, dl.manga_id, c.num ?? null, c.title || '', file, urls.length, Date.now())
|
||||
upscale.onChapterDownloaded(dl.manga_id, c.id)
|
||||
}
|
||||
|
||||
const dlUpdate = (mangaId, fields) => {
|
||||
@@ -504,13 +506,16 @@ function mangaDirOf(folder) {
|
||||
}
|
||||
function dirSize(dir) {
|
||||
let total = 0
|
||||
try { for (const f of fs.readdirSync(dir)) total += fs.statSync(path.join(dir, f)).size } catch {}
|
||||
try {
|
||||
for (const f of fs.readdirSync(dir, { withFileTypes: true })) total += f.isDirectory() ? dirSize(path.join(dir, f.name)) : fs.statSync(path.join(dir, f.name)).size
|
||||
} catch {}
|
||||
return total
|
||||
}
|
||||
function deleteDownload(mangaId) {
|
||||
const dl = db.prepare('SELECT folder FROM downloads WHERE manga_id = ?').get(mangaId)
|
||||
if (!dl) return false
|
||||
const dir = mangaDirOf(dl.folder)
|
||||
upscale.cancel(mangaId)
|
||||
db.prepare('DELETE FROM downloads WHERE manga_id = ?').run(mangaId) // borra también downloaded_chapters (CASCADE)
|
||||
dlCurrent.delete(mangaId)
|
||||
fs.rmSync(dir, { recursive: true, force: true })
|
||||
@@ -589,10 +594,11 @@ function enqueueDownload(mangaId, title, lang) {
|
||||
}
|
||||
|
||||
const cbzIndexCache = new Map()
|
||||
function localChapter(chapterId) {
|
||||
const row = db.prepare(`SELECT c.file, d.folder FROM downloaded_chapters c JOIN downloads d ON d.manga_id = c.manga_id WHERE c.chapter_id = ?`).get(chapterId)
|
||||
// hd: usar la versión reescalada si existe.
|
||||
function localChapter(chapterId, hd = false) {
|
||||
const row = db.prepare(`SELECT c.file, c.hd_file, d.folder FROM downloaded_chapters c JOIN downloads d ON d.manga_id = c.manga_id WHERE c.chapter_id = ?`).get(chapterId)
|
||||
if (!row) return null
|
||||
const file = path.join(MANGA_DIR, row.folder, row.file)
|
||||
const file = (hd && upscale.hdFileOf(chapterId)) || path.join(MANGA_DIR, row.folder, row.file)
|
||||
let st
|
||||
try { st = fs.statSync(file) } catch { return null }
|
||||
const key = `${file}:${st.mtimeMs}`
|
||||
@@ -697,6 +703,7 @@ async function route(req, res) {
|
||||
// Todo lo demás requiere sesión.
|
||||
const user = requireUser(req)
|
||||
if ((await features.route(p, method, q, req, res, user)) !== false) return
|
||||
if ((await upscale.route(p, method, req, res)) !== false) return
|
||||
|
||||
if (p === '/api/settings' && method === 'POST') {
|
||||
const body = await readBody(req)
|
||||
@@ -748,18 +755,20 @@ async function route(req, res) {
|
||||
const reads = db.prepare('SELECT chapter_id, page, total_pages FROM chapter_reads WHERE user_id = ? AND manga_id = ?').all(user.id, m[1])
|
||||
const progress = db.prepare('SELECT * FROM progress WHERE user_id = ? AND manga_id = ?').get(user.id, m[1]) ?? null
|
||||
const chapterLangs = chapters.length || manga.source === 'lc' || offline ? [] : await getChapterLangs(m[1]).catch(() => [])
|
||||
return json(res, 200, { manga, chapters, lang, reads, progress, chapterLangs, offline, downloaded, download: downloadInfo(m[1], true), ...features.mangaExtras(user.id, manga) })
|
||||
return json(res, 200, { manga, chapters, lang, reads, progress, chapterLangs, offline, downloaded, download: downloadInfo(m[1], true), upscale: upscale.info(m[1]), ...features.mangaExtras(user.id, manga) })
|
||||
}
|
||||
if ((m = /^\/api\/chapter\/([^/]+)$/.exec(p)) && CHAPTER_ID.test(m[1]) && method === 'GET') {
|
||||
const local = localChapter(m[1])
|
||||
const hdAvailable = !!upscale.hdFileOf(m[1])
|
||||
const hd = hdAvailable && q.get('hd') !== '0'
|
||||
const local = localChapter(m[1], hd)
|
||||
const pages = local
|
||||
? local.entries.map((_, i) => `/api/local/${m[1]}/${i}`)
|
||||
? local.entries.map((_, i) => `/api/local/${m[1]}/${i}${hd ? '?hd=1' : ''}`)
|
||||
: m[1].startsWith('lc~') ? await lcPages(m[1]) : await getPages(m[1], q.get('saver') === '1')
|
||||
const read = db.prepare('SELECT page FROM chapter_reads WHERE user_id = ? AND chapter_id = ?').get(user.id, m[1])
|
||||
return json(res, 200, { pages, savedPage: read?.page ?? 0, local: !!local })
|
||||
return json(res, 200, { pages, savedPage: read?.page ?? 0, local: !!local, hd, hdAvailable })
|
||||
}
|
||||
if ((m = /^\/api\/local\/([^/]+)\/(\d+)$/.exec(p)) && CHAPTER_ID.test(m[1]) && method === 'GET') {
|
||||
const local = localChapter(m[1])
|
||||
const local = localChapter(m[1], q.get('hd') === '1')
|
||||
const entry = local?.entries[Number(m[2])]
|
||||
if (!entry) throw new HttpError(404, 'Página no descargada')
|
||||
return sendCbzEntry(res, local.file, entry)
|
||||
@@ -841,6 +850,7 @@ async function route(req, res) {
|
||||
}
|
||||
|
||||
const features = setupFeatures({ db, fetchMangaFull, enqueueDownload, downloadInfo, HttpError, json, readBody, MANGA_ID })
|
||||
const upscale = setupUpscale({ db, MANGA_DIR, HttpError, json, readBody, MANGA_ID })
|
||||
|
||||
// Migración: portadas guardadas antes de firmar las URLs del proxy.
|
||||
for (const row of db.prepare("SELECT user_id, manga_id, cover FROM progress WHERE cover LIKE '/api/img?u=%' AND cover NOT LIKE '%&s=%'").all()) {
|
||||
|
||||
Reference in New Issue
Block a user