Initial commit - cleaned for CV
This commit is contained in:
440
backend/server.js
Normal file
440
backend/server.js
Normal file
@@ -0,0 +1,440 @@
|
||||
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`);
|
||||
});
|
||||
Reference in New Issue
Block a user