Skip to content

Instantly share code, notes, and snippets.

@Gishi1
Last active February 5, 2026 23:57
Show Gist options
  • Select an option

  • Save Gishi1/fc7c69aa994b3eebfe01e2d2b489ecd2 to your computer and use it in GitHub Desktop.

Select an option

Save Gishi1/fc7c69aa994b3eebfe01e2d2b489ecd2 to your computer and use it in GitHub Desktop.
WebNovel genre and paywall filter
// ==UserScript==
// @name WebNovel genre and paywall filter
// @namespace https://www.webnovel.com/
// @version 1.28
// @description Genre filter and paywall filter for WebNovel.com!
// @author idMysteries and Gishi
// @match https://www.webnovel.com/stories/*
// @match https://www.webnovel.com/tags/*
// @match https://www.webnovel.com/category/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=webnovel.com
// @license Unlicense
// @grant none
// ==/UserScript==
const PREFER_LIST = ['# ROMANCE', '# REINCARNATION', '# HAREM', '# SMUT', '# ISEKAI', '# R18', '# ACTION'];
const HIDE_LIST = ['# TRAGEDY', '# YAOI', '# NO-HAREM', '# BL'];
const DESCRIPTION_BLACKLIST = [
// Add words/phrases to hide novels containing these in description
// Example: 'cultivation', 'male protagonist only', etc.
];
const BLOCK_LIST = new Set([
'/book/the-dark-avatar_19831406406525805',
'/book/gamer-chat-group_19538419206720505',
'/book/solo-leveling-avatarr_20293566506098405',
'/book/attack-on-titan-mutant-system_19306939105958505',
'/book/dxd-meta-essence-gacha_27252217606169605',
'/book/naruto-beast-system_24893181105165405',
'/book/dxd-gilgamesh-with-solo-leveling-system_24419695505300205',
'/book/cyberpunk-edgerunners-hacker-system_24540721606161705',
'/book/chainsaw-man-system_24775381505806105',
'/book/marvel-mutant-beast-system_24957438305393705',
'/book/fate-villain-system_25206723206456305',
'/book/reborn-in-bleach-with-op-wishes_20989746706785305',
'/book/dxd-devil-sister-testament-hybrid_21128235906520105',
'/book/reborn-in-genshin-impact-with-system_21222837806880305',
'/book/solo-leveling-avatarrrrr_21223243805637705',
'/book/marvel-strongest-gamer_21525335306778905',
'/book/bleach-hollow-gamer_22269001605214405',
'/book/spider-verse-mutant-gamer_22337813206000805',
'/book/reborn-in-black-clover-with-op-system_23089787506156405',
'/book/naruto-dark-gamer_23521706406333205',
'/book/marvel-kryptonian-gamer_23573367306660105',
'/book/spy-x-family-system_23573658905399105',
'/book/netori-system_18189386705628305',
'/book/reborn-in-agk-with-system_19589246505698605',
'/book/marvel-super-god_19885230605644805',
'/book/chainsawman-fanfic-%5Bnew-version-is-out-darkgamerz%5D_20055982105930405',
'/book/mha-villain-gamer-system_20233449005308405',
'/book/lust-tamer-system_19969301806277005',
'/book/danmachi-hentai-system_25207275305196605',
'/book/dxd-meta-gacha-essencesssss-%5Bnew-fic-up%5D_25224104606505005',
'/book/kanokari-romcom-system_25224195905246205',
'/book/mha-villain-system_25405182406105505',
'/book/black-lagoon-mob-system_25498563906341005',
'/book/fairy-tail-dragon-shinigami_25920887106625905',
'/book/marvel-reborn-as-carnage_26103131205040205',
'/book/hentai-multiverse-system_26165372705251605',
'/book/pokemon-the-legend_26180589205305905',
'/book/bleach-gamer-system_26224342806689405',
'/book/genshin-impact-archon-gamer_26241045905504105',
'/book/dc-villain-gacha-essences_26355810906093805',
'/book/attack-on-titan-mutant-systemmm_26431338206335705',
'/book/marvel-anime-system_14381039706244105',
'/book/fate-hentai-system_25240165405284305',
'/book/marvel-lust-mage-%5Bhp-x-marvel%5D_27251688105919005',
'/book/in-black-clover-with-a-system_18327452705236505',
'/book/multiversal-harem-shop_26353605106088705',
'/book/multiverse-harem-shop_26353605106088705',
'/book/solo-leveling-avatar-%5Boriginal%5D_20293566506098405',
'/book/naruto-rape-system_27625512400450005',
'/book/marvel-demon-gamer_14615216705951205',
'/book/fairy-tail-dragon-god_15687353206759005',
'/book/dragon-ga-kill_16024345805688005',
'/book/naruto-resurrection_14622746106049205',
'/book/dragon-ga-kill-%5Boriginal%5D_13069022206601805',
'/book/reborn-as-op-gojo-in-jjk-with-bleach-powers_27736027100920905',
'/book/reborn-as-op-gojo-in-jjk_27736027100920905',
'/book/marvel-sex-summon-system_27796501508279105',
'/book/marvel-rivals-mutant-gamer-%5Br-18%5D_31658190308824605',
'/book/dc-i-am-batman-with-gacha-system_27979466000147405',
'/book/game-of-thrones-dragon-overlordd_17613176106920005'
]);
// Store for processed elements
const processedElements = new WeakSet();
// Helper functions
function shouldRemoveNovel(novelElement) {
if (novelElement.textContent.includes('ghostybones')) {
return true;
}
// Check description for blacklisted words
if (DESCRIPTION_BLACKLIST.length > 0) {
// Find description elements (different selectors for different page types)
const descriptionSelectors = [
'p.fw400', // stories page
'p.lh20', // stories page alt
'a._txt', // tags page
'.wn-description', // our custom class
'.wn-stories-description' // our custom class
];
for (const selector of descriptionSelectors) {
const descElement = novelElement.querySelector(selector);
if (descElement) {
const descText = descElement.textContent.toLowerCase();
for (const blacklistedWord of DESCRIPTION_BLACKLIST) {
if (descText.includes(blacklistedWord.toLowerCase())) {
return true;
}
}
break; // Found description, no need to check other selectors
}
}
}
const links = novelElement.querySelectorAll('a');
for (const link of links) {
const linkText = link.textContent.trim();
const href = link.getAttribute('href');
// Check both formats: "# TAG" and "TAG" (tags page uses no prefix)
for (const hiddenTag of HIDE_LIST) {
const tagWithoutPrefix = hiddenTag.replace('# ', '');
if (linkText === hiddenTag || linkText === tagWithoutPrefix) {
return true;
}
}
if (href && BLOCK_LIST.has(href)) {
return true;
}
}
return false;
}
function calculatePreferenceScore(novelElement) {
let score = 0;
const links = novelElement.querySelectorAll('a');
for (const link of links) {
const linkText = link.textContent;
for (const preferredTag of PREFER_LIST) {
const tagWithoutPrefix = preferredTag.replace('# ', '');
// Check both formats: "# TAG" and "TAG" (tags page uses no prefix)
if (linkText.includes(preferredTag) || linkText === tagWithoutPrefix) {
score = Math.min(score + 1, 5);
break;
}
}
if (score >= 5) break;
}
return score;
}
function createTextContentWrapper(detElement) {
if (!detElement) return null;
// Check if wrapper already exists
let wrapper = detElement.querySelector('.text-content-wrapper');
if (wrapper) return wrapper;
// Get all child nodes
const children = Array.from(detElement.children);
// Create new wrapper
wrapper = document.createElement('div');
wrapper.className = 'text-content-wrapper';
// Find the first image to know where to split content
let foundImage = false;
for (const child of children) {
if (child.tagName === 'IMG' || child.querySelector('img')) {
foundImage = true;
// Leave image outside wrapper (it's already positioned by CSS)
continue;
}
// Move all non-image content to wrapper
if (foundImage) {
wrapper.appendChild(child.cloneNode(true));
child.remove();
}
}
// Add wrapper back to det element if we have content
if (wrapper.children.length > 0) {
detElement.appendChild(wrapper);
return wrapper;
}
return null;
}
// Fix tags page layout - strip site classes and apply our own
function fixTagsPageLayout(novelElement) {
// Check if this is a tags page card (li.g_col._6)
if (!novelElement.classList.contains('_6')) return;
// Mark as processed to avoid re-processing
if (novelElement.classList.contains('wn-tags-fixed')) return;
novelElement.classList.add('wn-tags-fixed');
const bookItem = novelElement.querySelector('.g_book_item');
if (!bookItem) return;
// Strip all classes from book item and add our own
bookItem.className = 'wn-book-item';
// Fix the link containing image and title
const link = bookItem.querySelector('a');
if (link) {
link.className = 'wn-book-link';
}
// Fix the image container - strip ALL classes
const imgContainer = bookItem.querySelector('i');
if (imgContainer) {
imgContainer.className = 'wn-img-container';
// Style the image
const img = imgContainer.querySelector('img');
if (img) {
img.className = 'wn-img';
}
}
// Fix title
const title = link?.querySelector('h3');
if (title) {
title.className = 'wn-title';
}
// Fix tags
const tags = bookItem.querySelector('p.g_tags, p.c_s');
if (tags) {
tags.className = 'wn-tags';
}
// Fix rating
const rating = bookItem.querySelector('p.g_star_num');
if (rating) {
rating.className = 'wn-rating';
}
// Fix description
const desc = bookItem.querySelector('a._txt, a.oh');
if (desc && desc !== link) {
desc.className = 'wn-description';
}
}
// Fix stories page layout - transform to card-based layout like tags page
function fixStoriesPageLayout(novelElement) {
// Check if this is a stories page card (li.fl with w50%)
if (!novelElement.classList.contains('fl')) return;
if (!window.location.pathname.includes('/stories/')) return;
// Mark as processed to avoid re-processing
if (novelElement.classList.contains('wn-stories-fixed')) return;
novelElement.classList.add('wn-stories-fixed');
// Add our custom card class
novelElement.classList.add('wn-stories-card');
// Find the inner content wrapper (div.pr with fixed height)
const contentWrapper = novelElement.querySelector('div.pr');
if (contentWrapper) {
// Remove inline styles that constrain the layout
contentWrapper.removeAttribute('style');
contentWrapper.className = 'wn-stories-content';
}
// Fix the image container (span.pa)
const imgSpan = novelElement.querySelector('span.pa, span.l0');
if (imgSpan) {
imgSpan.removeAttribute('style');
imgSpan.className = 'wn-stories-img-container';
// Fix the image link
const imgLink = imgSpan.querySelector('a.g_thumb');
if (imgLink) {
imgLink.className = 'wn-stories-img-link';
}
// Fix the image itself
const img = imgSpan.querySelector('img');
if (img) {
img.className = 'wn-stories-img';
img.removeAttribute('width');
img.removeAttribute('height');
}
}
// Fix tags container (p with tag links)
const tagsContainer = novelElement.querySelector('p.mb4.pt4, p.oh.h16');
if (tagsContainer) {
tagsContainer.className = 'wn-stories-tags';
}
// Fix title
const title = novelElement.querySelector('h3');
if (title) {
title.className = 'wn-stories-title';
}
// Fix description
const descriptions = novelElement.querySelectorAll('p.fw400.lh20, p.ells');
descriptions.forEach(desc => {
if (desc.classList.contains('wn-stories-tags')) return;
if (desc.querySelector('strong') || desc.querySelector('svg')) return; // Skip stats
desc.className = 'wn-stories-description';
});
// Fix stats container (p.df.aic with rating and chapters)
const stats = novelElement.querySelector('p.df.aic');
if (stats) {
stats.className = 'wn-stories-stats';
}
}
// Transform stories page container
function fixStoriesPageContainer() {
if (!window.location.pathname.includes('/stories/')) return;
// Find the stories list container
const listContainer = document.querySelector('.j_category_wrapper ul');
if (listContainer && !listContainer.classList.contains('wn-stories-list')) {
listContainer.classList.add('wn-stories-list');
listContainer.removeAttribute('style');
}
}
function applyStyleToNovel(novelElement, score) {
// Fix page layouts first
fixTagsPageLayout(novelElement);
fixStoriesPageLayout(novelElement);
// Apply CSS class for styling
novelElement.classList.add('webnovel-filter-processed');
// Remove any existing score classes
for (let i = 0; i <= 5; i++) {
novelElement.classList.remove(`webnovel-score-${i}`);
}
novelElement.classList.add(`webnovel-score-${score}`);
// Create text content wrapper for better text management
// Try multiple selectors for different page layouts
const detElement = novelElement.querySelector('.g_col_det, .fl_det, .c_det, .det, .g_book_item');
if (detElement && !detElement.classList.contains('g_book_item')) {
createTextContentWrapper(detElement);
}
// Add score indicator badge
let scoreBadge = novelElement.querySelector('.novel-score');
if (!scoreBadge) {
scoreBadge = document.createElement('div');
scoreBadge.className = 'novel-score';
novelElement.appendChild(scoreBadge);
}
// Color code the score badge
let badgeColor = '#dc3545'; // Default red for low scores
if (score >= 4) badgeColor = '#38a9da'; // Blue for high scores
else if (score >= 3) badgeColor = '#28a745'; // Green for medium-high
else if (score >= 1) badgeColor = '#6c757d'; // Gray for low
scoreBadge.style.background = `linear-gradient(135deg, rgba(0,0,0,0.9), ${badgeColor}99)`;
scoreBadge.textContent = `⭐ ${score}/5`;
// Add remove button for low-scoring novels
if (score === 0) {
let removeBtn = novelElement.querySelector('.remove-novel');
if (!removeBtn) {
removeBtn = document.createElement('button');
removeBtn.className = 'remove-novel';
removeBtn.textContent = 'Γ— Remove';
removeBtn.title = 'Remove this novel from view';
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
novelElement.classList.add('webnovel-hidden');
setTimeout(() => {
if (novelElement.isConnected) {
novelElement.classList.add('webnovel-removing');
setTimeout(() => {
if (novelElement.isConnected) novelElement.remove();
}, 300);
}
}, 100);
});
novelElement.appendChild(removeBtn);
}
} else {
// Remove the remove button if it exists and score > 0
const removeBtn = novelElement.querySelector('.remove-novel');
if (removeBtn) {
removeBtn.remove();
}
}
}
function processNovels() {
// Fix page containers first
fixStoriesPageContainer();
// Try multiple selectors for different page types
const selectors = [
'li.fl',
'li.g_col',
'.c_item',
'.j_catalogItem',
'.catalog-item',
'.tag-catalog-list li',
'.j_catalogList li',
'.catalog-list li'
];
let novels = [];
for (const selector of selectors) {
const found = document.querySelectorAll(selector);
if (found.length > 0) {
novels = Array.from(found);
break;
}
}
// Also look for any list items in catalog containers
if (novels.length === 0) {
const catalogContainers = document.querySelectorAll('.tag-catalog-list, .j_catalogList, .catalog-list, .g_col_lst, .fl_lst');
catalogContainers.forEach(container => {
const items = container.querySelectorAll('li');
novels = novels.concat(Array.from(items));
});
}
for (const novel of novels) {
if (!novel.isConnected || processedElements.has(novel)) {
continue;
}
processedElements.add(novel);
if (shouldRemoveNovel(novel)) {
novel.classList.add('webnovel-hidden');
setTimeout(() => {
if (novel.isConnected) {
novel.classList.add('webnovel-removing');
setTimeout(() => {
if (novel.isConnected) novel.remove();
}, 300);
}
}, 300);
continue;
}
const score = calculatePreferenceScore(novel);
applyStyleToNovel(novel, score);
}
// Ensure containers have proper layout
const containers = [
'.g_col_lst', '.fl_lst',
'.tag-catalog-list', '.j_catalogList', '.catalog-list',
'.g_wrap', '.g_bd', '.g_main'
];
containers.forEach(selector => {
const container = document.querySelector(selector);
if (container) {
container.style.display = 'flex';
container.style.flexWrap = 'wrap';
container.style.gap = '30px';
container.style.justifyContent = 'flex-start';
container.style.alignItems = 'stretch';
container.style.width = '100%';
container.style.boxSizing = 'border-box';
}
});
}
// Debounced processing
let processTimeout;
function debouncedProcess() {
clearTimeout(processTimeout);
processTimeout = setTimeout(() => {
processNovels();
}, 150);
}
// Handle scroll loading
let scrollCheckTimeout;
function handleScroll() {
clearTimeout(scrollCheckTimeout);
scrollCheckTimeout = setTimeout(() => {
const scrollPosition = window.scrollY + window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
if (documentHeight - scrollPosition < 1000) {
processNovels();
}
}, 500);
}
// Setup Intersection Observer for lazy loaded content
let intersectionObserver;
function setupIntersectionObserver() {
if (intersectionObserver) {
intersectionObserver.disconnect();
}
intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
debouncedProcess();
}
});
}, {
root: null,
rootMargin: '200px',
threshold: 0.1
});
// Observe likely loading areas
const observeTargets = [
'.g_col_lst', '.fl_lst',
'.tag-catalog-list', '.j_catalogList',
'.catalog-list', '.j_loadMore',
'.load-more', '.more-btn'
];
observeTargets.forEach(selector => {
const target = document.querySelector(selector);
if (target) {
intersectionObserver.observe(target);
}
});
}
// Remove native tooltips from header elements
function removeHeaderTooltips() {
const header = document.querySelector('.g_header');
if (!header) return;
// Remove title attributes from all header elements to prevent native tooltips
header.querySelectorAll('[title]').forEach(el => {
// Store the title in a data attribute in case it's needed later
const title = el.getAttribute('title');
if (title) {
el.setAttribute('data-original-title', title);
el.removeAttribute('title');
}
});
}
(function() {
'use strict';
// Initial processing with delay
setTimeout(() => {
processNovels();
removeHeaderTooltips();
// Apply width to main containers
document.querySelectorAll('.g_wrap, .g_row, .main-wrap, .content-wrap').forEach(container => {
container.style.width = '80%';
container.style.maxWidth = '80%';
container.style.margin = '0 auto';
});
}, 1000);
// Setup intersection observer
setupIntersectionObserver();
// Set up scroll listener
window.addEventListener('scroll', handleScroll, { passive: true });
// Set up mutation observer for dynamic content
const mutationObserver = new MutationObserver((mutations) => {
let shouldProcess = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
shouldProcess = true;
break;
}
}
if (shouldProcess) {
debouncedProcess();
setupIntersectionObserver();
removeHeaderTooltips();
}
});
// Start observing the entire document
mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
// Also observe for URL changes (SPA navigation)
let lastUrl = location.href;
const urlObserver = new MutationObserver(() => {
const currentUrl = location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
// Clear processed elements
processedElements.clear();
// Reprocess
setTimeout(() => {
processNovels();
setupIntersectionObserver();
}, 1500);
}
});
urlObserver.observe(document, { subtree: true, childList: true });
// Periodic check (for safety)
setInterval(() => {
processNovels();
}, 5000);
// Add manual refresh button
setTimeout(() => {
const refreshBtn = document.createElement('button');
refreshBtn.id = 'webnovel-filter-refresh';
refreshBtn.textContent = 'πŸ”„ Refresh Filter';
refreshBtn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
background: #4a5568;
color: white;
border: none;
border-radius: 20px;
padding: 10px 20px;
font-size: 14px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
transition: all 0.3s;
`;
refreshBtn.addEventListener('click', () => {
// Clear processed elements and reprocess
processedElements.clear();
document.querySelectorAll('.webnovel-filter-processed').forEach(el => {
el.classList.remove('webnovel-filter-processed');
for (let i = 0; i <= 5; i++) {
el.classList.remove(`webnovel-score-${i}`);
}
const scoreBadge = el.querySelector('.novel-score');
if (scoreBadge) scoreBadge.remove();
const removeBtn = el.querySelector('.remove-novel');
if (removeBtn) removeBtn.remove();
const textWrapper = el.querySelector('.text-content-wrapper');
if (textWrapper) textWrapper.remove();
});
processNovels();
refreshBtn.textContent = 'βœ“ Refreshed!';
setTimeout(() => {
refreshBtn.textContent = 'πŸ”„ Refresh Filter';
}, 2000);
});
refreshBtn.addEventListener('mouseenter', () => {
refreshBtn.style.transform = 'scale(1.05)';
refreshBtn.style.background = '#2d3748';
});
refreshBtn.addEventListener('mouseleave', () => {
refreshBtn.style.transform = 'scale(1)';
refreshBtn.style.background = '#4a5568';
});
document.body.appendChild(refreshBtn);
}, 2000);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment