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.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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

})();