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:
renato97
2025-11-04 04:25:25 +00:00
commit 829996b41e
7 changed files with 1148 additions and 0 deletions

278
background.js Normal file
View 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
});
}