XNXX - Search Filters

Various search filters

As of 2020-12-11. See the latest version.

// ==UserScript==
// @name         XNXX - Search Filters
// @namespace    brazenvoid
// @version      4.1.4
// @author       brazenvoid
// @license      GPL-3.0-only
// @description  Various search filters
// @include      https://www.xnxx.com/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js
// @require      https://greasyfork.org/scripts/375557-base-resource/code/Base%20Resource.js?version=879318
// @require      https://greasyfork.org/scripts/416104-brazen-ui-generator/code/Brazen%20UI%20Generator.js?version=877816
// @require      https://greasyfork.org/scripts/416105-brazen-base-search-enhancer/code/Brazen%20Base%20Search%20Enhancer.js?version=879317
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

GM_addStyle(`#settings-wrapper{background-color:#ffa31a;top:20vh;width:270px}.form-group{margin-bottom:0}label{margin-bottom:0}input.form-input,select.form-input,textarea.form-input{background-color:white;font-size:inherit}`)

const PAGE_PATH_NAME = window.location.pathname

const IS_HOME_PAGE = PAGE_PATH_NAME === ''
const IS_VIDEO_PAGE = PAGE_PATH_NAME.startsWith('/video-')

const ITEM_CLASSES = 'thumb-block:not(.thumb-cat)'

const FILTER_VIDEO_RESOLUTION = 'Minimum Resolution'
const FILTER_VIDEOS_RATING = 'Rating'
const FILTER_VIDEOS_DURATION = 'Duration'
const FILTER_VIDEOS_VIEWS = 'Views'

const VIDEO_DURATION_KEY = 'XNSSSFDuration'
const VIDEO_NAME_KEY = 'XNSSSFName'
const VIDEO_RATING_KEY = 'XNSSSFRating'
const VIDEO_RESOLUTION_KEY = 'XNSSSFResolution'
const VIDEO_VIEWS_KEY = 'XNSSSFViews'

class XNXXSearchFilters extends BrazenBaseSearchEnhancer
{
    constructor ()
    {
        super('xnxx-sf-', ITEM_CLASSES, false)

        this._configurationManager.
            addRadiosGroup(FILTER_VIDEO_RESOLUTION, ['Show All', 'SD 360p', 'SD 480p', 'HD 720p', 'HD 1080p'], 'Filter videos by minimum resolution.').
            addRangeField(FILTER_VIDEOS_DURATION, 0, 100000, 'Filter videos by duration.').
            addRangeField(FILTER_VIDEOS_RATING, 0, 100, 'Filter videos by ratings.').
            addRangeField(FILTER_VIDEOS_VIEWS, 0, 10000000, 'Filter videos by view count.').
            addRulesetField(FILTER_TEXT_BLACKLIST, 'Hide videos with specified phrases in their names. Separate the phrases with line breaks.').
            addRulesetField(FILTER_TEXT_SANITIZATION, 'Censor video names by substituting offensive phrases. Each rule in separate line with comma separated target phrases. Example Rule: boyfriend=stepson,stepdad').
            addRulesetField(FILTER_TEXT_WHITELIST, 'Show videos with specified phrases in their names. Separate the phrases with line breaks.')

        this._paginator = Paginator.create($('.pagination'), '.mozaique', ITEM_CLASSES, $('.pagination a:not(.no-page):last').attr('href')).
            onGetPageNoFromUrl((url) => {
                let pageNo = url.split('/').pop().replace(/[^0-9]/g, '')
                return pageNo === '' ? 1 : pageNo
            }).
            onGetPageUrlFromPageNo((newPageNo) => {
                let currentUrl = window.location.href
                let currentUrlSegments = currentUrl.split('/')
                if (currentUrlSegments[currentUrlSegments.length - 1].replace(REGEX_PRESERVE_NUMBERS, '') !== '') {
                    currentUrlSegments.pop()
                }
                currentUrlSegments.push(currentUrl + '/' + newPageNo)
                return currentUrlSegments.join('/')
            }).
            onGetPaginationElementForPageNo((pageNo, paginator) => {
                let elementSelector = pageNo === paginator.getCurrentPageNo() ? 'a.active' : 'a[href="' + paginator.getPageUrlFromPageNo(pageNo) + '"]'
                return paginator.getPaginationWrapper().find(elementSelector)
            }).
            onAfterPagination((paginator) => {
                let paginatorLinksAfterCurrent = $('.pagination a.active ~ a:not(.no-page)')
                if (paginator.paginatedPageNo === paginator.lastPageNo) {
                    paginatorLinksAfterCurrent.remove()
                    $('.pagination a.no-page').remove()
                } else {
                    paginatorLinksAfterCurrent.each((index, element) => {
                        let paginationLink = $(element)
                        if (paginator.getPageNoFromUrl($(element).attr('href')) <= paginator.paginatedPageNo) {
                            paginationLink.remove()
                        }
                    })
                    let nextPageUrl = paginator.getPageUrlFromPageNo(paginator.paginatedPageNo + 1)
                    let nextPageLink = $('.pagination a[href="' + nextPageUrl + '"]')
                    if (nextPageLink.length === 0) {
                        let lastPageLink = $('.pagination a:not(.no-page):last')
                        lastPageLink.clone().insertAfter(currentPaginationElement).text(paginator.paginatedPageNo + 1).attr('href', nextPageUrl)
                    }
                }
            })

        // UI Events

        this._onBeforeUIBuild = () => {
            if (IS_VIDEO_PAGE) {
                this._validator.sanitizeNodeOfSelector('.clear-infobar > strong:nth-child(1)', this._configurationManager.getField(FILTER_TEXT_SANITIZATION).optimized)
            }
        }

        this._onUIBuild = () =>
            this._uiGen.createSettingsSection().append([
                this._uiGen.createTabsSection(['Filters', 'Text', 'Global', 'Stats'], [
                    this._uiGen.createTabPanel('Filters', true).append([
                        this._configurationManager.createElement(FILTER_VIDEOS_DURATION),
                        this._configurationManager.createElement(FILTER_VIDEOS_RATING),
                        this._configurationManager.createElement(FILTER_VIDEOS_VIEWS),
                        this._uiGen.createSeparator(),
                        this._configurationManager.createElement(FILTER_VIDEO_RESOLUTION),
                        this._uiGen.createSeparator(),
                        this._configurationManager.createElement(OPTION_DISABLE_COMPLIANCE_VALIDATION),
                    ]),
                    this._uiGen.createTabPanel('Text').append([
                        this._configurationManager.createElement(FILTER_TEXT_SEARCH),
                        this._configurationManager.createElement(FILTER_TEXT_BLACKLIST),
                        this._configurationManager.createElement(FILTER_TEXT_WHITELIST),
                    ]),
                    this._uiGen.createTabPanel('Global').append([
                        this._configurationManager.createElement(FILTER_TEXT_SANITIZATION),
                        this._uiGen.createSeparator(),
//                        this._configurationManager.createElement(CONFIG_PAGINATOR_THRESHOLD),
//                        this._configurationManager.createElement(CONFIG_PAGINATOR_LIMIT),
                        this._configurationManager.createElement(OPTION_ALWAYS_SHOW_SETTINGS_PANE),
                    ]),
                    this._uiGen.createTabPanel('Stats').append([
                        this._uiGen.createStatisticsFormGroup(FILTER_TEXT_BLACKLIST),
                        this._uiGen.createStatisticsFormGroup(FILTER_VIDEOS_DURATION),
                        this._uiGen.createStatisticsFormGroup(FILTER_VIDEO_RESOLUTION),
                        this._uiGen.createStatisticsFormGroup(FILTER_VIDEOS_RATING),
                        this._uiGen.createStatisticsFormGroup(FILTER_TEXT_SEARCH),
                        this._uiGen.createStatisticsFormGroup(FILTER_VIDEOS_VIEWS),
                        this._uiGen.createSeparator(),
                        this._uiGen.createStatisticsTotalsGroup(),
                    ]),
                ]),
                this._createSettingsFormActions(),
                this._uiGen.createSeparator(),
                this._uiGen.createStatusSection(),
            ])

        this._onAfterUIBuild = () => {
            this._uiGen.getSelectedSection()[0].userScript = this
        }

        // Compliance Events

        this._onBeforeCompliance = (videoItem) => {
            let field = this._configurationManager.getField(FILTER_TEXT_WHITELIST)
            return field.value.length ? this._validator.validateTextContains(videoItem.find('.title > a').text(), field.optimized, FILTER_TEXT_WHITELIST) : true
        }

        this._onGetItemLists = () => $('.mozaique')

        this._onFirstHitBeforeCompliance = (item) => this._analyzeVideoItem(item)

        this._complianceFilters = [
            (videoItem) => this._validateSearch(videoItem),
            (videoItem) => this._validateRating(videoItem),
            (videoItem) => this._validateDuration(videoItem),
            (videoItem) => this._validateViews(videoItem),
            (videoItem) => this._validateResolution(videoItem),
            (videoItem) => this._validateBlacklist(videoItem),
        ]

        this._onFirstHitAfterCompliance =
            (item) => this._validator.sanitizeTextNode(item.find('.thumb-under a:first'), this._configurationManager.getField(FILTER_TEXT_SANITIZATION).optimized)
    }

    /**
     * @param {JQuery} videoItem
     * @private
     */
    _analyzeVideoItem (videoItem)
    {
        let videoMetadata = videoItem.find('.metadata')
        let videoItemElement = videoItem[0]

        if (videoMetadata) {
            if (IS_VIDEO_PAGE) {
                videoMetadata = videoMetadata.text().split(' ')
                videoItemElement[VIDEO_DURATION_KEY] = this._analyzeVideItemDuration(videoMetadata[1])
                videoItemElement[VIDEO_RATING_KEY] = 100
                videoItemElement[VIDEO_RESOLUTION_KEY] = parseInt(videoMetadata[5].replace('p', ''))
                videoItemElement[VIDEO_VIEWS_KEY] = videoMetadata[0]
            } else {
                videoMetadata = videoMetadata.text().split('\n')
                if (videoMetadata.length > 3) {
                    videoMetadata[1] = videoMetadata[1].split(' ')
                    videoItemElement[VIDEO_DURATION_KEY] = this._analyzeVideItemDuration(videoMetadata[2])
                    videoItemElement[VIDEO_RATING_KEY] = videoMetadata[1][1].replace('%', '')
                    videoItemElement[VIDEO_RESOLUTION_KEY] = parseInt(videoMetadata[3].replace(' -  ', '').replace('p', ''))
                    videoItemElement[VIDEO_VIEWS_KEY] = videoMetadata[1][0]
                } else {
                    videoItemElement[VIDEO_DURATION_KEY] = this._analyzeVideItemDuration(videoMetadata[1])
                    videoItemElement[VIDEO_RATING_KEY] = 100
                    videoItemElement[VIDEO_RESOLUTION_KEY] = parseInt(videoMetadata[2].replace(' -  ', '').replace('p', ''))
                    videoItemElement[VIDEO_VIEWS_KEY] = 0
                }
            }
            videoItemElement[VIDEO_NAME_KEY] = videoItem.find('.thumb-under > p:nth-child(1) > a:nth-child(1)')
        }
    }

    /**
     * @param {string} durationString
     * @return {number}
     * @private
     */
    _analyzeVideItemDuration (durationString)
    {
        let duration = 0, splitArray

        if (IS_VIDEO_PAGE) {
            splitArray = durationString.split(' ')
            for (let i = 0; i < splitArray.length; i++) {
                if (splitArray[i].endsWith('min')) {
                    duration += 60 * splitArray[i].replace('min', '')
                } else {
                    if (splitArray[i].endsWith('sec')) {
                        duration += splitArray[i].replace('sec', '')
                    }
                }
            }
        } else {
            splitArray = durationString.split('min')
            if (splitArray.length === 2) {
                duration = 60 * splitArray[0]
            } else {
                splitArray = durationString.split('sec')
                if (splitArray.length === 2) {
                    duration = splitArray[0]
                }
            }
        }
        return duration
    }

    /**
     * Validates non-existence of blacklisted words in the video name
     * @param {JQuery} videoItem
     * @return {boolean}
     * @private
     */
    _validateBlacklist (videoItem)
    {
        let field = this._configurationManager.getField(FILTER_TEXT_BLACKLIST)
        return field.value.length ? this._validator.validateTextDoesNotContain(videoItem.find('.title > a').text(), field.optimized, FILTER_TEXT_BLACKLIST) : true
    }

    /**
     * Validates video duration
     * @param {JQuery} videoItem
     * @return {boolean}
     * @private
     */
    _validateDuration (videoItem)
    {
        let range = this._configurationManager.getValue(FILTER_VIDEOS_DURATION)
        if (range.minimum > 0 || range.maximum > 0) {
            return this._validator.validateRange(FILTER_VIDEOS_DURATION, videoItem[0][VIDEO_DURATION_KEY], [range.minimum, range.maximum])
        }
        return true
    }

    /**
     * Validate video rating
     * @param {JQuery} videoItem
     * @return {boolean}
     * @private
     */
    _validateRating (videoItem)
    {
        let range = this._configurationManager.getValue(FILTER_VIDEOS_RATING)
        if (range.minimum > 0 || range.maximum > 0) {
            return this._validator.validateRange(FILTER_VIDEOS_RATING, videoItem[0][VIDEO_RATING_KEY], [range.minimum, range.maximum])
        }
        return true
    }

    /**
     * Validate video quality
     * @param {JQuery} videoItem
     * @return {boolean}
     * @private
     */
    _validateResolution (videoItem)
    {
        let validationCheck = true

        let resolution = this._configurationManager.getValue(FILTER_VIDEO_RESOLUTION)
        if (resolution !== 'Show All') {
            validationCheck = videoItem[0][VIDEO_RESOLUTION_KEY] >= parseInt(resolution.replace(REGEX_PRESERVE_NUMBERS, ''))
            this._statistics.record(FILTER_VIDEO_RESOLUTION, validationCheck)
        }
        return validationCheck
    }

    /**
     * Validates existence of searched words in the video name
     * @param {JQuery} videoItem
     * @return {boolean}
     * @private
     */
    _validateSearch (videoItem)
    {
        let search = this._configurationManager.getValue(FILTER_TEXT_SEARCH)
        return search !== '' ? this._statisticsRecorder.record(FILTER_TEXT_SEARCH, videoItem[0][VIDEO_NAME_KEY].includes(search)) : true
    }

    /**
     * Validate video view count
     * @param {JQuery} videoItem
     * @return {boolean}
     * @private
     */
    _validateViews (videoItem)
    {
        let range = this._configurationManager.getValue(FILTER_VIDEOS_VIEWS)
        if (range.minimum > 0 || range.maximum > 0) {
            return this._validator.validateRange(FILTER_VIDEOS_VIEWS, videoItem[0][VIDEO_VIEWS_KEY], [range.minimum, range.maximum])
        }
        return true
    }

    /**
     * Validates non-existence of blacklisted words in the video name
     * @param {JQuery} videoItem
     * @return {boolean}
     * @private
     */
    _validateWhitelist (videoItem)
    {
        let regex = this._configurationManager.getField(FILTER_TEXT_WHITELIST).optimized
        return regex ? this._validator.validateTextContains(videoItem[0][VIDEO_NAME_KEY], regex, FILTER_TEXT_WHITELIST) : true
    }
}

(new XNXXSearchFilters).init()