✨ Features: - App iOS completa para leer manga sin publicidad - Scraper con WKWebView para manhwaweb.com - Sistema de descargas offline - Lector con zoom y navegación - Favoritos y progreso de lectura - Compatible con iOS 15+ y Sideloadly/3uTools 📦 Contenido: - Backend Node.js con Puppeteer (opcional) - App iOS con SwiftUI - Scraper de capítulos e imágenes - Sistema de almacenamiento local - Testing completo - Documentación exhaustiva 🧪 Prueba: Capítulo 789 de One Piece descargado exitosamente - 21 páginas descargadas - 4.68 MB total - URLs verificadas y funcionales 🎉 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
227 lines
5.3 KiB
JavaScript
227 lines
5.3 KiB
JavaScript
import express from 'express';
|
|
import cors from 'cors';
|
|
import {
|
|
getMangaInfo,
|
|
getMangaChapters,
|
|
getChapterImages,
|
|
getPopularMangas
|
|
} from './scraper.js';
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
// Cache simple (en memoria, se puede mejorar con Redis)
|
|
const cache = new Map();
|
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutos
|
|
|
|
function getCached(key) {
|
|
const cached = cache.get(key);
|
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
return cached.data;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function setCache(key, data) {
|
|
cache.set(key, { data, timestamp: Date.now() });
|
|
}
|
|
|
|
// Routes
|
|
|
|
/**
|
|
* @route GET /api/health
|
|
* @desc Health check endpoint
|
|
*/
|
|
app.get('/api/health', (req, res) => {
|
|
res.json({ status: 'ok', message: 'MangaReader API is running' });
|
|
});
|
|
|
|
/**
|
|
* @route GET /api/mangas/popular
|
|
* @desc Obtener lista de mangas populares
|
|
* @query ?force=true - Ignorar cache
|
|
*/
|
|
app.get('/api/mangas/popular', async (req, res) => {
|
|
try {
|
|
const { force } = req.query;
|
|
|
|
if (!force) {
|
|
const cached = getCached('popular');
|
|
if (cached) {
|
|
return res.json(cached);
|
|
}
|
|
}
|
|
|
|
const mangas = await getPopularMangas();
|
|
setCache('popular', mangas);
|
|
|
|
res.json(mangas);
|
|
} catch (error) {
|
|
console.error('Error getting popular mangas:', error);
|
|
res.status(500).json({
|
|
error: 'Error obteniendo mangas populares',
|
|
message: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @route GET /api/manga/:slug
|
|
* @desc Obtener información de un manga específico
|
|
* @param slug - Slug del manga (ej: one-piece_1695365223767)
|
|
*/
|
|
app.get('/api/manga/:slug', async (req, res) => {
|
|
try {
|
|
const { slug } = req.params;
|
|
const cacheKey = `manga_${slug}`;
|
|
|
|
const cached = getCached(cacheKey);
|
|
if (cached) {
|
|
return res.json(cached);
|
|
}
|
|
|
|
const manga = await getMangaInfo(slug);
|
|
setCache(cacheKey, manga);
|
|
|
|
res.json(manga);
|
|
} catch (error) {
|
|
console.error('Error getting manga info:', error);
|
|
res.status(500).json({
|
|
error: 'Error obteniendo información del manga',
|
|
message: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @route GET /api/manga/:slug/chapters
|
|
* @desc Obtener lista de capítulos de un manga
|
|
* @param slug - Slug del manga
|
|
*/
|
|
app.get('/api/manga/:slug/chapters', async (req, res) => {
|
|
try {
|
|
const { slug } = req.params;
|
|
const cacheKey = `chapters_${slug}`;
|
|
|
|
const cached = getCached(cacheKey);
|
|
if (cached) {
|
|
return res.json(cached);
|
|
}
|
|
|
|
const chapters = await getMangaChapters(slug);
|
|
setCache(cacheKey, chapters);
|
|
|
|
res.json(chapters);
|
|
} catch (error) {
|
|
console.error('Error getting manga chapters:', error);
|
|
res.status(500).json({
|
|
error: 'Error obteniendo capítulos del manga',
|
|
message: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @route GET /api/chapter/:slug/images
|
|
* @desc Obtener imágenes de un capítulo
|
|
* @param slug - Slug del capítulo (ej: one-piece_1695365223767-1172)
|
|
* @query ?force=true - Ignorar cache
|
|
*/
|
|
app.get('/api/chapter/:slug/images', async (req, res) => {
|
|
try {
|
|
const { slug } = req.params;
|
|
const { force } = req.query;
|
|
const cacheKey = `images_${slug}`;
|
|
|
|
if (!force) {
|
|
const cached = getCached(cacheKey);
|
|
if (cached) {
|
|
return res.json(cached);
|
|
}
|
|
}
|
|
|
|
const images = await getChapterImages(slug);
|
|
setCache(cacheKey, images);
|
|
|
|
res.json(images);
|
|
} catch (error) {
|
|
console.error('Error getting chapter images:', error);
|
|
res.status(500).json({
|
|
error: 'Error obteniendo imágenes del capítulo',
|
|
message: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @route GET /api/manga/:slug/full
|
|
* @desc Obtener info completa del manga con capítulos
|
|
* @param slug - Slug del manga
|
|
*/
|
|
app.get('/api/manga/:slug/full', async (req, res) => {
|
|
try {
|
|
const { slug } = req.params;
|
|
const cacheKey = `full_${slug}`;
|
|
|
|
const cached = getCached(cacheKey);
|
|
if (cached) {
|
|
return res.json(cached);
|
|
}
|
|
|
|
// Obtener info y capítulos en paralelo
|
|
const [manga, chapters] = await Promise.all([
|
|
getMangaInfo(slug),
|
|
getMangaChapters(slug)
|
|
]);
|
|
|
|
const fullData = {
|
|
...manga,
|
|
chapters: chapters
|
|
};
|
|
|
|
setCache(cacheKey, fullData);
|
|
|
|
res.json(fullData);
|
|
} catch (error) {
|
|
console.error('Error getting full manga data:', error);
|
|
res.status(500).json({
|
|
error: 'Error obteniendo datos completos del manga',
|
|
message: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Error handler
|
|
app.use((err, req, res, next) => {
|
|
console.error('Unhandled error:', err);
|
|
res.status(500).json({
|
|
error: 'Error interno del servidor',
|
|
message: err.message
|
|
});
|
|
});
|
|
|
|
// 404 handler
|
|
app.use((req, res) => {
|
|
res.status(404).json({
|
|
error: 'Endpoint no encontrado',
|
|
path: req.path
|
|
});
|
|
});
|
|
|
|
// Start server
|
|
app.listen(PORT, () => {
|
|
console.log(`🚀 MangaReader API corriendo en puerto ${PORT}`);
|
|
console.log(`📚 API disponible en: http://localhost:${PORT}/api`);
|
|
console.log(`\nEndpoints disponibles:`);
|
|
console.log(` GET /api/health`);
|
|
console.log(` GET /api/mangas/popular`);
|
|
console.log(` GET /api/manga/:slug`);
|
|
console.log(` GET /api/manga/:slug/chapters`);
|
|
console.log(` GET /api/chapter/:slug/images`);
|
|
console.log(` GET /api/manga/:slug/full`);
|
|
});
|