From 311b5484101273a28c4c3361d330472ac2ebcc94 Mon Sep 17 00:00:00 2001 From: Nino Date: Fri, 6 Nov 2026 21:12:38 -0600 Subject: [PATCH] Quitar el laboratorio del giroscopio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/gyrolab.js | 37 ----- app/public/app.js | 11 -- app/public/book3d.js | 2 +- app/public/gyrolab.js | 312 ------------------------------------------ app/server.js | 3 - 5 files changed, 1 insertion(+), 364 deletions(-) delete mode 100644 app/gyrolab.js delete mode 100644 app/public/gyrolab.js diff --git a/app/gyrolab.js b/app/gyrolab.js deleted file mode 100644 index 3b9b3a5..0000000 --- a/app/gyrolab.js +++ /dev/null @@ -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 } -} diff --git a/app/public/app.js b/app/public/app.js index 533adc1..d629a70 100644 --- a/app/public/app.js +++ b/app/public/app.js @@ -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 = `

${esc(e.message)}

` @@ -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 = '

Cargando el laboratorio…

' - 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 = `

Ajustes

@@ -4483,9 +4475,6 @@ function viewSettings() {
` : `

Instalar la app y leer sin conexión necesita que la página se abra por HTTPS (el navegador no lo permite en http://${esc(location.host)}). Por ejemplo con tailscale serve o un proxy inverso con certificado.

`} -
${icon('phone')} Desarrollo: laboratorio del giroscopio -

Graba los sensores de movimiento de este dispositivo para calibrar el giroscopio del estante y del libro 3D. Es temporal: se quitará al terminar.

-
${icon('key')} Seguridad: IPs bloqueadas
Cargando…
${icon('bell')} Avisos de capítulos nuevos

El servidor revisa cada pocas horas los mangas que sigues (los que empiezas a leer se siguen solos). diff --git a/app/public/book3d.js b/app/public/book3d.js index 15c6151..064c344 100644 --- a/app/public/book3d.js +++ b/app/public/book3d.js @@ -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 diff --git a/app/public/gyrolab.js b/app/public/gyrolab.js deleted file mode 100644 index 5d79ac6..0000000 --- a/app/public/gyrolab.js +++ /dev/null @@ -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 = `

-

Laboratorio del giroscopio

-

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.

-
- - -
-

Toca «Activar sensores» para empezar.

-
- -
    -
    -
    ` - 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 `
  1. ${esc(s.name)}${esc(s.say)} (${s.secs} s)
    - ${done ? '✓ Guardado' : ''} -
  2. ` - }).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 = `
    ${esc(step.name)}
    ${esc(step.say)}
    -
    ` - 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 = ` -
    Orientación · ${hz(hist.ori).toFixed(0)} Hzα ${fmt(o?.a)}
    β ${fmt(o?.b)}
    γ ${fmt(o?.g)}
    -
    Giroscopio · ${hz(hist.mot).toFixed(0)} Hzα ${fmt(m?.r.alpha)}
    β ${fmt(m?.r.beta)}
    γ ${fmt(m?.r.gamma)}
    -
    Acelerómetro con gravedadx ${fmt(m?.g.x)}
    y ${fmt(m?.g.y)}
    z ${fmt(m?.g.z)}
    -
    Sin gravedadx ${fmt(m?.a.x)}
    y ${fmt(m?.a.y)}
    z ${fmt(m?.a.z)}
    -
    Pantalla · ${hz(hist.frames).toFixed(0)} Hzgiro ${screenNow()[0]}°
    ${innerWidth}×${innerHeight}
    intervalo ${interval ?? '—'} ms
    ` - } - 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() - } -} diff --git a/app/server.js b/app/server.js index 681001e..218ded3 100644 --- a/app/server.js +++ b/app/server.js @@ -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 })