xHamster Ultimate Enhancer

Enhances xHamster: hides watched videos, auto large player, advanced filters, mark viewed, scroll to top

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         xHamster Ultimate Enhancer
// @namespace    Violentmonkey Scripts
// @version      1.2
// @description  Enhances xHamster: hides watched videos, auto large player, advanced filters, mark viewed, scroll to top
// @author       dweebz
// @license      MIT
// @match        https://xhamster.com/*
// @match        https://*.xhamster.com/*
// @match        https://xhamster2.com/*
// @match        https://*.xhamster2.com/*
// @match        https://xhamster3.com/*
// @match        https://*.xhamster3.com/*
// @include      *xhamster.com/*
// @grant        GM.addStyle
// @require      https://code.jquery.com/jquery-3.7.1.min.js
// @run-at       document-end
// ==/UserScript==

/* global $ */

(function() {
    'use strict';

    // Options storage
    const options = {
        autoResizePlayer: JSON.parse(localStorage.getItem('xh_autoResizePlayer')) || false,
        hdOnly: JSON.parse(localStorage.getItem('xh_hdOnly')) || false,
        minViews: parseInt(localStorage.getItem('xh_minViews')) || 0,
        maxViews: parseInt(localStorage.getItem('xh_maxViews')) || 10000000,
        minDuration: parseInt(localStorage.getItem('xh_minDuration')) || 0,
        maxDuration: parseInt(localStorage.getItem('xh_maxDuration')) || 9999,
        minRating: parseInt(localStorage.getItem('xh_minRating')) || 0,
        maxRating: parseInt(localStorage.getItem('xh_maxRating')) || 100,
        textSearch: localStorage.getItem('xh_textSearch') || '',
        textWhitelist: localStorage.getItem('xh_textWhitelist') || '',
        textBlacklist: localStorage.getItem('xh_textBlacklist') || '',
        textSanitize: localStorage.getItem('xh_textSanitize') || '',
        disableFilters: JSON.parse(localStorage.getItem('xh_disableFilters')) || false
    };

    // CSS: watched hiding + layout tweaks + buttons
    GM.addStyle(`
        .plus-buttons {
            background: rgba(67,67,67,0.85);
            box-shadow: 0 0 12px rgba(20,111,223,0.85);
            font-size: 12px;
            position: fixed;
            bottom: 10px;
            padding: 10px 22px 8px 24px;
            right: 0;
            z-index: 100;
            transition: all 0.3s ease;
            color: white;
            border-radius: 8px 0 0 8px;
        }
        .plus-buttons:hover { box-shadow: 0 0 3px rgba(0,0,0,0.3); }
        .plus-button {
            margin: 10px 0;
            padding: 6px 15px;
            border-radius: 4px;
            font-weight: 700;
            display: block;
            text-align: center;
            cursor: pointer;
            border: none;
            text-decoration: none;
            background: rgb(221,221,221);
            color: rgb(51,51,51);
        }
        .plus-button:hover { background: rgb(187,187,187); }
        .plus-button.isOn { background: rgb(20,111,223); color: #fff; }
        .plus-button.isOn:hover { background: rgb(0,91,203); }
        .plus-hidden { display: none !important; }

        /* Hide watched videos - display:none for better reflow */
        .thumb-list__item:has(> a > div > div.thumb-image-container__watched),
        .thumb-list-mobile-item:has(div > a > div > div.thumb-image-container__watched),
        [class*="watched"], .watched-overlay, .thumb-watched {
            display: none !important;
        }

        /* Override on excluded pages (e.g., favorites) */
        .xh-no-hide-watched .thumb-list__item:has(> a > div > div.thumb-image-container__watched),
        .xh-no-hide-watched .thumb-list-mobile-item:has(div > a > div > div.thumb-image-container__watched),
        .xh-no-hide-watched [class*="watched"], .xh-no-hide-watched .watched-overlay, .xh-no-hide-watched .thumb-watched {
            display: block !important;
        }

        /* Layout improvements */
        .video-page.video-page--large-mode .player-container__player {
            height: 720px;
            max-height: 90vh !important;
        }
        .favorites-dropdown__list { max-height: unset !important; }
        .entity-container { margin: 22px 0; border-top: 1px solid #ccc; }
    `);

    function applyEnhancements() {
        $('.thumb-list__item, .thumb-list-mobile-item').each(function() {
            const item = $(this);
            if (item.hasClass('plus-hidden')) return;

            const titleEl = item.find('a.video-thumb-info__name, .video-thumb-info__name-link');
            const title = titleEl.length ? titleEl.text().toLowerCase() : '';

            const durationStr = item.find('.thumb-image-container__duration, [class*="duration"]').text() || '';
            const viewsStr = item.find('.views .metric-text, [class*="views"]').text().replace(/[^0-9]/g, '') || '0';
            const ratingStr = item.find('.rating .metric-text, [class*="rating"]').text().replace('%', '') || '0';
            const hasHD = item.find('i.thumb-image-container__icon--hd, i.thumb-image-container__icon--uhd, [class*="hd-icon"]').length > 0;

            const durationMin = parseDuration(durationStr);
            const views = parseInt(viewsStr) || 0;
            const rating = parseInt(ratingStr) || 0;

            let hide = false;

            if (options.hdOnly && !hasHD) hide = true;
            if (views < options.minViews || views > options.maxViews) hide = true;
            if (durationMin < options.minDuration || durationMin > options.maxDuration) hide = true;
            if (rating < options.minRating || rating > options.maxRating) hide = true;

            const searchTerms = options.textSearch.toLowerCase().split('\n').filter(t => t.trim());
            const whitelist = options.textWhitelist.toLowerCase().split('\n').filter(t => t.trim());
            const blacklist = options.textBlacklist.toLowerCase().split('\n').filter(t => t.trim());

            if (searchTerms.length && !searchTerms.every(term => title.includes(term))) hide = true;
            if (whitelist.length && !whitelist.some(term => title.includes(term))) hide = true;
            if (blacklist.some(term => title.includes(term))) hide = true;

            if (hide) item.addClass('plus-hidden');
        });
    }

    function parseDuration(str) {
        const parts = str.match(/(\d+):?(\d+)?/) || [];
        const min = parseInt(parts[1]) || 0;
        const sec = parseInt(parts[2]) || 0;
        return min + (sec / 60);
    }

    function applySanitization(text) {
        const rules = options.textSanitize.split('\n').filter(r => r.trim());
        rules.forEach(rule => {
            const [from, to = ''] = rule.split('=').map(s => s.trim());
            if (from) text = text.replace(new RegExp(from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), to);
        });
        return text;
    }

    function markAllViewed() {
        const currentUrl = window.location.href;
        $('a.video-thumb-info__name, .video-thumb-info__name-link').each((_, el) => {
            history.replaceState({}, '', el.href);
        });
        history.replaceState({}, '', currentUrl);
        location.reload();
    }

    function forceReflow() {
        const containers = ['.thumb-list', '.thumb-list-mobile', 'main', '.video-list'];
        const scrollPos = window.scrollY;
        containers.forEach(sel => {
            const el = document.querySelector(sel);
            if (el) {
                el.style.display = 'none';
                void el.offsetHeight; // trigger reflow
                el.style.display = '';
            }
        });
        window.scrollTo(0, scrollPos);
    }

    const observer = new MutationObserver(() => {
        if (!options.disableFilters) {
            applyEnhancements();
            forceReflow();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    window.addEventListener('DOMContentLoaded', () => {
        const isVideoPage = window.location.pathname.startsWith('/videos');
        const isFavoritesPage = window.location.pathname.startsWith('/my/favorites/videos');
        const player = document.querySelector('#player-container');
        const video = player ? player.querySelector('video') : null;

        // Exclude watched hiding on favorites page
        if (isFavoritesPage) {
            document.body.classList.add('xh-no-hide-watched');
        }

        if (video && options.autoResizePlayer && isVideoPage) {
            video.addEventListener('canplay', () => {
                const largeBtn = document.querySelector('.large-mode, [class*="large"], .player-large-toggle');
                if (largeBtn) largeBtn.click();
            }, { once: true });
        }

        if (player) {
            window.addEventListener('resize', () => {
                if (document.querySelector('.xplayer-large-mode, [class*="large-mode"]')) {
                    player.style.maxHeight = `${window.innerHeight - 60}px`;
                }
            });
        }

        if (options.textSanitize && isVideoPage) {
            const title = $('h1').first();
            if (title.length) title.text(applySanitization(title.text()));
        }

        if (!options.disableFilters) {
            applyEnhancements();
            forceReflow();
        }

        const buttons = $('<div class="plus-buttons"></div>');

        const resizeBtn = $('<a class="plus-button"></a>').text('Auto Large Player').addClass(options.autoResizePlayer ? 'isOn' : '');
        resizeBtn.click(() => {
            options.autoResizePlayer = !options.autoResizePlayer;
            localStorage.setItem('xh_autoResizePlayer', options.autoResizePlayer);
            resizeBtn.toggleClass('isOn');
            location.reload();
        });

        const scrollBtn = $('<a class="plus-button"></a>').text('Scroll to Top').click(() => window.scrollTo({ top: 0, behavior: 'smooth' }));

        const markBtn = $('<a class="plus-button"></a>').text('Mark All Viewed').click(markAllViewed);

        const disableBtn = $('<a class="plus-button"></a>').text('Disable Filters').addClass(options.disableFilters ? 'isOn' : '');
        disableBtn.click(() => {
            options.disableFilters = !options.disableFilters;
            localStorage.setItem('xh_disableFilters', options.disableFilters);
            disableBtn.toggleClass('isOn');
            location.reload();
        });

        const filtersForm = $(`
            <div id="xh-filters" style="background: rgba(255,255,255,0.92); color: black; padding: 12px; border: 1px solid #aaa; border-radius: 6px; margin-top: 10px; display: none; max-width: 280px;">
                <strong>Filters (save & reload):</strong><br>
                <label><input type="checkbox" ${options.hdOnly ? 'checked' : ''}> HD Only</label><br>
                Min Views: <input type="number" value="${options.minViews}" class="minViews"><br>
                Max Views: <input type="number" value="${options.maxViews}" class="maxViews"><br>
                Min Duration (min): <input type="number" value="${options.minDuration}" class="minDuration"><br>
                Max Duration (min): <input type="number" value="${options.maxDuration}" class="maxDuration"><br>
                Min Rating (%): <input type="number" value="${options.minRating}" class="minRating"><br>
                Max Rating (%): <input type="number" value="${options.maxRating}" class="maxRating"><br>
                Text Search:<br><textarea class="textSearch" rows="3">${options.textSearch}</textarea><br>
                Whitelist:<br><textarea class="textWhitelist" rows="3">${options.textWhitelist}</textarea><br>
                Blacklist:<br><textarea class="textBlacklist" rows="3">${options.textBlacklist}</textarea><br>
                Sanitize (old=new):<br><textarea class="textSanitize" rows="3">${options.textSanitize}</textarea><br>
                <button id="saveFilters" style="margin-top:8px; width:100%;">Save & Reload</button>
            </div>
        `);

        const showFiltersBtn = $('<a class="plus-button"></a>').text('Filters').click(() => $('#xh-filters').toggle());

        $('#saveFilters', filtersForm).click(() => {
            options.hdOnly = filtersForm.find('input[type=checkbox]').is(':checked');
            options.minViews = parseInt(filtersForm.find('.minViews').val()) || 0;
            options.maxViews = parseInt(filtersForm.find('.maxViews').val()) || 10000000;
            options.minDuration = parseInt(filtersForm.find('.minDuration').val()) || 0;
            options.maxDuration = parseInt(filtersForm.find('.maxDuration').val()) || 9999;
            options.minRating = parseInt(filtersForm.find('.minRating').val()) || 0;
            options.maxRating = parseInt(filtersForm.find('.maxRating').val()) || 100;
            options.textSearch = filtersForm.find('.textSearch').val();
            options.textWhitelist = filtersForm.find('.textWhitelist').val();
            options.textBlacklist = filtersForm.find('.textBlacklist').val();
            options.textSanitize = filtersForm.find('.textSanitize').val();

            Object.entries(options).forEach(([key, val]) => localStorage.setItem(`xh_${key}`, JSON.stringify(val)));
            location.reload();
        });

        buttons.append(resizeBtn, scrollBtn, markBtn, disableBtn, showFiltersBtn, filtersForm);
        $('body').append(buttons);

        // Dynamic slide-out
        setTimeout(() => {
            const width = buttons.outerWidth();
            GM.addStyle(`.plus-buttons { margin-right: -${width - 23}px; } .plus-buttons:hover { margin-right: 0; }`);
        }, 500);
    });
})();