PH - Search & UI Tweaks

Various search filters and user experience enhancers

As of 07/11/2020. See the latest version.

// ==UserScript==
// @name          PH - Search & UI Tweaks
// @namespace     brazenvoid
// @version       2.4.1
// @author        brazenvoid
// @license       GPL-3.0-only
// @description   Various search filters and user experience enhancers
// @include       https://*.pornhub.com/*
// @require       https://greasyfork.org/scripts/375557-base-resource/code/Base%20Resource.js?version=854444
// @grant         GM_addStyle
// @run-at        document-end
// ==/UserScript==

const PAGE_PATH_NAME = window.location.pathname

const IS_FEED_PAGE = PAGE_PATH_NAME.startsWith('/feeds')
const IS_PLAYLIST_PAGE = PAGE_PATH_NAME.startsWith('/playlist')
const IS_PROFILE_PAGE = PAGE_PATH_NAME.startsWith('/model') || PAGE_PATH_NAME.startsWith('/channels') || PAGE_PATH_NAME.startsWith('/user')
const IS_VIDEO_PAGE = PAGE_PATH_NAME.startsWith('/view_video')
const IS_VIDEO_SEARCH_PAGE = PAGE_PATH_NAME.startsWith('/video') || PAGE_PATH_NAME.startsWith('/categories')

const FILTER_BLACKLIST_KEY = 'Blacklist'
const FILTER_HD_VIDEOS_KEY = 'Show Only HD Videos'
const FILTER_PAID_VIDEOS_KEY = 'Hide Paid Videos'
const FILTER_PREMIUM_VIDEOS_KEY = 'Hide Premium Videos'
const FILTER_PRO_CHANNEL_VIDEOS_KEY = 'Hide Pro Channel Videos'
const FILTER_PRIVATE_VIDEOS_KEY = 'Hide Private Videos'
const FILTER_RATING_VIDEOS_KEY = 'Rating'
const FILTER_RECOMMENDED_VIDEOS_KEY = 'Hide Recommended Videos'
const FILTER_SEARCH_KEY = 'Search'
const FILTER_UNRATED_VIDEOS_KEY = 'Hide Unrated Videos'
const FILTER_VERIFIED_VIDEOS_KEY = 'Hide Verified Videos'
const FILTER_VIDEO_DURATION_KEY = 'Duration'
const FILTER_VIDEO_VIEWS_KEY = 'Views'
const FILTER_WATCHED_VIDEOS_KEY = 'Hide Watched Videos'
const FILTER_WHITELIST_KEY = 'Whitelist'

const LINK_DISABLE_PLAYLIST_CONTROLS_KEY = 'Disable Playlist Controls'
const LINK_USER_PUBLIC_VIDEOS_KEY = 'User Public Videos'

const OPTION_ALWAYS_SHOW_UI = 'Always Show This Settings Pane'
const OPTION_DISABLE_VIDEO_FILTERS = 'Disable All Video Filters'
const OPTION_SANITIZATION_KEY = 'Video Names Sanitization Rules'

const UI_REMOVE_IFRAMES = 'Remove Ad IFrames'
const UI_REMOVE_LIVE_MODELS_SECTIONS = 'Remove Live Models Sections'
const UI_REMOVE_PORN_STAR_SECTIONS = 'Remove Porn Star Sections'

class PHSearchAndUITweaks extends BaseHandler
{
    /**
     * @typedef {{hideSDVideos: boolean, removePornStarSections: boolean, rating: {maximum: number, minimum: number}, blacklist: [], hideProChannelVideos: boolean, whitelist: [],
     *     hideVerifiedVideos: boolean, hideUnratedVideos: boolean, duration: {maximum: number, minimum: number}, removeLiveModelsSections: boolean, hideWatchedVideos:
     *     boolean, search: [], linkDisablePlaylistControls: boolean, linkUserPublicVideos: boolean, hideRecommendedVideos: boolean, hidePaidVideos: boolean, removeIFrames:
     *     boolean, hidePrivateVideos: boolean, views: {maximum: number, minimum: number}, hidePremiumVideos: boolean, sanitize: {}, disableItemComplianceValidation: boolean,
     *     showUIAlways: boolean}} PHSUISettings
     */

    /**
     * Run the script
     */
    static initialize ()
    {
        return (new PHSearchAndUITweaks).init()
    }

    constructor ()
    {
        super('ph-sui-', 'videoblock', {
            blacklist: [],
            duration: { // In Seconds
                minimum: 60,
                maximum: 0,
            },
            rating: {
                minimum: 70,
                maximum: 0,
            },
            sanitize: {},
            search: [],
            views: {
                minimum: 0,
                maximum: 0,
            },
            whitelist: [],
            hideSDVideos: false,
            hidePaidVideos: false,
            hidePremiumVideos: false,
            hidePrivateVideos: false,
            hideProChannelVideos: false,
            hideRecommendedVideos: false,
            hideUnratedVideos: false,
            hideVerifiedVideos: false,
            hideWatchedVideos: false,
            linkDisablePlaylistControls: false,
            linkUserPublicVideos: false,
            removeIFrames: false,
            removeLiveModelsSections: false,
            removePornStarSections: false,
        })

        this._optimizedRegex = {
            blacklist: null,
            sanitizationRules: {},
            search: null,
            whitelist: null,
        }

        // UI Events

        this._onBeforeUIBuild = () => {
            this._optimizeRegexBasedFilters()
            this._removeIframes()

            if (IS_VIDEO_PAGE) {
                this._complyPaidVideosSectionOnVideoPage()
                this._removeLoadMoreButtons()
                this._validator.sanitizeNodeOfSelector('.inlineFree', this._optimizedRegex.sanitizationRules)
            } else {
                if (IS_VIDEO_SEARCH_PAGE) {
                    this._removePornStarSectionsFromSearchPage()
                    this._removePremiumSectionFromSearchPage()
                    this._fixLeftOverSpaceOnVideoSearchPage()
                    this._fixPaginationNavOnVideoSearchPage()
                } else {
                    if (IS_PROFILE_PAGE) {
                        this._removeVideoSectionsOnProfilePage()
                    }
                }
            }

            this._removeLiveModelsSections()
        }

        this._onUIBuild = () =>
            this._uiGen.createSection('settings', '#ffa31a', '5vh', '250px').
                addSectionChildren([
                    this._uiGen.createTabsSection(['Filters', 'Text Based', 'Global', 'Stats'], [
                        this._uiGen.createTabPanel('Filters', [
                            this._uiGen.createFormRangeInputGroup(FILTER_VIDEO_DURATION_KEY, 'number'),
                            this._uiGen.createFormRangeInputGroup(FILTER_RATING_VIDEOS_KEY, 'number'),
                            this._uiGen.createFormRangeInputGroup(FILTER_VIDEO_VIEWS_KEY, 'number'),
                            this._uiGen.createBreakSeparator(),
                            this._uiGen.createFormInputGroup(FILTER_HD_VIDEOS_KEY, 'checkbox', 'Hides videos of less than 720p resolution.'),
                            this._uiGen.createFormInputGroup(FILTER_PAID_VIDEOS_KEY, 'checkbox', 'Hide paid videos.'),
                            this._uiGen.createFormInputGroup(FILTER_PREMIUM_VIDEOS_KEY, 'checkbox', 'Hide Premium Only Videos.'),
                            this._uiGen.createFormInputGroup(FILTER_PRIVATE_VIDEOS_KEY, 'checkbox', 'Hide videos needing befriended status.'),
                            this._uiGen.createFormInputGroup(FILTER_PRO_CHANNEL_VIDEOS_KEY, 'checkbox', 'Hide videos from professional channels.'),
                            this._uiGen.createFormInputGroup(FILTER_RECOMMENDED_VIDEOS_KEY, 'checkbox', 'Hide recommended videos.'),
                            this._uiGen.createFormInputGroup(FILTER_UNRATED_VIDEOS_KEY, 'checkbox', 'Hide videos with 0% rating.'),
                            this._uiGen.createFormInputGroup(FILTER_VERIFIED_VIDEOS_KEY, 'checkbox', 'Hide videos from verified users, couples and models.'),
                            this._uiGen.createFormInputGroup(FILTER_WATCHED_VIDEOS_KEY, 'checkbox', 'Hide already watched videos.'),
                            this._uiGen.createSeparator(),
                            this._uiGen.createFormInputGroup(OPTION_DISABLE_VIDEO_FILTERS, 'checkbox', 'Disables all video filters.'),
                            this._uiGen.createSeparator(),
                            this._createSettingsFormActions(),
                            this._uiGen.createSeparator(),
                            this._uiGen.createStoreFormSection(this._settingsStore),
                        ]),
                        this._uiGen.createTabPanel('Text Based', [
                            this._uiGen.createFormTextAreaGroup(FILTER_SEARCH_KEY, 2, 'Show videos with these comma separated words in their names.'),
                            this._uiGen.createFormTextAreaGroup(FILTER_BLACKLIST_KEY, 2, 'Hide videos with these comma separated words in their names.'),
                            this._uiGen.createFormTextAreaGroup(FILTER_WHITELIST_KEY, 2, 'Exempt videos from all filters, with these comma separated words in their names.'),
                            this._uiGen.createSeparator(),
                            this._createSettingsFormActions(),
                            this._uiGen.createSeparator(),
                            this._uiGen.createStoreFormSection(this._settingsStore),
                        ]),
                        this._uiGen.createTabPanel('Global', [
                            this._uiGen.createFormTextAreaGroup(OPTION_SANITIZATION_KEY, 2,
                                'Censor video names by substituting offensive words. Each rule in separate line and target words must be comma separated. Example Rule: boyfriend=stepson,stepdad'),
                            this._uiGen.createSeparator(),
                            this._uiGen.createFormSection('Link Manipulations', [
                                this._uiGen.createFormInputGroup(LINK_DISABLE_PLAYLIST_CONTROLS_KEY, 'checkbox', 'Disable playlist controls on video pages.'),
                                this._uiGen.createFormInputGroup(LINK_USER_PUBLIC_VIDEOS_KEY, 'checkbox', 'Jump directly to public videos on any profile link click.'),
                            ]),
                            this._uiGen.createSeparator(),
                            this._uiGen.createFormSection('UI Manipulations', [
                                this._uiGen.createFormInputGroup(UI_REMOVE_IFRAMES, 'checkbox', 'Removes all ad iframes.'),
                                this._uiGen.createFormInputGroup(UI_REMOVE_LIVE_MODELS_SECTIONS, 'checkbox', 'Remove live model stream sections from search.'),
                                this._uiGen.createFormInputGroup(UI_REMOVE_PORN_STAR_SECTIONS, 'checkbox', 'Remove porn star listing sections from search.'),
                            ]),
                            this._uiGen.createFormInputGroup(OPTION_ALWAYS_SHOW_UI, 'checkbox', 'Always show this interface.'),
                            this._uiGen.createSeparator(),
                            this._createSettingsFormActions(),
                            this._uiGen.createSeparator(),
                            this._uiGen.createStoreFormSection(this._settingsStore),
                        ]),
                        this._uiGen.createTabPanel('Stats', [
                            this._uiGen.createStatisticsFormGroup(FILTER_BLACKLIST_KEY),
                            this._uiGen.createStatisticsFormGroup(FILTER_WHITELIST_KEY),
                            this._uiGen.createStatisticsFormGroup(FILTER_VIDEO_DURATION_KEY),
                            this._uiGen.createStatisticsFormGroup(FILTER_HD_VIDEOS_KEY, 'High Definition'),
                            this._uiGen.createStatisticsFormGroup(FILTER_SEARCH_KEY),
                            this._uiGen.createStatisticsFormGroup(FILTER_PAID_VIDEOS_KEY, 'Paid Videos'),
                            this._uiGen.createStatisticsFormGroup(FILTER_PREMIUM_VIDEOS_KEY, 'Premium Videos'),
                            this._uiGen.createStatisticsFormGroup(FILTER_PRIVATE_VIDEOS_KEY, 'Private Videos'),
                            this._uiGen.createStatisticsFormGroup(FILTER_PRO_CHANNEL_VIDEOS_KEY, 'Pro Channel Videos'),
                            this._uiGen.createStatisticsFormGroup(FILTER_RATING_VIDEOS_KEY),
                            this._uiGen.createStatisticsFormGroup(FILTER_RECOMMENDED_VIDEOS_KEY, 'Recommended'),
                            this._uiGen.createStatisticsFormGroup(FILTER_UNRATED_VIDEOS_KEY, 'Unrated'),
                            this._uiGen.createStatisticsFormGroup(FILTER_VERIFIED_VIDEOS_KEY, 'Verified'),
                            this._uiGen.createStatisticsFormGroup(FILTER_VIDEO_VIEWS_KEY),
                            this._uiGen.createStatisticsFormGroup(FILTER_WATCHED_VIDEOS_KEY, 'Watched'),
                            this._uiGen.createSeparator(),
                            this._uiGen.createStatisticsTotalsGroup(),
                        ]),
                    ]),
                    this._uiGen.createStatusSection(),
                ])

        this._onAfterUIBuild = () => {
            this._complyProfileLinks()
            this._uiGen.getSelectedSection().userScript = this
        }

        // Compliance Events

        this._onBeforeCompliance = (videoItem) => {
            return this._optimizedRegex.whitelist ?
                this._validator.validateTextDoesNotContain(videoItem.querySelector('.title > a').textContent, this._optimizedRegex.whitelist, FILTER_WHITELIST_KEY) : true
        }

        this._onGetItemLists = () => document.querySelectorAll('ul.videos')

        this._complianceFilters = [
            (videoItem) => this._validateSearch(videoItem),
            (videoItem) => this._validateWatchStatus(videoItem),
            (videoItem) => this._validateRating(videoItem),
            (videoItem) => this._validateDuration(videoItem),
            (videoItem) => this._validateViews(videoItem),
            (videoItem) => this._validateHD(videoItem),
            (videoItem) => this._validateVerifiedState(videoItem),
            (videoItem) => this._validateProfessionalChannelVideo(videoItem),
            (videoItem) => this._validatePaidVideo(videoItem),
            (videoItem) => this._validatePremiumVideo(videoItem),
            (videoItem) => this._validatePrivateVideo(videoItem),
            (videoItem) => this._validateRecommendedState(videoItem),
            (videoItem) => this._validateBlacklist(videoItem),
        ]

        this._onFirstHitAfterCompliance = (item) => {
            if (IS_PLAYLIST_PAGE) {
                this._validatePlaylistVideoLink(item)
            }
            this._validator.sanitizeTextNode(item.querySelector('.title > a'), this._optimizedRegex.sanitizationRules)
        }

        // Store Events

        this._onSettingsStoreUpdate = () => {

            /** @type {PHSUISettings} */
            let store = this._settingsStore.get()

            this._uiGen.setSettingsInputCheckedStatus(FILTER_HD_VIDEOS_KEY, store.hideSDVideos)
            this._uiGen.setSettingsInputCheckedStatus(FILTER_PAID_VIDEOS_KEY, store.hidePaidVideos)
            this._uiGen.setSettingsInputCheckedStatus(FILTER_PREMIUM_VIDEOS_KEY, store.hidePremiumVideos)
            this._uiGen.setSettingsInputCheckedStatus(FILTER_PRO_CHANNEL_VIDEOS_KEY, store.hideProChannelVideos)
            this._uiGen.setSettingsInputCheckedStatus(FILTER_PRIVATE_VIDEOS_KEY, store.hidePrivateVideos)
            this._uiGen.setSettingsInputCheckedStatus(FILTER_RECOMMENDED_VIDEOS_KEY, store.hideRecommendedVideos)
            this._uiGen.setSettingsInputCheckedStatus(FILTER_WATCHED_VIDEOS_KEY, store.hideWatchedVideos)
            this._uiGen.setSettingsInputCheckedStatus(FILTER_UNRATED_VIDEOS_KEY, store.hideUnratedVideos)
            this._uiGen.setSettingsInputCheckedStatus(FILTER_VERIFIED_VIDEOS_KEY, store.hideVerifiedVideos)
            this._uiGen.setSettingsInputCheckedStatus(LINK_DISABLE_PLAYLIST_CONTROLS_KEY, store.linkDisablePlaylistControls)
            this._uiGen.setSettingsInputCheckedStatus(LINK_USER_PUBLIC_VIDEOS_KEY, store.linkUserPublicVideos)
            this._uiGen.setSettingsInputCheckedStatus(OPTION_ALWAYS_SHOW_UI, store.showUIAlways)
            this._uiGen.setSettingsInputCheckedStatus(OPTION_DISABLE_VIDEO_FILTERS, store.disableItemComplianceValidation)
            this._uiGen.setSettingsInputCheckedStatus(UI_REMOVE_IFRAMES, store.removeIFrames)
            this._uiGen.setSettingsInputCheckedStatus(UI_REMOVE_LIVE_MODELS_SECTIONS, store.removeLiveModelsSections)
            this._uiGen.setSettingsInputCheckedStatus(UI_REMOVE_PORN_STAR_SECTIONS, store.removePornStarSections)

            this._uiGen.setSettingsInputValue(FILTER_BLACKLIST_KEY, store.blacklist.join(','))
            this._uiGen.setSettingsInputValue(FILTER_SEARCH_KEY, store.search.join(','))
            this._uiGen.setSettingsInputValue(FILTER_WHITELIST_KEY, store.whitelist.join(','))
            this._uiGen.setSettingsInputValue(OPTION_SANITIZATION_KEY, this._transformSanitizationRulesToText(store.sanitize))

            this._uiGen.setSettingsRangeInputValue(FILTER_VIDEO_DURATION_KEY, store.duration.minimum, store.duration.maximum)
            this._uiGen.setSettingsRangeInputValue(FILTER_RATING_VIDEOS_KEY, store.rating.minimum, store.rating.maximum)
            this._uiGen.setSettingsRangeInputValue(FILTER_VIDEO_VIEWS_KEY, store.views.minimum, store.views.maximum)
        }

        this._onSettingsApply = () => {

            /** @type {PHSUISettings} */
            let settings = this._settings

            settings.hideSDVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_HD_VIDEOS_KEY)
            settings.hidePaidVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_PAID_VIDEOS_KEY)
            settings.hidePremiumVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_PREMIUM_VIDEOS_KEY)
            settings.hidePrivateVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_PRIVATE_VIDEOS_KEY)
            settings.hideProChannelVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_PRO_CHANNEL_VIDEOS_KEY)
            settings.hideRecommendedVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_RECOMMENDED_VIDEOS_KEY)
            settings.hideWatchedVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_WATCHED_VIDEOS_KEY)
            settings.hideUnratedVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_UNRATED_VIDEOS_KEY)
            settings.hideVerifiedVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_VERIFIED_VIDEOS_KEY)
            settings.linkDisablePlaylistControls = this._uiGen.getSettingsInputCheckedStatus(LINK_DISABLE_PLAYLIST_CONTROLS_KEY)
            settings.linkUserPublicVideos = this._uiGen.getSettingsInputCheckedStatus(LINK_USER_PUBLIC_VIDEOS_KEY)
            settings.disableItemComplianceValidation = this._uiGen.getSettingsInputCheckedStatus(OPTION_DISABLE_VIDEO_FILTERS)
            settings.showUIAlways = this._uiGen.getSettingsInputCheckedStatus(OPTION_ALWAYS_SHOW_UI)
            settings.removeIFrames = this._uiGen.getSettingsInputCheckedStatus(UI_REMOVE_IFRAMES)
            settings.removeLiveModelsSections = this._uiGen.getSettingsInputCheckedStatus(UI_REMOVE_LIVE_MODELS_SECTIONS)
            settings.removePornStarSections = this._uiGen.getSettingsInputCheckedStatus(UI_REMOVE_PORN_STAR_SECTIONS)

            settings.duration.minimum = this._uiGen.getSettingsRangeInputValue(FILTER_VIDEO_DURATION_KEY, true)
            settings.duration.maximum = this._uiGen.getSettingsRangeInputValue(FILTER_VIDEO_DURATION_KEY, false)
            settings.rating.minimum = this._uiGen.getSettingsRangeInputValue(FILTER_RATING_VIDEOS_KEY, true)
            settings.rating.maximum = this._uiGen.getSettingsRangeInputValue(FILTER_RATING_VIDEOS_KEY, false)
            settings.views.minimum = this._uiGen.getSettingsRangeInputValue(FILTER_VIDEO_VIEWS_KEY, true)
            settings.views.maximum = this._uiGen.getSettingsRangeInputValue(FILTER_VIDEO_VIEWS_KEY, false)

            settings.blacklist = this._trimAndKeepNonEmptyStrings(this._uiGen.getSettingsInputValue(FILTER_BLACKLIST_KEY).split(','))
            settings.search = this._trimAndKeepNonEmptyStrings(this._uiGen.getSettingsInputValue(FILTER_SEARCH_KEY).split(','))
            settings.whitelist = this._trimAndKeepNonEmptyStrings(this._uiGen.getSettingsInputValue(FILTER_WHITELIST_KEY).split(','))

            this._validateAndSetSanitizationRules(this._uiGen.getSettingsInputValue(OPTION_SANITIZATION_KEY).split(/\r?\n/g))
            this._optimizeRegexBasedFilters()
        }

        // After Init

        if (IS_FEED_PAGE) {
            this._onAfterInitialization = () => ChildObserver.create().
                onNodesAdded((itemsAdded) => {
                    let itemsList
                    for (let item of itemsAdded) {
                        if (typeof item.querySelector === 'function') {
                            itemsList = item.querySelector('ul.videos')
                            if (itemsList) {
                                this._complyItemsList(itemsList)
                            }
                        }
                    }
                }).
                observe(document.querySelector('#moreData'))
        }
    }

    /**
     * @private
     */
    _optimizeRegexBasedFilters ()
    {
        this._optimizedRegex.blacklist = this._settings.blacklist.length ? buildWholeWordMatchingRegex(this._settings.blacklist) : null
        this._optimizedRegex.search = this._settings.search.length ? buildWholeWordMatchingRegex(this._settings.search) : null
        this._optimizedRegex.whitelist = this._settings.whitelist.length ? buildWholeWordMatchingRegex(this._settings.whitelist) : null

        if (this._settings.sanitize) {
            for (const substitute in this._settings.sanitize) {
                this._optimizedRegex.sanitizationRules[substitute] = buildWholeWordMatchingRegex(this._settings.sanitize[substitute])
            }
        }
    }

    /**
     * Remove paid videos listing
     * @private
     */
    _complyPaidVideosSectionOnVideoPage ()
    {
        if (this._settings.hidePaidVideos) {
            let paidVideosList = document.querySelector('#p2vVideosVPage')
            if (paidVideosList) {
                paidVideosList.remove()
            }
        }
    }

    /**
     * Changes profile links to directly point to public video listings
     * @private
     */
    _complyProfileLinks ()
    {
        if (this._settings.linkUserPublicVideos) {
            let userProfileLinks = document.querySelectorAll('.usernameBadgesWrapper a, a.usernameLink, .usernameWrap a'), href
            for (let userProfileLink of userProfileLinks) {
                href = userProfileLink.getAttribute('href')
                if (href.startsWith('/channels') || href.startsWith('/model')) {
                    userProfileLink.setAttribute('href', href + '/videos')
                } else {
                    if (href.startsWith('/user')) {
                        userProfileLink.setAttribute('href', href + '/videos/public')
                    }
                }
            }
        }
    }

    /**
     * Fixes left over space after ads removal
     * @private
     */
    _fixLeftOverSpaceOnVideoSearchPage ()
    {
        for (let div of document.querySelectorAll('.showingCounter, .tagsForWomen')) {
            div.style.height = 'auto'
        }
    }

    /**
     * Fixes pagination nav by moving it under video items list
     * @private
     */
    _fixPaginationNavOnVideoSearchPage ()
    {
        document.querySelector('.nf-videos').appendChild(document.querySelector('.pagination3'))
    }

    /**
     * Removes any IFrames being displayed by going over the page repeatedly till none exist
     * @private
     */
    _removeIframes ()
    {
        let removeMilkTruckIframes = () => {
            let iframes = document.getElementsByTagName('milktruck')
            for (let iframe of iframes) {
                iframe.remove()
            }
            return iframes.length
        }

        if (this._settings.removeIFrames) {
            Validator.iFramesRemover()
            let iframesCount
            do {
                iframesCount = removeMilkTruckIframes()
            } while (iframesCount)
        }
    }

    _removeLoadMoreButtons ()
    {
        document.querySelectorAll('.more_recommended_btn, #loadMoreRelatedVideosCenter').forEach((button) => button.remove())
    }

    /**
     * @private
     */
    _removeLiveModelsSections ()
    {
        if (this._settings.removeLiveModelsSections) {
            for (let section of document.querySelectorAll('.streamateContent')) {
                section.closest('.sectionWrapper').remove()
            }
        }
    }

    /**
     * @private
     */
    _removePornStarSectionsFromSearchPage ()
    {
        if (this._settings.removePornStarSections) {
            let section = document.querySelector('#relatedPornstarSidebar')
            if (section) {
                section.remove()
            }
        }
    }

    /**
     * @private
     */
    _removePremiumSectionFromSearchPage ()
    {
        if (this._settings.hidePremiumVideos) {
            let section = Array.from(document.querySelectorAll('.nf-videos .sectionWrapper')).pop()
            if (section && section.querySelector('h2')) {
                section.remove()
            }
        }
    }

    /**
     * Removes premium video sections from profiles
     * @private
     */
    _removeVideoSectionsOnProfilePage ()
    {
        const videoSections = [
            {setting: this._settings.hidePaidVideos, linkSuffix: FILTER_PAID_VIDEOS_KEY},
            {setting: this._settings.hidePremiumVideos, linkSuffix: 'fanonly'},
            {setting: this._settings.hidePrivateVideos, linkSuffix: FILTER_PRIVATE_VIDEOS_KEY},
        ]
        for (let videoSection of videoSections) {
            let videoSectionLink = document.querySelector('.videoSection > div > div > h2 > a[href$="/' + videoSection.linkSuffix + '"]')
            if (videoSectionLink !== null) {
                videoSectionLink.closest('.videoSection').style.display = videoSection.setting ? 'none' : 'block'
            }
        }
    }

    /**
     * Validates non-existence of blacklisted words in the video name
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validateBlacklist (videoItem)
    {
        return this._optimizedRegex.blacklist ?
            this._validator.validateTextDoesNotContain(videoItem.querySelector('.title > a').textContent, this._optimizedRegex.blacklist, FILTER_BLACKLIST_KEY) : true
    }

    /**
     * Validates video duration
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validateDuration (videoItem)
    {
        if (this._settings.duration.minimum > 0 || this._settings.duration.maximum > 0) {

            let durationNode = videoItem.querySelector('.duration')
            if (durationNode !== null) {
                let duration = durationNode.textContent.split(':')
                duration = (parseInt(duration[0]) * 60) + parseInt(duration[1])

                return this._validator.validateRange(FILTER_VIDEO_DURATION_KEY, duration, [this._settings.duration.minimum, this._settings.duration.maximum])
            }
        }
        return true
    }

    /**
     * Validate video quality
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validateHD (videoItem)
    {
        return this._settings.hideSDVideos ? this._validator.validateNodeExistence(FILTER_HD_VIDEOS_KEY, videoItem, '.hd-thumbnail') : true
    }

    /**
     * Validate paid video status
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validatePaidVideo (videoItem)
    {
        return this._settings.hidePaidVideos ? this._validator.validateNodeNonExistence(FILTER_PAID_VIDEOS_KEY, videoItem, '.p2v-icon, .fanClubVideoWrapper') : true
    }

    /**
     * Validate and change playlist video links
     * @param {Node|HTMLElement} videoItem
     * @private
     */
    _validatePlaylistVideoLink (videoItem)
    {
        if (this._settings.linkDisablePlaylistControls) {
            let playlistLinks = videoItem.querySelectorAll('a.linkVideoThumb, span.title a')
            for (let playlistLink of playlistLinks) {
                playlistLink.setAttribute('href', playlistLink.getAttribute('href').replace(/&pkey.*/, ''))
            }
        }
    }

    /**
     * Validate premium video status
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validatePremiumVideo (videoItem)
    {
        return this._settings.hidePremiumVideos ? this._validator.validateNodeNonExistence(FILTER_PREMIUM_VIDEOS_KEY, videoItem, '.premiumIcon') : true
    }

    /**
     * Validate private video status
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validatePrivateVideo (videoItem)
    {
        return this._settings.hidePrivateVideos ? this._validator.validateNodeNonExistence(FILTER_PRIVATE_VIDEOS_KEY, videoItem, '.privateOverlay') : true
    }

    /**
     * Validate whether video is provided by a professional porn channel
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validateProfessionalChannelVideo (videoItem)
    {
        return this._settings.hideProChannelVideos ? this._validator.validateNodeNonExistence(FILTER_PRO_CHANNEL_VIDEOS_KEY, videoItem, '.channel-icon') : true
    }

    /**
     * Validate video rating
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validateRating (videoItem)
    {
        let validationCheck = true

        if (this._settings.rating.minimum > 0 || this._settings.rating.maximum > 0) {

            let rating = videoItem.querySelector('.value')
            let isUnratedVideo = false

            if (rating === null) {
                isUnratedVideo = true
            } else {
                rating = parseInt(rating.textContent.replace('%', ''))
                if (rating === 0) {
                    isUnratedVideo = true
                } else {
                    validationCheck = this._validator.validateRange(FILTER_RATING_VIDEOS_KEY, rating, [this._settings.rating.minimum, this._settings.rating.maximum])
                }
            }
            if (isUnratedVideo && this._settings.hideUnratedVideos) {
                validationCheck = false
                this._statistics.record(FILTER_UNRATED_VIDEOS_KEY, validationCheck)
            }
        }
        return validationCheck
    }

    /**
     * Validate recommended video status
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validateRecommendedState (videoItem)
    {
        return this._settings.hideRecommendedVideos ? this._validator.validateNodeNonExistence(FILTER_RECOMMENDED_VIDEOS_KEY, videoItem, '.recommendedFor') : true
    }

    /**
     * Validates existence of searched words in the video name
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validateSearch (videoItem)
    {
        return this._optimizedRegex.search ?
            this._validator.validateTextContains(videoItem.querySelector('.title > a').textContent, this._optimizedRegex.search, FILTER_SEARCH_KEY) : true
    }

    /**
     * Validate verified status
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validateVerifiedState (videoItem)
    {
        return this._settings.hideVerifiedVideos ? this._validator.validateNodeNonExistence(FILTER_VERIFIED_VIDEOS_KEY, videoItem, '.own-video-thumbnail') : true
    }

    /**
     * Validate video view count
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validateViews (videoItem)
    {
        if (this._settings.views.minimum > 0 || this._settings.views.maximum > 0) {

            let viewsCountString = videoItem.querySelector('.views').textContent.replace(' views', '')
            let viewsCountMultiplier = 1
            let viewsCountStringLength = viewsCountString.length

            if (viewsCountString[viewsCountStringLength - 1] === 'K') {
                viewsCountMultiplier = 1000
                viewsCountString = viewsCountString.replace('K', '')
            } else {
                if (viewsCountString[viewsCountStringLength - 1] === 'M') {
                    viewsCountMultiplier = 1000000
                    viewsCountString = viewsCountString.replace('M', '')
                }
            }
            let viewsCount = parseFloat(viewsCountString) * viewsCountMultiplier

            return this._validator.validateRange(FILTER_VIDEO_VIEWS_KEY, viewsCount, [this._settings.views.minimum, this._settings.views.maximum])
        }
        return true
    }

    /**
     * Validate watched video status
     * @param {Node|HTMLElement} videoItem
     * @return {boolean}
     * @private
     */
    _validateWatchStatus (videoItem)
    {
        return this._settings.hideWatchedVideos ? this._validator.validateNodeNonExistence(FILTER_WATCHED_VIDEOS_KEY, videoItem, '.watchedVideoText') : true
    }
}

PHSearchAndUITweaks.initialize()