JustPaste
HomeCategoriesAboutDonateContactTerms of UsePrivacy Policy
JustPaste

Free online notepad — write and share instantly

Navigate

  • Home
  • Timeline
  • Categories

Info

  • About
  • Donate
  • Contact

Legal

  • Terms of Use
  • Privacy Policy

© 2026 JustPaste.app. All rights reserved.

Made with ♥ by JustPaste

Untitled Page | JustPaste.app
10 days ago4 views
👨‍💻Programming
// ==UserScript==
// @name         xHamster Zippser Filter Shuwix
// @namespace    http://tampermonkey.net/
// @version      1.0.3
// @description  Hides videos from specified channels, hides watched videos, and excludes videos by title phrases on Xhamster.
// @author       https://github.com/zippser
// @match        https://*.xhamster.com/*
// @exclude      https://*.xhamster.com/embedframe/*
// @exclude      https://*.xhamster.com/user/*/comments
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const LOGO = `[XHAMSTER FILTER V1.0.3]`;
    const VIDEO_CONTAINER_SELECTOR = 'div.thumb-list__item.video-thumb.video-thumb--type-video';
    const WATCHED_MARKER_SELECTOR = 'div.thumb-image-container__watched[data-role="video-watched"]';

    // --- CONFIGURATION ---
    const UPLOADERS_TO_HIDE = [
         "Jerk Off Instructions", "DownblouseJerk", "FinDom Goaldigger", "My Jerk Off Girls Channel", "Honey Sasha", "Jerk Your Dick Channel", "Zona901", "Dirty Talking Bitches Channel", "JOI Trainer"
    ];
    const EXCLUDE_PHRASES = [
                "cei", "midget", "fart", "pee", "eat", "Ballbusting", "bisexual", "bisex", "shemale", "tranny", "nurse", "Trans", "trans", "loser", "CEI", "findom", "financial", "piss", "crossdresser", "crossdressing", "findom", "transgirl", "feet", "pissing", "gay", "sph", "urethral", "femboy", "pregnant", "diaper", "smoking", "sph", "transgender", "transvestite", "armpit", "pit", "cum eating", "trap", "pregnant", "transgender", "trans",
      "foot"
    ];
    // --- END CONFIGURATION ---

    /** Hides the video card element. */
    function hideVideoCard(element) {
        if (element && element.style.display !== 'none') {
            element.style.display = 'none';
        }
    }

    /** Extracts video title. */
    function getTitleFromContainer(container) {
        const titleElement = container.querySelector('.video-thumb-info__name');
        return titleElement ? titleElement.textContent.trim() : "";
    }

    /** Extracts uploader name. */
    function getUploaderFromContainer(container) {
        const uploaderElement = container.querySelector('a.video-uploader__name');
        return uploaderElement ? uploaderElement.textContent.trim() : "";
    }

    /** Applies filtering logic to all video containers. */
    function applyFilters() {
        const videoContainers = document.querySelectorAll(VIDEO_CONTAINER_SELECTOR);
        let hiddenCount = 0;

        videoContainers.forEach(container => {
            if (container.style.display === 'none') return; // Skip if already hidden

            let shouldHide = false;

            // Check 1: Exclude by Title Phrase
            const videoTitle = getTitleFromContainer(container);
            if (videoTitle && EXCLUDE_PHRASES.some(phrase => videoTitle.toLowerCase().includes(phrase.toLowerCase()))) {
                console.log(`${LOGO} Hiding video (Title Excluded): "${videoTitle}"`);
                shouldHide = true;
            }
            if (shouldHide) {
                hideVideoCard(container);
                hiddenCount++;
                return;
            }

            // Check 2: Hide Watched Videos
            if (container.querySelector(WATCHED_MARKER_SELECTOR)) {
                // console.log(`${LOGO} Hiding watched video (marker found): ${container.dataset.videoId}`);
                shouldHide = true;
            }
            if (shouldHide) {
                hideVideoCard(container);
                hiddenCount++;
                return;
            }

            // Check 3: Hide Videos from Blacklisted Uploaders
            const uploaderName = getUploaderFromContainer(container);
            if (uploaderName && UPLOADERS_TO_HIDE.some(blockedName => uploaderName.toLowerCase().includes(blockedName.toLowerCase()))) {
                console.log(`${LOGO} Hiding video from blacklisted uploader: "${uploaderName}"`);
                shouldHide = true;
            }
            if (shouldHide) {
                hideVideoCard(container);
                hiddenCount++;
            }
        });

        if (hiddenCount > 0) {
            console.log(`${LOGO} Filtered ${hiddenCount} videos in this pass.`);
        }
    }

    // --- Observer Setup ---
    let observer;
    let observerTimeout;
    const observerTarget = document.getElementById('content') || document.body;

    function startObserver() {
        if (!observerTarget) {
            console.error(`${LOGO} Observer target not found. Dynamic filtering might fail.`);
            return;
        }

        // Re-apply filters immediately when the observer is activated or re-activated
        applyFilters();

        const observerConfig = { childList: true, subtree: true };
        observer = new MutationObserver((mutationsList) => {
            let contentAddedOrChanged = false;
            for (const mutation of mutationsList) {
                // Check if nodes were added OR removed (sometimes elements change attributes)
                if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
                    // More general check: look for video containers anywhere in the changes
                     for (const node of mutation.addedNodes) {
                         if (node.nodeType === 1 && (node.matches(VIDEO_CONTAINER_SELECTOR) || node.querySelectorAll(VIDEO_CONTAINER_SELECTOR).length > 0)) {
                             contentAddedOrChanged = true;
                             break;
                         }
                     }
                     // If not found in added nodes, check potentially changed parents
                     if (!contentAddedOrChanged && mutation.target && (mutation.target.matches(VIDEO_CONTAINER_SELECTOR) || mutation.target.querySelectorAll(VIDEO_CONTAINER_SELECTOR).length > 0)) {
                         contentAddedOrChanged = true;
                     }
                }
                if (contentAddedOrChanged) break;
            }

            if (contentAddedOrChanged) {
                clearTimeout(observerTimeout);
                observerTimeout = setTimeout(applyFilters, 250); // Debounce
            }
        });

        observer.observe(observerTarget, observerConfig);
        console.log(`${LOGO} Mutation Observer established.`);
    }

    // --- Initialization ---

    // Try to start the observer as early as possible.
    // Using requestAnimationFrame can help ensure it's set up after the initial DOM is ready,
    // but before the browser is fully idle, potentially catching faster loads.
    window.addEventListener('DOMContentLoaded', () => {
         // Apply filters once DOM is ready, before observer starts fully
         applyFilters();
         // Then start the observer which will also call applyFilters() initially
         startObserver();
    });

    // Fallback: if DOMContentLoaded doesn't fire as expected, ensure observer starts eventually
    // (though @run-at document-idle should cover this)
    if (document.readyState !== 'loading') {
         // If already past loading state, start observer immediately
         applyFilters(); // Apply filters once more just in case
         startObserver();
    } else {
        // Otherwise, wait for DOMContentLoaded
        window.addEventListener('DOMContentLoaded', () => {
             applyFilters(); // Apply filters once more just in case
             startObserver();
        });
    }

})();
← Back to timeline