PH - Search & UI Tweaks

Various search filters and user experience enhancers

As of 2020-05-06. See the latest version.

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

// Settings & Defaults

let settings = {
    blacklist: [ // case-insensitive
        'asmr',
        'urine',
        'arab',
        'mmd',
        'muslim',
        'desi',
        'fake',
        'pee',
        'granny',
    ],
    sanitize: { // Substitutes values with key (case-insensitive)
        '': ['neighbor', 'step'],
        boyfriend: ['brother', 'bro', 'daddy', 'dad', 'son', 'father', 'stepbro', 'stepbrother'],
        girlfriend: ['daughter', 'mom', 'mother', 'sister', 'sis', 'stepsis', 'stepsister', 'stepdaughter'],
    },
    duration: { // In Seconds
        minimum: 60,
        maximum: 0,
    },
    rating: {
        minimum: 70,
        maximum: 0,
    },
    views: {
        minimum: 0,
        maximum: 0,
    },
    hideSDVideos: false,
    hidePaidVideos: true,
    hidePremiumVideos: true,
    hidePrivateVideos: true,
    hideProChannelVideos: true,
    hideUnratedVideos: false,
    hideWatchedVideos: false,
    removeIFrames: true,
    removeLiveModelsSection: true,
    removePornStarsListingInSidebar: true,
    showUIAlways: false, // Desktop
    debugLogging: false,
}

// Base Resources Initialization

const scriptPrefix = 'ph-sui-'

let storage = new LocalStore(scriptPrefix + 'settings', settings)
settings = storage.retrieve().get()

let logger = new Logger(settings.debugLogging)
let selectorGenerator = new SelectorGenerator(scriptPrefix)
let statistics = new StatisticsRecorder(logger, selectorGenerator)
let UI = new UIGenerator(settings.showUIAlways, selectorGenerator)

let validator = new Validator(statistics)
validator.addBlacklistFilter(settings.blacklist).addSanitizationFilter(settings.sanitize).optimize()

function getVideoLists ()
{
    return document.querySelectorAll('ul.videos')
}

// Local Store Events

let refreshUI = function () {
    let store = this.get()
    UI.setSettingsRangeInputValue('Duration', store.duration.minimum, store.duration.maximum)
    UI.setSettingsRangeInputValue('Rating', store.rating.minimum, store.rating.maximum)
    UI.setSettingsRangeInputValue('Views', store.views.minimum, store.views.maximum)
    UI.setSettingsInputCheckedStatus('Always Show UI', store.showUIAlways)
    UI.setSettingsInputCheckedStatus('Only HD Videos', store.hideSDVideos)
    UI.setSettingsInputCheckedStatus('Paid', store.hidePaidVideos)
    UI.setSettingsInputCheckedStatus('Premium', store.hidePremiumVideos)
    UI.setSettingsInputCheckedStatus('Pro Channel', store.hideProChannelVideos)
    UI.setSettingsInputCheckedStatus('Private', store.hidePrivateVideos)
    UI.setSettingsInputCheckedStatus('Watched', store.hideWatchedVideos)
    UI.setSettingsInputCheckedStatus('Unrated', store.hideUnratedVideos)
}
storage.onDefaultsLoaded = refreshUI
storage.onRetrieval = refreshUI
storage.onUpdated = refreshUI

// Validators
// -- Duration validation

let validateDuration = function (videoItem) {

    if (settings.duration.minimum > 0 || 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 validator.validateRange('Duration', duration, [
                settings.duration.minimum,
                settings.duration.maximum,
            ])
        }
    }
    return true
}

// -- High definition validation

let validateHD = function (videoItem) {

    let validationCheck = true

    if (settings.hideSDVideos) {

        validationCheck = videoItem.querySelector('.hd-thumbnail') !== null

        statistics.record('SD', validationCheck)
    }
    return validationCheck
}

// -- Paid video validation

let validatePaidVideo = function (videoItem) {

    let validationCheck = true

    if (settings.hidePaidVideos) {

        validationCheck = videoItem.querySelector('.p2v-icon') === null

        logger.logValidation('Paid Video', validationCheck)
    }
    return validationCheck
}

// -- Premium video validation

let validatePremiumVideo = function (videoItem) {

    let validationCheck = true

    if (settings.hidePremiumVideos) {

        validationCheck = videoItem.querySelector('.premiumIcon') === null

        logger.logValidation('Premium Video', validationCheck)
    }
    return validationCheck
}

// -- Private video validation

let validatePrivateVideo = function (videoItem) {

    let validationCheck = true

    if (settings.hidePrivateVideos) {

        validationCheck = videoItem.querySelector('.privateOverlay') === null

        logger.logValidation('Private Video', validationCheck)
    }
    return validationCheck
}

// -- Professional channel video validation

let validateProfessionalChannelVideo = function (videoItem) {

    let validationCheck = true

    if (settings.hideProChannelVideos) {

        validationCheck = videoItem.querySelector('.channel-icon') === null

        statistics.record('Pro Channel', validationCheck)
    }
    return validationCheck
}

// -- Rating validation

let validateRating = function (videoItem) {

    let validationCheck = true

    if (settings.rating.minimum > 0 || settings.rating.maximum > 0) {

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

        if (rating === null) {
            validationCheck = !settings.hideUnratedVideos
            statistics.record('Rating', validationCheck)
        } else {
            rating = parseInt(rating.textContent.replace('%', ''))
            if (rating === 0) {
                validationCheck = !settings.hideUnratedVideos
            } else {
                validationCheck = validator.validateRange('Rating', rating, [
                    settings.rating.minimum,
                    settings.rating.maximum,
                ])
            }
        }
    }
    return validationCheck
}

// -- View count validation

let validateViews = function (videoItem) {

    if (settings.views.minimum > 0 || 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 validator.validateRange('Views', viewsCount, [
            settings.views.minimum,
            settings.views.maximum,
        ])
    }
    return true
}

// -- Watched video validation

let validateWatchStatus = function (videoItem) {

    let validationCheck = true

    if (settings.hideWatchedVideos) {

        validationCheck = videoItem.querySelector('.watchedVideoText') === null

        statistics.record('Watched', validationCheck)
    }
    return validationCheck
}

// -- Compliance logic

let complianceCallback = function (target) {

    complyVideoSections()

    let videoItems = target.querySelectorAll('.videoblock')
    let videoName, videoComplies

    for (let videoItem of videoItems) {

        videoName = videoItem.querySelector('.title > a')
        logger.logVideoCheck(videoName.textContent)

        videoComplies =
            validatePaidVideo(videoItem) &&
            validatePremiumVideo(videoItem) &&
            validatePrivateVideo(videoItem) &&
            validateProfessionalChannelVideo(videoItem) &&
            validateWatchStatus(videoItem) &&
            validateHD(videoItem) &&
            validateRating(videoItem) &&
            validator.validateBlackList(videoName.textContent) &&
            validateDuration(videoItem) &&
            validateViews(videoItem)

        if (videoComplies) {
            videoItem.style.display = 'inline-block'
            validator.sanitizeVideoItem(videoName)
        } else {
            videoItem.style.display = 'none'
        }

        logger.logSeparator()
    }
    statistics.updateUI()
}

// UI Composition
// -- Control Panel

let section = UI.createSection('settings', '#ffa31a', '150px', '240px').
    addSectionChildren([
        UI.createSettingsHideButton(),
        UI.createSeparator(),
        UI.createFormRangeInputGroup('Duration', 'number', [
            settings.duration.minimum,
            settings.duration.maximum,
        ]),
        UI.createFormRangeInputGroup('Rating', 'number', [
            settings.rating.minimum,
            settings.rating.maximum,
        ]),
        UI.createFormRangeInputGroup('Views', 'number', [
            settings.views.minimum,
            settings.views.maximum,
        ]),
        UI.createFormInputGroup(
            'Always Show UI', 'checkbox', settings.showUIAlways, 'Always show this interface.'),
        UI.createFormInputGroup(
            'Only HD Videos', 'checkbox', settings.hideSDVideos, 'Show only HD videos.'),
        UI.createSeparator(),
        UI.createFormSection('Hide Videos', [
            UI.createFormActions([
                UI.createFormInputGroup(
                    'Paid', 'checkbox', settings.hidePaidVideos, 'Hide paid videos.'),
                UI.createFormInputGroup(
                    'Premium', 'checkbox', settings.hidePremiumVideos, 'Hide Premium Only Videos.'),
                UI.createFormInputGroup(
                    'Private', 'checkbox', settings.hidePrivateVideos, 'Hide videos needing befriended status.'),
            ]),
            UI.createFormActions([
                UI.createFormInputGroup(
                    'Pro Channel', 'checkbox', settings.hideProChannelVideos, 'Hide videos from professional channels.'),
                UI.createFormInputGroup(
                    'Watched', 'checkbox', settings.hideWatchedVideos, 'Hide already watched videos.'),
            ]),
            UI.createFormActions([
                UI.createFormInputGroup(
                    'Unrated', 'checkbox', settings.hideUnratedVideos, 'Hide videos with 0% rating.'),
            ]),
        ]),
        UI.createSeparator(),
        UI.createSettingsFormActions(storage, function () {
            settings.duration.minimum = UI.getSettingsRangeInputValue('Duration', true)
            settings.duration.maximum = UI.getSettingsRangeInputValue('Duration', false)
            settings.rating.minimum = UI.getSettingsRangeInputValue('Rating', true)
            settings.rating.maximum = UI.getSettingsRangeInputValue('Rating', false)
            settings.views.minimum = UI.getSettingsRangeInputValue('Views', true)
            settings.views.maximum = UI.getSettingsRangeInputValue('Views', false)
            settings.showUIAlways = UI.getSettingsInputCheckedStatus('Always Show UI')
            settings.hideSDVideos = UI.getSettingsInputCheckedStatus('Only HD Videos')
            settings.hidePaidVideos = UI.getSettingsInputCheckedStatus('Paid')
            settings.hidePremiumVideos = UI.getSettingsInputCheckedStatus('Premium')
            settings.hidePrivateVideos = UI.getSettingsInputCheckedStatus('Private')
            settings.hideProChannelVideos = UI.getSettingsInputCheckedStatus('Pro Channel')
            settings.hideWatchedVideos = UI.getSettingsInputCheckedStatus('Watched')
            settings.hideUnratedVideos = UI.getSettingsInputCheckedStatus('Unrated')

            statistics.reset()
            for (let videoList of getVideoLists()) {
                complianceCallback(videoList)
            }
        }),
        UI.createSeparator(),
        UI.createStoreFormSection(storage),
        UI.createSeparator(),
        UI.createStatisticsFormGroup('SD', 'Low Res'),
        UI.createStatisticsFormGroup('Duration', 'Short'),
        UI.createStatisticsFormGroup('Rating', 'Low Rated'),
        UI.createStatisticsFormGroup('Views', 'By Views'),
        UI.createStatisticsFormGroup('Blacklist', 'Blacklisted'),
        UI.createStatisticsFormGroup('Pro Channel', 'Pro Channel'),
        UI.createStatisticsFormGroup('Watched', 'Watched'),
        UI.createStatisticsFormGroup('Total'),
        UI.createSeparator(),
        UI.createStatusSection(),
    ])
UI.constructor.appendToBody(section)
UI.constructor.appendToBody(UI.createSettingsShowButton('Search & Tweaks', section))

logger.logTaskCompletion('Building UI')

// -- Move pagination section and other fixes

let videosSection = document.querySelector('.nf-videos')
if (videosSection !== null) {

    let paginationBlock = document.querySelector('.pagination3')
    videosSection.appendChild(paginationBlock)
}

// -- Fix search section div heights

for (let div of document.querySelectorAll('.showingCounter, .tagsForWomen')) {
    div.style.height = 'auto'
}

// Script run
// -- Videos sections removal

function complyVideoSections ()
{
    function complyVideoSection (setting, linkSuffix)
    {
        let videoSectionLink = document.querySelector('.videoSection > div > div > h2 > a[href$="/' + linkSuffix + '"]')
        if (videoSectionLink !== null) {
            videoSectionLink.closest('.videoSection').style.display = setting ? 'none' : 'block'
        }
    }

    complyVideoSection(settings.hidePaidVideos, 'paid')
    complyVideoSection(settings.hidePremiumVideos, 'fanonly')
    complyVideoSection(settings.hidePrivateVideos, 'private')
}

// -- Initial compliance run & observer attachment

ChildObserver.observe(getVideoLists(), complianceCallback, true)
validator.sanitizeVideoPage('.inlineFree')

UI.updateStatus('Initial run completed.')
logger.logTaskCompletion('Initial run and observer attachment.')

// -- IFrames removal

let removePHIframes = function () {

    let iframes = document.getElementsByTagName('milktruck')
    for (let iframe of iframes) {
        iframe.remove()
    }
    return iframes.length
}

if (settings.removeIFrames) {
    Validator.iFramesRemover()
    let iframesCount
    do {
        iframesCount = removePHIframes()
    } while (iframesCount)
}

// -- Live models section removal

if (settings.removeLiveModelsSection) {

    let liveModelStreamsSection = document.querySelector('.streamateContent')
    if (liveModelStreamsSection !== null) {
        liveModelStreamsSection.closest('.sectionWrapper').remove()
    }
    logger.logTaskCompletion('Live model section removal.')
}

// -- Porn stars listing in sidebar removal

if (settings.removePornStarsListingInSidebar) {

    let pornStarsSection = document.getElementById('relatedPornstarSidebar')
    if (pornStarsSection !== null) {
        pornStarsSection.remove()
    }
    logger.logTaskCompletion('Sidebar porn start listing removal.')
}