PornHub+

Ad blocker, filters, no autoplay, fast search etc.

// ==UserScript==
// @name PornHub+
// @namespace -
// @version 1.6.1
// @description Ad blocker, filters, no autoplay, fast search etc.
// @author NotYou
// @match *://pornhub.com/*
// @match *://pornhubpremium.com/*
// @match *://*.pornhub.com/*
// @match *://*.pornhubpremium.com/*
// @compatible Firefox Version 78
// @compatible Chrome Version 88
// @compatible Edge Version 88
// @compatible Opera Version 74
// @compatible Safari Version 14
// @run-at document-end
// @license GPL-3.0-or-later
// @grant none
// ==/UserScript==

(function() {
    const TITLE = 'PornHub+'
    const LOCAL_SETTINGS = 'settings_pp'

    const DEFAULT_SETTINGS = JSON.stringify({
        stepLike: 0,
        incest: 0,
        nonHetero: 0,
        bdsm: 0
    })

    const LABELS = {
        stepLike: 'Step-like',
        incest: 'Incest',
        nonHetero: 'Non-Hetero',
        bdsm: 'BDSM or Hardcore'
    }

    class Settings {
        static getSettings() {
            return JSON.parse(localStorage.getItem(LOCAL_SETTINGS))
        }

        static getItem(key) {
            const settings = this.getSettings()

            return settings[key]
        }

        static setItem(key, value) {
            const settings = this.getSettings()

            settings[key] = value

            localStorage.setItem(LOCAL_SETTINGS, JSON.stringify(settings))
        }
    }

    class CSS {
        static init() {
            const _CSS = `
            #header.hasAdAlert {
              grid-template-rows: unset !important;
            }

            #headerContainer {
              padding-bottom: 10px;
            }

            #fast-search-pp {
              position: fixed;
              z-index: 2147483646;
              width: 400px;
              height: 40px;
              padding: 16px 28px 28px 16px;
              background-color: rgba(21, 21, 21, 0.57);
              border-radius: 3px;
              left: 32vw;
              top: 20vh;
            }

            #fast-search-input-parent-pp {
              padding: 5px;
              display: flex;
              border: 0;
              height: 40px;
              width: 100%;
              background-color: rgb(61, 54, 49);
              border-radius: 3px;
            }

            #fast-search-input-parent-pp .ph-icon-search {
              margin-top: 10px;
              margin-right: 10px;
              font-size: 20px;
            }

            #fast-search-input-pp {
              background-color: unset;
              width: 100%;
              height: 100%;
              font-size: 20px;
              border: 0;
            }

            #fast-search-input-pp::placeholder {
              font-style: italic;
              color: rgb(164, 164, 164);
            }

            .active-category-pp a {
              color: rgb(255, 153, 0) !important;
            }`

            addStyle(_CSS)
        }
    }

    class ClearBtn {
        static init() {
            const NETWORK_LIST = document.querySelector('.networkListContent')

            if(NETWORK_LIST) {
                const CLEAR_BTN = document.createElement('li')
                const CLEAR_PP = document.createElement('a')
                const ID = NETWORK_LIST.children.length

                CLEAR_PP.className = 'networkTab'
                CLEAR_PP.href = 'javascript:;'
                CLEAR_PP.title = 'Clear watched videos, search requests from browser\'s local storage'
                CLEAR_PP.textContent = 'Clear'
                CLEAR_BTN.dataset.id = ID

                CLEAR_BTN.addEventListener('click', clearStorage)

                CLEAR_BTN.appendChild(CLEAR_PP)
                NETWORK_LIST.appendChild(CLEAR_BTN)

                function clearStorage() {
                    const STORAGE_KEYS = ['watchedVideoStorage', 'watchedVideoIds', 'currentTimeStamp', 'recentSearch']

                    for (let i = 0; i < STORAGE_KEYS.length; i++) {
                        const STORAGE_KEY = STORAGE_KEYS[i]

                        localStorage.removeItem(STORAGE_KEY)
                    }
                }
            }
        }
    }

    class AdBlock {
        static init() {
            let css = ''
            const bodies = []

            for (let i = 0; i < 10; i++) {
                bodies.push(`body > [class*=${i}]`)
            }

            const SELECTORS = [
                '.cookiesBanner',
                '#pb_template',
                '.adsbytrafficjunky',
                '.adLinks',
                '.adLink',
                '#hd-rightColVideoPage > .clearfix:first-child',
                '#popsByTrafficJunky',
                bodies.join(','),
                '#videoSearchResult > :first-child:not(.userCardLi)',
                '#welcome',
                '#vpContentContainer > div:nth-child(2) > :first-child',
                '.video-wrapper > .hd.clear',
                '#js-abContainterMain',
                '.t8fer',
                '.t8fer k0g8',
                '#footerMenu_advertising',
                '.ad-link',
                'body > *:not(:where(div, template, script, style, link, ins, noscript, iframe, img, pre))',
                '.carouselOverlay',
                '.cookiesBanner + * + *',
                `:where(#popularPornstars,
                .gifsWrapper > ul > :first-child,
                #hottestVideosSection,
                #photosAlbumsSection.withAd,
                #hotVideosSection,
                #videoCategory, .container > div[class=""],
                #videoSearchResult,
                #singleFeedSection) > :where(:first-child:not(:where(li, div)), .sniperModeEngaged)`,
                '#channelsBody .leftSide.floatLeft .rankWrapper + :not(.clearfix)',
                '.alpha ~ .emptyBlockSpace[style="display: block;"]',
                '.sponsor-text'
            ]

            for (let i = 0; i < SELECTORS.length; i++) {
                const SELECTOR = SELECTORS[i]

                css += SELECTOR + '{ display: none !important; }'
            }

            addStyle(css)
        }
    }

    class AdBlockAdvanced {
        static init() {
            removeAds()

            const obs = new MutationObserver(removeAds)
            obs.observe(document.body, {
                childList: true,
                subtree: true,
            })

            function removeAds() {
                const ADS_ELEMENTS = document.querySelectorAll('[data-embeddedads="true"]:not([style="all: unset !important; display: none !important;"])')

                for (let i = 0; i < ADS_ELEMENTS.length; i++) {
                    const ADS_ELEMENT = ADS_ELEMENTS[i]
                    const ADS_PARENT_ELEMENT = ADS_ELEMENT.parentNode
                    const IS_ADS_ELEMENT = ADS_PARENT_ELEMENT.children.length === 1 && ADS_PARENT_ELEMENT.className

                    if(IS_ADS_ELEMENT) {
                        ADS_PARENT_ELEMENT.setAttribute('style', 'all: unset !important; display: none !important;')
                    }
                }
            }
        }
    }

    class PlusSymbol {
        static init() {
            const LOGO_WRAPPER = document.querySelector('.logo .logoWrapper')
            const PLUS = document.createElement('i')
            PLUS.style.marginTop = '10px'
            PLUS.style.fontSize = '20px'
            PLUS.className = 'ph-icon-add'

            if(LOGO_WRAPPER) {
                LOGO_WRAPPER.style.display = 'flex'
                LOGO_WRAPPER.appendChild(PLUS)
            }
        }
    }

    class NoAutoplay {
        static init() {
            const LOCAL_PLAYER_NAME = 'mgp_player'
            const LOCAL_PLAYER_SETTINGS = localStorage.getItem(LOCAL_PLAYER_NAME)

            let playerSettings = JSON.parse(LOCAL_PLAYER_SETTINGS)

            if(playerSettings) {
                playerSettings.autoplay = false

                const PLAYER_SETTINGS = JSON.stringify(playerSettings)

                localStorage.setItem(LOCAL_PLAYER_NAME, PLAYER_SETTINGS)
            }
        }
    }

    class FastSearch {
        static init() {
            const FAST_SEARCH_EL = document.createElement('div')
            FAST_SEARCH_EL.id = 'fast-search-pp'

            toggleVisibility(FAST_SEARCH_EL)

            const FAST_SEARCH_PARENT_EL = document.createElement('span')
            FAST_SEARCH_PARENT_EL.id = 'fast-search-input-parent-pp'

            const ICON = document.createElement('i')
            ICON.className = 'ph-icon-search'

            const INPUT = document.createElement('input')
            INPUT.id = 'fast-search-input-pp'
            INPUT.placeholder = 'Fast Search Pornhub'

            FAST_SEARCH_PARENT_EL.appendChild(ICON)
            FAST_SEARCH_PARENT_EL.appendChild(INPUT)
            FAST_SEARCH_EL.appendChild(FAST_SEARCH_PARENT_EL)

            INPUT.addEventListener('keydown', fastSearchHandler)
            ICON.addEventListener('click', fastSearchHandler)

            document.body.appendChild(FAST_SEARCH_EL)

            window.addEventListener('keydown', e => {
                if((e.metaKey || e.ctrlKey) && e.code === 'KeyK') {
                    e.preventDefault()
                    toggleVisibility(FAST_SEARCH_EL)
                    INPUT.focus()
                }
            })

            function fastSearchHandler(e) {
                if((e.type === 'keydown' && e.code === 'Enter') || e.type === 'click') {
                    const QUERY = getQuery()

                    search(QUERY)
                }

                function search(query) {
                    replaceLocation(location.origin + '/video/search?search=' + query)
                }

                function getQuery() {
                    const VALUE = INPUT.value
                    const RE_UNUSED = /[^0-9a-zA-Z\\s-]+/g
                    const VALUE_CLEAN = VALUE.replaceAll(RE_UNUSED, '').replaceAll(/\s/g, '+')

                    return VALUE_CLEAN
                }
            }
        }
    }

    class FastButtons {
        static init() {
            const SEARCH_PARAMS = getSearchParams()
            const VIEWKEY = SEARCH_PARAMS.get('viewkey')
            const MENU_ROW = document.querySelector('#js-shareData > .tab-menu-wrapper-row')
            const MENU_CELL = document.createElement('div')
            MENU_CELL.className = 'tab-menu-wrapper-cell'

            const MENU_CELL_LINK = document.createElement('a')
            MENU_CELL_LINK.href = location.origin + '/embed/' + VIEWKEY
            MENU_CELL_LINK.target = '_self'

            const EMBED_BTN = document.createElement('div')
            EMBED_BTN.id = 'embed_mode-btn'
            EMBED_BTN.className = 'flag-btn tab-menu-item tooltipTrig'
            EMBED_BTN.dataset.title = 'Open video in embed mode'
            EMBED_BTN.setAttribute('role', 'button')
            EMBED_BTN.tabindex = 0

            const EMBED_ICON = document.createElement('i')
            EMBED_ICON.className = 'ph-icon-channel'

            const EMBED_TEXT = document.createElement('span')
            EMBED_TEXT.textContent = 'Embed'

            EMBED_BTN.appendChild(EMBED_ICON)
            EMBED_BTN.appendChild(EMBED_TEXT)
            MENU_CELL_LINK.appendChild(EMBED_BTN)
            MENU_CELL.appendChild(MENU_CELL_LINK)
            MENU_ROW.appendChild(MENU_CELL)
        }
    }

    class CopyableTitle {
        static init() {
            const TITLE_EL = document.querySelector('.title-container > .title > span')
            TITLE_EL.title = 'Click to copy'
            TITLE_EL.addEventListener('click', () => {
                navigator.clipboard.writeText(TITLE_EL.innerText)
            })
        }
    }

    class BetterVideoPage {
        static init() {
            const IS_VIDEO = isVideoPage()

            if(IS_VIDEO) {
                const MODULES = [
                    FastButtons,
                    CopyableTitle
                ]

                initModules(MODULES)
            }
        }
    }

    class Filters {
        static init() {
            const SEARCH_PARAMS = getSearchParams()
            const IS_VALID_PAGE = SEARCH_PARAMS.has('search') ||
                  SEARCH_PARAMS.has('page') ||
                  SEARCH_PARAMS.has('o') ||
                  (location.pathname === '/video' && SEARCH_PARAMS.toString() === '') ||
                  location.pathname.indexOf('categories/') !== -1

            if(IS_VALID_PAGE) {
                const FILTERS = {
                    stepLike: [
                        'stepmom', 'stepmother', 'stepmommy', 'stepmum', 'step-mom', 'step-mother', 'step-mum', 'step-mommy', 'step mom', 'step mother', 'step mum', 'step mommy',
                        'stepdad', 'step-dad', 'step dad',
                        'stepson', 'step-son', 'step son',
                        'stepsis', 'stepsister', 'step-sister', 'step-sis', 'step sister', 'step sis',
                        'stepdaughter', 'step-daughter', 'step daughter'
                    ],
                    incest: [
                        'mom', 'mother', 'mommy', 'milf',
                        'dad', 'father', 'daddy',
                        'bro', 'brother',
                        'sis', 'sister',
                        'daughter',
                        'son',

                        // Series

                        'mylf', 'gotmylf', 'mylfdom', 'badmilfs', 'bad milfs', 'analmom', 'anal mom', 'pervmom', 'perv mom', 'milfbody', 'pervnana', 'perv nana', 'momswap', 'momshoot', 'momsteachsex', 'mommy\'s girl',
                        'sis loves me', 'brattysis', 'sisswap', 'tinysis',
                        'dad crush',
                        'not my grandpa', 'notmygrandpa',
                        'spyfam', 'spy fam', 'family sinners', 'myfamilypies', 'familystrokes', 'family strokes'
                    ],
                    nonHetero: [
                        'lesbian',
                        'gay', 'homo', 'homosexual',
                        'bi', 'bisexual',
                        'trans', 'transgender',

                        // Series

                        'helloladyboy', 'mommy\'s girl', 'girlsway', 'the lesbian experience', 'all girl massage',
                    ],
                    bdsm: [
                        'hardcore',
                        'bdsm',
                        'biting',
                        'scratching',
                        'dom', 'femdom', 'maledom', 'domination',
                        'wax', 'waxing',
                        'gag', 'gagging',
                        'whip', 'whiping',
                        'tied', 'tying',
                        'bondage'
                    ]
                }

                let appliedFilters = {}

                const HEADER = createSidebarHeader('Filter', 'filter')
                const CATEGORIES = createCategories()

                const FILTERS_KEYS = Object.keys(FILTERS)

                for (let i = 0; i < FILTERS_KEYS.length; i++) {
                    const FILTER_KEY = FILTERS_KEYS[i]
                    const LABEL = LABELS[FILTER_KEY]

                    CATEGORIES.createCategory(LABEL, FILTER_KEY, 'filter')
                }

                const FILTER_CATEGORIES = CATEGORIES.create()

                const TO_INSERT_ELS = [
                    HEADER,
                    FILTER_CATEGORIES
                ]
                const DURATION_WRAPPER = document.querySelector('#duration-wrapper')
                const DURATION_HEADER = DURATION_WRAPPER.previousElementSibling

                for (let i = 0; i < TO_INSERT_ELS.length; i++) {
                    const TO_INSERT_EL = TO_INSERT_ELS[i]

                    DURATION_HEADER.insertAdjacentElement('beforebegin', TO_INSERT_EL)
                }

                function createCategories() {
                    const WRAPPER = document.createElement('div')
                    WRAPPER.className = 'sidebar_wrapper'

                    const CATEGORIES = document.createElement('ul')
                    CATEGORIES.className = 'nf-categories'

                    WRAPPER.appendChild(CATEGORIES)

                    return {
                        createCategory,
                        create
                    }

                    function createCategory(label, category, type) {
                        const CATEGORY = document.createElement('li')
                        CATEGORY.className = 'sidebarIndent'

                        const IS_ENABLED_BY_DEFAULT = Boolean(Settings.getItem(category))

                        appliedFilters[category] = IS_ENABLED_BY_DEFAULT

                        if(IS_ENABLED_BY_DEFAULT) {
                            CATEGORY.classList.add('active-category-pp')
                            changeVideosVisibility(FILTERS[category], appliedFilters[category])
                        }


                        CATEGORY.addEventListener('click', () => {
                            const IS_ENABLED = !Settings.getItem(category)

                            Settings.setItem(category, IS_ENABLED ? 1 : 0)

                            appliedFilters[category] = IS_ENABLED

                            CATEGORY.classList[IS_ENABLED ? 'add' : 'remove']('active-category-pp')

                            changeVideosVisibility(FILTERS[category], appliedFilters[category])
                        })

                        const CATEGORY_LINK = document.createElement('a')
                        CATEGORY_LINK.className = 'sidebarIndent'
                        CATEGORY_LINK.href = 'javascript:;'

                        const CATEGORY_TEXT = document.createTextNode(label)

                        CATEGORY_LINK.appendChild(CATEGORY_TEXT)
                        CATEGORY.appendChild(CATEGORY_LINK)
                        CATEGORIES.appendChild(CATEGORY)

                        return this
                    }

                    function create() {
                        return WRAPPER
                    }
                }

                function createSidebarHeader(text, icon) {
                    const SECTION_HEADER = document.createElement('div')
                    SECTION_HEADER.className = 'section_bar_sidebar'

                    const SECTION_TITLE = document.createElement('div')
                    SECTION_TITLE.className = 'section_title'

                    const ICON = document.createElement('i')
                    ICON.className = 'ph-icon-' + icon

                    const TEXT = document.createTextNode(' ' + text + ' ')

                    const SUBTITLE = document.createElement('span')
                    SUBTITLE.className = 'subtitle'
                    SUBTITLE.textContent = '( PornHub+ )'

                    SECTION_TITLE.appendChild(ICON)
                    SECTION_TITLE.appendChild(TEXT)
                    SECTION_TITLE.appendChild(SUBTITLE)
                    SECTION_HEADER.appendChild(SECTION_TITLE)

                    return SECTION_HEADER
                }

                function changeVideosVisibility(filter, isHiding) {
                    const VIDEO_BOXES = document.querySelectorAll('.videoBox')

                    for (let i = 0; i < VIDEO_BOXES.length; i++) {
                        const VIDEO_BOX = VIDEO_BOXES[i]
                        const TITLE = VIDEO_BOX.querySelector('.thumbnail-info-wrapper > .title a').title.toLowerCase().trim()

                        for (let j = 0; j < filter.length; j++) {
                            const CURRENT_WORD = filter[j]
                            const WORD_RE = createRegExp(CURRENT_WORD)
                            const IS_VALID = WORD_RE.test(TITLE)

                            if(isHiding && IS_VALID) {
                                VIDEO_BOX.style.display = 'none'
                            } else if(!isHiding && !IS_VALID) {
                                VIDEO_BOX.style.display = ''
                            }
                        }
                    }
                }

                function createRegExp(word) {
                    return new RegExp(`^${word}$|^${word}|${word}$|\\s${word}\\s`)
                }
            }
        }
    }

    class ChangeStars {
        static init() {
            const IS_VALID_PAGE = location.pathname.includes('/pornstar/') || location.pathname.includes('/model/')

            if(IS_VALID_PAGE) {
                window.addEventListener('keydown', e => {
                    if(e.shiftKey && (e.key === 'ArrowRight' || e.key === 'ArrowLeft')) {
                        e.preventDefault()

                        const TYPE_OF_ARROW = e.key === 'ArrowRight' ? '.pornstarPrev' : '.pornstarNext'
                        const ARROW = document.querySelector('.pornstarsNavButtons > ' + TYPE_OF_ARROW)

                        ARROW.click()
                    }
                })
            }
        }
    }

    class Main {
        static init() {
            if(!localStorage.getItem(LOCAL_SETTINGS)) {
                localStorage.setItem(LOCAL_SETTINGS, DEFAULT_SETTINGS)
            }

            const MODULES = [
                CSS,
                ClearBtn,
                AdBlock,
                AdBlockAdvanced,
                PlusSymbol,
                NoAutoplay,
                FastSearch,
                BetterVideoPage,
                Filters,
                ChangeStars
            ]

            initModules(MODULES)

            log('Loaded')
        }
    }

    Main.init()

    function initModules(modules) {
        for (let i = 0; i < modules.length; i++) {
            const MODULE = modules[i]

            initModule(MODULE)
        }
    }

    function initModule(module) {
        try {
            module.init()
        } catch(e) {
            console.error(TITLE, module.name + ' module, has error:', e)
        }
    }

    function toggleVisibility(el) {
        if(el.style.display === 'none') {
            el.style.display = 'block'

            return void 0
        }

        el.style.display = 'none'
    }

    function log(msg) {
        console.log(
            'Porn%cHub%c+',
            'background: #f90;color: #000;border-radius: 3px;padding: 1px;',
            '-',
            msg
        )
    }

    function getSearchParams() {
        return new URLSearchParams(location.search)
    }

    function isVideoPage() {
        const SEARCH_PARAMS = getSearchParams()

        return SEARCH_PARAMS.has('viewkey')
    }

    function addStyle(css) {
        const styleNode = document.createElement('style')
        styleNode.appendChild(document.createTextNode(css))
        document.head.appendChild(styleNode)
    }

    function replaceLocation(newUrl) {
        if(location.replace) {
            return location.replace(newUrl)
        }

        location.href = newUrl
    }
})()