Quitar el laboratorio del giroscopio

Ya sirvió para calibrar createTilt con grabaciones del iPad mini 6 y el Pixel 10: fuera la página #/gyrolab, su botón en
Ajustes y la ruta /api/gyrolab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 21:12:38 -06:00
co-authored by Claude Opus 5.5
parent 8ae3594803
commit 311b548410
5 changed files with 1 additions and 364 deletions
-37
View File
@@ -1,37 +0,0 @@
// Laboratorio del giroscopio (solo para desarrollo; se borrará junto con public/gyrolab.js y su botón en Ajustes).
// Guarda las grabaciones de los sensores en DATA_DIR/gyrolab/, una por paso del guion.
import fs from 'node:fs'
import path from 'node:path'
const MAX_BYTES = 4 * 1024 * 1024
const SESSION = /^[a-z0-9]{4,24}$/, STEP = /^[a-z0-9-]{2,24}$/
export function setupGyroLab({ DATA_DIR, HttpError, json }) {
const dir = path.join(DATA_DIR, 'gyrolab')
async function readRaw(req) {
let size = 0
const chunks = []
for await (const c of req) {
size += c.length
if (size > MAX_BYTES) throw new HttpError(413, 'Grabación demasiado grande')
chunks.push(c)
}
try { return JSON.parse(Buffer.concat(chunks).toString()) } catch { throw new HttpError(400, 'JSON inválido') }
}
async function route(p, method, req, res, user) {
if (p !== '/api/gyrolab') return false
if (method === 'GET') {
const mine = fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => f.startsWith(`u${user.id}-`)) : []
return json(res, 200, { files: mine.sort() })
}
if (method !== 'POST') throw new HttpError(405, 'Método no permitido')
const body = await readRaw(req)
if (!SESSION.test(body?.session || '') || !STEP.test(body?.step || '')) throw new HttpError(400, 'Sesión o paso no válidos')
if (!Array.isArray(body.ori) || !Array.isArray(body.mot)) throw new HttpError(400, 'Faltan lecturas')
fs.mkdirSync(dir, { recursive: true })
const name = `u${user.id}-${body.session}-${body.step}.json`
fs.writeFileSync(path.join(dir, name), JSON.stringify({ ...body, user: user.username, savedAt: Date.now() }))
return json(res, 200, { ok: true, file: name })
}
return { route }
}
-11
View File
@@ -395,7 +395,6 @@ async function router() {
if (parts[0] === 'stats') return await viewStats()
if (parts[0] === 'clips') return await viewClips(params)
if (parts[0] === 'settings') return viewSettings()
if (parts[0] === 'gyrolab') return await viewGyroLab() // solo desarrollo
return await viewHome(params)
} catch (e) {
if (e.message !== 'No autenticado') app.innerHTML = `<p class="error">${esc(e.message)}</p>`
@@ -4441,13 +4440,6 @@ async function tableJpeg(file) {
return blob
} finally { URL.revokeObjectURL(url) }
}
// Laboratorio del giroscopio: solo para desarrollo, se borrará (con public/gyrolab.js, gyrolab.js del servidor y su botón en Ajustes).
async function viewGyroLab() {
app.innerHTML = '<p class="muted">Cargando el laboratorio…</p>'
const m = await import(`/gyrolab.js?v=${Date.now()}`)
if (!location.hash.startsWith('#/gyrolab')) return
cleanup = m.mountGyroLab(app, { esc, toast })
}
function viewSettings() {
app.innerHTML = `<h1>Ajustes</h1>
<div class="settings">
@@ -4483,9 +4475,6 @@ function viewSettings() {
<button class="btn small" id="install" hidden>${icon('download')} Instalar como app</button></div>`
: `<p class="muted" style="margin:0;font-size:.88rem">Instalar la app y leer sin conexión necesita que la página se abra por <b>HTTPS</b> (el navegador no lo permite en <code>http://${esc(location.host)}</code>). Por ejemplo con <code>tailscale serve</code> o un proxy inverso con certificado.</p>`}
</section>
<section><b>${icon('phone')} Desarrollo: laboratorio del giroscopio</b>
<p class="muted" style="margin:0;font-size:.88rem">Graba los sensores de movimiento de este dispositivo para calibrar el giroscopio del estante y del libro 3D. Es temporal: se quitará al terminar.</p>
<div class="row"><a class="btn small" href="#/gyrolab">${icon('phone')} Iniciar el laboratorio</a></div></section>
<section id="secsec"><b>${icon('key')} Seguridad: IPs bloqueadas</b><div id="bans" class="muted" style="font-size:.88rem">Cargando…</div></section>
<section><b>${icon('bell')} Avisos de capítulos nuevos</b>
<p class="muted" style="margin:0;font-size:.88rem">El servidor revisa cada pocas horas los mangas que sigues (los que empiezas a leer se siguen solos).
+1 -1
View File
@@ -307,7 +307,7 @@ function rimShape(pts, y0, y1, thick) {
// el dispositivo vertical. La orientación del sistema (deviceorientation) solo se usa si no hay giroscopio: sus ángulos β/γ saltan
// cerca de β = 90°, aunque el dispositivo esté quieto. La inclinación acumulada vuelve sola al centro con el tiempo y se acerca al
// tope con suavidad (tanh). Al caminar, la agitación que mide el acelerómetro baja la sensibilidad.
// Calibrado con grabaciones reales (laboratorio del giroscopio, ver HANDOFF-LIBRO3D.md).
// Calibrado con grabaciones reales del iPad mini 6 y el Pixel 10 (ver HANDOFF-LIBRO3D.md).
// MAX_DEG: lo que hay que girar el dispositivo para llegar al 76 % del tope. ORBIT_*: cuánto rodea la cámara al libro en el tope, en grados.
export const MAX_DEG = 15
const FADE_S = 2.5, QUIET = 0.6, DEAD = 1, ORBIT_TILT = 10, ORBIT_YAW = 15, ORBIT_BACK = 0.12
-312
View File
@@ -1,312 +0,0 @@
// Laboratorio del giroscopio (solo para desarrollo; se borrará junto con /gyrolab.js del servidor y su botón en Ajustes).
// Graba a la vez las tres fuentes: la orientación que calcula el sistema (deviceorientation), el giroscopio
// (rotationRate) y el acelerómetro con y sin gravedad (devicemotion), siguiendo un guion de situaciones.
// Cada paso se sube a /api/gyrolab en cuanto termina.
const STEPS = [
{ id: 'mesa', name: 'Apoyado en la mesa', say: 'Déjalo sobre la mesa con la pantalla hacia arriba y no lo toques.', secs: 10, lead: 5 },
{ id: 'quieto', name: 'En la mano, quieto', say: 'Sostenlo como cuando lees, sin moverlo a propósito.', secs: 10, lead: 3 },
{ id: 'lento', name: 'Inclinar despacio', say: 'Inclínalo despacio hacia ti y sostén 2 s. Luego hacia un lado y sostén. Luego vuelve.', secs: 14, lead: 3 },
{ id: 'rapido', name: 'Inclinar rápido', say: 'Inclínalo rápido a un lado y vuelve, varias veces, a los dos lados.', secs: 10, lead: 3 },
{ id: 'vertical', name: 'Casi vertical', say: 'Sostenlo casi vertical frente a ti, como leyendo de pie, e inclínalo un poco a los lados.', secs: 12, lead: 3 },
{ id: 'acostado', name: 'Acostado', say: 'Recuéstate o acuéstate como para leer y, a la mitad, cambia de postura una vez.', secs: 15, lead: 5 },
{ id: 'girar', name: 'Girar la pantalla', say: 'Gíralo a horizontal, espera 2 s y vuelve a vertical. Quita antes el bloqueo de rotación.', secs: 12, lead: 3 },
{ id: 'caminar', name: 'Caminando', say: 'Camina unos pasos sosteniéndolo como para leer.', secs: 12, lead: 3 },
]
const KEEP_S = 5 // lo que enseña la gráfica en vivo
const COLORS = ['#ff7a8a', '#6fb3ff', '#8be28b']
const CSS = `
.glab { max-width: 760px; margin: 0 auto; display: grid; gap: 14px; }
.glab h1 { margin: 0; }
.glab .gtop { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.glab .gtop input { flex: 1; min-width: 140px; }
.glab .glive { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 8px; font: 12px/1.45 ui-monospace, Menlo, monospace; }
.glab .glive div { background: var(--surface); border: 1px solid var(--surface-2); border-radius: var(--radius); padding: 8px 10px; }
.glab .glive b { font-family: system-ui, sans-serif; font-size: 13px; display: block; }
.glab canvas { width: 100%; height: 240px; background: var(--surface); border: 1px solid var(--surface-2); border-radius: var(--radius); }
.glab ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.glab li { display: flex; gap: 12px; align-items: center; background: var(--surface); border: 1px solid var(--surface-2); border-radius: var(--radius); padding: 10px 12px; }
.glab li .gtxt { flex: 1; min-width: 0; }
.glab li .gtxt small { display: block; color: var(--muted); }
.glab li.done { border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
.glab li .gok { color: var(--accent); font-weight: 600; white-space: nowrap; }
.gover { position: fixed; inset: 0; z-index: 1000; background: rgba(10, 9, 14, .94); display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 18px; padding: 24px; text-align: center; }
.gover .gbig { font-size: clamp(48px, 18vw, 120px); font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
.gover .gsay { font-size: 1.15rem; max-width: 34em; }
.gover .gbar { width: min(420px, 80vw); height: 6px; border-radius: 3px; background: var(--surface-2); overflow: hidden; }
.gover .gbar i { display: block; height: 100%; width: 0; background: var(--accent); }
`
const r3 = v => (v == null || !Number.isFinite(v) ? null : Math.round(v * 1000) / 1000)
const fmt = v => (v == null ? ' — ' : (v >= 0 ? ' ' : '') + v.toFixed(2))
const guessDevice = () => {
const ua = navigator.userAgent
if (/iPad|Macintosh/.test(ua) && navigator.maxTouchPoints > 1) return 'iPad'
if (/iPhone/.test(ua)) return 'iPhone'
const m = /Android [^;]*; ([^;)]+)/.exec(ua)
return m ? m[1].trim() : 'otro'
}
const load = () => { try { return JSON.parse(localStorage.getItem('gyrolab')) || null } catch { return null } }
const save = s => { try { localStorage.setItem('gyrolab', JSON.stringify(s)) } catch {} }
const newSession = () => ({ session: Date.now().toString(36), done: [] })
export function mountGyroLab(app, { esc, toast }) {
const style = document.createElement('style')
style.textContent = CSS
document.head.append(style)
let state = load() || newSession()
let device = localStorage.getItem('gyrolabDevice') || guessDevice()
let listening = false, audio = null, wake = null, raf = 0, rec = null, timers = [], pending = null, dead = false
let interval = null, permission = 'sin pedir', lastOri = null, lastMot = null
const hist = { ori: [], mot: [], frames: [] } // lecturas recientes, para la gráfica y los Hz
app.innerHTML = `<div class="glab">
<h1>Laboratorio del giroscopio</h1>
<p class="muted" style="margin:0">Solo para desarrollo. Graba la orientación del sistema, el giroscopio y el acelerómetro mientras haces cada paso.
Cada grabación se sube sola al servidor. Haz los pasos en el orden que quieras y repite los que salgan mal.</p>
<div class="gtop">
<label class="muted" for="gdev">Dispositivo</label><input id="gdev" value="${esc(device)}" maxlength="40" autocomplete="off">
<button class="btn primary" id="gperm">Activar sensores</button>
</div>
<p class="muted" id="gmsg" style="margin:0">Toca «Activar sensores» para empezar.</p>
<div class="glive" id="glive"></div>
<canvas id="gplot"></canvas>
<ol id="gsteps"></ol>
<div class="gtop"><span class="muted" id="gsess"></span><button class="btn small" id="gnew">Nueva sesión</button></div>
</div>`
const $ = s => app.querySelector(s)
const msg = t => { $('#gmsg').textContent = t }
function renderSteps() {
$('#gsess').textContent = `Sesión ${state.session} · ${state.done.length} de ${STEPS.length} pasos guardados`
$('#gsteps').innerHTML = STEPS.map(s => {
const done = state.done.includes(s.id)
return `<li class="${done ? 'done' : ''}"><div class="gtxt"><b>${esc(s.name)}</b><small>${esc(s.say)} (${s.secs} s)</small></div>
${done ? '<span class="gok">✓ Guardado</span>' : ''}
<button class="btn small${done ? '' : ' primary'}" data-step="${s.id}" ${listening ? '' : 'disabled'}>${done ? 'Repetir' : 'Grabar'}</button></li>`
}).join('')
}
renderSteps()
$('#gsteps').addEventListener('click', e => {
const b = e.target.closest('[data-step]')
if (b && !rec) start(STEPS.find(s => s.id === b.dataset.step))
})
$('#gdev').addEventListener('change', e => { device = e.target.value.trim() || guessDevice(); localStorage.setItem('gyrolabDevice', device) })
$('#gnew').addEventListener('click', () => { state = newSession(); save(state); renderSteps(); toast('Sesión nueva') })
// ---- Sensores ----
const now = e => (e.timeStamp > 0 ? e.timeStamp : performance.now())
function onOri(e) {
const t = now(e)
lastOri = { a: e.alpha, b: e.beta, g: e.gamma, abs: e.absolute }
hist.ori.push([t, e.alpha, e.beta, e.gamma])
if (rec?.on) rec.ori.push([r3(t - rec.t0), r3(e.alpha), r3(e.beta), r3(e.gamma)])
}
function onMot(e) {
const t = now(e), r = e.rotationRate || {}, g = e.accelerationIncludingGravity || {}, a = e.acceleration || {}
if (e.interval != null) interval = e.interval
lastMot = { r, g, a }
hist.mot.push([t, r.alpha, r.beta, r.gamma, g.x, g.y, g.z, a.x, a.y, a.z])
if (rec?.on) rec.mot.push([r3(t - rec.t0), r3(r.alpha), r3(r.beta), r3(r.gamma), r3(g.x), r3(g.y), r3(g.z), r3(a.x), r3(a.y), r3(a.z)])
}
const screenNow = () => [screen.orientation?.angle ?? window.orientation ?? 0, innerWidth, innerHeight]
function onScreen() { if (rec?.on) rec.screen.push([r3(performance.now() - rec.t0), ...screenNow()]) }
function listen(on) {
if (on === listening) return
listening = on
const m = on ? 'addEventListener' : 'removeEventListener'
window[m]('deviceorientation', onOri)
window[m]('devicemotion', onMot)
window[m]('resize', onScreen)
screen.orientation?.[m]('change', onScreen)
}
async function keepAwake() {
try { if (!wake && navigator.wakeLock && document.visibilityState === 'visible') { wake = await navigator.wakeLock.request('screen'); wake.addEventListener('release', () => { wake = null }) } } catch {}
}
$('#gperm').addEventListener('click', async () => {
try { audio ||= new (window.AudioContext || window.webkitAudioContext)(); audio.resume() } catch {}
if (!isSecureContext) return msg('Los sensores solo funcionan por HTTPS.')
for (const C of [window.DeviceMotionEvent, window.DeviceOrientationEvent]) {
if (!C?.requestPermission) continue
permission = await C.requestPermission().catch(err => `error: ${err?.message || err}`)
if (permission !== 'granted') return msg(`El navegador no dio permiso para los sensores (${permission}). En iOS: Ajustes → Safari → Movimiento y orientación.`)
}
if (permission === 'sin pedir') permission = 'no hace falta'
listen(true)
keepAwake()
$('#gperm').hidden = true
renderSteps()
msg('Sensores activos. Escoge un paso y toca «Grabar».')
timers.push(setTimeout(() => {
if (!hist.ori.length && !hist.mot.length) msg('No llega ninguna lectura de los sensores. ¿Este dispositivo tiene giroscopio?')
else if (!hist.ori.length) msg('Llega el giroscopio pero no la orientación del sistema.')
else if (!hist.mot.length) msg('Llega la orientación pero no el giroscopio ni el acelerómetro.')
}, 1500))
})
const onVis = () => {
if (document.visibilityState === 'visible') { if (listening) keepAwake() } else if (rec) cancel('Se canceló porque la página dejó de verse.')
}
document.addEventListener('visibilitychange', onVis)
// ---- Avisos sonoros: sirven con el dispositivo sobre la mesa o caminando ----
function beep(freq = 880, ms = 120, at = 0) {
try {
if (!audio) return
const o = audio.createOscillator(), g = audio.createGain(), t = audio.currentTime + at
o.frequency.value = freq
g.gain.setValueAtTime(0.0001, t)
g.gain.exponentialRampToValueAtTime(0.25, t + 0.01)
g.gain.exponentialRampToValueAtTime(0.0001, t + ms / 1000)
o.connect(g).connect(audio.destination)
o.start(t)
o.stop(t + ms / 1000 + 0.02)
} catch {}
}
const buzz = p => { try { navigator.vibrate?.(p) } catch {} }
// ---- Grabar un paso ----
let over = null
function overlay(step) {
over = document.createElement('div')
over.className = 'gover'
over.innerHTML = `<div class="muted">${esc(step.name)}</div><div class="gbig"></div><div class="gsay">${esc(step.say)}</div>
<div class="gbar"><i></i></div><button class="btn" id="gcancel">Cancelar</button>`
document.body.append(over)
over.querySelector('#gcancel').addEventListener('click', () => cancel('Grabación cancelada.'))
}
function start(step) {
overlay(step)
rec = { step, on: false }
const big = over.querySelector('.gbig')
let left = step.lead
big.textContent = left
beep(660, 80)
const tick = () => {
if (!rec) return
left--
if (left > 0) { big.textContent = left; beep(660, 80); timers.push(setTimeout(tick, 1000)); return }
big.textContent = 'Grabando'
beep(1040, 220)
buzz(80)
Object.assign(rec, { on: true, t0: performance.now(), ori: [], mot: [], frames: [], screen: [[0, ...screenNow()]], startedAt: Date.now() })
const bar = over.querySelector('.gbar i'), end = rec.t0 + step.secs * 1000
const show = () => {
if (!rec?.on) return
const p = Math.min(1, (performance.now() - rec.t0) / (step.secs * 1000))
bar.style.width = `${p * 100}%`
big.textContent = Math.max(0, Math.ceil((end - performance.now()) / 1000))
if (p >= 1) finish()
else timers.push(setTimeout(show, 100))
}
show()
}
timers.push(setTimeout(tick, 1000))
}
function closeOverlay() { over?.remove(); over = null }
function cancel(text) { rec = null; closeOverlay(); msg(text) }
async function finish() {
const r = rec
rec = null
beep(880, 120); beep(880, 120, 0.2); buzz([60, 60, 60])
closeOverlay()
const body = {
v: 1, session: state.session, step: r.step.id, device, secs: r.step.secs, startedAt: r.startedAt,
ua: navigator.userAgent, dpr: devicePixelRatio, screenSize: [screen.width, screen.height], interval, permission,
absolute: lastOri?.abs ?? null,
cols: { ori: ['t', 'alpha', 'beta', 'gamma'], mot: ['t', 'rrAlpha', 'rrBeta', 'rrGamma', 'gx', 'gy', 'gz', 'ax', 'ay', 'az'], screen: ['t', 'angle', 'w', 'h'], frames: ['t'] },
ori: r.ori, mot: r.mot, screen: r.screen, frames: r.frames,
}
msg(`Subiendo «${r.step.name}»: ${r.ori.length} lecturas de orientación y ${r.mot.length} de movimiento…`)
await upload(body, r.step)
}
async function upload(body, step) {
try {
const res = await fetch('/api/gyrolab', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
const data = await res.json().catch(() => ({}))
if (!res.ok) throw new Error(data.error || `Error ${res.status}`)
pending = null
if (!state.done.includes(step.id)) state.done.push(step.id)
save(state)
if (dead) return
renderSteps()
msg(`«${step.name}» guardado (${body.ori.length} + ${body.mot.length} lecturas).`)
} catch (err) {
pending = { body, step }
if (dead) return
msg(`No se pudo subir «${step.name}»: ${err.message}. `)
const b = document.createElement('button')
b.className = 'btn small'
b.textContent = 'Reintentar'
b.addEventListener('click', () => pending && upload(pending.body, pending.step))
$('#gmsg').append(b)
}
}
// ---- Vista en vivo ----
const canvas = $('#gplot'), ctx = canvas.getContext('2d')
const STRIPS = [
{ name: 'Orientación del sistema β γ (°)', src: 'ori', cols: [2, 3], range: 100 },
{ name: 'Giroscopio α β γ (°/s)', src: 'mot', cols: [1, 2, 3], range: 200 },
{ name: 'Acelerómetro con gravedad x y z (m/s²)', src: 'mot', cols: [4, 5, 6], range: 12 },
]
const hz = list => { const n = list.length; return n > 1 ? ((n - 1) * 1000) / (list[n - 1][0] - list[0][0]) : 0 }
let liveAt = 0
function frame(t) {
raf = requestAnimationFrame(frame)
hist.frames.push([t])
if (rec?.on) rec.frames.push(r3(t - rec.t0))
const cut = t - KEEP_S * 1000
for (const k of ['ori', 'mot', 'frames']) { const l = hist[k]; let i = 0; while (i < l.length && l[i][0] < cut) i++; if (i) l.splice(0, i) }
if (rec?.on) return // grabando: no se dibuja nada que no haga falta
const w = Math.round(canvas.clientWidth * devicePixelRatio), h = Math.round(canvas.clientHeight * devicePixelRatio)
if (canvas.width !== w || canvas.height !== h) Object.assign(canvas, { width: w, height: h })
ctx.clearRect(0, 0, w, h)
const sh = h / STRIPS.length, d = devicePixelRatio
ctx.font = `${11 * d}px system-ui, sans-serif`
STRIPS.forEach((s, i) => {
const y0 = i * sh, mid = y0 + sh / 2
ctx.strokeStyle = 'rgba(255,255,255,.08)'
ctx.beginPath(); ctx.moveTo(0, mid); ctx.lineTo(w, mid); ctx.stroke()
ctx.fillStyle = 'rgba(255,255,255,.55)'
ctx.fillText(s.name, 6 * d, y0 + 14 * d)
s.cols.forEach((c, j) => {
ctx.strokeStyle = COLORS[j]
ctx.lineWidth = 1.5 * d
ctx.beginPath()
let first = true
for (const row of hist[s.src]) {
const v = row[c]
if (v == null) continue
const x = ((row[0] - cut) / (KEEP_S * 1000)) * w, y = mid - (Math.max(-s.range, Math.min(s.range, v)) / s.range) * (sh / 2 - 4 * d)
first ? ctx.moveTo(x, y) : ctx.lineTo(x, y)
first = false
}
ctx.stroke()
})
})
if (t - liveAt < 150) return
liveAt = t
const o = lastOri, m = lastMot
$('#glive').innerHTML = `
<div><b>Orientación · ${hz(hist.ori).toFixed(0)} Hz</b>α ${fmt(o?.a)}<br>β ${fmt(o?.b)}<br>γ ${fmt(o?.g)}</div>
<div><b>Giroscopio · ${hz(hist.mot).toFixed(0)} Hz</b>α ${fmt(m?.r.alpha)}<br>β ${fmt(m?.r.beta)}<br>γ ${fmt(m?.r.gamma)}</div>
<div><b>Acelerómetro con gravedad</b>x ${fmt(m?.g.x)}<br>y ${fmt(m?.g.y)}<br>z ${fmt(m?.g.z)}</div>
<div><b>Sin gravedad</b>x ${fmt(m?.a.x)}<br>y ${fmt(m?.a.y)}<br>z ${fmt(m?.a.z)}</div>
<div><b>Pantalla · ${hz(hist.frames).toFixed(0)} Hz</b>giro ${screenNow()[0]}°<br>${innerWidth}×${innerHeight}<br>intervalo ${interval ?? '—'} ms</div>`
}
raf = requestAnimationFrame(frame)
return () => {
dead = true
cancelAnimationFrame(raf)
timers.forEach(clearTimeout)
rec = null
closeOverlay()
listen(false)
document.removeEventListener('visibilitychange', onVis)
try { wake?.release() } catch {}
try { audio?.close() } catch {}
style.remove()
}
}
-3
View File
@@ -18,7 +18,6 @@ import { setupMeta } from './meta.js'
import { setupVolumes } from './volumes.js'
import { setupCredits } from './credits.js'
import { setupShelf } from './shelf.js'
import { setupGyroLab } from './gyrolab.js' // solo desarrollo
import { setupBooks, isBookId } from './books.js'
const PORT = Number(process.env.PORT || 3352)
@@ -928,7 +927,6 @@ async function route(req, res) {
if ((await features.route(p, method, q, req, res, user)) !== false) return
if ((await credits.route(p, method, req, res, user)) !== false) return
if (shelf.route(p, method, res, user) !== false) return
if ((await gyrolab.route(p, method, req, res, user)) !== false) return // solo desarrollo
if ((await upscale.route(p, method, req, res)) !== false) {
// Pedir el reescalado (o el automático) lo pone en la biblioteca de quien lo pide.
if (method === 'POST' && (m = /^\/api\/upscale\/([^/]+)(\/auto)?$/.exec(p)) && db.prepare('SELECT 1 FROM downloads WHERE manga_id = ?').get(m[1])) linkDownload(user.id, m[1])
@@ -1179,7 +1177,6 @@ net.whenOnline(() => pumpDownloads())
const meta = setupMeta({ db, md, sourceOf, fetchMangaFull, HttpError, json, net, decode, imgUrl, UA, prepareBanner })
const credits = setupCredits({ db, HttpError, json, readBody, MANGA_ID, CHAPTER_ID })
const shelf = setupShelf({ db, json, isBookId, downloadCover, MY_DOWNLOADS })
const gyrolab = setupGyroLab({ DATA_DIR, HttpError, json }) // solo desarrollo
const volumes = setupVolumes({ md, sourceOf, fetchMangaFull, stored: meta.stored, mdMatches: meta.mdMatches, muVolumes: meta.muVolumes, similar: meta.similar, miSearch: q => mi.search(q), imgUrl, DATA_DIR, UA, HttpError, net })
const clips = setupClips({ db, DATA_DIR, HttpError, json, readBody, MANGA_ID, CHAPTER_ID, upscale })