P0rnXP Enhanced

Autoplay in frame video previews plus case insensitive tag blocking

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         P0rnXP Enhanced
// @namespace    http://tampermonkey.net/
// @version      1.4.0
// @description  Autoplay in frame video previews plus case insensitive tag blocking
// @include      /^https?:\/\/([^\/.]+\.)?porn-xp\.[^\/]+\/.*$/
// @include      /^https?:\/\/([^\/.]+\.)?pornxp\.[^\/]+\/.*$/
// @include      /^https?:\/\/([^\/.]+\.)?xpxp\.[^\/]+\/.*$/
// @include      /^https?:\/\/([^\/.]+\.)?pxp\.[^\/]+\/.*$/
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'pxp_filters_v3';
    const DEFAULTS = { blockedTags: [], hideBlocked: true };
    
    const $ = (s, c = document) => c.querySelector(s);
    const $$ = (s, c = document) => [...c.querySelectorAll(s)];
    const escapeHtml = str => String(str).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));

    // Safe state loading to prevent crashes from corrupted storage
    let state = Object.assign({}, DEFAULTS);
    try {
        const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
        if (Array.isArray(saved.blockedTags)) state.blockedTags = saved.blockedTags;
        if (typeof saved.hideBlocked === 'boolean') state.hideBlocked = saved.hideBlocked;
    } catch (e) {
        localStorage.removeItem(STORAGE_KEY);
    }
    let blockedSet = new Set(state.blockedTags.map(t => String(t).toLowerCase()));

    const syncState = () => {
        state.blockedTags = [...blockedSet];
        localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
        applyFilter();
        refreshPanelTagList();
    };

    // Wrapped in try-catch to prevent crashes on malformed URL tags
    const parseTagName = href => {
        try {
            const m = href?.match(/\/tags\/([^/?#]+)/i);
            return m ? decodeURIComponent(m[1]).toLowerCase() : null;
        } catch { return null; }
    };

    const getItemTags = cont => $$('.item_tags a', cont).map(a => parseTagName(a.getAttribute('href'))).filter(Boolean);

    function applyFilter() {
        $$('.item_cont').forEach(cont => {
            const blocked = getItemTags(cont).some(t => blockedSet.has(t));
            if (blocked) {
                cont.style.display = state.hideBlocked ? 'none' : '';
                cont.style.opacity = state.hideBlocked ? '' : '0.2';
                cont.style.filter = state.hideBlocked ? '' : 'blur(5px) grayscale(100%)';
                cont.style.pointerEvents = state.hideBlocked ? '' : 'none';
            } else {
                cont.style.cssText = '';
            }
        });
    }

    const visObserver = new IntersectionObserver(entries => {
        entries.forEach(e => {
            const vid = e.target.querySelector('video.pxp-preview');
            if (!vid) return;
            if (e.isIntersecting) {
                if (!vid.src && e.target.dataset.preview) vid.src = e.target.dataset.preview;
                vid.style.opacity = '1';
                vid.play().catch(() => {});
            } else {
                vid.pause();
                vid.style.opacity = '0';
            }
        });
    }, { rootMargin: '200px', threshold: 0.1 });

    function initPreviews() {
        $$('.item.preview').forEach(item => {
            if (item.querySelector('video.pxp-preview')) return;
            const thumb = $('.item_thumb', item);
            if (!item.dataset.preview || !thumb) return;
            const video = document.createElement('video');
            video.className = 'pxp-preview';
            video.loop = video.muted = video.playsInline = true;
            video.preload = 'none';
            thumb.appendChild(video);
            visObserver.observe(item);
        });
    }

    function addBlockButtons() {
        $$('.item_tags a').forEach(a => {
            if (a.querySelector('.pxp-block-btn')) return;
            const tag = parseTagName(a.getAttribute('href'));
            if (!tag) return;
            const btn = document.createElement('span');
            btn.className = 'pxp-block-btn';
            btn.textContent = '×';
            btn.title = `Block "${tag}"`;
            btn.addEventListener('click', e => { 
                e.preventDefault(); e.stopPropagation(); 
                blockedSet.add(tag); syncState(); 
            });
            a.appendChild(btn);
        });
    }

    function refreshPanelTagList() {
        const list = $('#pxp-taglist');
        if (!list) return;
        const tags = [...blockedSet].filter(Boolean);
        list.innerHTML = tags.length ? tags.map(tag => 
            `<div class="pxp-tag"><span>${escapeHtml(tag)}</span><button data-tag="${escapeHtml(tag)}">×</button></div>`
        ).join('') : '<div class="pxp-empty">No blocked tags</div>';
        
        list.querySelectorAll('button').forEach(btn => btn.onclick = () => {
            blockedSet.delete(btn.dataset.tag);
            syncState();
        });
    }

    function buildPanel() {
        const panel = document.createElement('div');
        panel.id = 'pxp-settings';
        panel.innerHTML = `
            <div class="pxp-header"><span>Filters</span><button class="pxp-close">×</button></div>
            <div class="pxp-body">
                <label class="pxp-row"><input type="checkbox" id="pxp-hide" ${state.hideBlocked ? 'checked' : ''}><span>Hide blocked items</span></label>
                <div class="pxp-section">Blocked Tags</div>
                <div class="pxp-taglist" id="pxp-taglist"></div>
                <div class="pxp-addtag">
                    <input type="text" id="pxp-newtag" placeholder="Enter tag name">
                    <button id="pxp-add">Block</button>
                </div>
            </div>`;

        panel.querySelector('.pxp-close').onclick = () => panel.classList.remove('open');
        panel.querySelector('#pxp-hide').onchange = e => { state.hideBlocked = e.target.checked; syncState(); };

        const addTag = () => {
            const input = panel.querySelector('#pxp-newtag');
            const tag = input.value.trim().toLowerCase();
            if (tag && !blockedSet.has(tag)) { blockedSet.add(tag); syncState(); }
            input.value = '';
        };
        
        panel.querySelector('#pxp-add').onclick = addTag;
        panel.querySelector('#pxp-newtag').onkeydown = e => { if (e.key === 'Enter') addTag(); };

        refreshPanelTagList();
        return panel;
    }

    GM_addStyle(`
        .item_thumb { position: relative !important; overflow: hidden !important; }
        .item_thumb img { display: block !important; position: relative !important; z-index: 1 !important; }
        video.pxp-preview {
            position: absolute !important; top: 0 !important; left: 0 !important;
            width: 100% !important; height: 100% !important; object-fit: cover !important;
            z-index: 2 !important; opacity: 0; transition: opacity .25s ease; pointer-events: none;
        }
        #pxp-settings {
            position:fixed; top:60px; right:-320px; width:280px; max-width:calc(100vw - 32px); z-index:99999;
            background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%);
            border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#ccc;
            font-family: -apple-system, BlinkMacSystemFont, "system-ui", sans-serif; font-size:13px;
            transition: right .35s cubic-bezier(0.34, 1.56, 0.64, 1); box-shadow:0 10px 40px rgba(0,0,0,.8); overflow:hidden;
            box-sizing: border-box;
        }
        #pxp-settings.open { right:16px; }
        .pxp-header { display:flex; justify-content:space-between; align-items:center; padding:14px 16px; border-bottom:1px solid rgba(255,255,255,0.08); font-weight:600; font-size:15px; color:#fff; background:rgba(255,255,255,0.03);}
        .pxp-close { background:rgba(255,255,255,0.1); border:none; color:#fff; font-size:14px; cursor:pointer; width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; transition:background .2s; }
        .pxp-close:hover { background:rgba(255,255,255,0.2); }
        .pxp-body { padding:16px; }
        .pxp-row { display:flex; align-items:center; gap:10px; margin-bottom:12px; cursor:pointer; color:#ddd; }
        .pxp-section { margin:16px 0 8px; font-weight:600; color:rgba(255,255,255,0.4); text-transform:uppercase; font-size:11px; letter-spacing:1px; }
        .pxp-taglist { max-height:180px; overflow-y:auto; margin-bottom:12px; display:flex; flex-direction:column; gap:8px; -webkit-overflow-scrolling:touch; }
        .pxp-taglist::-webkit-scrollbar { width:4px; } .pxp-taglist::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.2); border-radius:2px; }
        .pxp-empty { color:rgba(255,255,255,0.3); font-style:italic; padding:8px 0; text-align:center; }
        .pxp-tag { display:flex; justify-content:space-between; align-items:center; background:rgba(255,255,255,0.06); padding:8px 12px; border-radius:8px; font-size:12px; color:#eee; }
        .pxp-tag button { background:none; border:none; color:#ff6b6b; cursor:pointer; font-size:16px; line-height:1; padding:0 0 0 8px; }
        .pxp-addtag { display:flex; gap:8px; }
        .pxp-addtag input { flex:1; background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.1); color:#fff; padding:10px 12px; border-radius:8px; font-size:12px; outline:none; box-sizing:border-box; }
        .pxp-addtag input:focus { border-color:rgba(255,255,255,0.3); background:rgba(255,255,255,0.12); }
        .pxp-addtag button { background:rgba(255,255,255,0.12); color:#fff; border:1px solid rgba(255,255,255,0.15); padding:8px 16px; border-radius:8px; cursor:pointer; font-size:12px; font-weight:500; transition:.2s; }
        .pxp-addtag button:hover { background:rgba(255,255,255,0.2); }
        #pxp-toggle { position:fixed; top:16px; right:16px; z-index:99999; background:rgba(0,0,0,0.6); backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px); color:#fff; border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:20px; cursor:pointer; font-weight:600; font-size:13px; box-shadow:0 4px 15px rgba(0,0,0,.4); transition:.2s; }
        #pxp-toggle:hover { background:rgba(0,0,0,0.8); transform:scale(1.05); }
        .item_tags a { position:relative; padding-right:14px!important; }
        .pxp-block-btn { display:inline-block; margin-left:4px; color:#ff6b6b; cursor:pointer; font-weight:bold; font-size:12px; opacity:0; transition:opacity .15s; }
        .item_tags a:hover .pxp-block-btn { opacity:1; }
        @media (max-width: 480px) {
            #pxp-toggle { padding:8px 14px; font-size:12px; top:10px; right:10px; }
            #pxp-settings { top:50px; }
        }
    `);

    GM_registerMenuCommand('Reset Filters', () => {
        localStorage.removeItem(STORAGE_KEY);
        state = { ...DEFAULTS };
        blockedSet = new Set();
        syncState();
        initPreviews();
    });

    function init() {
        let toggle = $('#pxp-toggle') || document.createElement('button');
        toggle.id = 'pxp-toggle';
        toggle.textContent = 'Filters';
        toggle.onclick = () => {
            let panel = $('#pxp-settings');
            if (!panel) { panel = buildPanel(); document.body.appendChild(panel); }
            panel.classList.toggle('open');
        };
        if (!toggle.parentElement) document.body.appendChild(toggle);

        syncState();
        initPreviews();
        addBlockButtons();

        let pending;
        new MutationObserver(m => {
            // Only trigger if new video containers or tags are added to the DOM
            const hasRelevantMutations = m.some(r => [...r.addedNodes].some(n => 
                n.nodeType === 1 && 
                (n.matches?.('.item_cont, .item.preview, .item_tags') || n.querySelector?.('.item_cont, .item.preview, .item_tags'))
            ));
            if (!hasRelevantMutations) return;
            
            clearTimeout(pending);
            pending = setTimeout(() => { 
                if (!$('#pxp-toggle')) document.body.appendChild(toggle); // Re-add toggle if site SPA removes it
                initPreviews(); 
                addBlockButtons(); 
                applyFilter(); 
            }, 150);
        }).observe(document.body, { childList: true, subtree: true });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
})();