import express from 'express'; import cors from 'cors'; import path from 'path'; import { fileURLToPath } from 'url'; import { getMangaInfo, getMangaChapters, getChapterImages, getPopularMangas } from './scraper.js'; import storageService from './storage.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(express.json()); // Serve static files from storage directory app.use('/storage', express.static(path.join(__dirname, '../storage'))); // 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 }); } }); // ==================== STORAGE ENDPOINTS ==================== /** * @route POST /api/download * @desc Request to download a chapter * @body { mangaSlug, chapterNumber, imageUrls } */ app.post('/api/download', async (req, res) => { try { const { mangaSlug, chapterNumber, imageUrls } = req.body; if (!mangaSlug || !chapterNumber || !imageUrls) { return res.status(400).json({ error: 'Missing required fields', required: ['mangaSlug', 'chapterNumber', 'imageUrls'] }); } if (!Array.isArray(imageUrls)) { return res.status(400).json({ error: 'imageUrls must be an array' }); } console.log(`\n📥 Download request received:`); console.log(` Manga: ${mangaSlug}`); console.log(` Chapter: ${chapterNumber}`); console.log(` Images: ${imageUrls.length}`); const result = await storageService.downloadChapter( mangaSlug, chapterNumber, imageUrls ); res.json(result); } catch (error) { console.error('Error downloading chapter:', error); res.status(500).json({ error: 'Error descargando capítulo', message: error.message }); } }); /** * @route GET /api/storage/chapters/:mangaSlug * @desc List downloaded chapters for a manga * @param mangaSlug - Slug of the manga */ app.get('/api/storage/chapters/:mangaSlug', (req, res) => { try { const { mangaSlug } = req.params; const chapters = storageService.listDownloadedChapters(mangaSlug); res.json({ mangaSlug: mangaSlug, totalChapters: chapters.length, chapters: chapters }); } catch (error) { console.error('Error listing downloaded chapters:', error); res.status(500).json({ error: 'Error listando capítulos descargados', message: error.message }); } }); /** * @route GET /api/storage/chapter/:mangaSlug/:chapterNumber * @desc Check if a chapter is downloaded and return manifest * @param mangaSlug - Slug of the manga * @param chapterNumber - Chapter number */ app.get('/api/storage/chapter/:mangaSlug/:chapterNumber', (req, res) => { try { const { mangaSlug, chapterNumber } = req.params; const manifest = storageService.getChapterManifest( mangaSlug, parseInt(chapterNumber) ); if (!manifest) { return res.status(404).json({ error: 'Capítulo no encontrado', message: `Chapter ${chapterNumber} of ${mangaSlug} is not downloaded` }); } res.json(manifest); } catch (error) { console.error('Error getting chapter manifest:', error); res.status(500).json({ error: 'Error obteniendo manifest del capítulo', message: error.message }); } }); /** * @route GET /api/storage/image/:mangaSlug/:chapterNumber/:pageIndex * @desc Serve an image from a downloaded chapter * @param mangaSlug - Slug of the manga * @param chapterNumber - Chapter number * @param pageIndex - Page index (1-based) */ app.get('/api/storage/image/:mangaSlug/:chapterNumber/:pageIndex', (req, res) => { try { const { mangaSlug, chapterNumber, pageIndex } = req.params; const imagePath = storageService.getImagePath( mangaSlug, parseInt(chapterNumber), parseInt(pageIndex) ); if (!imagePath) { return res.status(404).json({ error: 'Imagen no encontrada', message: `Page ${pageIndex} of chapter ${chapterNumber} not found` }); } res.sendFile(imagePath); } catch (error) { console.error('Error serving image:', error); res.status(500).json({ error: 'Error sirviendo imagen', message: error.message }); } }); /** * @route DELETE /api/storage/chapter/:mangaSlug/:chapterNumber * @desc Delete a downloaded chapter * @param mangaSlug - Slug of the manga * @param chapterNumber - Chapter number */ app.delete('/api/storage/chapter/:mangaSlug/:chapterNumber', (req, res) => { try { const { mangaSlug, chapterNumber } = req.params; const result = storageService.deleteChapter( mangaSlug, parseInt(chapterNumber) ); if (!result.success) { return res.status(404).json({ error: 'Capítulo no encontrado', message: result.error }); } res.json({ success: true, message: `Chapter ${chapterNumber} of ${mangaSlug} deleted successfully` }); } catch (error) { console.error('Error deleting chapter:', error); res.status(500).json({ error: 'Error eliminando capítulo', message: error.message }); } }); /** * @route GET /api/storage/stats * @desc Get storage statistics */ app.get('/api/storage/stats', (req, res) => { try { const stats = storageService.getStorageStats(); res.json({ totalMangas: stats.totalMangas, totalChapters: stats.totalChapters, totalSize: stats.totalSize, totalSizeMB: (stats.totalSize / 1024 / 1024).toFixed(2), totalSizeFormatted: storageService.formatFileSize(stats.totalSize), mangaDetails: stats.mangaDetails }); } catch (error) { console.error('Error getting storage stats:', error); res.status(500).json({ error: 'Error obteniendo estadísticas de almacenamiento', message: error.message }); } }); // ==================== END OF STORAGE ENDPOINTS ==================== // 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(`\n 📖 MANGA ENDPOINTS:`); 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`); console.log(`\n 💾 STORAGE ENDPOINTS:`); console.log(` POST /api/download`); console.log(` GET /api/storage/chapters/:mangaSlug`); console.log(` GET /api/storage/chapter/:mangaSlug/:chapterNumber`); console.log(` GET /api/storage/image/:mangaSlug/:chapterNumber/:pageIndex`); console.log(` DEL /api/storage/chapter/:mangaSlug/:chapterNumber`); console.log(` GET /api/storage/stats`); console.log(`\n 📁 Static files: /storage`); });