Hides inaccurate counts for Character and Copyright tags, and shows exact and accurate clean counts which are aligned with the user's blacklist. Good for getting AI slop-less statistics.
// ==UserScript==
// @name Rule34 Blacklist Tag Count Fix
// @namespace http://tampermonkey.net/
// @version 2.0
// @description Hides inaccurate counts for Character and Copyright tags, and shows exact and accurate clean counts which are aligned with the user's blacklist. Good for getting AI slop-less statistics.
// @author rushia816
// @match *://rule34.xxx/*
// @match *://rule34.xxx/index.php*
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ==========================================
// CONFIGURATION
// ==========================================
const HIDE_UNSEARCHED = true;
const CACHE_KEY = 'rule34_clean_counts';
const INIT_DELAY_MS = 100;
const CACHE_TTL_MS = 600000; // 10 Minutes
const CORRECTION_INTERVAL_MS = 10;
// AI Autocomplete Filter Configuration
const FILTER_AI_AUTOCOMPLETE = true; // Set to false to disable
const AI_KEYWORDS = [
'ai_generated', 'ai_art', 'midjourney', 'stable_diffusion',
'dalle', 'novelai', 'waifu_diffusion', 'deepdream', 'gan',
'neural_network', 'machine_learning', 'synthetic', 'generated_by_ai'
];
// ==========================================
const TARGET_TYPES = ['tag-type-character', 'tag-type-copyright'];
const processingQueue = new Set();
let isProcessing = false;
let acInterval = null;
let calculatedCounts = {};
function loadCache() {
try {
const stored = localStorage.getItem(CACHE_KEY);
if (stored) {
calculatedCounts = JSON.parse(stored);
} else {
calculatedCounts = {};
}
} catch (e) {
calculatedCounts = {};
}
}
function saveCache() {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify(calculatedCounts));
} catch (e) {}
}
// Initialize cache on load
loadCache();
// ==========================================
// INSTANT HIDE CSS (Sidebar Only)
// ==========================================
const style = document.createElement('style');
style.innerHTML = `
li.tag-type-character .tag-count,
li.tag-type-copyright .tag-count {
display: none !important;
visibility: hidden !important;
}
li.tag-type-character .tag-count::after,
li.tag-type-copyright .tag-count::after {
content: "...";
color: #888;
font-style: italic;
font-weight: normal;
visibility: visible !important;
}
li.tag-type-character .tag-count.revealed,
li.tag-type-copyright .tag-count.revealed {
display: inline !important;
visibility: visible !important;
}
li.tag-type-character .tag-count.revealed::after,
li.tag-type-copyright .tag-count.revealed::after {
content: "" !important;
visibility: hidden !important;
}
`;
if (document.head) {
document.head.appendChild(style);
} else {
document.addEventListener('DOMContentLoaded', () => {
if (document.head) document.head.appendChild(style);
});
}
// ==========================================
// HELPER FUNCTIONS
// ==========================================
function revealCount(element, number) {
if (!element) return;
element.classList.add('revealed');
element.textContent = number;
element.style.color = "";
element.style.fontStyle = "";
element.style.fontWeight = "";
}
function isAiTag(tagName) {
if (!FILTER_AI_AUTOCOMPLETE) return false;
const lowerName = tagName.toLowerCase();
return AI_KEYWORDS.some(keyword => lowerName.includes(keyword));
}
// ==========================================
// BRUTE-FORCE AUTOCOMPLETE CORRECTION
// ==========================================
function correctAutocomplete() {
const ul = document.querySelector('.awesomplete > ul');
if (!ul || ul.children.length === 0) return;
const items = ul.querySelectorAll('li');
items.forEach(item => {
if (!TARGET_TYPES.some(type => item.classList.contains(type))) return;
const text = item.textContent;
// AI Filter
if (FILTER_AI_AUTOCOMPLETE) {
const matchName = text.match(/^(.+)\s*\(/);
if (matchName) {
const tagName = matchName[1].trim();
if (isAiTag(tagName)) {
item.style.display = 'none';
return;
}
}
}
const numMatch = text.match(/\((\d+)\)$/);
const maskedMatch = text.match(/\(\.\.\.\)$/);
if (numMatch) {
const dirtyCount = parseInt(numMatch[1], 10);
const matchName = text.match(/^(.+)\s*\(\d+\)$/);
if (matchName) {
const tagName = matchName[1].trim();
const cachedData = calculatedCounts[tagName];
const now = Date.now();
const isFresh = cachedData && cachedData.count && (now - cachedData.timestamp < CACHE_TTL_MS);
if (cachedData && cachedData.count) {
if (isFresh && cachedData.count === dirtyCount) {
item.textContent = `${tagName} (...)`;
} else {
item.textContent = `${tagName} (${cachedData.count})`;
}
} else {
item.textContent = `${tagName} (...)`;
}
}
} else if (maskedMatch) {
const matchName = text.match(/^(.+)\s*\(\.\.\.\)$/);
if (matchName) {
const tagName = matchName[1].trim();
const cachedData = calculatedCounts[tagName];
if (cachedData && cachedData.count) {
item.textContent = `${tagName} (${cachedData.count})`;
}
}
}
});
}
// ==========================================
// SIDEBAR PROCESSING
// ==========================================
function getActiveTagElements() {
const tagItems = document.querySelectorAll('li.tag-type-character, li.tag-type-copyright');
if (tagItems.length === 0) return {};
const urlParams = new URLSearchParams(window.location.search);
const isTagsPage = urlParams.get('page') === 'tags';
const isListPage = urlParams.get('page') === 'post' && urlParams.get('s') === 'list';
// FIXED: Replaced optional chaining (?.) with standard check for compatibility
const searchInput = document.querySelector('input[name="tags"]');
const currentTagsStr = searchInput ? searchInput.value : '';
const currentTags = currentTagsStr.split('+').map(t => t.trim());
const activeTagMap = {};
tagItems.forEach(item => {
const tagLink = item.querySelector('a[href*="tags="]');
if (!tagLink) return;
const href = tagLink.getAttribute('href');
const tagParam = href.split('tags=')[1].split('&')[0];
let itemTagName = decodeURIComponent(tagParam).replace(/\+/g, ' ');
const isActive = currentTags.includes(itemTagName);
const countSpan = item.querySelector('.tag-count');
if (!countSpan) return;
// 1. Check Cache (with Stale Check)
const cachedData = calculatedCounts[itemTagName];
const now = Date.now();
const isStale = cachedData && cachedData.count && (now - cachedData.timestamp >= CACHE_TTL_MS);
const hasValidCache = cachedData && cachedData.count;
// 1. Check Cache
if (hasValidCache) {
revealCount(countSpan, cachedData.count);
if (isStale && isActive) {
if (!processingQueue.has(itemTagName)) {
calculateTagCount(itemTagName, (tag, count) => {
updateDisplay(tag, count);
});
}
}
return;
}
// 2. Tags Page (No pagination available)
if (isTagsPage) return;
// 3. Unsearched Tags
if (!isActive) {
if (HIDE_UNSEARCHED) return;
return;
}
activeTagMap[itemTagName] = { countSpan, parentItem: item };
});
return activeTagMap;
}
function processSidebarTags() {
if (isProcessing) return;
isProcessing = true;
setTimeout(() => { isProcessing = false; }, 10);
getActiveTagElements();
}
// ==========================================
// OBSERVERS
// ==========================================
function setupObservers() {
if (!document.body) return;
const observer = new MutationObserver((mutations) => {
if (isProcessing) return;
isProcessing = true;
setTimeout(() => { isProcessing = false; }, 50);
processSidebarTags();
});
observer.observe(document.body, { childList: true, subtree: true });
const searchInput = document.querySelector('input[name="tags"]');
if (searchInput) {
searchInput.addEventListener('input', () => {
processSidebarTags();
correctAutocomplete();
});
}
}
// ==========================================
// CALCULATION LOGIC
// ==========================================
/**
* Calculates the exact clean count by loading the last page in a hidden iframe
*/
function calculateTagCount(tagName, callback) {
if (processingQueue.has(tagName)) return;
processingQueue.add(tagName);
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('page') !== 'post' || urlParams.get('s') !== 'list') {
processingQueue.delete(tagName);
return;
}
const lastPageLink = document.querySelector('a[alt="last page"]');
if (!lastPageLink) {
const currentPosts = document.querySelectorAll('.thumb, .post-list .post-item').length;
callback(tagName, currentPosts);
processingQueue.delete(tagName);
return;
}
const lastHref = lastPageLink.getAttribute('href');
const urlParamsLast = new URLSearchParams(lastHref.split('?')[1]);
const lastPid = parseInt(urlParamsLast.get('pid'), 10);
if (isNaN(lastPid)) {
processingQueue.delete(tagName);
return;
}
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.onload = function() {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const lastPagePosts = iframeDoc.querySelectorAll('.thumb, .post-list .post-item').length;
const totalClean = lastPid + lastPagePosts;
callback(tagName, totalClean);
} catch (e) {
console.error("[Calc] Iframe error:", e);
} finally {
if (document.body.contains(iframe)) document.body.removeChild(iframe);
processingQueue.delete(tagName);
}
};
iframe.onerror = function() {
processingQueue.delete(tagName);
};
iframe.src = lastHref;
}
/**
* Updates the display (sidebar and autocomplete) with the calculated count
* Saves the count with a timestamp to the cache
*/
function updateDisplay(tagName, cleanCount) {
// Save with timestamp
calculatedCounts[tagName] = { count: cleanCount, timestamp: Date.now() }; saveCache();
// Reveal Sidebar (if still active)
const activeTags = getActiveTagElements();
if (activeTags[tagName]) {
revealCount(activeTags[tagName].countSpan, cleanCount);
} else {
const tagItems = document.querySelectorAll('li.tag-type-character, li.tag-type-copyright');
tagItems.forEach(item => {
const tagLink = item.querySelector('a[href*="tags="]');
if (!tagLink) return;
const href = tagLink.getAttribute('href');
const tagParam = href.split('tags=')[1].split('&')[0];
let itemTagName = decodeURIComponent(tagParam).replace(/\+/g, ' ');
if (itemTagName === tagName) {
const countSpan = item.querySelector('.tag-count');
if (countSpan) revealCount(countSpan, cleanCount);
}
});
}
correctAutocomplete();
}
/**
* Triggers calculation for all active tags in the current search
*/
function processActiveTags() {
const urlParams = new URLSearchParams(window.location.search);
if (!tagsParam) return;
if (urlParams.get('page') !== 'post' || urlParams.get('s') !== 'list') return;
const activeTagMap = getActiveTagElements();
Object.keys(activeTagMap).forEach(tagName => {
calculateTagCount(tagName, (tag, count) => {
updateDisplay(tag, count);
});
});
}
// ==========================================
// INITIALIZATION
// ==========================================
// Start the script with minimal delay
setTimeout(() => {
setupObservers();
processSidebarTags();
processActiveTags();
acInterval = setInterval(correctAutocomplete, CORRECTION_INTERVAL_MS);
setTimeout(() => {
processSidebarTags();
processActiveTags();
}, 500);
}, INIT_DELAY_MS);
// Navigation Handler: Re-run logic when URL changes
let lastUrl = location.href;
const navObserver = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
loadCache();
setTimeout(() => {
processSidebarTags();
correctAutocomplete();
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('page') === 'post' && urlParams.get('s') === 'list') {
processActiveTags();
}
}, 100);
}
});
if (document.body) {
navObserver.observe(document.body, { subtree: true, childList: true });
}
})();