coom & keem enhance (supports new domains

Adds infinite scroll to user/post/popular pages, an auto-random post feature, removes sidebar ads, and adds various UI tweaks on coomer/kemono.

// ==UserScript==
// @name         coom & keem enhance (supports new domains
// @namespace    http://minoa.cat/
// @version      1.4
// @description  Adds infinite scroll to user/post/popular pages, an auto-random post feature, removes sidebar ads, and adds various UI tweaks on coomer/kemono.
// @author       minoa.cat & Gemini
// @match        https://coomer.su/*
// @match        https://coomer.party/*
// @match        https://coomer.st/*
// @match        https://kemono.su/*
// @match        https://kemono.party/*
// @match        https://kemono.cr/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// @run-at       document-body
// ==/UserScript==

(function() {
    'use strict';

    //================================================================================
    // 1. STYLES
    //================================================================================
    GM_addStyle(`
        .global-sidebar { width: 12.8rem !important; }
        #ar-wrapper {}
        #ar-main-link { display: flex !important; justify-content: space-between; align-items: center; width: 100%; }
        #ar-main-link.ar-active, #ar-main-link.ar-active:hover { color: #4CAF50 !important; font-weight: bold; }
        #ar-button-content { display: flex; align-items: center; flex-grow: 1; }
        #ar-expand-arrow { padding: 0 8px 0 0; font-size: 16px; line-height: 1; transform: rotate(0deg); transition: transform 0.2s ease-in-out; cursor: pointer; }
        #ar-expand-arrow.ar-expanded { transform: rotate(90deg); }
        #ar-settings-panel { display: none; padding: 10px 16px; background-color: #2d2d2d; border-top: 1px solid #444; }
        #ar-settings-panel.ar-visible { display: block; }
        #ar-settings-panel label { display: block; margin-bottom: 5px; color: #eee; font-size: 14px; }
        #ar-settings-panel input, #ar-settings-panel textarea { width: 100%; padding: 8px; margin-bottom: 10px; background-color: #1a1a1a; border: 1px solid #555; color: #ddd; border-radius: 4px; box-sizing: border-box; }
        .ar-standard-video { width: 100%; max-height: 80vh; border-radius: 4px; background-color: #000; }
        #infinite-scroll-loader { text-align: center; padding: 20px; font-size: 1.2em; color: #888; display: none; }
        body.coom-enhance-post-page .post__info > * { margin: 0.45rem 0; }
        body.coom-enhance-post-page .post__actions { font-size: 1.2em; display: grid; }
    `);

    //================================================================================
    // 2. CONFIG & STATE
    //================================================================================
    const Config = {
        KEY: 'autoRandomConfig_v1',
        DEFAULTS: { isEnabled: false, speed: 8, blacklist: 'man, cock, dick, male, yaoi, hairy, bear' },
        load: () => {
            const saved = GM_getValue(Config.KEY);
            try { return saved ? { ...Config.DEFAULTS, ...JSON.parse(saved) } : Config.DEFAULTS; }
            catch (e) { console.error("coom enhance: Failed to parse saved config.", e); return Config.DEFAULTS; }
        },
        save: (config) => GM_setValue(Config.KEY, JSON.stringify(config))
    };

    let config = Config.load();
    let countdownInterval = null;
    let debounceTimer = null;
    let lastProcessedURL = '';

    let infiniteScrollState = {
        isLoading: false, allLoaded: false, offset: 0, total: Infinity,
        apiEndpoint: null, intersectionObserver: null
    };

    //================================================================================
    // 3. UTILITY FUNCTIONS
    //================================================================================
    function buildThumbnailURL(filePath) {
        const domain = window.location.hostname;
        if (domain.includes('coomer.su') || domain.includes('coomer.party')) {
            return `//img.coomer.su/thumbnail/data${filePath}`;
        }
        return `//img.kemono.su/thumbnail${filePath}`;
    }

    //================================================================================
    // 4. UI & AUTO-RANDOM MODULE
    //================================================================================
    function injectUI() {
        if (document.getElementById('ar-wrapper')) return;
        const randomPostLink = document.querySelector('a.global-sidebar-entry-item[href="/posts/random"]');
        if (!randomPostLink) return;

        const wrapper = document.createElement('div');
        wrapper.id = 'ar-wrapper';
        const mainLink = document.createElement('a');
        mainLink.id = 'ar-main-link';
        mainLink.className = 'global-sidebar-entry-item';
        mainLink.href = '#';
        mainLink.innerHTML = `
            <span id="ar-button-content">
                <span id="ar-expand-arrow">▶</span>
                <span id="ar-main-button-text"></span>
            </span>`;
        const settingsPanel = document.createElement('div');
        settingsPanel.id = 'ar-settings-panel';
        settingsPanel.innerHTML = `
            <label for="ar-speed">Speed (seconds)</label>
            <input type="number" id="ar-speed" min="1">
            <label for="ar-blacklist">Blacklist (comma-separated)</label>
            <textarea id="ar-blacklist" rows="3"></textarea>`;

        wrapper.appendChild(mainLink);
        wrapper.appendChild(settingsPanel);
        randomPostLink.parentNode.insertBefore(wrapper, randomPostLink.nextSibling);

        document.getElementById('ar-speed').value = config.speed;
        document.getElementById('ar-blacklist').value = config.blacklist;

        mainLink.addEventListener('click', (e) => {
            e.preventDefault();
            config.isEnabled = !config.isEnabled;
            Config.save(config);
            updateButtonState();
            if (config.isEnabled) {
                if (window.location.pathname.match(/\/user\/[^\/]+\/post\//)) {
                    runAutoRandom();
                } else {
                    window.location.href = '/posts/random';
                }
            } else {
                if (countdownInterval) {
                    clearInterval(countdownInterval);
                    countdownInterval = null;
                }
            }
        });

        document.getElementById('ar-expand-arrow').addEventListener('click', (e) => {
            e.stopPropagation();
            e.currentTarget.classList.toggle('ar-expanded');
            document.getElementById('ar-settings-panel').classList.toggle('ar-visible');
        });

        document.getElementById('ar-speed').addEventListener('change', (e) => {
            const newSpeed = parseInt(e.target.value, 10);
            if (newSpeed > 0) { config.speed = newSpeed; Config.save(config); }
        });
        document.getElementById('ar-blacklist').addEventListener('input', (e) => {
            config.blacklist = e.target.value;
            Config.save(config);
        });

        updateButtonState();
    }

    function updateButtonState() {
        const mainLink = document.getElementById('ar-main-link');
        const buttonText = document.getElementById('ar-main-button-text');
        if (!mainLink || !buttonText) return;
        if (countdownInterval && config.isEnabled) return;
        mainLink.classList.toggle('ar-active', config.isEnabled);
        buttonText.textContent = `Auto-Random (${config.isEnabled ? 'ON' : 'OFF'})`;
    }

    function runAutoRandom() {
        if (countdownInterval) return;
        if (!config.isEnabled || !window.location.pathname.match(/\/user\/[^\/]+\/post\//)) {
            if (countdownInterval) clearInterval(countdownInterval);
            countdownInterval = null;
            return;
        }

        const contentElement = document.querySelector('.post__content pre, .post__content div');
        const titleElement = document.querySelector('.post__title h1');
        let textToScan = (contentElement?.textContent || '') + ' ' + (titleElement?.textContent || '');

        if (textToScan.trim()) {
            textToScan = textToScan.toLowerCase();
            const blacklist = config.blacklist.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
            if (blacklist.some(term => textToScan.includes(term))) {
                window.location.href = '/posts/random';
                return;
            }
        }

        let countdown = config.speed;
        const buttonText = document.getElementById('ar-main-button-text');
        const updateTimerDisplay = () => { if (buttonText) buttonText.textContent = `Next in ${countdown}s...`; };
        updateTimerDisplay();
        countdownInterval = setInterval(() => {
            countdown--;
            updateTimerDisplay();
            if (countdown <= 0) {
                clearInterval(countdownInterval);
                countdownInterval = null;
                window.location.href = '/posts/random';
            }
        }, 1000);
    }

    //================================================================================
    // 5. CORE PAGE MODULES
    //================================================================================
    function removeSidebarAd() {
        const adLink = document.querySelector('a.sidebar-extra-style[href*="tsyndicate.com"]');
        if (adLink) {
            adLink.parentElement.remove();
        }
    }

    function replaceVideoPlayers() {
        document.querySelectorAll('div.fluid_video_wrapper:not([data-ar-replaced])').forEach(playerWrapper => {
            playerWrapper.setAttribute('data-ar-replaced', 'true');
            const sourceElement = playerWrapper.querySelector('video > source');
            if (!sourceElement?.src) return;
            const newVideo = document.createElement('video');
            newVideo.src = sourceElement.src;
            newVideo.controls = true;
            newVideo.preload = 'metadata';
            newVideo.className = 'ar-standard-video';
            playerWrapper.parentNode.replaceChild(newVideo, playerWrapper);
        });
    }

    //================================================================================
    // 6. INFINITE SCROLL SYSTEM
    //================================================================================
    function getApiConfig() {
        const { pathname, search } = window.location;
        const userPageMatch = pathname.match(/\/(\w+)\/user\/([\w-]+)/);

        if (userPageMatch) {
            const [, service, userId] = userPageMatch;
            return {
                type: 'user',
                endpoint: `/api/v1/${service}/user/${userId}/posts-legacy${search}${search ? '&' : '?'}o=`
            };
        }

        // CRITICAL FIX: Add specific handler for the popular posts page
        if (pathname.startsWith('/posts/popular')) {
            const query = search || '?period=recent'; // Default to 'recent' if no params
            return {
                type: 'posts',
                endpoint: `/api/v1/posts/popular${query}${query.includes('?') ? '&' : '?'}o=`
            };
        }

        if (pathname.startsWith('/posts')) {
            return {
                type: 'posts',
                endpoint: `/api/v1/posts${search}${search ? '&' : '?'}o=`
            };
        }

        return null;
    }

    function createPostCardHTML(post) {
        const sanitizedTitle = post.title ? post.title.replace(/</g, "<").replace(/>/g, ">") : '';
        const imageHTML = post.file?.path ? `<div class="post-card__image-container"><img class="post-card__image" src="${buildThumbnailURL(post.file.path)}"></div>` : '';
        return `<article class="post-card post-card--preview co-parsed card--nevermet" data-id="${post.id}" data-service="${post.service}" data-user="${post.user}"><a class="fancy-link fancy-link--kemono" href="/${post.service}/user/${post.user}/post/${post.id}" data-discover="true"><header class="post-card__header">${sanitizedTitle}</header>${imageHTML}<footer class="post-card__footer"><div>${post.user}</div></footer></a></article>`;
    }

    async function fetchMorePosts() {
        if (infiniteScrollState.isLoading || infiniteScrollState.allLoaded) return;
        infiniteScrollState.isLoading = true;
        const loader = document.getElementById('infinite-scroll-loader');
        if (loader) loader.style.display = 'block';

        try {
            const response = await fetch(infiniteScrollState.apiEndpoint + infiniteScrollState.offset);
            if (!response.ok) throw new Error(`API request failed: ${response.status}`);
            const data = await response.json();
            const posts = data.posts || data.results;
            const cardListItems = document.querySelector('.card-list__items');

            if (posts?.length > 0 && cardListItems) {
                cardListItems.insertAdjacentHTML('beforeend', posts.map(createPostCardHTML).join(''));
                infiniteScrollState.offset += posts.length;
                if (infiniteScrollState.offset >= infiniteScrollState.total) {
                    infiniteScrollState.allLoaded = true;
                }
            } else {
                infiniteScrollState.allLoaded = true;
            }
        } catch (error) {
            console.error('[coom enhance] Failed to fetch more posts:', error);
            if (loader) loader.textContent = 'Error loading posts.';
        } finally {
            infiniteScrollState.isLoading = false;
            if (infiniteScrollState.allLoaded && loader) {
                loader.textContent = 'End of results.';
                if (infiniteScrollState.intersectionObserver) infiniteScrollState.intersectionObserver.disconnect();
            }
        }
    }

    function initInfiniteScroll() {
        const cardList = document.querySelector('.card-list__items');
        if (!cardList || document.getElementById('infinite-scroll-loader')) return;

        const apiConfig = getApiConfig();
        if (!apiConfig) return;

        infiniteScrollState = {
            isLoading: false,
            allLoaded: false,
            offset: cardList.children.length,
            total: window.__NEXT_DATA__?.props?.pageProps?.count || Infinity,
            apiEndpoint: apiConfig.endpoint,
            intersectionObserver: null
        };

        const paginator = document.querySelector('.paginator');
        if (paginator) {
            const paginationLinks = paginator.querySelectorAll('.pagination, .page-count, nav');
            paginationLinks.forEach(link => link.remove());
        }

        const loader = document.createElement('div');
        loader.id = 'infinite-scroll-loader';
        cardList.parentNode.appendChild(loader);

        infiniteScrollState.intersectionObserver = new IntersectionObserver((entries) => {
            if (entries[0].isIntersecting) fetchMorePosts();
        }, { rootMargin: '1500px 0px' });

        infiniteScrollState.intersectionObserver.observe(loader);
        fetchMorePosts();
    }

    function cleanupInfiniteScroll() {
        if (infiniteScrollState.intersectionObserver) {
            infiniteScrollState.intersectionObserver.disconnect();
            infiniteScrollState.intersectionObserver = null;
        }
        const loader = document.getElementById('infinite-scroll-loader');
        if (loader) loader.remove();
    }

    //================================================================================
    // 7. MAIN EXECUTION & OBSERVERS
    //================================================================================
    function onPageChange() {
        const currentURL = window.location.href;
        if (currentURL === lastProcessedURL) {
            return;
        }
        lastProcessedURL = currentURL;

        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => {
            injectUI();
            removeSidebarAd();
            replaceVideoPlayers();
            runAutoRandom();

            const { pathname } = window.location;
            document.body.className = document.body.className.replace(/coom-enhance-\w+-page/g, '');

            cleanupInfiniteScroll();

            if (pathname.match(/\/[^/]+\/user\/[^/]+\/post\//)) {
                document.body.classList.add('coom-enhance-post-page');
            }

            const apiConfig = getApiConfig();
            if (apiConfig) {
                initInfiniteScroll();
            }

        }, 300);
    }

    if (window.coomEnhanceLoaded) return;
    window.coomEnhanceLoaded = true;

    const observer = new MutationObserver(onPageChange);
    const attachObserverInterval = setInterval(() => {
        const rootElement = document.getElementById('root');
        if (rootElement) {
            clearInterval(attachObserverInterval);
            observer.observe(rootElement, { childList: true, subtree: true });
            onPageChange();
            console.log('[coom enhance] Observer attached.');
        }
    }, 100);

})();