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