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