// Background Service Worker para Manga Mass Downloader // Basado en el addon original optimizado importScripts('jszip.min.js'); chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'downloadManga') { downloadManga(request.metadata, request.imageUrls) .then(result => sendResponse({ success: true, data: result })) .catch(error => sendResponse({ success: false, error: error.message })); return true; } }); // Función principal para descargar el manga async function downloadManga(metadata, imageUrls) { console.log('Iniciando descarga del manga:', metadata.title); try { // 1. Descargar todas las imágenes const images = await downloadAllImages(imageUrls, metadata.title); // 2. Crear archivo ZIP const zipBlob = await createZipArchive(images, metadata.title); // 3. Descargar el archivo await downloadZipFile(zipBlob, metadata.title); return { success: true, title: metadata.title, totalImages: images.length }; } catch (error) { console.error('Error en descarga:', error); throw error; } } // Descargar todas las imágenes en PARALELO (máxima velocidad) async function downloadAllImages(imageUrls, title) { console.log(`🚀 Descargando ${imageUrls.length} imágenes en paralelo...`); // Procesar en lotes de 5 imágenes simultáneas const batchSize = 5; const allImages = []; for (let i = 0; i < imageUrls.length; i += batchSize) { const batch = imageUrls.slice(i, i + batchSize); console.log(`\n=== Procesando lote ${Math.floor(i/batchSize) + 1}: imágenes ${i + 1}-${i + batch.length}`); // Crear promesas para el lote actual const batchPromises = batch.map(async (imageInfo, batchIndex) => { const globalIndex = i + batchIndex; try { // Delay fijo de 20ms await new Promise(resolve => setTimeout(resolve, 20)); const imageData = await downloadImage(imageInfo, title); console.log(`✓ Lote ${Math.floor(i/batchSize) + 1}: Descargada ${globalIndex + 1}/${imageUrls.length}`); // Enviar progreso chrome.runtime.sendMessage({ action: 'downloadProgress', progress: { current: globalIndex + 1, total: imageUrls.length, filename: imageData.filename, errors: 0 } }).catch(() => {}); return imageData; } catch (error) { console.error(`✗ Error en imagen ${globalIndex + 1}:`, error.message); return { error: true, index: globalIndex, message: error.message }; } }); // Esperar a que termine todo el lote const batchResults = await Promise.all(batchPromises); const successful = batchResults.filter(r => !r.error); allImages.push(...successful); console.log(`✓ Lote ${Math.floor(i/batchSize) + 1} completado: ${successful.length}/${batch.length} imágenes`); } console.log(`\n✅ Descarga paralela completada: ${allImages.length}/${imageUrls.length} imágenes`); return allImages; } // Descargar una imagen individual async function downloadImage(imageInfo, title) { const { url, index } = imageInfo; try { // Hacer fetch de la página /s/ const response = await fetch(url, { credentials: 'include' }); if (!response.ok) { throw new Error(`Error ${response.status} al acceder a la página ${index + 1}`); } const html = await response.text(); // Extraer URL real de imagen const imageUrl = extractImageUrlFromHtml(html); if (!imageUrl) { throw new Error(`No se encontró imagen en la página ${index + 1}`); } // Hacer fetch de la imagen real const imageResponse = await fetch(imageUrl, { credentials: 'include' }); if (!imageResponse.ok) { throw new Error(`Error ${imageResponse.status} al descargar imagen ${index + 1}`); } const blob = await imageResponse.blob(); // Validación if (blob.size < 500) { throw new Error(`Imagen demasiado pequeña: ${blob.size} bytes`); } if (!blob.type || !blob.type.startsWith('image/')) { throw new Error(`Tipo de archivo inválido: ${blob.type}`); } const extension = 'jpg'; const paddedIndex = String(index + 1).padStart(3, '0'); return { blob: blob, filename: `page_${paddedIndex}.${extension}`, index: index }; } catch (error) { console.error(`Error en imagen ${index + 1}:`, error.message); throw error; } } // Extraer URL de imagen desde HTML function extractImageUrlFromHtml(html) { try { // Método 1: img#img let match = html.match(/]+id=["']img["'][^>]*>/i); if (match) { const srcMatch = match[0].match(/src=["']([^"']+)["']/i); if (srcMatch && srcMatch[1]) return srcMatch[1]; } // Método 2: img con hath match = html.match(/]*src=["']([^"']*hath[^"']*)["'][^>]*>/i); if (match) { const srcMatch = match[0].match(/src=["']([^"']+)["']/i); if (srcMatch && srcMatch[1]) return srcMatch[1]; } // Método 3: Cualquier img match = html.match(/]+src=["']([^"']+)["'][^>]*>/i); if (match) { const srcMatch = match[0].match(/src=["']([^"']+)["']/i); if (srcMatch && srcMatch[1]) return srcMatch[1]; } // Método 4: data-src match = html.match(/<[^>]+data-src=["']([^"']+)["'][^>]*>/i); if (match) { const dataSrcMatch = match[0].match(/data-src=["']([^"']+)["']/i); if (dataSrcMatch && dataSrcMatch[1]) return dataSrcMatch[1]; } // Método 5: style background match = html.match(/<[^>]+style=["'][^"']*background[^"']*url\(["']?([^"')]+)["']?\)[^"']*["'][^>]*>/i); if (match) { const imageUrl = match[1]; const isValidImage = imageUrl.includes('hath') || imageUrl.match(/\.(jpg|jpeg|png|gif|webp)/i); if (isValidImage) return imageUrl; } // Método 6: srcset match = html.match(/]+srcset=["']([^"']+)["'][^>]*>/i); if (match) { const urls = match[1].split(',')[0].trim(); const urlMatch = urls.match(/(https?:\/\/\S+)/); if (urlMatch && urlMatch[1]) return urlMatch[1]; } // Método 7: URLs directas hath match = html.match(/(https?:\/\/[^"'\s]*hath\.network[^"'\s]*)/i); if (match && match[1]) return match[1]; // Método 8: Fallback match = html.match(/(https?:\/\/[^"'\s]*\.(jpg|jpeg|png|gif|webp))/i); if (match && match[1]) return match[1]; return null; } catch (error) { console.error('Error extrayendo URL de imagen:', error); return null; } } // Crear archivo ZIP async function createZipArchive(images, title) { console.log('\n========== CREANDO ZIP =========='); console.log('Total de imágenes a comprimir:', images.length); const cleanTitle = title .replace(/[<>:"/\\|?*]/g, '_') .replace(/\s+/g, '_') .substring(0, 100); if (images.length === 0) { throw new Error('No hay imágenes para comprimir'); } const zip = new JSZip(); images.forEach((imageData, i) => { if (imageData.blob && imageData.blob.size > 0) { zip.file(imageData.filename, imageData.blob); } }); const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } }); console.log(`✓ ZIP generado: ${zipBlob.size} bytes`); return zipBlob; } // Descargar archivo ZIP async function downloadZipFile(zipBlob, title) { let cleanTitle = title || 'Manga'; cleanTitle = cleanTitle.replace(/[<>:"/\\|?*]/g, '_'); cleanTitle = cleanTitle.replace(/[\x00-\x1F\x7F]/g, ''); cleanTitle = cleanTitle.replace(/\s+/g, '_'); cleanTitle = cleanTitle.replace(/_+/g, '_'); cleanTitle = cleanTitle.substring(0, 80); if (!cleanTitle || cleanTitle.length === 0 || cleanTitle === '.') { cleanTitle = 'Manga'; } if (cleanTitle.startsWith('.')) { cleanTitle = 'Manga_' + cleanTitle; } const filename = `${cleanTitle}.zip`; const arrayBuffer = await zipBlob.arrayBuffer(); const uint8Array = new Uint8Array(arrayBuffer); let binary = ''; for (let i = 0; i < uint8Array.length; i++) { binary += String.fromCharCode(uint8Array[i]); } const base64 = btoa(binary); const dataUrl = `data:application/zip;base64,${base64}`; await chrome.downloads.download({ url: dataUrl, filename: filename, saveAs: true }); }