// ==UserScript==
// @name PH - Search & UI Tweaks
// @namespace brazenvoid
// @version 1.15.2
// @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=835825
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
// Settings & Defaults
let settings = {
blacklist: [ // case-insensitive
// 'ebony',
// 'arab',
],
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,
removeIFrames: true,
removeLiveModelsSection: true,
removePornStarsListingInSidebar: true,
showUIAlways: false,
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 = (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 = (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('Pro Channel', validationCheck)
}
return validationCheck
}
// -- 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('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 = (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 = (videoItem) => {
let validationCheck = true
if (settings.hideWatchedVideos) {
validationCheck = videoItem.querySelector('.watchedVideoText') === null
statistics.record('Watched', 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)
} else {
videoItem.style.display = 'none'
}
logger.logSeparator()
}
statistics.updateUI()
}
// UI Composition
// -- Control Panel
let section = UI.createSection('settings', '#ffa31a', '5vh', '240px').
addSectionChildren([
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, () => {
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('', 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
for (let list of getVideoLists()) {
ChildObserver.create().onNodesAdded((nodes) => complianceCallback(nodes)).observe(list)
complianceCallback(list)
}
let recommendedVideosHandler = (waitIteration = 1) => {
let button = document.querySelector('.more_related_btn')
button.click()
button.click()
if (button.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)
}
// -- 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.')
}