Sleazy Fork is available in English.

PH - Search & UI Tweaks

Various search filters and user experience enhancers

La data de 31-08-2020. Vezi ultima versiune.

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          PH - Search & UI Tweaks
// @namespace     brazenvoid
// @version       2.1.2
// @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=842141
// @grant         GM_addStyle
// @run-at        document-end
// ==/UserScript==

const PAGE_PATH_NAME = window.location.pathname

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')

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_UNRATED_VIDEOS_KEY = 'Hide Unrated Videos'
const FILTER_VIDEO_DURATION_KEY = 'Duration'
const FILTER_VIDEO_VIEWS_KEY = 'Views'
const FILTER_WATCHED_VIDEOS_KEY = 'Hide Watched Videos'

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 = 'Sanitization Rules'

const SCRIPT_PREFIX = 'ph-sui-'

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
{
    static initialize ()
    {
        return (new PHSearchAndUITweaks).init()
    }

    constructor ()
    {        
        /**
         * Local storage store with defaults
         * @type {LocalStore}
         * @private
         */
        this._settingsStore = (new LocalStore(SCRIPT_PREFIX + 'settings', {
            blacklist: [],
            sanitize: {},
            duration: { // In Seconds
                minimum: 60,
                maximum: 0,
            },
            rating: {
                minimum: 70,
                maximum: 0,
            },
            views: {
                minimum: 0,
                maximum: 0,
            },
            disableVideoFilters: false,
            hideSDVideos: false,
            hidePaidVideos: true,
            hidePremiumVideos: true,
            hidePrivateVideos: true,
            hideProChannelVideos: true,
            hideRecommendedVideos: true,
            hideUnratedVideos: false,
            hideWatchedVideos: false,
            linkDisablePlaylistControls: false,
            linkUserPublicVideos: false,
            removeIFrames: true,
            removeLiveModelsSections: true,
            removePornStarSections: true,
            showUIAlways: false,
        }))

        /**
         * @type {{hideSDVideos: boolean, removePornStarSections: boolean, disableVideoFilters: boolean, rating: {maximum: number, minimum: number}, blacklist: [],
         *     hideProChannelVideos: boolean, hideUnratedVideos: boolean, duration: {maximum: number, minimum: number}, removeLiveModelsSections: boolean, hideWatchedVideos:
         *     boolean, linkDisablePlaylistControls: boolean, linkUserPublicVideos: boolean, hideRecommendedVideos: boolean, hidePaidVideos: boolean, removeIFrames: boolean,
         *     showUIAlways: boolean, hidePrivateVideos: boolean, views: {maximum: number, minimum: number}, hidePremiumVideos: boolean, sanitize: {}}}
         * @private
         */
        this._settings = this._settingsStore.retrieve().get()

        /**
         * @type {StatisticsRecorder}
         * @private
         */
        this._statistics = new StatisticsRecorder(SCRIPT_PREFIX)

        /**
         * @type {UIGenerator}
         * @private
         */
        this._uiGen = new UIGenerator(this._settings.showUIAlways, SCRIPT_PREFIX)

        /**
         * @type {Validator}
         * @private
         */
        this._validator = (new Validator(this._statistics)).setBlacklist(this._settings.blacklist).setSanitizationRules(this._settings.sanitize)

        /**
         * @type {Function[]}
         * @private
         */
        this._videoFilters = [
            (videoItem) => this._validateWatchStatus(videoItem),
            (videoItem) => this._validateRating(videoItem),
            (videoItem) => this._validateDuration(videoItem),
            (videoItem) => this._validateViews(videoItem),
            (videoItem) => this._validateHD(videoItem),
            (videoItem) => this._validateProfessionalChannelVideo(videoItem),
            (videoItem) => this._validatePaidVideo(videoItem),
            (videoItem) => this._validatePremiumVideo(videoItem),
            (videoItem) => this._validatePrivateVideo(videoItem),
            (videoItem) => this._validateRecommendedState(videoItem),
            (videoItem) => this._validator.validateBlackList(videoItem.querySelector('.title > a').textContent),
        ]
    }

    /**
     * Generates the settings UI
     * @private
     */
    _buildUI ()
    {
        let section = this._uiGen.createSection('this._settings', '#ffa31a', '5vh', '250px').
            addSectionChildren([
                this._uiGen.createTabsSection(['Videos', 'Censoring', 'Global'], [
                    this._uiGen.createTabPanel('Videos', [
                        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_WATCHED_VIDEOS_KEY, 'checkbox', 'Hide already watched videos.'),
                        this._uiGen.createFormInputGroup(FILTER_UNRATED_VIDEOS_KEY, 'checkbox', 'Hide videos with 0% rating.'),
                        this._uiGen.createSeparator(),
                        this._uiGen.createFormInputGroup(OPTION_DISABLE_VIDEO_FILTERS, 'checkbox', 'Disables all video filters.'),
                        this._uiGen.createSeparator(),
                        this._uiGen.createSettingsFormActions(this._settingsStore, () => this._onUIApplyClick()),
                    ]),
                    this._uiGen.createTabPanel('Censoring', [
                        this._uiGen.createFormTextAreaGroup(FILTER_BLACKLIST_KEY, 2, 'Hide videos with these comma separated words in their names.'),
                        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.createSettingsFormActions(this._settingsStore, () => this._onUIApplyClick()),
                    ]),
                    this._uiGen.createTabPanel('Global', [
                        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._uiGen.createSettingsFormActions(this._settingsStore, () => this._onUIApplyClick()),
                        this._uiGen.createSeparator(),
                        this._uiGen.createStoreFormSection(this._settingsStore),
                    ]),
                ]),
                this._uiGen.createStatisticsFormGroup(FILTER_BLACKLIST_KEY),
                this._uiGen.createStatisticsFormGroup(FILTER_VIDEO_DURATION_KEY),
                this._uiGen.createStatisticsFormGroup(FILTER_HD_VIDEOS_KEY, 'High Definition'),
                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_UNRATED_VIDEOS_KEY, 'Unrated'),
                this._uiGen.createStatisticsFormGroup(FILTER_VIDEO_VIEWS_KEY),
                this._uiGen.createStatisticsFormGroup(FILTER_WATCHED_VIDEOS_KEY, 'Watched'),
                this._uiGen.createStatisticsTotalsGroup(),
                this._uiGen.createSeparator(),
                this._uiGen.createStatusSection(),
            ])
        this._uiGen.constructor.appendToBody(section)
        this._uiGen.constructor.appendToBody(this._uiGen.createSettingsShowButton('', section, true, () => {
            if (!this._settings.showUIAlways) {
                section.style.display = 'none'
            }
        }))
        this._onSettingsStoreUpdate()
    }

    /**
     * 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')
                    }
                }
            }
        }
    }

    /**
     * Filters recommended videos list
     * @private
     */
    _complyRecommendedVideosListOnVideoPage ()
    {
        let recommendedVideosLoadMoreButton = document.querySelector('.more_related_btn')
        recommendedVideosLoadMoreButton.removeAttribute('href')

        let recommendedVideosHandler = (waitIteration = 1) => {
            recommendedVideosLoadMoreButton.click()
            recommendedVideosLoadMoreButton.click()

            if (recommendedVideosLoadMoreButton.style.display !== 'none') {
                waitIteration += 1
                if (waitIteration < 30) {
                    sleep(1000).then(() => recommendedVideosHandler(waitIteration))
                }
            } else {
                this._complyVideoList(document.querySelector('#relateRecommendedItems'))
            }
        }
        if (document.querySelector('#relateRecommendedItems')) {
            sleep(2000).then(() => recommendedVideosHandler())
        }
    }

    /**
     * Filters videos as per settings
     * @param target
     * @private
     */
    _complyVideoList (target)
    {
        for (let videoItem of this._getVideoItemsFromVideoList(target)) {

            if (typeof videoItem.PHSUIProcessedOnce === 'undefined') {
                videoItem.PHSUIProcessedOnce = false
            }

            this._processVideoCompliance(videoItem)

            if (!videoItem.PHSUIProcessedOnce) {
                if (IS_PLAYLIST_PAGE) {
                    this._validatePlaylistVideoLink(videoItem)
                }
                this._validator.sanitizeVideoItem(videoItem.querySelector('.title > a'))
                videoItem.PHSUIProcessedOnce = true
            }

            this._statistics.updateUI()
        }
    }

    /**
     * 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'))
    }

    /**
     * @return {NodeListOf<Element>}
     * @private
     */
    _getVideoLists ()
    {
        return document.querySelectorAll('ul.videos')
    }

    /**
     * @param {Node|HTMLElement} videoList
     * @return {Node[]|HTMLElement[]}
     * @private
     */
    _getVideoItemsFromVideoList (videoList)
    {
        let videoItems = []
        if (videoList instanceof NodeList) {
            videoList.forEach((node) => {
                if (typeof node.classList !== 'undefined' && node.classList.contains('videoblock')) {
                    videoItems.push(node)
                }
            })
        } else {
            videoItems = videoList.querySelectorAll('.videoblock')
        }
        return videoItems
    }

    /**
     * Initializes settings UI again when settings store experiences any change
     * @private
     */
    _onSettingsStoreUpdate ()
    {
        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(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.disableVideoFilters)
        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(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)
    }

    /**
     * @private
     */
    _onUIApplyClick ()
    {
        this._settings.hideSDVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_HD_VIDEOS_KEY)
        this._settings.hidePaidVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_PAID_VIDEOS_KEY)
        this._settings.hidePremiumVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_PREMIUM_VIDEOS_KEY)
        this._settings.hidePrivateVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_PRIVATE_VIDEOS_KEY)
        this._settings.hideProChannelVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_PRO_CHANNEL_VIDEOS_KEY)
        this._settings.hideRecommendedVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_RECOMMENDED_VIDEOS_KEY)
        this._settings.hideWatchedVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_WATCHED_VIDEOS_KEY)
        this._settings.hideUnratedVideos = this._uiGen.getSettingsInputCheckedStatus(FILTER_UNRATED_VIDEOS_KEY)
        this._settings.linkDisablePlaylistControls = this._uiGen.getSettingsInputCheckedStatus(LINK_DISABLE_PLAYLIST_CONTROLS_KEY)
        this._settings.linkUserPublicVideos = this._uiGen.getSettingsInputCheckedStatus(LINK_USER_PUBLIC_VIDEOS_KEY)
        this._settings.disableVideoFilters = this._uiGen.getSettingsInputCheckedStatus(OPTION_DISABLE_VIDEO_FILTERS)
        this._settings.showUIAlways = this._uiGen.getSettingsInputCheckedStatus(OPTION_ALWAYS_SHOW_UI)
        this._settings.removeIFrames = this._uiGen.getSettingsInputCheckedStatus(UI_REMOVE_IFRAMES)
        this._settings.removeLiveModelsSections = this._uiGen.getSettingsInputCheckedStatus(UI_REMOVE_LIVE_MODELS_SECTIONS)
        this._settings.removePornStarSections = this._uiGen.getSettingsInputCheckedStatus(UI_REMOVE_PORN_STAR_SECTIONS)

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

        this._validateAndSetBlacklistedWords(this._uiGen.getSettingsInputValue(FILTER_BLACKLIST_KEY).split(','))
        this._validateAndSetSanitizationRules(this._uiGen.getSettingsInputValue(OPTION_SANITIZATION_KEY).split(/\r?\n/g))
        this._statistics.reset()

        for (let videoList of this._getVideoLists()) {
            this._complyVideoList(videoList)
        }
    }

    _processVideoCompliance (videoItem)
    {
        let videoComplies = true

        if (!this._settings.disableVideoFilters) {
            for (let videoFilter of this._videoFilters) {
                if (!videoFilter(videoItem)) {
                    videoComplies = false
                    break
                }
            }
        }
        videoItem.style.display = videoComplies ? 'inline-block' : 'none'
    }

    /**
     * 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)
        }
    }

    /**
     * @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()
            }
        }
    }

    /**
     * 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'
            }
        }
    }

    /**
     * @param {Object} sanitizationRules
     * @return {string}
     * @private
     */
    _transformSanitizationRulesToText (sanitizationRules)
    {
        let sanitizationRulesText = []
        for (let substitute in sanitizationRules) {
            sanitizationRulesText.push(substitute + '=' + sanitizationRules[substitute].join(','))
        }
        return sanitizationRulesText.join('\n')
    }

    /**
     * @param {string[]} strings
     * @private
     */
    _trimAndKeepNonEmptyStrings (strings)
    {
        let nonEmptyStrings = []
        for (let string of strings) {
            string = string.trim()
            if (string !== '') {
                nonEmptyStrings.push(string)
            }
        }
        return nonEmptyStrings
    }

    /**
     * @param {string[]} blacklistedWords
     * @private
     */
    _validateAndSetBlacklistedWords (blacklistedWords)
    {
        this._settings.blacklist = this._trimAndKeepNonEmptyStrings(blacklistedWords)
        this._validator.setBlacklist(this._settings.blacklist)
    }

    /**
     * @param {string[]} sanitizationRules
     * @private
     */
    _validateAndSetSanitizationRules (sanitizationRules)
    {
        let fragments, validatedTargetWords
        this._settings.sanitize = {}

        for (let sanitizationRule of sanitizationRules) {
            if (sanitizationRule.includes('=')) {

                fragments = sanitizationRule.split('=')
                if (fragments[0] === '') {
                    fragments[0] = ' '
                }

                validatedTargetWords = this._trimAndKeepNonEmptyStrings(fragments[1].split(','))
                if (validatedTargetWords.length) {
                    this._settings.sanitize[fragments[0]] = validatedTargetWords
                }
            }
        }
        this._validator.setSanitizationRules(this._settings.sanitize)
    }

    /**
     * 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 ? !videoItem.classList.contains('.recommendedVideos') : 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
    }

    /**
     * Initialize the script and do basic UI removals
     */
    init ()
    {
        this._removeIframes()

        if (IS_PROFILE_PAGE) {
            this._removeVideoSectionsOnProfilePage()
        }
        if (IS_VIDEO_PAGE) {
            this._complyPaidVideosSectionOnVideoPage()
            this._complyRecommendedVideosListOnVideoPage()
            this._validator.sanitizeVideoPage('.inlineFree')
        }
        if (IS_VIDEO_SEARCH_PAGE) {
            this._removePornStarSectionsFromSearchPage()
            this._fixLeftOverSpaceOnVideoSearchPage()
            this._fixPaginationNavOnVideoSearchPage()
        }

        this._removeLiveModelsSections()
        this._buildUI()
        this._complyProfileLinks()

        for (let videoList of this._getVideoLists()) {
            ChildObserver.create().onNodesAdded((videoItemsAdded) => this._complyVideoList(videoItemsAdded)).observe(videoList)
            this._complyVideoList(videoList)
        }
        this._uiGen.updateStatus('Initial run completed.')

        this._settingsStore.onChange(() => this._onSettingsStoreUpdate())
    }
}

PHSearchAndUITweaks.initialize()