NHentai Infinite Load

Seamlessly load more comics as you scroll on nhentai.net, with support for blacklisted content removal. Enhanced performance and reliability.

// ==UserScript==
// @name         NHentai Infinite Load
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Seamlessly load more comics as you scroll on nhentai.net, with support for blacklisted content removal. Enhanced performance and reliability.
// @author       Hentiedup (original), [Snow2122] (adaptation)
// @license      MIT
// @match        https://nhentai.net/*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @icon         https://i.imgur.com/1lihxY2.png
// @noframes
// ==/UserScript==

(() => {
    // Configuration constants
    const CONFIG = {
        SCROLL_THRESHOLD: 100, // Pixels before bottom to trigger load
        LOADER_HEIGHT: "355px",
        PAGE_PARAM: "page=",
        MAX_FETCH_ATTEMPTS: 5,
        DEBOUNCE_WAIT: 100, // ms for scroll debounce
        RETRY_BASE_DELAY: 1000 // ms for AJAX retry delay
    };

    // User-configurable settings
    const settings = {
        infinite_load: GM_getValue("infinite_load", true),
        remove_native_blacklisted: GM_getValue("remove_native_blacklisted", true)
    };

    // Initialize blacklist
    const nativeBlacklist = [];

    // Global variable to track loading state
    let infinite_load_isLoadingNextPage = false;

    // Cache frequently used selectors
    const $containers = $(".container.index-container, #favcontainer.container, #recent-favorites-container, #related-container");

    // Log for debugging
    console.log("NHI: Script initialized", { infinite_load: settings.infinite_load, remove_native_blacklisted: settings.remove_native_blacklisted });

    // Initialize settings UI
    initSettingsUI();

    // Apply stylesheets
    AddInfiniteLoadStylesheets();

    // Handle blacklisted tags and infinite load on page load
    if ($containers.length) {
        HandleBlacklistedRemoval();
        InfiniteLoadHandling();
    } else {
        console.warn("NHI: No matching containers found, script may not work as expected");
    }

    // FUNCTIONS

    // Settings UI
    function initSettingsUI() {
        GM_registerMenuCommand("Toggle Infinite Load", () => {
            const newValue = !settings.infinite_load;
            GM_setValue("infinite_load", newValue);
            settings.infinite_load = newValue;
            alert(`Infinite Load is now ${newValue ? "enabled" : "disabled"}. Refresh to apply.`);
        });
        GM_registerMenuCommand("Toggle Blacklist Removal", () => {
            const newValue = !settings.remove_native_blacklisted;
            GM_setValue("remove_native_blacklisted", newValue);
            settings.remove_native_blacklisted = newValue;
            alert(`Blacklist Removal is now ${newValue ? "enabled" : "disabled"}. Refresh to apply.`);
        });
    }

    // Blacklist related functions
    function HandleBlacklistedRemoval() {
        if (settings.remove_native_blacklisted) {
            $(".gallery.blacklisted").remove();
            console.log("NHI: Removed blacklisted galleries on page load");

            if (settings.infinite_load) {
                const $scriptWithTags = $("script:not([src])").filter((i, el) => $(el).html().includes("blacklisted_tags"));
                const tagsMatch = $scriptWithTags.html()?.match(/blacklisted_tags:\s*\[([^\]]*)\]/);

                if (tagsMatch?.[1]) {
                    try {
                        nativeBlacklist.push(...JSON.parse(`[${tagsMatch[1]}]`));
                        console.log("NHI: Parsed blacklisted tags", nativeBlacklist);
                    } catch {
                        console.warn("NHI: Failed to parse blacklisted tags, disabling blacklist filtering for infinite load");
                        settings.remove_native_blacklisted = false;
                    }
                } else {
                    console.warn("NHI: Blacklisted tags not found, disabling blacklist filtering for infinite load");
                    settings.remove_native_blacklisted = false;
                }
            }
        }
    }

    // Infinite load related functions
    function AddInfiniteLoadStylesheets() {
        if (settings.infinite_load) {
            GM_addStyle(`
                #NHI_loader_icon {
                    height: ${CONFIG.LOADER_HEIGHT};
                    line-height: ${CONFIG.LOADER_HEIGHT};
                    display: none;
                }
                #NHI_loader_icon.active {
                    display: block;
                }
                #NHI_loader_icon > div {
                    display: inline-flex;
                }
                .loader {
                    color: #ed2553;
                    font-size: 10px;
                    width: 1em;
                    height: 1em;
                    border-radius: 50%;
                    position: relative;
                    text-indent: -9999em;
                    animation: mulShdSpin 1.3s infinite linear;
                    transform: translateZ(0);
                }

                @keyframes mulShdSpin {
                    0%, 100% {
                        box-shadow: 0 -3em 0 0.2em,
                        2em -2em 0 0em, 3em 0 0 -1em,
                        2em 2em 0 -1em, 0 3em 0 -1em,
                        -2em 2em 0 -1em, -3em 0 0 -1em,
                        -2em -2em 0 0;
                    }
                    12.5% {
                        box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,
                        3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,
                        -2em 2em 0 -1em, -3em 0 0 -1em,
                        -2em -2em 0 -1em;
                    }
                    25% {
                        box-shadow: 0 -3em 0 -0.5em,
                        2em -2em 0 0, 3em 0 0 0.2em,
                        2em 2em 0 0, 0 3em 0 -1em,
                        -2em 2em 0 -1em, -3em 0 0 -1em,
                        -2em -2em 0 -1em;
                    }
                    37.5% {
                        box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,
                        3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,
                        -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;
                    }
                    50% {
                        box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,
                        3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,
                        -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;
                    }
                    62.5% {
                        box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,
                        3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,
                        -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;
                    }
                    75% {
                        box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,
                        3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,
                        -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;
                    }
                    87.5% {
                        box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,
                        3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,
                        -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;
                    }
                }
            `);
            console.log("NHI: Applied infinite load stylesheets");
        }
    }

    // Utility functions
    function debounce(func, wait) {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    function InfiniteLoadHandling() {
        if (settings.infinite_load) {
            const $paginator = $(".pagination");
            if (!$paginator.length || window.location.pathname === "/favorites/") {
                console.warn("NHI: Pagination not found or on favorites page, disabling infinite load");
                return;
            }

            const $lastPageLink = $paginator.find(".last");
            if (!$lastPageLink.length) {
                console.warn("NHI: Pagination last page link not found, disabling infinite load");
                return;
            }

            const lastPageNum = Number.parseInt($lastPageLink.attr("href")?.split(CONFIG.PAGE_PARAM)[1]);
            if (isNaN(lastPageNum)) {
                console.warn("NHI: Invalid last page number, disabling infinite load");
                return;
            }

            const queryWithNoPage = window.location.search.replace(/[\?\&]page=\d+/, "").replace(/^\&/, "?");
            const finalUrlWithoutPageNum = `${window.location.pathname + queryWithNoPage + (queryWithNoPage.length ? "&" : "?")}page=`;

            console.log("NHI: Infinite load initialized", { lastPageNum, finalUrlWithoutPageNum });

            // Scroll-based infinite load
            $(window).scroll(debounce(() => {
                if ($(window).scrollTop() + (window.visualViewport?.height || $(window).height()) >= $(document).height() - CONFIG.SCROLL_THRESHOLD) {
                    const $currentPage = $(".pagination > .page.current:last");
                    if (!$currentPage.length) {
                        console.warn("NHI: Current page not found, skipping load");
                        return;
                    }
                    const loadingPageNum = Number.parseInt($currentPage.attr("href").split(CONFIG.PAGE_PARAM)[1]) + 1;
                    TryLoadInNextPageComics(loadingPageNum, lastPageNum, finalUrlWithoutPageNum);
                }
            }, CONFIG.DEBOUNCE_WAIT));

            // One-time check for small pages
            const doc = document.documentElement;
            if (doc.scrollHeight <= doc.clientHeight) {
                const $currentPage = $(".pagination > .page.current:last");
                if ($currentPage.length) {
                    const loadingPageNum = Number.parseInt($currentPage.attr("href").split(CONFIG.PAGE_PARAM)[1]) + 1;
                    if (loadingPageNum <= lastPageNum) {
                        TryLoadInNextPageComics(loadingPageNum, lastPageNum, finalUrlWithoutPageNum);
                    }
                }
            }
        }
    }

    function TryLoadInNextPageComics(pageNumToLoad, lastPageNum, fetchUrlWithoutPageNum, retryNum = 0, maxFetchAttempts = CONFIG.MAX_FETCH_ATTEMPTS) {
        if (retryNum === 0 && infinite_load_isLoadingNextPage) {
            console.log("NHI: Already loading next page, skipping");
            return;
        }
        if (pageNumToLoad > lastPageNum) {
            console.log("NHI: Reached last page, stopping");
            return;
        }

        infinite_load_isLoadingNextPage = true;

        const $indexContainer = $(".index-container:not(.advertisement, .index-popular)").first();
        if (!$indexContainer.length) {
            console.warn("NHI: Index container not found, aborting load");
            infinite_load_isLoadingNextPage = false;
            return;
        }

        let $loader = $("#NHI_loader_icon");
        if (!$loader.length) {
            $loader = $('<div id="NHI_loader_icon" class="gallery"><div><span class="loader"></span></div></div>');
            $indexContainer.append($loader);
        }
        $loader.addClass("active");

        console.log(`NHI: Loading page ${pageNumToLoad}`);

        $.get({
            url: fetchUrlWithoutPageNum + pageNumToLoad,
            dataType: "html"
        }, (data) => {
            const $fragment = $(document.createDocumentFragment());
            const $existingCovers = $indexContainer.find(".cover").map((_, el) => $(el).attr("href")).get();

            $(data).find("div.gallery").each((i, el) => {
                if (settings.remove_native_blacklisted) {
                    if ($(el).hasClass("blacklisted")) return;
                    const tags = $(el).attr("data-tags")?.trim().split(" ") || [];
                    if (nativeBlacklist.some(nblTag => tags.includes(String(nblTag)))) return;
                }

                const href = $(el).find(".cover").attr("href");
                if ($existingCovers.includes(href)) return;

                $(el).find("img").attr("src", $(el).find("img").attr("data-src"));
                $fragment.append(el);
            });

            $indexContainer.append($fragment);
            console.log(`NHI: Loaded ${$(data).find("div.gallery").length} galleries for page ${pageNumToLoad}`);

            const $paginatorItem = $(`.pagination > .page[href$='${CONFIG.PAGE_PARAM}${pageNumToLoad}']`);
            if ($paginatorItem.length) {
                $paginatorItem.addClass("current");
            } else {
                $(".pagination > .next").before(`<a href="${fetchUrlWithoutPageNum}${pageNumToLoad}" class="page current">${pageNumToLoad}</a>`);
            }

            $loader.removeClass("active");
            infinite_load_isLoadingNextPage = false;

        }).fail((jqXHR, textStatus, errorThrown) => {
            if (retryNum < maxFetchAttempts) {
                const delay = jqXHR.status === 429 ? Math.pow(2, retryNum) * CONFIG.RETRY_BASE_DELAY : CONFIG.RETRY_BASE_DELAY;
                console.log(`NHI: Failed loading page ${pageNumToLoad} - ${textStatus} | ${errorThrown} - retrying in ${delay}ms (retry ${retryNum + 1})`);
                setTimeout(() => {
                    TryLoadInNextPageComics(pageNumToLoad, lastPageNum, fetchUrlWithoutPageNum, retryNum + 1, maxFetchAttempts);
                }, delay);
            } else {
                $loader.removeClass("active");
                console.log(`NHI: Failed loading page ${pageNumToLoad} - ${textStatus} | ${errorThrown} - Giving up after ${retryNum} retries`);
                infinite_load_isLoadingNextPage = false;
            }
        });
    }
})();