import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Configuración const STORAGE_BASE_DIR = path.join(__dirname, '../storage'); const MANHWA_BASE_URL = 'https://manhwaweb.com'; /** * Servicio de almacenamiento para capítulos descargados * Gestiona la descarga, almacenamiento y serving de imágenes */ class StorageService { constructor() { this.ensureDirectories(); } /** * Crea los directorios necesarios si no existen */ ensureDirectories() { const dirs = [ STORAGE_BASE_DIR, path.join(STORAGE_BASE_DIR, 'manga'), path.join(STORAGE_BASE_DIR, 'temp') ]; dirs.forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); console.log(`📁 Directorio creado: ${dir}`); } }); } /** * Obtiene la ruta del directorio de un manga */ getMangaDir(mangaSlug) { const mangaDir = path.join(STORAGE_BASE_DIR, 'manga', mangaSlug); if (!fs.existsSync(mangaDir)) { fs.mkdirSync(mangaDir, { recursive: true }); } return mangaDir; } /** * Obtiene la ruta del directorio de un capítulo */ getChapterDir(mangaSlug, chapterNumber) { const mangaDir = this.getMangaDir(mangaSlug); const chapterDir = path.join(mangaDir, `chapter_${chapterNumber}`); if (!fs.existsSync(chapterDir)) { fs.mkdirSync(chapterDir, { recursive: true }); } return chapterDir; } /** * Descarga una imagen desde una URL y la guarda */ async downloadImage(url, filepath) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const buffer = Buffer.from(await response.arrayBuffer()); fs.writeFileSync(filepath, buffer); return { success: true, size: buffer.length, path: filepath }; } catch (error) { console.error(`Error descargando ${url}:`, error.message); return { success: false, error: error.message }; } } /** * Descarga todas las imágenes de un capítulo */ async downloadChapter(mangaSlug, chapterNumber, imageUrls) { const chapterDir = this.getChapterDir(mangaSlug, chapterNumber); const manifestPath = path.join(chapterDir, 'manifest.json'); // Verificar si ya está descargado if (fs.existsSync(manifestPath)) { const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); return { success: true, alreadyDownloaded: true, manifest: manifest }; } console.log(`📥 Descargando capítulo ${chapterNumber} de ${mangaSlug}...`); console.log(` Directorio: ${chapterDir}`); const downloaded = []; const failed = []; // Descargar cada imagen for (let i = 0; i < imageUrls.length; i++) { const url = imageUrls[i]; const filename = `page_${String(i + 1).padStart(3, '0')}.jpg`; const filepath = path.join(chapterDir, filename); process.stdout.write(`\r ⏳ ${i + 1}/${imageUrls.length} (${Math.round((i / imageUrls.length) * 100)}%)`); const result = await this.downloadImage(url, filepath); if (result.success) { downloaded.push({ page: i + 1, filename: filename, url: url, size: result.size, sizeKB: (result.size / 1024).toFixed(2) }); process.stdout.write(`\r ✓ ${i + 1}/${imageUrls.length} (${((result.size / 1024)).toFixed(2)} KB) `); } else { failed.push({ page: i + 1, url: url, error: result.error }); process.stdout.write(`\r ✗ ${i + 1}/${imageUrls.length} (ERROR) `); } } console.log(); // Nueva línea // Crear manifest const manifest = { mangaSlug: mangaSlug, chapterNumber: chapterNumber, totalPages: imageUrls.length, downloadedPages: downloaded.length, failedPages: failed.length, downloadDate: new Date().toISOString(), totalSize: downloaded.reduce((sum, img) => sum + img.size, 0), images: downloaded.map(img => ({ page: img.page, filename: img.filename, url: img.url, size: img.size })) }; fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); return { success: true, alreadyDownloaded: false, manifest: manifest, downloaded: downloaded.length, failed: failed.length }; } /** * Verifica si un capítulo está descargado */ isChapterDownloaded(mangaSlug, chapterNumber) { const chapterDir = this.getChapterDir(mangaSlug, chapterNumber); const manifestPath = path.join(chapterDir, 'manifest.json'); return fs.existsSync(manifestPath); } /** * Obtiene el manifest de un capítulo descargado */ getChapterManifest(mangaSlug, chapterNumber) { const chapterDir = this.getChapterDir(mangaSlug, chapterNumber); const manifestPath = path.join(chapterDir, 'manifest.json'); if (!fs.existsSync(manifestPath)) { return null; } return JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); } /** * Obtiene la ruta de una imagen específica */ getImagePath(mangaSlug, chapterNumber, pageIndex) { const chapterDir = this.getChapterDir(mangaSlug, chapterNumber); const filename = `page_${String(pageIndex).padStart(3, '0')}.jpg`; const imagePath = path.join(chapterDir, filename); if (fs.existsSync(imagePath)) { return imagePath; } return null; } /** * Lista todos los capítulos descargados de un manga */ listDownloadedChapters(mangaSlug) { const mangaDir = this.getMangaDir(mangaSlug); if (!fs.existsSync(mangaDir)) { return []; } const chapters = []; const items = fs.readdirSync(mangaDir); items.forEach(item => { const match = item.match(/^chapter_(\d+)$/); if (match) { const chapterNumber = parseInt(match[1]); const manifest = this.getChapterManifest(mangaSlug, chapterNumber); if (manifest) { chapters.push({ chapterNumber: chapterNumber, downloadDate: manifest.downloadDate, totalPages: manifest.totalPages, downloadedPages: manifest.downloadedPages, totalSize: manifest.totalSize, totalSizeMB: (manifest.totalSize / 1024 / 1024).toFixed(2) }); } } }); return chapters.sort((a, b) => a.chapterNumber - b.chapterNumber); } /** * Elimina un capítulo descargado */ deleteChapter(mangaSlug, chapterNumber) { const chapterDir = this.getChapterDir(mangaSlug, chapterNumber); if (fs.existsSync(chapterDir)) { fs.rmSync(chapterDir, { recursive: true, force: true }); return { success: true }; } return { success: false, error: 'Chapter not found' }; } /** * Obtiene estadísticas de almacenamiento */ getStorageStats() { const stats = { totalMangas: 0, totalChapters: 0, totalSize: 0, mangaDetails: [] }; const mangaDir = path.join(STORAGE_BASE_DIR, 'manga'); if (!fs.existsSync(mangaDir)) { return stats; } const mangas = fs.readdirSync(mangaDir); mangas.forEach(mangaSlug => { const chapters = this.listDownloadedChapters(mangaSlug); const totalSize = chapters.reduce((sum, ch) => sum + ch.totalSize, 0); stats.totalMangas++; stats.totalChapters += chapters.length; stats.totalSize += totalSize; stats.mangaDetails.push({ mangaSlug: mangaSlug, chapters: chapters.length, totalSize: totalSize, totalSizeMB: (totalSize / 1024 / 1024).toFixed(2) }); }); return stats; } /** * Formatea tamaño de archivo */ formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; } } // Exportar instancia singleton export default new StorageService();