Nhentai Auto Collector

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

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