Initial commit: MangaReader iOS App

 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>
This commit is contained in:
2026-02-04 15:34:18 +01:00
commit b474182dd9
6394 changed files with 1063909 additions and 0 deletions

226
backend/server.js Normal file
View File

@@ -0,0 +1,226 @@
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`);
});