PH - Search & UI Tweaks

Various search filters and user experience enhancers

Ekde 2018/12/15. Vidu La ĝisdata versio.

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