Sleazy Fork is available in English.

PH - Search & UI Tweaks

Various search filters and user experience enhancers

  1. // ==UserScript==
  2. // @name PH - Search & UI Tweaks
  3. // @namespace brazenvoid
  4. // @version 4.0.0
  5. // @author brazenvoid
  6. // @license GPL-3.0-only
  7. // @description Various search filters and user experience enhancers
  8. // @match https://*.pornhub.com/*
  9. // @match https://*.pornhub.org/*
  10. // @match https://*.pornhubpremium.com/*
  11. // @match https://*.pornhubpremium.org/*
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
  13. // @require https://update.greasyfork.org/scripts/375557/1244990/Base%20Brazen%20Resource.js
  14. // @require https://update.greasyfork.org/scripts/416104/1498249/Brazen%20UI%20Generator.js
  15. // @require https://update.greasyfork.org/scripts/418665/1481350/Brazen%20Configuration%20Manager.js
  16. // @require https://update.greasyfork.org/scripts/429587/1244644/Brazen%20Item%20Attributes%20Resolver.js
  17. // @require https://update.greasyfork.org/scripts/424516/1114774/Brazen%20Subscriptions%20Loader.js
  18. // @require https://update.greasyfork.org/scripts/416105/1478692/Brazen%20Base%20Search%20Enhancer.js
  19. // @grant GM_addStyle
  20. // @run-at document-end
  21. // ==/UserScript==
  22.  
  23. GM_addStyle(`#settings-wrapper{min-width:390px;width:390px}`)
  24.  
  25. // Environment
  26.  
  27. const PAGE_PATH_NAME = window.location.pathname
  28.  
  29. const IS_FEED_PAGE = PAGE_PATH_NAME.startsWith('/feeds')
  30. const IS_PLAYLIST_PAGE = PAGE_PATH_NAME.startsWith('/playlist')
  31. const IS_PROFILE_PAGE = PAGE_PATH_NAME.startsWith('/model') || PAGE_PATH_NAME.startsWith('/channels') || PAGE_PATH_NAME.startsWith('/user') ||
  32. PAGE_PATH_NAME.startsWith('/pornstar')
  33. const IS_VIDEO_PAGE = PAGE_PATH_NAME.startsWith('/view_video')
  34. const IS_VIDEO_SEARCH_PAGE = PAGE_PATH_NAME.startsWith('/video') || PAGE_PATH_NAME.startsWith('/categories')
  35.  
  36. // Filters and configuration
  37.  
  38. const FILTER_PAID_VIDEOS = 'Hide Paid Videos'
  39. const FILTER_PREMIUM_VIDEOS = 'Hide Premium Videos'
  40. const FILTER_PRO_CHANNEL_VIDEOS = 'Hide Pro Channel Videos'
  41. const FILTER_PRIVATE_VIDEOS = 'Hide Private Videos'
  42. const FILTER_RECOMMENDED_VIDEOS = 'Hide Recommended Videos'
  43. const FILTER_VIDEOS_VIEWS = 'Views'
  44. const FILTER_USER = 'User Blacklist'
  45. const FILTER_WATCHED_VIDEOS = 'Watched Filters'
  46.  
  47. const LINK_DISABLE_PLAYLIST_CONTROLS = 'Disable Playlist Controls'
  48. const LINK_USER_PUBLIC_VIDEOS = 'User Public Videos'
  49.  
  50. const UI_AUTO_NEXT = 'Auto Next'
  51. const UI_LARGE_PLAYER_ALWAYS = 'Always Enlarge Player'
  52. const UI_REMOVE_LIVE_MODELS_SECTIONS = 'Remove Live Models Sections'
  53. const UI_REMOVE_PORN_STAR_SECTIONS = 'Remove Porn Star Sections'
  54.  
  55. class PHSearchAndUITweaks extends BrazenBaseSearchEnhancer
  56. {
  57. constructor()
  58. {
  59. super({
  60. isUserLoggedIn: $('#topRightProfileMenu').length > 0,
  61. itemDeepAnalysisSelector: '.video-wrapper',
  62. itemLinkSelector: '.title > a',
  63. itemListSelectors: 'ul.videos',
  64. itemNameSelector: '.title > a',
  65. itemSelectors: '.videoblock',
  66. requestDelay: 0,
  67. scriptPrefix: 'ph-sui-',
  68. })
  69.  
  70. this._playlistPageUsername = ''
  71. this._profilePageUsername = ''
  72.  
  73. this._setupFeatures()
  74. this._setupComplianceFilters()
  75. this._setupUI()
  76. this._setupEvents()
  77. }
  78.  
  79. /**
  80. * Automatic next search page
  81. * @private
  82. */
  83. _autoNext()
  84. {
  85. let allVideos = $('.nf-videos ' + this._config.itemSelectors)
  86. if (allVideos.length > 0 && !allVideos.is(':visible')) {
  87. let nextButton = $('.page_next:not(.disabled) > a')
  88. if (nextButton.length) {
  89. window.location = nextButton.attr('href')
  90. }
  91. }
  92. }
  93.  
  94. /**
  95. * Changes profile links to directly point to public video listings
  96. * @private
  97. */
  98. _complyProfileLinks()
  99. {
  100. $('.usernameBadgesWrapper a, a.usernameLink, .usernameWrap a').each((index, profileLink) => {
  101. profileLink = $(profileLink)
  102. let href = profileLink.attr('href')
  103. if (href.startsWith('/channels') || href.startsWith('/model')) {
  104. profileLink.attr('href', href + '/videos')
  105. } else if (href.startsWith('/user')) {
  106. profileLink.attr('href', href + '/videos/public')
  107. }
  108. })
  109. }
  110.  
  111. /**
  112. * @private
  113. */
  114. _enlargePlayer()
  115. {
  116. let player = $('#player')
  117. if (player.hasClass('original')) {
  118. player.removeClass('original').addClass('wide')
  119. }
  120. }
  121.  
  122. /**
  123. * Fixes left over space after ads removal
  124. * @private
  125. */
  126. _fixLeftOverSpaceOnVideoSearchPage()
  127. {
  128. $('.showingCounter, .tagsForWomen').each((index, div) => {
  129. div.style.height = 'auto'
  130. })
  131. }
  132.  
  133. /**
  134. * Fixes pagination nav by moving it under video items list
  135. * @private
  136. */
  137. _fixPaginationNavOnVideoSearchPage()
  138. {
  139. $('.pagination3').insertAfter($('div.nf-videos .search-video-thumbs'))
  140. }
  141.  
  142. _removeLoadMoreButtons()
  143. {
  144. $('.more_recommended_btn, #loadMoreRelatedVideosCenter').remove()
  145. }
  146.  
  147. /**
  148. * @private
  149. */
  150. _removePremiumSectionFromSearchPage()
  151. {
  152. $('.nf-videos .sectionWrapper .sectionTitle h2').each((index, element) => {
  153. let sectionTitle = $(element)
  154. if (sectionTitle.text().trim() === 'Premium Videos') {
  155. sectionTitle.parents('.sectionWrapper:first').remove()
  156. return false
  157. }
  158. })
  159. }
  160.  
  161. /**
  162. * Removes premium video sections from profiles
  163. * @private
  164. */
  165. _removeVideoSectionsOnProfilePage()
  166. {
  167. const videoSections = [
  168. {setting: this._getConfig(FILTER_PAID_VIDEOS), linkSuffix: 'paid'},
  169. {setting: this._getConfig(FILTER_PREMIUM_VIDEOS), linkSuffix: 'fanonly'},
  170. {setting: this._getConfig(FILTER_PRIVATE_VIDEOS), linkSuffix: 'private'},
  171. ]
  172. for (let videoSection of videoSections) {
  173. let videoSectionWrapper = $('.videoSection > div > div > h2 > a[href$="/' + videoSection.linkSuffix + '"]').parents('.videoSection:first')
  174. videoSection.setting ? videoSectionWrapper.show() : videoSectionWrapper.hide()
  175. }
  176. }
  177.  
  178. /**
  179. * @private
  180. */
  181. _setupComplianceFilters()
  182. {
  183. this._addItemTextSanitizationFilter(
  184. 'Censor video names by substituting offensive phrases. Each rule in separate line with comma separated target phrases. ' +
  185. 'Requires page reload to apply. Example Rule: boyfriend=stepson,stepdad')
  186. this._addItemWhitelistFilter('Show videos with specified phrases in their names. Separate the phrases with line breaks.')
  187. this._addItemTextSearchFilter()
  188. this._addItemComplianceFilter(FILTER_WATCHED_VIDEOS, (item, value) => {
  189. if (value === '1') {
  190. return !this._get(item, FILTER_WATCHED_VIDEOS)
  191. } else if (value === '2') {
  192. return this._get(item, FILTER_WATCHED_VIDEOS)
  193. }
  194. return true
  195. })
  196. this._addItemPercentageRatingRangeFilter('.value')
  197. this._addItemDurationRangeFilter('.duration')
  198. this._addItemComplianceFilter(FILTER_VIDEOS_VIEWS)
  199. this._addItemComplianceFilter(FILTER_PRO_CHANNEL_VIDEOS)
  200. this._addItemComplianceFilter(FILTER_PAID_VIDEOS)
  201. this._addItemComplianceFilter(FILTER_PREMIUM_VIDEOS)
  202. this._addItemComplianceFilter(FILTER_PRIVATE_VIDEOS)
  203. this._addItemComplianceFilter(FILTER_RECOMMENDED_VIDEOS)
  204. this._addItemComplianceFilter(FILTER_USER, (item, users) => !users.includes(this._get(item, FILTER_USER)))
  205. this._addSubscriptionsFilter(() => !IS_FEED_PAGE, (item) => {
  206. let username = this._get(item, FILTER_USER)
  207. return (username === this._playlistPageUsername || username === this._profilePageUsername) ? false : username
  208. })
  209. this._addItemBlacklistFilter('Hide videos with specified phrases in their names.')
  210. }
  211.  
  212. /**
  213. * @private
  214. */
  215. _setupEvents()
  216. {
  217. if (IS_FEED_PAGE) {
  218. this._onAfterInitialization.push(() => ChildObserver.create().
  219. onNodesAdded((itemsAdded) => {
  220. let itemsList
  221. for (let item of itemsAdded) {
  222. if (typeof item.querySelector === 'function') {
  223. itemsList = item.querySelector(this._config.itemListSelectors)
  224. if (itemsList) {
  225. this._complyItemsList($(itemsList))
  226. }
  227. }
  228. }
  229. }).
  230. observe($('#moreData')[0]))
  231.  
  232. } else if (IS_VIDEO_SEARCH_PAGE) {
  233. this._onAfterInitialization.push(() => this._performOperation(UI_AUTO_NEXT, () => this._autoNext()))
  234. }
  235.  
  236. this._onBeforeUIBuild.push(() => {
  237.  
  238. if (IS_VIDEO_PAGE) {
  239. this._performOperation(FILTER_PAID_VIDEOS, () => $('#p2vVideosVPage').remove())
  240. this._performOperation(UI_LARGE_PLAYER_ALWAYS, () => this._enlargePlayer())
  241. this._removeLoadMoreButtons()
  242. Validator.sanitizeNodeOfSelector('.inlineFree', this._configurationManager.getFieldOrFail(FILTER_TEXT_SANITIZATION).optimized)
  243.  
  244. } else if (IS_VIDEO_SEARCH_PAGE) {
  245. this._performOperation(UI_REMOVE_PORN_STAR_SECTIONS, () => $('#relatedPornstarSidebar').remove())
  246. this._performOperation(FILTER_PREMIUM_VIDEOS, () => this._removePremiumSectionFromSearchPage())
  247. this._fixLeftOverSpaceOnVideoSearchPage()
  248. this._fixPaginationNavOnVideoSearchPage()
  249.  
  250. } else if (IS_PROFILE_PAGE) {
  251. this._removeVideoSectionsOnProfilePage()
  252. this._profilePageUsername = PAGE_PATH_NAME.split('/')[1]
  253.  
  254. } else if (IS_PLAYLIST_PAGE) {
  255. this._playlistPageUsername = $('#js-aboutPlaylistTabView .usernameWrap a').text().trim()
  256.  
  257. if (this._getConfig(LINK_DISABLE_PLAYLIST_CONTROLS)) {
  258. this._onFirstHitAfterCompliance.push((item) => this._validatePlaylistVideoLink(item))
  259. }
  260. }
  261.  
  262. this._performOperation(
  263. UI_REMOVE_LIVE_MODELS_SECTIONS,
  264. () => $('.streamateContent').
  265. each((index, element) => {$(element).parents('.sectionWrapper:first').remove()}),
  266. )
  267. })
  268.  
  269. this._onAfterUIBuild.push(() => {
  270. this._performOperation(LINK_USER_PUBLIC_VIDEOS, () => this._complyProfileLinks())
  271. this._uiGen.getSelectedSection()[0].userScript = this
  272. })
  273. }
  274.  
  275. /**
  276. * @private
  277. */
  278. _setupFeatures()
  279. {
  280. this._configurationManager.
  281. addFlagField(FILTER_PAID_VIDEOS, 'Hide paid videos.').
  282. addFlagField(FILTER_PREMIUM_VIDEOS, 'Hide premium videos.').
  283. addFlagField(FILTER_PRIVATE_VIDEOS, 'Hide private Videos.').
  284. addFlagField(FILTER_PRO_CHANNEL_VIDEOS, 'Hide videos from professional channels.').
  285. addFlagField(FILTER_RECOMMENDED_VIDEOS, 'Hide recommended videos.').
  286. addFlagField(LINK_DISABLE_PLAYLIST_CONTROLS, 'Disable playlist controls on video pages.').
  287. addFlagField(LINK_USER_PUBLIC_VIDEOS, 'Jump directly to public videos on any profile link click.').
  288. addFlagField(UI_AUTO_NEXT, 'Automatically go to next search page if no videos match after first run.').
  289. addFlagField(UI_LARGE_PLAYER_ALWAYS, 'Enlarges player on all video pages.').
  290. addFlagField(UI_REMOVE_LIVE_MODELS_SECTIONS, 'Remove live model stream sections from search.').
  291. addFlagField(UI_REMOVE_PORN_STAR_SECTIONS, 'Remove porn star listings from search.').
  292. addRadiosGroup(FILTER_WATCHED_VIDEOS, [
  293. ['No Filtering', 0],
  294. ['Hide Watched Videos', 1],
  295. ['Show Only Watched Videos', 2],
  296. ], 'Control fate of already watched videos.').
  297. addRangeField(FILTER_VIDEOS_VIEWS, 0, 10000000, 'Filter videos by view count.').
  298. addRulesetField(FILTER_USER, 6, 'Hides videos from specified users/channels.')
  299.  
  300. this._itemAttributesResolver.
  301. addAttribute(FILTER_PAID_VIDEOS, (item) => Validator.isChildMissing(item, '.p2v-icon, .fanClubVideoWrapper')).
  302. addAttribute(FILTER_PREMIUM_VIDEOS, (item) => Validator.isChildMissing(item, '.marker-overlays > .premiumIcon')).
  303. addAttribute(FILTER_PRIVATE_VIDEOS, (item) => Validator.isChildMissing(item, '.privateOverlay')).
  304. addAttribute(FILTER_PRO_CHANNEL_VIDEOS, (item) => Validator.isChildMissing(item, '.channel-icon')).
  305. addAttribute(FILTER_RECOMMENDED_VIDEOS, (item) => Validator.isChildMissing(item, '.recommendedFor')).
  306. addAttribute(FILTER_USER, (item) => item.find('.usernameWrap a').attr('title')).
  307. addAttribute(FILTER_VIDEOS_VIEWS, (item) => {
  308. let viewsCountString = item.find('.views var').text()
  309. let viewsCountMultiplier = 1
  310. let viewsCountStringLength = viewsCountString.length
  311.  
  312. if (viewsCountString[viewsCountStringLength - 1] === 'K') {
  313. viewsCountMultiplier = 1000
  314. viewsCountString = viewsCountString.replace('K', '')
  315. } else if (viewsCountString[viewsCountStringLength - 1] === 'M') {
  316. viewsCountMultiplier = 1000000
  317. viewsCountString = viewsCountString.replace('M', '')
  318. }
  319. return parseFloat(viewsCountString) * viewsCountMultiplier
  320. }).
  321. addAttribute(FILTER_WATCHED_VIDEOS, (item) => Validator.doesChildExist(item, '.watchedVideoText') || Validator.doesChildExist(item, '.watchedVideo'))
  322.  
  323. this._setupSubscriptionLoader().addConfig({
  324. url: window.location.origin + $('#profileMenuDropdown > li > span > a').first().attr('href') + '/subscriptions',
  325. getPageCount: (page) => parseInt(page.children().first().text().replace(REGEX_PRESERVE_NUMBERS, '')) / 100,
  326. getPageUrl: (baseUrl, pageNo) => baseUrl + '?page=' + pageNo + ' .userWidgetWrapperGrid',
  327. subscriptionsCountSelector: '.profileContentLeft .showingInfo',
  328. subscriptionNameSelector: 'a.usernameLink',
  329. })
  330. }
  331.  
  332. /**
  333. * @private
  334. */
  335. _setupUI()
  336. {
  337. this._userInterface = [
  338. this._uiGen.createTabsSection(['Filters 1', 'Filters 2', 'Interface', 'Settings', 'Stats'], [
  339. this._uiGen.createTabPanel('Filters 1', true).append([
  340. this._configurationManager.createElement(FILTER_DURATION_RANGE),
  341. this._configurationManager.createElement(FILTER_PERCENTAGE_RATING_RANGE),
  342. this._configurationManager.createElement(FILTER_VIDEOS_VIEWS),
  343. this._uiGen.createBreakSeparator(),
  344. this._configurationManager.createElement(FILTER_PAID_VIDEOS),
  345. this._configurationManager.createElement(FILTER_PREMIUM_VIDEOS),
  346. this._configurationManager.createElement(FILTER_PRIVATE_VIDEOS),
  347. this._configurationManager.createElement(FILTER_PRO_CHANNEL_VIDEOS),
  348. this._configurationManager.createElement(FILTER_RECOMMENDED_VIDEOS),
  349. this._configurationManager.createElement(FILTER_SUBSCRIBED_VIDEOS),
  350. this._configurationManager.createElement(FILTER_UNRATED),
  351. this._uiGen.createSeparator(),
  352. this._configurationManager.createElement(FILTER_WATCHED_VIDEOS),
  353. this._uiGen.createSeparator(),
  354. this._configurationManager.createElement(OPTION_DISABLE_COMPLIANCE_VALIDATION),
  355.  
  356. ]),
  357. this._uiGen.createTabPanel('Filters 2').append([
  358. this._configurationManager.createElement(FILTER_TEXT_SEARCH),
  359. this._configurationManager.createElement(FILTER_TEXT_BLACKLIST),
  360. this._configurationManager.createElement(FILTER_TEXT_WHITELIST),
  361. this._configurationManager.createElement(FILTER_TEXT_SANITIZATION),
  362. this._configurationManager.createElement(FILTER_USER),
  363. ]),
  364. this._uiGen.createTabPanel('Interface').append([
  365. this._configurationManager.createElement(UI_LARGE_PLAYER_ALWAYS),
  366. this._configurationManager.createElement(LINK_DISABLE_PLAYLIST_CONTROLS),
  367. this._configurationManager.createElement(LINK_USER_PUBLIC_VIDEOS),
  368. this._configurationManager.createElement(UI_AUTO_NEXT),
  369. this._uiGen.createSeparator(),
  370. this._configurationManager.createElement(UI_REMOVE_LIVE_MODELS_SECTIONS),
  371. this._configurationManager.createElement(UI_REMOVE_PORN_STAR_SECTIONS),
  372. ]),
  373. this._uiGen.createTabPanel('Settings').append([
  374. this._configurationManager.createElement(OPTION_ALWAYS_SHOW_SETTINGS_PANE),
  375. this._uiGen.createSeparator(),
  376. this._uiGen.createFormSection('Account').append([
  377. this._createSubscriptionLoaderControls(),
  378. ]),
  379. this._uiGen.createSeparator(),
  380. this._createSettingsBackupRestoreFormActions(),
  381. ]),
  382. this._uiGen.createTabPanel('Stats').append([
  383. this._uiGen.createStatisticsFormGroup(FILTER_TEXT_BLACKLIST),
  384. this._uiGen.createStatisticsFormGroup(FILTER_TEXT_WHITELIST),
  385. this._uiGen.createStatisticsFormGroup(FILTER_DURATION_RANGE),
  386. this._uiGen.createStatisticsFormGroup(FILTER_TEXT_SEARCH),
  387. this._uiGen.createStatisticsFormGroup(FILTER_PAID_VIDEOS, 'Paid Videos'),
  388. this._uiGen.createStatisticsFormGroup(FILTER_PREMIUM_VIDEOS, 'Premium Videos'),
  389. this._uiGen.createStatisticsFormGroup(FILTER_PRIVATE_VIDEOS, 'Private Videos'),
  390. this._uiGen.createStatisticsFormGroup(FILTER_PRO_CHANNEL_VIDEOS, 'Pro Channel Videos'),
  391. this._uiGen.createStatisticsFormGroup(FILTER_PERCENTAGE_RATING_RANGE),
  392. this._uiGen.createStatisticsFormGroup(FILTER_RECOMMENDED_VIDEOS, 'Recommended'),
  393. this._uiGen.createStatisticsFormGroup(FILTER_SUBSCRIBED_VIDEOS, 'Subscribed'),
  394. this._uiGen.createStatisticsFormGroup(FILTER_UNRATED, 'Unrated'),
  395. this._uiGen.createStatisticsFormGroup(FILTER_VIDEOS_VIEWS),
  396. this._uiGen.createStatisticsFormGroup(FILTER_WATCHED_VIDEOS, 'Watched'),
  397. this._uiGen.createSeparator(),
  398. this._uiGen.createStatisticsTotalsGroup(),
  399. ]),
  400. ]),
  401. this._createSettingsFormActions(),
  402. this._uiGen.createSeparator(),
  403. this._uiGen.createStatusSection(),
  404. ]
  405. }
  406.  
  407. /**
  408. * Validate and change playlist video links
  409. * @param {JQuery} videoItem
  410. * @private
  411. */
  412. _validatePlaylistVideoLink(videoItem)
  413. {
  414. videoItem.find('a').each((_i, playlistLink) => {
  415. playlistLink = $(playlistLink)
  416. playlistLink.attr('href', playlistLink.attr('href').replace(/&pkey.*/, ''))
  417. })
  418. }
  419. }
  420.  
  421. (new PHSearchAndUITweaks).init()