nhentai Download

Download images from nhentai gallery as a zip archive

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         nhentai Download
// @version      1.5
// @description  Download images from nhentai gallery as a zip archive
// @match        https://nhentai.net/g/*
// @grant        GM_xmlhttpRequest
// @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';

    // Создание кнопки "Download Fetch"
    const buttonsContainer = document.querySelector('.buttons');
    const fetchButton = document.createElement('button');
    fetchButton.id = 'download-fetch';
    fetchButton.className = 'btn btn-secondary';
    fetchButton.innerHTML = '<i class="fa fa-download"></i> Download Fetch';
    buttonsContainer.appendChild(fetchButton);

    // Создаем элемент для отображения статуса загрузки
    const statusContainer = document.createElement('div');
    statusContainer.style.marginTop = '10px';
    statusContainer.style.fontSize = '14px';
    statusContainer.style.color = '#666';
    buttonsContainer.appendChild(statusContainer);

    // Функция для задержки между запросами
    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    // Функция для получения ссылок на изображения с подстраниц с обработкой ошибок
    async function getImageUrls() {
        const imageUrls = [];
        const pageLinks = document.querySelectorAll('.gallerythumb');
        const retryDelay = 2000; // Задержка перед повтором запроса в случае ошибки (5 секунд)
        const delayBetweenRequests = 200; // Задержка между запросами (1 секунда)

        statusContainer.textContent = `Found ${pageLinks.length} pages. Fetching images...`;

        for (let i = 0; i < pageLinks.length; i++) {
            const link = pageLinks[i];
            const pageUrl = link.href;

            statusContainer.textContent = `Processing page ${i + 1} of ${pageLinks.length}`;

            let success = false;
            let attempts = 0;

            while (!success && attempts < 3) { // Пытаемся получить изображение, максимум 3 попытки
                attempts++;
                try {
                    const response = await fetch(pageUrl);
                    if (response.status === 429) {
                        statusContainer.textContent = `Too Many Requests. Waiting for ${retryDelay / 1000} seconds...`;
                        await delay(retryDelay);
                        continue;
                    }

                    const text = await response.text();
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(text, 'text/html');
                    const imgElement = doc.querySelector('#image-container img');
                    if (imgElement) {
                        const imageUrl = imgElement.src;
                        imageUrls.push(imageUrl);
                        success = true;
                    } else {
                        console.error(`Image not found on page: ${pageUrl}`);
                        success = true; // Прекращаем попытки, так как элемент не найден
                    }
                } catch (error) {
                    console.error(`Failed to fetch page: ${pageUrl}. Attempt ${attempts} of 3`, error);
                    if (attempts >= 3) {
                        statusContainer.textContent = `Failed to fetch after 3 attempts. Skipping page ${i + 1}`;
                    } else {
                        statusContainer.textContent = `Error fetching page ${i + 1}. Retrying in ${retryDelay / 1000} seconds...`;
                        await delay(retryDelay);
                    }
                }
            }

            // Задержка перед следующим запросом
            if (success) {
                await delay(delayBetweenRequests);
            }
        }

        statusContainer.textContent = `Image URLs fetched successfully!`;

        return imageUrls;
    }

    // Функция для загрузки изображения с использованием GM_xmlhttpRequest
    function downloadImageWithGM(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'blob',
                onload: function(response) {
                    if (response.status === 200) {
                        resolve(response.response);
                    } else {
                        reject(new Error(`Failed to download image: ${url}`));
                    }
                },
                onerror: function() {
                    reject(new Error(`Failed to download image: ${url}`));
                }
            });
        });
    }

    // Функция для скачивания изображений и создания архива
    async function downloadImages() {
        const imageUrls = await getImageUrls();
        if (imageUrls.length === 0) {
            statusContainer.textContent = 'No images found. Exiting download.';
            return;
        }

        const zip = new JSZip();
        const errors = [];
        const titleElement = document.querySelector('h1.title .pretty');
        let title = titleElement.textContent.split('|')[0].trim();
        let count = 1;

        statusContainer.textContent = 'Starting download...';

        for (let imageUrl of imageUrls) {
            try {
                const blob = await downloadImageWithGM(imageUrl);
                const fileName = `${count} - ${imageUrl.split('/').pop()}`;
                zip.file(fileName, blob);

                statusContainer.textContent = `Downloaded ${count} of ${imageUrls.length} images`;
                count++;
            } catch (error) {
                console.error(`Failed to download image: ${imageUrl}`, error);
                errors.push(`Image URL: ${imageUrl}\nError: ${error.message}`);
            }
        }

        // Добавление логов ошибок в архив
        errors.forEach((error, index) => {
            zip.file(`log ${index + 1}.txt`, error);
        });

        zip.generateAsync({ type: 'blob' }).then(function(content) {
            saveAs(content, `${title}.zip`);
            statusContainer.textContent = 'Download completed!';
        }).catch(error => {
            console.error('Failed to generate zip:', error);
            statusContainer.textContent = 'Failed to generate zip. Check console for details.';
        });
    }

    // Обработчик нажатия на кнопку
    fetchButton.addEventListener('click', function() {
        statusContainer.textContent = 'Starting the process...';
        downloadImages().catch(error => {
            console.error('Error during download:', error);
            statusContainer.textContent = 'Error during download. Check console for details.';
        });
    });
})();