Initial commit: Manga Mass Downloader Chrome Extension
✨ Features: - Multi-selection checkboxes on manga listings - Batch download selected manga or all manga from page - Optimized parallel downloading (20ms delays, 5 concurrent) - Visual progress tracking - Popup UI for easy control - Fixed duplicate checkbox issue with deduplication logic 📁 Files: - manifest.json: Extension configuration - content.js: Checkbox injection & manga detection - background.js: Optimized download engine - popup.html/js: User interface - README.md: Complete documentation
This commit is contained in:
278
background.js
Normal file
278
background.js
Normal file
@@ -0,0 +1,278 @@
|
||||
// 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(/<img[^>]+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(/<img[^>]*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(/<img[^>]+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(/<img[^>]+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
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user