Nhentai Auto Collector

Extract/Copy and Store Tags, Artists, Characters, Parodies from nhentai.net

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 Auto Collector
// @namespace    https://greasyfork.org/users/1261593
// @version      1.0
// @description  Extract/Copy and Store Tags, Artists, Characters, Parodies from nhentai.net
// @author       john doe4
// @icon         https://nhentai.net/favicon.ico
// @match        *://nhentai.net/tags/*
// @match        *://nhentai.net/artists/*
// @match        *://nhentai.net/characters/*
// @match        *://nhentai.net/parodies/*
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @grant        GM_notification
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @license GPLv3
// ==/UserScript==

(function() {
    'use strict';

    const DEFAULT_CONFIG = {
        tag: {
            name: "Tag",
            startUrl: "https://nhentai.net/tags/?page=1",
            endPage: 35,
            buttonColor: "#6a1b9a"
        },
        artist: {
            name: "Artist",
            startUrl: "https://nhentai.net/artists/?page=1",
            endPage: 259,
            buttonColor: "#9c27b0"
        },
        character: {
            name: "Character",
            startUrl: "https://nhentai.net/characters/?page=1",
            endPage: 139,
            buttonColor: "#3f51b5"
        },
        parody: {
            name: "Parody",
            startUrl: "https://nhentai.net/parodies/?page=1",
            endPage: 35,
            buttonColor: "#2196f3"
        }
    };


    let CONFIG = GM_getValue('collectorConfig', DEFAULT_CONFIG);


    const SPEED_MODES = {
        slow: { delay: 2000, loadCheckInterval: 200, name: "Slow" },
        normal: { delay: 1000, loadCheckInterval: 100, name: "Normal" },
        fast: { delay: 300, loadCheckInterval: 50, name: "Fast" }
    };


    let currentSpeed = GM_getValue('collectorSpeed', 'normal');
    let menuOpen = GM_getValue('menuOpenState', false);
    let ignoreClick = false;


    GM_addStyle(`
        #nhentaiCollectorMenuBtn {
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 9999;
            background: #2e51a2;
            color: white;
            border: none;
            padding: 10px 15px;
            border-radius: 5px;
            cursor: pointer;
            font-weight: bold;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            font-family: Arial, sans-serif;
        }
        #nhentaiCollectorMenu {
            display: none;
            position: fixed;
            bottom: 60px;
            right: 20px;
            width: 250px;
            background: #1a1a1a;
            border: 1px solid #333;
            border-radius: 5px;
            padding: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.3);
            z-index: 9998;
        }
        .collectorBtn {
            width: 100%;
            padding: 8px 10px;
            margin: 5px 0;
            border: none;
            border-radius: 4px;
            color: white;
            font-weight: bold;
            cursor: pointer;
            text-align: left;
        }
        .collectorBtn:hover {
            opacity: 0.9;
        }
        #collectorStatus {
            margin-top: 10px;
            padding: 8px;
            background: #2a2a2a;
            border-radius: 4px;
            font-size: 12px;
            color: #ccc;
            display: none;
        }
        #collectorProgress {
            height: 3px;
            background: #333;
            border-radius: 2px;
            margin-top: 5px;
            overflow: hidden;
            display: none;
        }
        #collectorProgressBar {
            height: 100%;
            background: #4CAF50;
            width: 0%;
            transition: width 0.3s;
        }
        .menuSection {
            margin: 10px 0;
            border-bottom: 1px solid #333;
            padding-bottom: 10px;
        }
        .menuSection:last-child {
            border-bottom: none;
        }
        .menuSectionTitle {
            color: #4a8eff;
            font-size: 14px;
            margin-bottom: 8px;
        }
        .stopBtn {
            background: #d32f2f !important;
        }
        .confirmationDialog {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #1a1a1a;
            padding: 20px;
            border-radius: 5px;
            border: 1px solid #333;
            z-index: 10000;
            box-shadow: 0 0 20px rgba(0,0,0,0.5);
            width: 300px;
            max-width: 90%;
        }
        .confirmationButtons {
            display: flex;
            justify-content: center;
            gap: 10px;
            margin-top: 15px;
        }
        .confirmationBtn {
            padding: 8px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
        }
        .confirmBtn {
            background: #4CAF50;
            color: white;
        }
        .cancelBtn {
            background: #f44336;
            color: white;
        }
        .speedModeContainer {
            display: flex;
            justify-content: space-between;
            margin-top: 5px;
        }
        .speedModeBtn {
            flex: 1;
            padding: 5px;
            margin: 0 2px;
            font-size: 12px;
            border-radius: 3px;
            border: none;
            color: white;
            cursor: pointer;
        }
        .activeSpeedMode {
            outline: 2px solid #4CAF50;
            font-weight: bold;
        }
        .pageConfigContainer {
            margin-top: 5px;
        }
        .pageConfigItem {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin: 5px 0;
            font-size: 12px;
        }
        .pageConfigInput {
            width: 50px;
            padding: 3px;
            background: #2a2a2a;
            border: 1px solid #333;
            color: white;
            text-align: center;
            border-radius: 3px;
        }
        .pageConfigSaveBtn {
            width: 100%;
            padding: 5px;
            margin-top: 5px;
            background: #4CAF50;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
        }
    `);


    const menuBtn = document.createElement('button');
    menuBtn.id = 'nhentaiCollectorMenuBtn';
    menuBtn.textContent = 'NH Collector';
    document.body.appendChild(menuBtn);


    const menu = document.createElement('div');
    menu.id = 'nhentaiCollectorMenu';


    const quickSection = document.createElement('div');
    quickSection.className = 'menuSection';
    const quickTitle = document.createElement('div');
    quickTitle.className = 'menuSectionTitle';
    quickTitle.textContent = 'Quick Actions';
    quickSection.appendChild(quickTitle);

    const showItemsBtn = document.createElement('button');
    showItemsBtn.className = 'collectorBtn';
    showItemsBtn.style.background = '#2e51a2';
    showItemsBtn.textContent = 'Show & Copy Current';
    quickSection.appendChild(showItemsBtn);


    const speedSection = document.createElement('div');
    speedSection.className = 'menuSection';
    const speedTitle = document.createElement('div');
    speedTitle.className = 'menuSectionTitle';
    speedTitle.textContent = 'Speed Mode';
    speedSection.appendChild(speedTitle);

    const speedModeContainer = document.createElement('div');
    speedModeContainer.className = 'speedModeContainer';

    for (const [modeKey, modeConfig] of Object.entries(SPEED_MODES)) {
        const speedBtn = document.createElement('button');
        speedBtn.className = `speedModeBtn ${modeKey === currentSpeed ? 'activeSpeedMode' : ''}`;
        speedBtn.textContent = modeConfig.name;
        speedBtn.style.background = modeKey === 'slow' ? '#d32f2f' : modeKey === 'normal' ? '#ff9800' : '#4CAF50';
        speedBtn.addEventListener('click', () => {
            currentSpeed = modeKey;
            GM_setValue('collectorSpeed', modeKey);
            updateSpeedModeButtons();
            showStatus(`Speed mode set to: ${modeConfig.name}`, 1500);
        });
        speedModeContainer.appendChild(speedBtn);
    }
    speedSection.appendChild(speedModeContainer);


    const configSection = document.createElement('div');
    configSection.className = 'menuSection';
    const configTitle = document.createElement('div');
    configTitle.className = 'menuSectionTitle';
    configTitle.textContent = 'Page Configuration';
    configSection.appendChild(configTitle);

    const pageConfigContainer = document.createElement('div');
    pageConfigContainer.className = 'pageConfigContainer';


    Object.keys(CONFIG).forEach(type => {
        const config = CONFIG[type];
        const itemDiv = document.createElement('div');
        itemDiv.className = 'pageConfigItem';

        const label = document.createElement('span');
        label.textContent = `${config.name}s:`;

        const input = document.createElement('input');
        input.type = 'number';
        input.className = 'pageConfigInput';
        input.value = config.endPage;
        input.min = 1;

        itemDiv.appendChild(label);
        itemDiv.appendChild(input);
        pageConfigContainer.appendChild(itemDiv);
    });

    const saveConfigBtn = document.createElement('button');
    saveConfigBtn.className = 'pageConfigSaveBtn';
    saveConfigBtn.textContent = 'Save Page Numbers';
    saveConfigBtn.addEventListener('click', savePageConfig);
    pageConfigContainer.appendChild(saveConfigBtn);
    configSection.appendChild(pageConfigContainer);

    const autoSection = document.createElement('div');
    autoSection.className = 'menuSection';
    const autoTitle = document.createElement('div');
    autoTitle.className = 'menuSectionTitle';
    autoTitle.textContent = 'Auto Collect';
    autoSection.appendChild(autoTitle);


    const autoTagBtn = createAutoButton('tag');
    const autoArtistBtn = createAutoButton('artist');
    const autoCharacterBtn = createAutoButton('character');
    const autoParodyBtn = createAutoButton('parody');

    autoSection.appendChild(autoTagBtn);
    autoSection.appendChild(autoArtistBtn);
    autoSection.appendChild(autoCharacterBtn);
    autoSection.appendChild(autoParodyBtn);


    const statusArea = document.createElement('div');
    statusArea.id = 'collectorStatus';

    const progressArea = document.createElement('div');
    progressArea.id = 'collectorProgress';
    const progressBar = document.createElement('div');
    progressBar.id = 'collectorProgressBar';
    progressArea.appendChild(progressBar);


    menu.appendChild(quickSection);
    menu.appendChild(speedSection);
    menu.appendChild(configSection);
    menu.appendChild(autoSection);
    menu.appendChild(statusArea);
    menu.appendChild(progressArea);
    document.body.appendChild(menu);


    menu.style.display = menuOpen ? 'block' : 'none';


    menuBtn.addEventListener('click', function(e) {
        e.stopPropagation();
        menuOpen = !menuOpen;
        menu.style.display = menuOpen ? 'block' : 'none';
        GM_setValue('menuOpenState', menuOpen);
    });


    menu.addEventListener('click', function(e) {
        e.stopPropagation();
    });


    document.addEventListener('click', function() {
        menuOpen = false;
        menu.style.display = 'none';
        GM_setValue('menuOpenState', false);
    });


    function savePageConfig() {
        const inputs = menu.querySelectorAll('.pageConfigInput');
        let index = 0;

        Object.keys(CONFIG).forEach(type => {
            const newValue = parseInt(inputs[index].value);
            if (!isNaN(newValue) && newValue > 0) {
                CONFIG[type].endPage = newValue;
            }
            index++;
        });

        GM_setValue('collectorConfig', CONFIG);
        updateAutoButtons();
        showStatus('Page numbers saved!', 2000);
    }


    function updateSpeedModeButtons() {
        const buttons = menu.querySelectorAll('.speedModeBtn');
        buttons.forEach(btn => {
            btn.classList.remove('activeSpeedMode');
            const modeKey = Object.keys(SPEED_MODES).find(key => SPEED_MODES[key].name === btn.textContent);
            if (modeKey === currentSpeed) {
                btn.classList.add('activeSpeedMode');
            }
        });
    }


    function updateAutoButtons() {
        Object.keys(CONFIG).forEach(type => {
            const config = CONFIG[type];
            const btn = document.getElementById(`auto${config.name}Btn`);
            if (btn) {
                btn.textContent = `${config.name}s (${config.endPage}p)`;
            }
        });
    }


    GM_registerMenuCommand("Show & Copy Current Items", () => showItemsBtn.click());
    GM_registerMenuCommand("Auto Collect Tags", () => autoTagBtn.click());
    GM_registerMenuCommand("Auto Collect Artists", () => autoArtistBtn.click());
    GM_registerMenuCommand("Auto Collect Characters", () => autoCharacterBtn.click());
    GM_registerMenuCommand("Auto Collect Parodies", () => autoParodyBtn.click());

    function createAutoButton(type) {
        const config = CONFIG[type];
        const btn = document.createElement('button');
        btn.className = 'collectorBtn';
        btn.id = `auto${config.name}Btn`;
        btn.textContent = `${config.name}s (${config.endPage}p)`;
        btn.style.background = config.buttonColor;
        return btn;
    }


    function extractItems() {
        const sections = document.querySelectorAll('#tag-container section, #content .container section');
        const items = [];

        sections.forEach(section => {
            const itemLinks = section.querySelectorAll('a');
            itemLinks.forEach(link => {
                const nameSpan = link.querySelector('span.name');
                if (nameSpan) {
                    const itemName = nameSpan.textContent.trim();
                    if (itemName) {
                        items.push(itemName);
                    }
                }
            });
        });

        return items.sort((a, b) => a.localeCompare(b));
    }


    function formatItems(items) {
        return `[${items.map(item => `"${item}"`).join(', ')}]`;
    }


    showItemsBtn.addEventListener('click', function() {
        const items = extractItems();

        if (items.length === 0) {
            showStatus('No items found on this page.', 3000);
            return;
        }


        const formattedItems = formatItems(items);
        GM_setClipboard(formattedItems, 'text');


        showStatus(`${items.length} items copied to clipboard!`, 2000);
    });


    function showStatus(message, duration = 0) {
        statusArea.textContent = message;
        statusArea.style.display = 'block';

        if (duration > 0) {
            setTimeout(() => {
                statusArea.style.display = 'none';
            }, duration);
        }
    }


    async function waitForPageLoad() {
        return new Promise((resolve) => {
            const checkInterval = SPEED_MODES[currentSpeed].loadCheckInterval;
            const maxChecks = 10;
            let checks = 0;

            const interval = setInterval(() => {

                const container = document.querySelector('#tag-container, #content .container');
                if (container && container.textContent.trim().length > 0) {
                    clearInterval(interval);
                    resolve(true);
                } else if (checks >= maxChecks) {
                    clearInterval(interval);
                    resolve(false);
                }
                checks++;
            }, checkInterval);
        });
    }


    function setupAutoButton(type) {
        const config = CONFIG[type];
        const btn = document.getElementById(`auto${config.name}Btn`);

        btn.addEventListener('click', async function() {
            if (btn.textContent.startsWith('Stop')) {
                stopAutoCollection(type);
                return;
            }


            const storedData = GM_getValue(`auto${config.name}Data`);
            if (storedData && storedData.items && storedData.items.length > 0) {
                showConfirmationDialog(type);
            } else {
                startAutoCollection(type);
            }
        });
    }

    function showConfirmationDialog(type) {
        const config = CONFIG[type];
        const dialog = document.createElement('div');
        dialog.className = 'confirmationDialog';

        const text = document.createElement('p');
        text.textContent = `You already have ${config.name}s stored (${GM_getValue(`auto${config.name}Data`).items.length} items). Do you want to recollect them?`;

        const buttons = document.createElement('div');
        buttons.className = 'confirmationButtons';

        const confirmBtn = document.createElement('button');
        confirmBtn.className = 'confirmationBtn confirmBtn';
        confirmBtn.textContent = 'Yes, recollect';
        confirmBtn.addEventListener('click', () => {
            document.body.removeChild(dialog);
            startAutoCollection(type);
        });

        const cancelBtn = document.createElement('button');
        cancelBtn.className = 'confirmationBtn cancelBtn';
        cancelBtn.textContent = 'No, use stored';
        cancelBtn.addEventListener('click', () => {
            document.body.removeChild(dialog);
            useStoredData(type);
        });

        buttons.appendChild(confirmBtn);
        buttons.appendChild(cancelBtn);
        dialog.appendChild(text);
        dialog.appendChild(buttons);
        document.body.appendChild(dialog);
    }

    function useStoredData(type) {
        const config = CONFIG[type];
        const storedData = GM_getValue(`auto${config.name}Data`);

        if (storedData && storedData.items) {
            const formattedItems = formatItems(storedData.items);
            GM_setClipboard(formattedItems, 'text');

            showStatus(`Used stored ${config.name.toLowerCase()}s (${storedData.items.length} items)`);
        }
    }

    function startAutoCollection(type) {
        const config = CONFIG[type];
        GM_setValue(`auto${config.name}Running`, true);
        GM_setValue(`auto${config.name}Page`, 1);
        GM_setValue(`auto${config.name}Items`, []);

        const btn = document.getElementById(`auto${config.name}Btn`);
        btn.textContent = `Stop ${config.name}s`;
        btn.classList.add('stopBtn');

        showStatus(`Starting Auto ${config.name}s collection...`);
        progressBar.style.width = '0%';
        progressArea.style.display = 'block';


        navigateToPage(type, 1);
    }

    function stopAutoCollection(type) {
        const config = CONFIG[type];
        GM_deleteValue(`auto${config.name}Running`);
        GM_deleteValue(`auto${config.name}Page`);
        GM_deleteValue(`auto${config.name}Items`);

        const btn = document.getElementById(`auto${config.name}Btn`);
        btn.textContent = `${config.name}s (${config.endPage}p)`;
        btn.classList.remove('stopBtn');
        btn.style.background = config.buttonColor;

        showStatus(`Auto ${config.name}s stopped by user`);
        progressArea.style.display = 'none';
    }

    async function navigateToPage(type, page) {
        const config = CONFIG[type];
        const currentUrl = new URL(window.location.href);


        let expectedPath;
        if (type === 'tag') expectedPath = '/tags/';
        else if (type === 'artist') expectedPath = '/artists/';
        else if (type === 'character') expectedPath = '/characters/';
        else if (type === 'parody') expectedPath = '/parodies/';

        if (currentUrl.pathname === expectedPath && currentUrl.searchParams.get('page') === page.toString()) {

            await processPage(type);
        } else {

            showStatus(`Loading ${config.name.toLowerCase()}s page ${page}...`);
            window.location.href = `${config.startUrl.split('?')[0]}?page=${page}`;
        }
    }

    async function processPage(type) {
        const config = CONFIG[type];
        const currentPage = GM_getValue(`auto${config.name}Page`, 1);


        const pageLoaded = await waitForPageLoad();
        if (!pageLoaded) {
            showStatus(`Warning: Page ${currentPage} may not have loaded completely`);
        }

        const items = extractItems();
        const allItems = GM_getValue(`auto${config.name}Items`, []).concat(items);

        GM_setValue(`auto${config.name}Items`, allItems);


        const progress = Math.floor((currentPage / config.endPage) * 100);
        progressBar.style.width = `${progress}%`;
        showStatus(`${config.name}s page ${currentPage}/${config.endPage} processed. Found ${items.length} items (Total: ${allItems.length})`);

        if (currentPage >= config.endPage) {

            finishAutoCollection(type, allItems);
            return;
        }


        GM_setValue(`auto${config.name}Page`, currentPage + 1);
        setTimeout(() => {
            navigateToPage(type, currentPage + 1);
        }, SPEED_MODES[currentSpeed].delay);
    }

    function finishAutoCollection(type, allItems) {
        const config = CONFIG[type];
        const formattedItems = formatItems(allItems);
        GM_setClipboard(formattedItems, 'text');


        GM_setValue(`auto${config.name}Data`, {
            items: allItems,
            collectedAt: new Date().toISOString()
        });


        GM_deleteValue(`auto${config.name}Running`);
        GM_deleteValue(`auto${config.name}Page`);
        GM_deleteValue(`auto${config.name}Items`);


        const btn = document.getElementById(`auto${config.name}Btn`);
        btn.textContent = `${config.name}s (${config.endPage}p)`;
        btn.classList.remove('stopBtn');
        btn.style.background = config.buttonColor;

        showStatus(`All done! Copied ${allItems.length} ${config.name.toLowerCase()}s to clipboard.`);
        progressBar.style.width = '100%';

        setTimeout(() => {
            progressArea.style.display = 'none';
        }, 3000);
    }


    setupAutoButton('tag');
    setupAutoButton('artist');
    setupAutoButton('character');
    setupAutoButton('parody');


    Object.keys(CONFIG).forEach(type => {
        const config = CONFIG[type];
        if (GM_getValue(`auto${config.name}Running`, false)) {
            const currentPage = GM_getValue(`auto${config.name}Page`, 1);
            const currentUrl = new URL(window.location.href);


            let expectedPath;
            if (type === 'tag') expectedPath = '/tags/';
            else if (type === 'artist') expectedPath = '/artists/';
            else if (type === 'character') expectedPath = '/characters/';
            else if (type === 'parody') expectedPath = '/parodies/';

            if (currentUrl.pathname === expectedPath) {

                const btn = document.getElementById(`auto${config.name}Btn`);
                btn.textContent = `Stop ${config.name}s`;
                btn.classList.add('stopBtn');
                progressArea.style.display = 'block';


                setTimeout(() => {
                    processPage(type);
                }, 1000);
            }
        }
    });
})();