wnacg Download

Download images from wnacg gallery as a zip archive

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Advertisement:

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

Advertisement:

// ==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();
    });

})();