wnacg Download

Download images from wnacg gallery as a zip archive

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

Advertisement:

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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

})();