PH - Search & UI Tweaks

Various search filters and user experience enhancers

Fra 15.12.2018. Se den seneste versjonen.

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       1.7.1
// @author        brazenvoid
// @license       GPL-3.0-only
// @description   Various search filters and user experience enhancers
// @include       https://www.pornhub.com/*
// @run-at	      document-end
// ==/UserScript==

// Settings & Defaults

let settings = {
    blacklist: [ // case-insensitive
        'urin',
        'arab',
        'muslim',
        'desi',
        'squirt',
        'fake',
        'pregnant',
        'pee',
    ],
    sanitize: { // Substitutes values with key (case-insensitive)
        ' ': ['neighbor', 'step'],
        'Boyfriend': ['brother', 'bro', 'daddy', 'dad'],
        'Girlfriend': ['daughter', 'mom', 'mother', 'sister', 'sis']
    },
    duration: [60, 0], // In Seconds
    rating: [80, 0],
    views: [0, 0],
    fixCounterHeight: true,
    hideSDVideos: true,
    hidePrivateVideos: true,
    hideUnratedVideos: true,
    hideWatchedVideos: false,
    removeIFrames: true,
    removeLiveModelsSection: true,
    removePornStarsListingInSidebar: true,
    showUIAlways: false, // Desktop/Tablet
    debugLogging: false
};

// Base Methods
// -- Selector naming

const selectorPrefix = 'ph-sui-';

let getSelector = function (selector) {
    return selectorPrefix + selector;
};
let getStatLabelSelector = function (type) {
    return getSelector(type + '-stat')
};

// -- Logging

let log = function (message) {
    if (settings.debugLogging) {
        console.log(message);
    }
};
let logAction = function (action) {
    log('Completed: ' + action);
    logSeparator();
};
let logValidationResult = function (filterName, statusVariable = null) {
    log('Satisfies ' + filterName + ' Filter: ' + (statusVariable ? 'true' : 'false'));
};
let logSeparator = function () {
    log('------------------------------------------------------------------------------------');
};
let logVideoName = function (videoName) {
    log('Checking Video: ' + videoName);
};

// -- Statistics

let statistics;

let recordStatistic = function (validationStatus, type) {

    if (!validationStatus) {
        if (typeof statistics[type] !== 'undefined') {
            statistics[type] += 1;
        } else {
            statistics[type] = 1;
        }
        statistics.total += 1;
    }
};
let resetStatistic = function () {
    statistics = {total: 0};
};
let updateStatisticUI = function () {

    let statLabel;
    for (const statisticType in statistics) {
        statLabel = document.getElementById(getStatLabelSelector(statisticType));
        statLabel.textContent = statistics[statisticType];
    }
};

// -- UI

let addFormButton = function (caption, onClick) {

    let button = document.createElement('button');
    button.textContent = caption;
    button.style.height = '30px';
    button.style.width = '100%';
    button.addEventListener('click', onClick);

    return button;
};
let addFormGroup = function () {

    let divFormGroup = document.createElement('div');
    divFormGroup.style.display = 'block';
    divFormGroup.style.height = '18px';
    divFormGroup.style.marginBottom = '2px';
    divFormGroup.style.padding = '5px 0';

    return divFormGroup;
};
let addFormGroupLabel = function (label, id) {

    let labelFormGroup = document.createElement('label');
    labelFormGroup.style.float = 'left';
    labelFormGroup.style.padding = '2px 0';
    labelFormGroup.setAttribute('for', getSelector(id));
    labelFormGroup.textContent = label + ': ';

    return labelFormGroup;
};
let addFormGroupStatLabel = function (statisticType) {

    let labelFormGroup = document.createElement('label');
    labelFormGroup.id = getStatLabelSelector(statisticType);
    labelFormGroup.style.float = 'right';
    labelFormGroup.style.padding = '2px 0';
    labelFormGroup.textContent = '0';

    return labelFormGroup;
};
let addFormGroupInput = function (id, defaultValue) {
    let inputFormGroup = document.createElement('input');
    inputFormGroup.id = getSelector(id);
    inputFormGroup.style.float = 'right';
    inputFormGroup.style.width = '100px';
    inputFormGroup.style.textAlign = 'center';
    inputFormGroup.value = defaultValue;

    return inputFormGroup;
};
let addSeparator = function () {

    let separator = document.createElement('hr');
    separator.style.margin = '5px 5px 0 5px';

    return separator;
};
let addSettingsFormGroup = function (label, id, defaultValue) {

    let divFormGroup = addFormGroup();
    let labelFormGroup = addFormGroupLabel(label, id);
    let inputFormGroup = addFormGroupInput(id, defaultValue);

    divFormGroup.appendChild(labelFormGroup);
    divFormGroup.appendChild(inputFormGroup);

    return divFormGroup;
};
let addStatisticsFormGroup = function (label, id) {

    let divFormGroup = addFormGroup();
    let labelFormGroup = addFormGroupLabel('Filtered ' + label + ' Videos', id);
    let statLabelFormGroup = addFormGroupStatLabel(id);

    divFormGroup.appendChild(labelFormGroup);
    divFormGroup.appendChild(statLabelFormGroup);

    return divFormGroup;
};

// -- Filters
// -- -- Range Filtering

let rangeFilter = function (value, bounds) {

    let validationCheck = true;

    if (bounds[0] > 0) {
        validationCheck = value >= bounds[0];
    }
    if (bounds[1] > 0) {
        validationCheck = value <= bounds[1];
    }
    return validationCheck;
};

// -- -- Blacklist validation

let validateBlacklist = function (videoItem, videoName) {

    let validationCheck = true;

    if (settings.blacklist.length > 0) {

        for (const blacklistedWord of settings.blacklist) {

            validationCheck = videoName.match(blacklistedWord) === null;

            if (!validationCheck) {
                break;
            }
        }

        logValidationResult('Blacklist', validationCheck);
        recordStatistic(validationCheck, 'blacklisted');
    }
    return validationCheck;
};

// -- -- Duration validation

let validateDuration = function (videoItem) {

    let validationCheck = true;

    if (settings.duration[0] > 0 || settings.duration[1] > 0) {

        let duration = videoItem.querySelector('.duration').textContent.split(':');
        duration = (parseInt(duration[0]) * 60) + parseInt(duration[1]);

        validationCheck = rangeFilter(duration, settings.duration);

        logValidationResult('Duration', validationCheck);
        recordStatistic(validationCheck, 'short');
    }
    return validationCheck;
};

// -- -- High definition validation

let validateHD = function (videoItem) {

    let validationCheck = true;

    if (settings.hideSDVideos) {

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

        logValidationResult('HD', validationCheck);
    }
    return validationCheck;
};

// -- -- Private video validation

let validatePrivateVideo = function (videoItem) {

    let validationCheck = true;

    if (settings.hidePrivateVideos) {

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

        logValidationResult('Private Video', validationCheck);
    }
    return validationCheck;
};

// -- -- Rating validation

let validateRating = function (videoItem) {

    let validationCheck = true;

    if (settings.rating[0] > 0 || settings.rating[1] > 0) {

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

        if (rating === null) {
            validationCheck = !settings.hideUnratedVideos;
        } else {
            rating = parseInt(rating.textContent.replace('%', ''));
            validationCheck = rangeFilter(rating, settings.rating);
        }

        logValidationResult('Rating', validationCheck);
        recordStatistic(validationCheck, 'rating');
    }
    return validationCheck;
};

// -- -- Video name sanitation

let sanitationFilter = function (content) {

    for (const substitute in settings.sanitize) {
        for (const subject of settings.sanitize[substitute]) {
            content = content.replace(subject, substitute);
        }
    }
    return content;
};
let sanitizeVideoPage = function () {

    let videoTitle = document.querySelector('.inlineFree');
    if (videoTitle !== null) {

        let sanitizedVideoName = sanitationFilter(videoTitle.textContent);
        videoTitle.textContent = sanitizedVideoName;
        document.title = sanitizedVideoName;
    }

};
let sanitizeVideoItem = function (videoItem, videoName) {

    videoItem.querySelector('.title > a').textContent = sanitationFilter(videoName);
};

// -- -- View count validation

let validateViews = function (videoItem) {

    let validationCheck = true;

    if (settings.views[0] > 0 || settings.views[1] > 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;
        validationCheck = rangeFilter(viewsCount, settings.views);

        logValidationResult('View Count', validationCheck);
        recordStatistic(validationCheck, 'views');
    }
    return validationCheck;
};

// -- -- Watched video validation

let validateWatchStatus = function (videoItem) {

    let validationCheck = true;

    if (settings.hideWatchedVideos) {

        validationCheck = videoItem.querySelector('.watchedVideoOverlay') === null;

        logValidationResult('Watched', validationCheck);
        recordStatistic(validationCheck, 'watched');
    }
    return validationCheck;
};

// -- Compliance logic

let complianceCallback = function (target) {

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

    resetStatistic();

    for (let videoItem of videoItems) {

        videoName = videoItem.querySelector('.title > a').textContent;
        logVideoName(videoName);

        videoComplies =
            validatePrivateVideo(videoItem) &&
            validateWatchStatus(videoItem) &&
            validateHD(videoItem) &&
            validateRating(videoItem) &&
            validateBlacklist(videoItem, videoName) &&
            validateDuration(videoItem) &&
            validateViews(videoItem);

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

        logSeparator();
    }
    updateStatisticUI();
};

// UI Composition
// -- Settings Dialog
// -- -- Section

let section = document.createElement('section');
section.id = getSelector('settings');
section.style.color = 'black';
section.style.display = settings.showUIAlways ? 'block' : 'none';
section.style.fontWeight = 'bold';
section.style.position = 'fixed';
section.style.top = '250px';
section.style.left = '0';
section.style.width = '200px';
section.style.padding = '5px 10px';
section.style.backgroundColor = '#ffa31a';
section.style.zIndex = '1000';

// -- -- Settings

let durationSection = addSettingsFormGroup('Min Duration', 'min-duration', settings.duration[0].toString());
let ratingSection = addSettingsFormGroup('Min Rating', 'min-rating', settings.rating[0].toString());
let viewsSection = addSettingsFormGroup('Min Views', 'min-views', settings.views[0].toString());
let applyButton = addFormButton('Apply', function () {

    let videoLists = document.querySelectorAll('.videos');
    settings.duration[0] = document.getElementById(getSelector('min-duration')).value;
    settings.rating[0] = document.getElementById(getSelector('min-rating')).value;
    settings.views[0] = document.getElementById(getSelector('min-views')).value;

    for (let videoList of videoLists) {
        complianceCallback(videoList);
    }
});

// -- -- Statistics

let BlacklistedStatSection = addStatisticsFormGroup('Blacklisted', 'blacklisted');
let lowRatedStatSection = addStatisticsFormGroup('Low Rated', 'rating');
let lowViewsStatSection = addStatisticsFormGroup('By Views', 'views');
let shortStatSection = addStatisticsFormGroup('Short', 'short');
let watchedVideosStatSection = addStatisticsFormGroup('Watched', 'watched');
let totalVideosStatSection = addStatisticsFormGroup('', 'total');

// -- -- Hide Button

let hideButton = addFormButton('Hide', function () {
    document.getElementById(getSelector('settings')).style.display = 'none';
});

// -- -- Composition

section.appendChild(durationSection);
section.appendChild(ratingSection);
section.appendChild(viewsSection);
section.appendChild(applyButton);
section.appendChild(addSeparator());
section.appendChild(shortStatSection);
section.appendChild(lowRatedStatSection);
section.appendChild(lowViewsStatSection);
section.appendChild(BlacklistedStatSection);
section.appendChild(watchedVideosStatSection);
section.appendChild(totalVideosStatSection);
section.appendChild(hideButton);

let bodyTag = document.getElementsByTagName('body')[0];
bodyTag.appendChild(section);

// -- Settings Button
// -- -- Section

let controlButton = document.createElement('button');
controlButton.textContent = 'Search & Tweaks';
controlButton.style.width = '100%';
controlButton.style.margin = '2px 5px';
controlButton.style.padding = '2px 5px';
controlButton.style.backgroundColor = '#ffa31a';
controlButton.style.border = '0';

// -- -- Events

controlButton.addEventListener('click', function () {

    let settingsUI = document.getElementById(getSelector('settings'));
    settingsUI.style.display = settingsUI.style.display === 'none' ? 'block' : 'none';
});

// -- -- Composition

let controlListItem = document.createElement('li');
controlListItem.appendChild(controlButton);

let networkBarList = document.querySelector('.networkListContent');
networkBarList.appendChild(controlListItem);

logAction('Building UI');

// Optimizations and prep
// -- Pre-building regular expressions

for (let i = 0; i < settings.blacklist.length; i++) {
    settings.blacklist[i] = new RegExp(settings.blacklist[i], 'ig');
}
for (const substitute in settings.sanitize) {
    for (let i = 0; i < settings.sanitize[substitute].length; i++) {
        settings.sanitize[substitute][i] = new RegExp(settings.sanitize[substitute][i], 'ig');
    }
}

// -- Move pagination section

let videosSection = document.querySelector('.nf-videos');
if (videosSection !== null) {
    let paginationBlock = document.querySelector('.pagination3');
    videosSection.appendChild(paginationBlock);
}

// Script run
// -- Initial compliance run & observer attachment

let observer = new MutationObserver(function (mutations) {
    for (let mutation of mutations) {
        complianceCallback(mutation.target);
    }
});

let observerConfig = {
    attributes: false,
    childList: true,
    subtree: false
};

let videoLists = document.querySelectorAll('ul.videos');
for (let videoList of videoLists) {
    complianceCallback(videoList);
    observer.observe(videoList, observerConfig);
}
sanitizeVideoPage();

logAction('Initial run and observer attachment.');

// -- Fix Counter Height

if (settings.fixCounterHeight) {

    let counter = document.querySelector('.showingCounter');

    if (counter !== null) {
        counter.style.height = 'auto';
    }
    logAction('Fix counter height.');
}

// -- IFrames Removal

if (settings.removeIFrames) {

    let iFramesFilter = function (target) {

        let iFrames = target.getElementsByTagName('iFrame');
      
        for (let iFrame of iFrames) {
            iFrame.remove();
        }
        logAction('IFrames removal.');
    };
    let iFramesObserver = new MutationObserver(function (mutations) {
        for (let mutation of mutations) {
            iFramesFilter(mutation.target);
        }
    });
    let iFrameParents = document.querySelectorAll('.sectionWrapper, #videoSearchResult');
    for (let iFrameParent of iFrameParents) {
        iFramesFilter(iFrameParent);
        iFramesObserver.observe(iFrameParent, observerConfig);
    }
}

// -- Live Models Section Removal

if (settings.removeLiveModelsSection) {

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

// -- Porn stars listing in sidebar removal

if (settings.removePornStarsListingInSidebar) {

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