Rule34 Blacklist Tag Count Fix

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.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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

})();