PH - Search & UI Tweaks

Various search filters and user experience enhancers

Verzia zo dňa 15.12.2018. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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