Download images from wnacg gallery as a zip archive
// ==UserScript==
// @name wnacg Download
// @version 0.1
// @description Download images from wnacg gallery as a zip archive
// @match *://*.wnacg.com/*
// @match *://*.wnacg.ru/*
// @match *://wnacg01.link/*
// @grant GM_xmlhttpRequest
// @connect *
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.5.0/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @namespace https://greasyfork.org/users/789838
// ==/UserScript==
(function() {
'use strict';
const container = document.querySelector('.asTBcell.uwthumb');
if (!container) return;
const aidMatch = location.href.match(/-aid-(\d+)/);
if (!aidMatch) return;
const aid = aidMatch[1];
const downloadBtn = document.createElement('a');
downloadBtn.id = 'download-fetch-zip';
downloadBtn.className = 'btn';
downloadBtn.style.width = '130px';
downloadBtn.style.marginTop = '10px';
downloadBtn.style.backgroundColor = '#d22';
downloadBtn.style.borderColor = '#c11';
downloadBtn.style.color = '#fff';
downloadBtn.style.cursor = 'pointer';
downloadBtn.textContent = 'Download ZIP';
const readerBtn = document.getElementById('reader-btn');
if (readerBtn) {
readerBtn.parentNode.insertBefore(downloadBtn, readerBtn.nextSibling);
} else {
container.appendChild(downloadBtn);
}
const statusContainer = document.createElement('div');
statusContainer.style.marginTop = '10px';
statusContainer.style.fontSize = '12px';
statusContainer.style.color = '#666';
statusContainer.style.lineHeight = '1.4';
container.appendChild(statusContainer);
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function transformThumbToOriginal(thumbSrc) {
let src = thumbSrc;
if (src.includes('/data/thumb/')) {
src = src.replace('/data/thumb/', '/data/');
}
src = src.replace(/\/\/t(\d*)\./, '//img$1.');
return src;
}
function downloadImageWithGM(url) {
return new Promise((resolve, reject) => {
if (url.startsWith('//')) {
url = location.protocol + url;
}
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'blob',
headers: {
"Referer": location.origin + '/',
"Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
},
onload: function(response) {
if (response.status === 200) {
resolve(response.response);
} else {
reject(new Error(`Status ${response.status}`));
}
},
onerror: function() {
reject(new Error('Network error'));
}
});
});
}
async function downloadWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const blob = await downloadImageWithGM(url);
return blob;
} catch (e) {
if (i === retries - 1) throw e;
await delay(1500);
}
}
}
async function fetchRealUrlFromPage(photoPagePath) {
const pageUrl = location.origin + photoPagePath;
try {
const response = await fetch(pageUrl, { credentials: 'same-origin' });
const text = await response.text();
const doc = new DOMParser().parseFromString(text, 'text/html');
const imgElement = doc.querySelector('#picarea');
if (imgElement && imgElement.src) {
return imgElement.src;
}
} catch (e) {
console.error('Резервный запрос к HTML не удался', e);
}
return null;
}
async function getAllItemsData() {
const uniqueItems = [];
const seenUrls = new Set();
const maxPageMatch = document.querySelectorAll('.paginator a');
let maxPage = 1;
maxPageMatch.forEach(a => {
const m = a.href.match(/page-(\d+)/);
if (m) maxPage = Math.max(maxPage, parseInt(m[1]));
});
function extractFromDoc(doc) {
doc.querySelectorAll('.gallary_item .pic_box').forEach(box => {
const a = box.querySelector('a');
const img = box.querySelector('img');
if (a && img) {
const pageUrl = a.getAttribute('href');
if (!seenUrls.has(pageUrl)) {
seenUrls.add(pageUrl);
uniqueItems.push({
pageUrl: pageUrl,
guessedUrl: transformThumbToOriginal(img.src)
});
}
}
});
}
extractFromDoc(document);
for (let p = 2; p <= maxPage; p++) {
statusContainer.textContent = `Сканирование страницы ${p} из ${maxPage}...`;
const indexUrl = `${location.origin}/photos-index-page-${p}-aid-${aid}.html`;
try {
const response = await fetch(indexUrl, { credentials: 'same-origin' });
if (response.ok) {
const text = await response.text();
const doc = new DOMParser().parseFromString(text, 'text/html');
extractFromDoc(doc);
}
} catch (e) {
console.error(`Ошибка при сканировании страницы ${p}:`, e);
}
await delay(300);
}
return uniqueItems;
}
async function downloadImages() {
try {
downloadBtn.style.pointerEvents = 'none';
downloadBtn.style.opacity = '0.5';
statusContainer.textContent = 'Сбор всех изображений с галереи...';
const itemsData = await getAllItemsData();
if (itemsData.length === 0) {
statusContainer.textContent = 'Изображения не найдены.';
return;
}
const zip = new JSZip();
const errors = [];
const titleElement = document.querySelector('h2');
let title = titleElement ? titleElement.textContent.trim() : `wnacg_${aid}`;
title = title.replace(/[\\/:*?"<>|]/g, '');
let count = 1;
for (let item of itemsData) {
statusContainer.textContent = `Скачивание: ${count} из ${itemsData.length}...`;
let success = false;
let imageUrl = item.guessedUrl;
let blob;
try {
blob = await downloadWithRetry(imageUrl, 2);
success = true;
} catch (e) {
console.warn(`Ошибка по прямой ссылке ${imageUrl}, попытка резервного запроса...`);
const realUrl = await fetchRealUrlFromPage(item.pageUrl);
if (realUrl) {
imageUrl = realUrl;
try {
blob = await downloadWithRetry(imageUrl, 3);
success = true;
} catch (e2) {
console.error(`Не удалось загрузить и по точной ссылке: ${imageUrl}`, e2);
}
}
}
if (success && blob) {
const urlPathParts = imageUrl.split('?')[0].split('/');
const originalFullName = urlPathParts[urlPathParts.length - 1];
const nameParts = originalFullName.split('.');
const originalExt = nameParts.length > 1 ? nameParts.pop() : '';
const originalName = nameParts.join('.');
let ext = 'jpg';
if (blob.type) {
const mime = blob.type.toLowerCase();
if (mime.includes('webp')) ext = 'webp';
else if (mime.includes('png')) ext = 'png';
else if (mime.includes('gif')) ext = 'gif';
else if (mime.includes('jpeg') || mime.includes('jpg')) ext = 'jpg';
else if (mime.includes('avif')) ext = 'avif';
else {
ext = originalExt.length <= 4 && originalExt.length > 0 ? originalExt : 'jpg';
}
} else {
ext = originalExt.length <= 4 && originalExt.length > 0 ? originalExt : 'jpg';
}
const paddedCount = String(count).padStart(3, '0');
let fileName;
if (/^\d+$/.test(originalName)) {
fileName = `${paddedCount}.${ext}`;
} else {
fileName = `${paddedCount}_${originalName}.${ext}`;
}
zip.file(fileName, blob);
} else {
errors.push(`URL: ${imageUrl}\nОшибка: Загрузка не удалась (статус 403 или 404)`);
}
count++;
await delay(200);
}
if (errors.length > 0) {
zip.file('errors_log.txt', errors.join('\n\n'));
}
statusContainer.textContent = 'Архивация ZIP...';
const content = await zip.generateAsync({ type: 'blob' });
saveAs(content, `${title}.zip`);
statusContainer.textContent = `Загрузка завершена! Сохранено: ${count - 1 - errors.length} шт.`;
} catch (err) {
console.error('Критическая ошибка:', err);
statusContainer.textContent = 'Произошла ошибка (подробности в консоли).';
} finally {
downloadBtn.style.pointerEvents = 'auto';
downloadBtn.style.opacity = '1';
}
}
downloadBtn.addEventListener('click', function(e) {
e.preventDefault();
downloadImages();
});
})();