PH - Search & UI Tweaks

Various search filters and user experience enhancers

Versión del día 14/8/2020. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name          PH - Search & UI Tweaks
// @namespace     brazenvoid
// @version       1.16.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=835825
// @grant         GM_addStyle
// @run-at        document-end
// ==/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', 'stepson'],
        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,
    linkDisablePlaylistControls: false,
    linkUserPublicVideos: false,
    removeIFrames: true,
    removeLiveModelsSection: true,
    removePornStarsListingInSidebar: true,
    showUIAlways: false,
    debugLogging: false,
}

const IS_PLAYLIST_PAGE = window.location.pathname.startsWith('/playlist')

const FILTER_DURATION_KEY = 'Duration'
const FILTER_HD_VIDEOS_KEY = 'Only HD Videos'
const FILTER_PAID_VIDEOS_KEY = 'Paid'
const FILTER_PREMIUM_VIDEOS_KEY = 'Premium'
const FILTER_PRO_CHANNEL_VIDEOS_KEY = 'Pro Channel'
const FILTER_PRIVATE_VIDEOS_KEY = 'Private'
const FILTER_RATING_KEY = 'Rating'
const FILTER_UNRATED_VIDEOS_KEY = 'Unrated'
const FILTER_VIEWS_KEY = 'Views'
const FILTER_WATCHED_VIDEOS_KEY = 'Watched'
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 UI'

// 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(FILTER_DURATION_KEY, store.duration.minimum, store.duration.maximum)
    UI.setSettingsRangeInputValue(FILTER_RATING_KEY, store.rating.minimum, store.rating.maximum)
    UI.setSettingsRangeInputValue(FILTER_VIEWS_KEY, store.views.minimum, store.views.maximum)
    UI.setSettingsInputCheckedStatus(OPTION_ALWAYS_SHOW_UI, store.showUIAlways)
    UI.setSettingsInputCheckedStatus(FILTER_HD_VIDEOS_KEY, store.hideSDVideos)
    UI.setSettingsInputCheckedStatus(FILTER_PAID_VIDEOS_KEY, store.hidePaidVideos)
    UI.setSettingsInputCheckedStatus(FILTER_PREMIUM_VIDEOS_KEY, store.hidePremiumVideos)
    UI.setSettingsInputCheckedStatus(FILTER_PRO_CHANNEL_VIDEOS_KEY, store.hideProChannelVideos)
    UI.setSettingsInputCheckedStatus(FILTER_PRIVATE_VIDEOS_KEY, store.hidePrivateVideos)
    UI.setSettingsInputCheckedStatus(FILTER_WATCHED_VIDEOS_KEY, store.hideWatchedVideos)
    UI.setSettingsInputCheckedStatus(FILTER_UNRATED_VIDEOS_KEY, store.hideUnratedVideos)
    UI.setSettingsInputCheckedStatus(LINK_DISABLE_PLAYLIST_CONTROLS_KEY, store.linkDisablePlaylistControls)
    UI.setSettingsInputCheckedStatus(LINK_USER_PUBLIC_VIDEOS_KEY, store.linkUserPublicVideos)
}
storage.onDefaultsLoaded = refreshUI
storage.onRetrieval = refreshUI
storage.onUpdated = refreshUI

// Validators
// -- Duration validation

let validateDuration = (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(FILTER_DURATION_KEY, duration, [settings.duration.minimum, settings.duration.maximum])
        }
    }
    return true
}

// -- High definition validation

let validateHD = (videoItem) => {

    let validationCheck = true

    if (settings.hideSDVideos) {

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

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

// -- Paid video validation

let validatePaidVideo = (videoItem) => {

    let validationCheck = true

    if (settings.hidePaidVideos) {

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

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

// -- Premium video validation

let validatePremiumVideo = (videoItem) => {

    let validationCheck = true

    if (settings.hidePremiumVideos) {

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

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

// -- Private video validation

let validatePrivateVideo = (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 = (videoItem) => {

    let validationCheck = true

    if (settings.hideProChannelVideos) {

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

        statistics.record(FILTER_PRO_CHANNEL_VIDEOS_KEY, validationCheck)
    }
    return validationCheck
}

// -- Playlist link manipulation

let validateVideoLink = (videoItem) => {

    if (settings.linkDisablePlaylistControls) {
        let playlistLinks = videoItem.querySelectorAll('a.linkVideoThumb, span.title a')
        for (let playlistLink of playlistLinks) {
            playlistLink.setAttribute('href', playlistLink.getAttribute('href').replace(/&pkey.*/, ''))
        }
    }
}

// -- Rating validation

let validateRating = (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(FILTER_RATING_KEY, validationCheck)
        } else {
            rating = parseInt(rating.textContent.replace('%', ''))
            if (rating === 0) {
                validationCheck = !settings.hideUnratedVideos
            } else {
                validationCheck = validator.validateRange(FILTER_RATING_KEY, rating, [
                    settings.rating.minimum,
                    settings.rating.maximum,
                ])
            }
        }
    }
    return validationCheck
}

// -- View count validation

let validateViews = (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(FILTER_VIEWS_KEY, viewsCount, [
            settings.views.minimum,
            settings.views.maximum,
        ])
    }
    return true
}

// -- Watched video validation

let validateWatchStatus = (videoItem) => {

    let validationCheck = true

    if (settings.hideWatchedVideos) {

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

        statistics.record(FILTER_WATCHED_VIDEOS_KEY, validationCheck)
    }
    return validationCheck
}

// -- Compliance logic

let complianceCallback = (target) => {

    complyVideoSections()

    let videoItems, videoName, videoComplies

    if (target instanceof NodeList) {
        videoItems = []
        target.forEach((node) => {
            if (typeof node.classList !== 'undefined' && node.classList.contains('videoblock')) {
                videoItems.push(node)
            }
        })
    } else {
        videoItems = target.querySelectorAll('.videoblock')
    }

    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)

            if (IS_PLAYLIST_PAGE) {
                validateVideoLink(videoItem)
            }
        } else {
            videoItem.style.display = 'none'
        }

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

// UI Composition
// -- Control Panel

let section = UI.createSection('settings', '#ffa31a', '5vh', '240px').
    addSectionChildren([
        UI.createFormRangeInputGroup(FILTER_DURATION_KEY, 'number', [
            settings.duration.minimum,
            settings.duration.maximum,
        ]),
        UI.createFormRangeInputGroup(FILTER_RATING_KEY, 'number', [
            settings.rating.minimum,
            settings.rating.maximum,
        ]),
        UI.createFormRangeInputGroup(FILTER_VIEWS_KEY, 'number', [
            settings.views.minimum,
            settings.views.maximum,
        ]),
        UI.createFormInputGroup(
            OPTION_ALWAYS_SHOW_UI, 'checkbox', settings.showUIAlways, 'Always show this interface.'),
        UI.createFormInputGroup(
            FILTER_HD_VIDEOS_KEY, 'checkbox', settings.hideSDVideos, 'Show only HD videos.'),
        UI.createSeparator(),
        UI.createFormSection('Hide Videos', [
            UI.createFormActions([
                UI.createFormInputGroup(
                    FILTER_PAID_VIDEOS_KEY, 'checkbox', settings.hidePaidVideos, 'Hide paid videos.'),
                UI.createFormInputGroup(
                    FILTER_PREMIUM_VIDEOS_KEY, 'checkbox', settings.hidePremiumVideos, 'Hide Premium Only Videos.'),
                UI.createFormInputGroup(
                    FILTER_PRIVATE_VIDEOS_KEY, 'checkbox', settings.hidePrivateVideos, 'Hide videos needing befriended status.'),
            ]),
            UI.createFormActions([
                UI.createFormInputGroup(
                    FILTER_PRO_CHANNEL_VIDEOS_KEY, 'checkbox', settings.hideProChannelVideos, 'Hide videos from professional channels.'),
                UI.createFormInputGroup(
                    FILTER_WATCHED_VIDEOS_KEY, 'checkbox', settings.hideWatchedVideos, 'Hide already watched videos.'),
            ]),
            UI.createFormActions([
                UI.createFormInputGroup(
                    FILTER_UNRATED_VIDEOS_KEY, 'checkbox', settings.hideUnratedVideos, 'Hide videos with 0% rating.'),
            ]),
        ]),
        UI.createSeparator(),
        UI.createFormSection('Link Changes', [
            UI.createFormInputGroup(
                LINK_DISABLE_PLAYLIST_CONTROLS_KEY, 'checkbox', settings.linkDisablePlaylistControls, 'Disable playlist controls on video pages.'),
            UI.createFormInputGroup(
                LINK_USER_PUBLIC_VIDEOS_KEY, 'checkbox', settings.linkUserPublicVideos, 'Jump directly to user public videos on any profile link click.'),
        ]),
        UI.createSeparator(),
        UI.createSettingsFormActions(storage, () => {
            settings.duration.minimum = UI.getSettingsRangeInputValue(FILTER_DURATION_KEY, true)
            settings.duration.maximum = UI.getSettingsRangeInputValue(FILTER_DURATION_KEY, false)
            settings.rating.minimum = UI.getSettingsRangeInputValue(FILTER_RATING_KEY, true)
            settings.rating.maximum = UI.getSettingsRangeInputValue(FILTER_RATING_KEY, false)
            settings.views.minimum = UI.getSettingsRangeInputValue(FILTER_VIEWS_KEY, true)
            settings.views.maximum = UI.getSettingsRangeInputValue(FILTER_VIEWS_KEY, false)
            settings.showUIAlways = UI.getSettingsInputCheckedStatus(OPTION_ALWAYS_SHOW_UI)
            settings.hideSDVideos = UI.getSettingsInputCheckedStatus(FILTER_HD_VIDEOS_KEY)
            settings.hidePaidVideos = UI.getSettingsInputCheckedStatus(FILTER_PAID_VIDEOS_KEY)
            settings.hidePremiumVideos = UI.getSettingsInputCheckedStatus(FILTER_PREMIUM_VIDEOS_KEY)
            settings.hidePrivateVideos = UI.getSettingsInputCheckedStatus(FILTER_PRIVATE_VIDEOS_KEY)
            settings.hideProChannelVideos = UI.getSettingsInputCheckedStatus(FILTER_PRO_CHANNEL_VIDEOS_KEY)
            settings.hideWatchedVideos = UI.getSettingsInputCheckedStatus(FILTER_WATCHED_VIDEOS_KEY)
            settings.hideUnratedVideos = UI.getSettingsInputCheckedStatus(FILTER_UNRATED_VIDEOS_KEY)
            settings.linkDisablePlaylistControls = UI.getSettingsInputCheckedStatus(LINK_DISABLE_PLAYLIST_CONTROLS_KEY)
            settings.linkUserPublicVideos = UI.getSettingsInputCheckedStatus(LINK_USER_PUBLIC_VIDEOS_KEY)

            statistics.reset()
            for (let videoList of getVideoLists()) {
                complianceCallback(videoList)
            }
        }),
        UI.createSeparator(),
        UI.createStoreFormSection(storage),
        UI.createSeparator(),
        UI.createStatisticsFormGroup('SD', 'Low Res'),
        UI.createStatisticsFormGroup(FILTER_DURATION_KEY, 'Short'),
        UI.createStatisticsFormGroup(FILTER_RATING_KEY, 'Low Rated'),
        UI.createStatisticsFormGroup(FILTER_VIEWS_KEY, 'By Views'),
        UI.createStatisticsFormGroup('Blacklist', 'Blacklisted'),
        UI.createStatisticsFormGroup(FILTER_PRO_CHANNEL_VIDEOS_KEY, FILTER_PRO_CHANNEL_VIDEOS_KEY),
        UI.createStatisticsFormGroup(FILTER_WATCHED_VIDEOS_KEY, FILTER_WATCHED_VIDEOS_KEY),
        UI.createStatisticsFormGroup('Total'),
        UI.createSeparator(),
        UI.createStatusSection(),
    ])
UI.constructor.appendToBody(section)
UI.constructor.appendToBody(UI.createSettingsShowButton('', 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, FILTER_PAID_VIDEOS_KEY)
    complyVideoSection(settings.hidePremiumVideos, 'fanonly')
    complyVideoSection(settings.hidePrivateVideos, FILTER_PRIVATE_VIDEOS_KEY)
}

// -- Initial compliance run & observer attachment

for (let list of getVideoLists()) {
    ChildObserver.create().onNodesAdded((nodes) => complianceCallback(nodes)).observe(list)
    complianceCallback(list)
}

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 < 5) {
            sleep(1000).then(() => recommendedVideosHandler(waitIteration))
        }
    } else {
        complianceCallback(document.querySelector('#relateRecommendedItems'))
    }
}
if (document.querySelector('#relateRecommendedItems')) {
    sleep(2000).then(() => recommendedVideosHandler())
}

validator.sanitizeVideoPage('.inlineFree')

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

// -- IFrames removal

let removePHIframes = () => {

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

// -- Link Manipulations

if (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')
            }
        }
    }
}

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