// ==UserScript==
// @name Viz Manga Universal Downloader (CORS Fix)
// @namespace shadows
// @version 203.2.3
// @description Descarga capítulos de Viz, permite recomponer <img> evitando CORS/hotlinking, enviando cookies y referer.
// @author
// @license MIT
// @match https://www.viz.com/vizmanga/*
// @match https://www.viz.com/shonenjump/*
// @grant GM_download
// @grant GM_xmlhttpRequest
// @connect viz.com
// @connect cdn.viz.com
// @connect i0.viz.com
// @connect i1.viz.com
// @connect i2.viz.com
// @connect *
// ==/UserScript==
"use strict";
(function() {
// --- Crear panel de opciones flotante ---
const panel = document.createElement('div');
panel.style.position = 'fixed';
panel.style.top = '10px';
panel.style.right = '10px';
panel.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
panel.style.color = '#fff';
panel.style.padding = '10px';
panel.style.zIndex = '10000';
panel.style.borderRadius = '5px';
panel.style.fontSize = '13px';
panel.style.maxWidth = '300px';
panel.style.lineHeight = '1.4';
panel.innerHTML = "<strong>Viz Manga Downloader</strong><br>";
// Botón: Descargar Capítulo (todas las <img>)
const downloadChapterButton = document.createElement('button');
downloadChapterButton.textContent = 'Descargar Capítulo';
downloadChapterButton.style.margin = "3px";
panel.appendChild(downloadChapterButton);
// Botón: Auto-Descargar (activar/desactivar con IntersectionObserver)
const autoDownloadToggle = document.createElement('button');
autoDownloadToggle.textContent = 'Auto-Descargar: OFF';
autoDownloadToggle.style.margin = "3px";
panel.appendChild(autoDownloadToggle);
// Botón: Actualizar lista de imágenes (selección manual)
const updateListButton = document.createElement('button');
updateListButton.textContent = 'Actualizar lista';
updateListButton.style.margin = "3px";
panel.appendChild(updateListButton);
// Botón: Descargar seleccionados
const downloadSelectedButton = document.createElement('button');
downloadSelectedButton.textContent = 'Descargar seleccionados';
downloadSelectedButton.style.margin = "3px";
panel.appendChild(downloadSelectedButton);
// Botón: Recomponer <img> (GM_xhr con cookies/referer)
const reassemblePuzzleButton = document.createElement('button');
reassemblePuzzleButton.textContent = 'Recomponer <img> (GM_xhr)';
reassemblePuzzleButton.style.margin = "3px";
panel.appendChild(reassemblePuzzleButton);
// Contenedor para la lista de imágenes con checkboxes
const imageListContainer = document.createElement('div');
imageListContainer.style.maxHeight = '200px';
imageListContainer.style.overflowY = 'auto';
imageListContainer.style.marginTop = '5px';
panel.appendChild(imageListContainer);
document.body.appendChild(panel);
// --- Función para descargar una imagen usando GM_download ---
function descargarImagen(url, nombre) {
GM_download({
url: url,
name: nombre,
onerror: err => console.error('Error al descargar', url, err),
onload: () => console.log('Descargado:', url)
});
}
// --- Botón: Descargar Capítulo (todas las <img>) ---
downloadChapterButton.addEventListener('click', function() {
const images = document.querySelectorAll('img');
let count = 0;
images.forEach((img, index) => {
if (img.src) {
const nombre = `page-${index + 1}.jpg`;
descargarImagen(img.src, nombre);
count++;
}
});
alert(`Iniciada descarga de ${count} imágenes.`);
});
// --- Auto-Descargar usando IntersectionObserver ---
let autoDownloadEnabled = false;
autoDownloadToggle.addEventListener('click', function() {
autoDownloadEnabled = !autoDownloadEnabled;
autoDownloadToggle.textContent = `Auto-Descargar: ${autoDownloadEnabled ? 'ON' : 'OFF'}`;
});
const descargadas = new Set(); // Para evitar descargas duplicadas
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && autoDownloadEnabled) {
const img = entry.target;
if (img.src && !descargadas.has(img.src)) {
descargadas.add(img.src);
const nombre = `auto-${Date.now()}-${Math.random().toString(36).substr(2,6)}.jpg`;
descargarImagen(img.src, nombre);
}
}
});
}, { threshold: 0.5 });
// Observar todas las imágenes existentes
function observarImagenes() {
document.querySelectorAll('img').forEach(img => {
observer.observe(img);
});
}
observarImagenes();
// Observar dinámicamente si se agregan imágenes
const mutationObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName === 'IMG') {
observer.observe(node);
} else {
node.querySelectorAll('img').forEach(img => observer.observe(img));
}
}
});
});
});
mutationObserver.observe(document.body, { childList: true, subtree: true });
// --- Actualizar lista de imágenes para selección manual ---
updateListButton.addEventListener('click', function() {
imageListContainer.innerHTML = ''; // Limpiar lista actual
const images = document.querySelectorAll('img');
images.forEach((img, index) => {
if (img.src) {
const label = document.createElement('label');
label.style.display = 'block';
label.style.marginBottom = '3px';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = true;
checkbox.dataset.url = img.src;
label.appendChild(checkbox);
label.appendChild(document.createTextNode(` Imagen ${index + 1}`));
imageListContainer.appendChild(label);
}
});
});
// --- Descargar imágenes seleccionadas ---
downloadSelectedButton.addEventListener('click', function() {
const checkboxes = imageListContainer.querySelectorAll('input[type="checkbox"]');
let count = 0;
checkboxes.forEach((checkbox, index) => {
if (checkbox.checked) {
const url = checkbox.dataset.url;
const nombre = `selected-${index + 1}.jpg`;
descargarImagen(url, nombre);
count++;
}
});
alert(`Iniciada descarga de ${count} imágenes seleccionadas.`);
});
// --- Botón para recomponer la página dividida en varios <img> (usando GM_xmlhttpRequest con cookies/referer) ---
reassemblePuzzleButton.addEventListener('click', async function() {
try {
await reassemblePuzzleFromImgs();
} catch (e) {
console.error(e);
alert("Error inesperado al recomponer la imagen.");
}
});
async function reassemblePuzzleFromImgs() {
// Ajusta este selector si quieres filtrar <img> de un contenedor específico
const puzzleImgs = Array.from(document.querySelectorAll('img'));
if (!puzzleImgs.length) {
alert("No se encontraron <img> para recomponer.");
return;
}
// 1. Calcular bounding box
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
puzzleImgs.forEach(img => {
const rect = img.getBoundingClientRect();
if (rect.left < minX) minX = rect.left;
if (rect.top < minY) minY = rect.top;
if (rect.right > maxX) maxX = rect.right;
if (rect.bottom > maxY) maxY = rect.bottom;
});
const totalWidth = Math.ceil(maxX - minX);
const totalHeight = Math.ceil(maxY - minY);
if (totalWidth <= 0 || totalHeight <= 0) {
alert("Las dimensiones calculadas son inválidas. ¿Están las imágenes visibles en pantalla?");
return;
}
// 2. Crear canvas
const canvas = document.createElement('canvas');
canvas.width = totalWidth;
canvas.height = totalHeight;
const ctx = canvas.getContext('2d');
// 3. Cargar cada <img> con GM_xmlhttpRequest (enviando cookies y referer)
const promises = puzzleImgs.map(img => {
return loadImageWithGM(img.src).then(loadedImg => {
return { el: img, img: loadedImg };
});
});
let loadedImages;
try {
loadedImages = await Promise.all(promises);
} catch (err) {
alert("Error al cargar una de las imágenes (CORS/hotlinking).");
console.error(err);
return;
}
// 4. Dibujar cada imagen en su posición
loadedImages.forEach(obj => {
const { el, img } = obj;
const rect = el.getBoundingClientRect();
const offsetX = Math.round(rect.left - minX);
const offsetY = Math.round(rect.top - minY);
ctx.drawImage(img, offsetX, offsetY, rect.width, rect.height);
});
// 5. Descargar resultado
canvas.toBlob(blob => {
const blobUrl = URL.createObjectURL(blob);
GM_download({
url: blobUrl,
name: 'recomposed_page.png',
onerror: err => {
console.error("Error descargando la imagen recompuesta:", err);
},
onload: () => {
console.log("Imagen recompuesta descargada correctamente.");
URL.revokeObjectURL(blobUrl);
}
});
});
}
/**
* Carga una imagen usando GM_xmlhttpRequest, enviando cookies y referer.
* Devuelve una Promise que resuelve con un objeto Image listo para dibujar.
*/
function loadImageWithGM(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'blob',
// Enviar cookies y referer
withCredentials: true,
headers: {
"Referer": location.href
},
onload: function(response) {
if (response.status !== 200) {
reject(new Error('Status code ' + response.status + ' al cargar ' + url));
return;
}
const blob = response.response;
const objectURL = URL.createObjectURL(blob);
const img = new Image();
// Ya es un blob local, no hace falta crossOrigin
img.onload = () => {
URL.revokeObjectURL(objectURL);
resolve(img);
};
img.onerror = err => {
URL.revokeObjectURL(objectURL);
reject(err);
};
img.src = objectURL;
},
onerror: function(err) {
reject(err);
}
});
});
}
})();