您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Dynamically loads more comics as you scroll, blocks extra ads, pop-up video ads, and unwanted new tab redirects.
// ==UserScript== // @name NHentai - Infinite Scroll & Enhanced Ad Blocker // @namespace http://tampermonkey.net/ // @version 2.2 // @description Dynamically loads more comics as you scroll, blocks extra ads, pop-up video ads, and unwanted new tab redirects. // @author Hentiedup (original), [Snow2122] (adaptation) // @license MIT // @match https://nhentai.net/* // @match *://*/* // Added broader match for universal ad blocking features // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js // @grant GM_getValue // @grant GM_addStyle // @noframes // ==/UserScript== (function() { 'use strict'; // --- NHENTAI SPECIFIC SETTINGS --- // These features are on by default. You can manually set them to false in your // UserScript manager (e.g., Tampermonkey) if you need to disable them. const infinite_load = GM_getValue("infinite_load", true); const block_extra_ads_nhentai_specific = GM_getValue("block_extra_ads", true); // Renamed to avoid conflict // --- GLOBAL VARIABLES --- let infinite_load_isLoadingNextPage = false; // --- AD BLOCKER CONFIGURATION --- // A list of common ad-related CSS selectors to hide or remove. // This list is based on common patterns found in advertising elements. const adSelectors = [ // Generic ad containers '.ad', '.ads', '.advert', '.ad-container', '.banner-ad', '.google-ad', '.top-ad', '.bottom-ad', '.sidebar-ad', '.popup-ad', // Common element IDs '#ad', '#ads', '#advertisement', '#banner', '#google_ads_iframe', // Elements commonly used by ad networks or for injecting ads 'iframe[src*="adserver"]', 'iframe[src*="doubleclick.net"]', 'iframe[src*="googlesyndication.com"]', 'iframe[src*="adnxs.com"]', 'iframe[src*="taboola.com"]', 'iframe[src*="outbrain.com"]', 'iframe[src*="mgid.com"]', 'iframe[src*="monetize"]', 'div[id*="ad_"]', 'div[class*="ad_"]', 'div[id*="banner"]', 'div[class*="banner"]', 'div[id*="advert"]', 'div[class*="advert"]', 'div[data-google-query-id]', // Google AdSense specific // Elements often associated with "suggested content" or native ads '.native-ad', '.recommended-content', '.sponsored-content', // Pop-up related '.modal-backdrop', '.ad-popup-overlay', '.no-scroll', 'body.adblock-active', // Some sites add this class when detecting adblock 'div[style*="z-index: 99999"]', // Common for pop-ups 'div[style*="position: fixed"]', // Common for sticky ads/pop-ups // --- Selectors specifically for video ads --- 'video', // Directly target video tags 'div[class*="video-ad"]', 'div[id*="video-ad"]', // Common video ad containers 'div[class*="video-overlay"]', 'div[id*="video-overlay"]', // Overlays often used for video pop-ups 'div[class*="video-player-ad"]', 'div[id*="video-player-ad"]', // More specific video player ad identifiers 'iframe[src*="videoplaza.tv"]', // Known video ad server 'iframe[src*="adform.net"]', // Known video ad server ]; // CSS rules to hide elements immediately. This is injected via GM_addStyle. // Using !important to try and override inline styles. const hideCss = adSelectors.join(', ') + ' { display: none !important; visibility: hidden !important; }'; // Anti-adblock detection circumvention attempts. // These are common variables or functions websites might check. const antiAdblockDefeaters = { // Common global variables checked by adblock detection scripts 'AdBlock': false, 'adblock': false, 'blockAdblock': false, '_AdBlock_': false, 'canRunAds': true, // Some scripts check this // Overriding common detection functions/properties 'checkAdblock': () => false, 'isAdblockActive': false, }; // Blacklist for unwanted pop-up/redirect URLs const popupRedirectBlacklist = [ 'doubleclick.net', 'googlesyndication.com', 'adserver', 'popads.net', 'onclickads.net', 'admaven.com', 'redirect.', 'trafficjunky.net', 'exoclick.com', 'propellerads.com', 'adsterra.com', 'mgid.com', 'popunder.', 'popcash.net', 'cpm-gate.com', 'adclick', 'ad-track' ]; // --- NHENTAI SPECIFIC FUNCTIONS --- /** * Adds CSS to hide specific ad elements in the navigation bar for NHentai. */ function addExtraAdBlockingStylesheets() { if (block_extra_ads_nhentai_specific) { GM_addStyle(` /* Hides the 'Porn Z' and similar ad links in the header menu */ nav ul.menu.left > li:has(a[href^="//tsyndicate.com"]) { display: none; } `); } } /** * Adds CSS for the loading spinner animation used by the infinite scroll. */ function addInfiniteLoadStylesheets() { if (infinite_load) { GM_addStyle(` #NHI_loader_icon { height: 355px; line-height: 355px; width: 100%; text-align: center; } #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; } } `); } } /** * Applies the necessary CSS styles for both NHentai and universal ad blocking. */ function applyStylesheets() { addExtraAdBlockingStylesheets(); // NHentai specific ads addInfiniteLoadStylesheets(); // NHentai infinite scroll loader injectHideCss(); // Universal ad blocker CSS } /** * Initializes the infinite scroll functionality for NHentai. * Sets up scroll listeners and determines the URLs for fetching subsequent pages. */ function infiniteLoadHandling() { if (!infinite_load) return; const paginator = $(".pagination"); // Only run if a paginator exists on the page if (paginator?.length && window.location.pathname !== "/favorites/") { const lastPageNum = Number.parseInt(paginator.find(".last").attr("href")?.split("page=")[1] || '1'); // Build the base URL for fetching pages, removing any existing page parameter const queryWithNoPage = window.location.search.replace(/[\?\&]page=\d+/, "").replace(/^\&/, "?"); const finalUrlWithoutPageNum = `${window.location.pathname + queryWithNoPage + (queryWithNoPage.length ? "&" : "?")}page=`; // Add scroll event listener to trigger loading new pages $(window).scroll(() => { if ($(window).scrollTop() + (window.visualViewport?.height || $(window).height()) >= $(document).height() - 500) { const loadingPageNum = Number.parseInt($(".pagination > .page.current:last").attr("href")?.split("page=")[1] || '1') + 1; tryLoadInNextPageComics(loadingPageNum, lastPageNum, finalUrlWithoutPageNum); } }); // If the initial page content is not tall enough to have a scrollbar, // keep loading pages until it does or until the last page is reached. const autoLoadWhileScrollNotAvailableInterval = setInterval(() => { const loadingPageNum = Number.parseInt($(".pagination > .page.current:last").attr("href")?.split("page=")[1] || '1') + 1; if (loadingPageNum > lastPageNum) { clearInterval(autoLoadWhileScrollNotAvailableInterval); return; } const doc = document.documentElement; if (doc.scrollHeight <= doc.clientHeight) { tryLoadInNextPageComics(loadingPageNum, lastPageNum, finalUrlWithoutPageNum); } else { clearInterval(autoLoadWhileScrollNotAvailableInterval); } }, 200); } } /** * Fetches and appends comics from the next page. * @param {number} pageNumToLoad The page number to fetch. * @param {number} lastPageNum The last available page number. * @param {string} fetchUrlWithoutPageNum The base URL for fetching. * @param {number} retryNum Current retry attempt number. * @param {number} maxFetchAttempts Maximum number of retries. */ function tryLoadInNextPageComics(pageNumToLoad, lastPageNum, fetchUrlWithoutPageNum, retryNum = 0, maxFetchAttempts = 5) { if (retryNum === 0 && infinite_load_isLoadingNextPage) return; if (pageNumToLoad > lastPageNum) return; infinite_load_isLoadingNextPage = true; // Add the loading spinner to the UI if ($("#NHI_loader_icon").length === 0) { $(".container.index-container:not(.advertisement, .index-popular)").first().append('<div id="NHI_loader_icon" class="gallery"><div><span class="loader"></span></div></div>'); } $.get({ url: fetchUrlWithoutPageNum + pageNumToLoad, dataType: "html" }, (data) => { const galleryContainer = $(".container.index-container:not(.advertisement, .index-popular)").first(); // Process each comic gallery found on the fetched page $(data).find("div.gallery").each((i, el) => { const $el = $(el); // If comic is already on the page, skip it if ($(`.cover[href='${$el.find(".cover").attr("href")}']`, galleryContainer).length > 0) return; // The thumbnail lazy-loads, so we must set the 'src' from 'data-src' $el.find("img").attr("src", $el.find("img").attr("data-src")); galleryContainer.append($el); }); // Update the paginator to show the newly loaded page as "current" const paginatorItem = $(`.pagination > .page[href$='page=${pageNumToLoad}']`); if (paginatorItem?.length) { paginatorItem.addClass("current"); } else { $(".pagination > .next").before(`<a href="${fetchUrlWithoutPageNum}${pageNumToLoad}" class="page current">${pageNumToLoad}</a>`); } $("#NHI_loader_icon").remove(); infinite_load_isLoadingNextPage = false; }).fail((jqXHR, textStatus, errorThrown) => { if (retryNum < maxFetchAttempts) { console.log(`NHI: Infinite load - Failed loading page ${pageNumToLoad} - Retrying... (${retryNum + 1})`); setTimeout(() => { tryLoadInNextPageComics(pageNumToLoad, lastPageNum, fetchUrlWithoutPageNum, retryNum + 1, maxFetchAttempts); }, 1000); // Wait 1 second before retrying } else { $("#NHI_loader_icon").remove(); console.log(`NHI: Infinite load - Failed loading page ${pageNumToLoad} - Giving up after ${maxFetchAttempts} retries.`); infinite_load_isLoadingNextPage = false; } }); } // --- UNIVERSAL AD BLOCKER FUNCTIONS --- /** * Injects CSS rules into the document head to hide ad elements using GM_addStyle. */ function injectHideCss() { GM_addStyle(hideCss); console.log('[Universal Ad Blocker] Injected CSS to hide ads.'); } /** * Attempts to apply anti-adblock detection circumvention. * This tries to make the browser appear as if no ad blocker is present. */ function circumventAntiAdblock() { for (const prop in antiAdblockDefeaters) { if (Object.prototype.hasOwnProperty.call(antiAdblockDefeaters, prop)) { try { Object.defineProperty(window, prop, { value: antiAdblockDefeaters[prop], writable: false, configurable: true }); console.log(`[Universal Ad Blocker] Set window.${prop} to ${antiAdblockDefeaters[prop]}`); } catch (e) { console.warn(`[Universal Ad Blocker] Failed to define window.${prop}:`, e); window[prop] = antiAdblockDefeaters[prop]; } } } const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth'); const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight'); if (originalOffsetWidth) { Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { get: function() { if (this.id && this.id.includes('ad') || this.className && this.className.includes('ad')) { return 100; } return originalOffsetWidth.get.apply(this); }, configurable: true }); } if (originalOffsetHeight) { Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { get: function() { if (this.id && this.id.includes('ad') || this.className && this.className.includes('ad')) { return 100; } return originalOffsetHeight.get.apply(this); }, configurable: true }); } console.log('[Universal Ad Blocker] Attempted to circumvent anti-adblock size checks.'); } /** * Overrides window.open to block unwanted pop-up and redirect tabs. */ function blockPopunders() { const originalWindowOpen = window.open; window.open = function(url, name, features) { const isBlocked = popupRedirectBlacklist.some(pattern => url && url.includes(pattern)); if (isBlocked) { console.warn(`[Universal Ad Blocker] Blocked pop-under/redirect attempt to: ${url}`); return null; } return originalWindowOpen.apply(this, arguments); }; console.log('[Universal Ad Blocker] window.open override active for pop-under blocking.'); } /** * Removes or hides elements matching ad selectors. * This function can be called repeatedly, e.g., on DOM mutations. * @param {HTMLElement | Document} container - The element or document to search within. */ function blockAds(container = document) { let blockedCount = 0; adSelectors.forEach(selector => { try { const elements = container.querySelectorAll(selector); elements.forEach(el => { if (el.style.display !== 'none' && el.style.visibility !== 'hidden') { if (el.tagName === 'IFRAME') { el.remove(); console.log(`[Universal Ad Blocker] Removed iframe: ${selector}`); } else if (el.tagName === 'VIDEO') { if (!el.paused) el.pause(); el.src = ''; while (el.firstChild) { el.removeChild(el.firstChild); } el.remove(); console.log(`[Universal Ad Blocker] Removed video ad: ${selector}`); } else { el.style.setProperty('display', 'none', 'important'); el.style.setProperty('visibility', 'hidden', 'important'); console.log(`[Universal Ad Blocker] Hidden element: ${selector}`); } blockedCount++; } }); } catch (e) { console.error(`[Universal Ad Blocker] Error querying selector ${selector}:`, e); } }); if (blockedCount > 0) { console.log(`[Universal Ad Blocker] Blocked ${blockedCount} elements.`); } } /** * Initializes the MutationObserver to watch for DOM changes. * When new nodes are added, it re-applies ad-blocking logic. */ function setupMutationObserver() { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { // Node.ELEMENT_NODE blockAds(node); } }); } }); }); // Start observing the entire document body for child list changes and subtree changes observer.observe(document.body, { childList: true, subtree: true }); console.log('[Universal Ad Blocker] MutationObserver set up.'); } // --- SCRIPT EXECUTION --- // 1. Run universal ad blocker's anti-adblock circumvention and pop-under blocking // attempts immediately at document-start, before most scripts have a chance to run their checks. circumventAntiAdblock(); blockPopunders(); // 2. Apply all CSS styles (NHentai specific and universal ad blocker) applyStylesheets(); // 3. Perform an initial ad blocking pass on the existing document. // This catches elements present in the initial HTML. blockAds(); // 4. Set up a MutationObserver to catch dynamically loaded ads or elements // that change after the initial page load. This ensures continuous blocking. // Wait for the document body to be available before setting up the observer. if (document.body) { setupMutationObserver(); } else { document.addEventListener('DOMContentLoaded', setupMutationObserver); } // 5. Initialize the NHentai infinite scroll functionality if on a comic list page. if ($(".container.index-container, #favcontainer.container, #recent-favorites-container, #related-container").length !== 0) { infiniteLoadHandling(); } console.log('[NHentai - Infinite Scroll & Enhanced Ad Blocker] UserScript initialized.'); })();