PornHub Improved

Infinite scroll (optional). Lazy loading. Filter by duration, include/exclude phrases

Fra og med 07.05.2024. Se den nyeste version.

  1. // ==UserScript==
  2. // @name PornHub Improved
  3. // @namespace http://tampermonkey.net/
  4. // @license MIT
  5. // @version 1.5.3
  6. // @description Infinite scroll (optional). Lazy loading. Filter by duration, include/exclude phrases
  7. // @author smartacephale
  8. // @match https://*.pornhub.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=pornhub.com
  10. // @grant GM_addStyle
  11. // @run-at document-end
  12. // @require https://unpkg.com/vue@3.4.21/dist/vue.global.prod.js
  13. // @require https://update.greasyfork.org/scripts/494206/utils.user.js
  14. // @require data:, let tempVue = unsafeWindow.Vue; unsafeWindow.Vue = Vue; const { ref, watch, reactive, createApp } = Vue;
  15. // @require https://update.greasyfork.org/scripts/494207/persistent-state.user.js
  16. // @require https://update.greasyfork.org/scripts/494204/data-manager.user.js
  17. // @require https://update.greasyfork.org/scripts/494205/pagination-manager.user.js
  18. // @require https://update.greasyfork.org/scripts/494203/vue-ui.user.js
  19. // @run-at document-idle
  20. // ==/UserScript==
  21. /* globals jQuery, $, Vue, createApp, watch, reactive, findNextSibling, watchElementChildrenCount,
  22. timeToSeconds, parseDOM, parseIntegerOr, fetchHtml, stringToWords, Observer
  23. LazyImgLoader, PersistentState, DataManager, PaginationManager, VueUI */
  24.  
  25. const LOGO = `
  26. ⣞⣞⣞⣞⣞⣞⣞⣞⢞⣖⢔⠌⢆⠇⡇⢇⡓⡆⢠⠰⠠⡄⡄⡠⡀⡄⣄⠢⡂⡆⢆⢆⢒⢔⢕⢜⢔⢕⢕⢗⣗⢗⢗⢕⠕⠕⢔⢲⣲⢮⣺⣺⡺⡸⡪⡚⢎⢎⢺⢪
  27. ⣺⣺⣺⣺⣺⣺⡺⣮⣳⣳⡳⣕⢥⠱⡘⢔⠱⠑⠡⣫⠈⡞⢼⠔⡑⡕⢕⢝⢜⢜⢜⢔⢅⢣⠪⡸⢸⠸⡸⡱⡑⢍⠢⡑⢌⢪⢨⢪⣺⡻⣗⣷⡿⣽⣺⡦⡵⣔⢷⢽
  28. ⣺⣺⣺⣺⣺⣺⢽⣺⣺⣺⣺⣺⣪⢣⡑⡠⠐⠈⠨⡪⣂⠉⡪⡪⡆⢕⠅⡕⢕⢕⢕⢕⢕⢕⠱⡨⠢⡑⢌⢂⢊⢆⢕⠸⡨⢢⠱⡑⡜⡎⡞⡮⡯⡯⡯⡻⡽⡽⡽⣽
  29. ⣺⣺⣞⣞⣞⡾⣽⣺⣺⣺⣺⣺⢮⡳⡽⣔⢔⢈⠐⠈⡎⡀⠅⢣⢫⡢⡑⠌⢜⠸⡸⡸⡱⠡⡱⢐⢅⠪⡐⡅⢕⠌⢆⠣⡊⢆⠣⡑⢌⠪⡊⡎⡎⡗⡝⡜⡜⢜⢹⢸
  30. ⣞⣞⣾⣺⣳⢯⣗⣿⣺⣳⣗⡯⣗⡯⣟⣮⡳⣧⡳⣢⢢⠐⢰⣀⠂⡑⢕⠨⢂⠕⡑⢌⠢⠡⠠⡁⢂⢑⠨⠠⠡⡈⡂⡢⡂⡆⣆⡪⡨⡊⡂⠎⡢⡑⢕⢱⢘⢌⢎⢎
  31. ⣳⣳⣳⣻⣺⢽⣺⣗⡿⣞⣷⣻⣗⣯⣗⣗⣟⡮⡯⣮⡳⡳⡔⡨⢆⠂⠱⡌⡐⠌⡂⠅⢅⠣⡑⢌⠢⡢⡑⡱⡡⡣⡣⡣⡳⣝⢮⣟⣿⣿⣿⣦⡈⢎⢎⢪⣪⢮⣳⣳
  32. ⣺⣺⣳⣻⣞⣿⣺⢷⣟⣯⣿⣽⢿⣾⣳⣟⣮⢯⣟⢮⢯⢯⢮⢪⠪⡊⡈⢆⠢⢑⠨⠨⠢⡑⢌⠢⣃⢪⠨⡢⠱⡘⢜⢜⢜⢜⢗⣝⡯⡿⡿⣟⢦⣕⣞⣾⣺⣻⣞⣾
  33. ⣺⣞⣷⣳⣟⣾⡽⣯⣟⣯⣿⣾⣿⣻⣽⣾⣽⣻⣺⢽⢽⢯⡯⣧⡣⡓⡔⠠⡑⠄⢕⠡⡑⢌⢢⢑⠔⡐⠅⠪⠨⢊⢢⢑⢕⢕⢕⢕⠱⡑⢕⢕⢗⣿⣺⣯⢿⣳⣟⢾
  34. ⣞⣾⣺⣞⡷⣗⣿⣽⡾⣿⣟⣿⣟⣿⣿⣷⣿⣽⢾⡽⣽⢽⣽⣳⢽⡜⡌⢦⢘⢌⠢⡑⡌⢆⠕⡐⡁⠂⢅⠪⡘⢌⢪⠨⡢⡃⢎⠢⡑⢅⠕⢌⢎⣞⣞⢾⣝⣞⡮⡯
  35. ⢷⣳⣗⡯⣟⡷⣿⢾⣻⣟⣿⣻⣿⣿⢿⣿⣾⣟⣿⢯⣯⣟⡾⣽⢯⣯⢯⣇⢇⢆⢣⠱⡘⡐⡁⡂⡐⠅⡅⢕⠸⡨⠢⡑⡰⢡⠡⡊⢌⠢⡑⡑⢜⢜⢮⠳⢓⢣⢫⢮
  36. ⢽⣺⢾⣽⣳⣟⣟⣿⣯⣿⣿⣿⣿⣿⣿⣿⣻⣿⣻⡿⣞⣷⣻⣽⣻⣞⣿⡾⡵⡱⡡⢑⠈⠄⠂⠄⠂⡑⠨⡂⡣⡊⡪⢂⢎⠢⡃⡪⠢⣑⢱⡨⡮⣺⣖⠈⡜⢜⢼⢽
  37. ⢽⣺⢽⣞⣾⣳⡯⣷⢿⡷⣿⣿⣻⣿⣟⣿⣽⣿⣻⣟⣯⡿⣞⣷⣻⣞⣷⣻⡳⡑⡅⠅⠠⠁⠌⡠⠁⠄⢅⢐⢐⠌⢔⠡⡢⢱⠨⡊⡎⡎⡮⡪⡯⡳⠡⡪⡪⡪⣯⣿
  38. ⢽⣺⢽⣺⣳⢯⡿⣽⣟⣿⣻⣿⡿⣿⡿⣿⣿⣻⣯⣿⢷⣻⡽⣞⣷⣳⣳⡳⡱⡐⠄⡁⡂⡁⢂⠂⢅⠕⢅⢊⠢⡑⢡⢑⢌⣂⡣⡑⡅⣇⢇⢧⢕⢜⢜⢜⢮⢯⣿⣺
  39. ⡻⡮⣟⣾⣺⢽⢯⣗⣿⣺⣽⡷⣿⣿⢿⣿⣯⣿⣯⣿⢿⣽⣻⡽⣞⣞⡞⡎⡎⠄⢂⠐⡀⡂⢅⠪⡐⡅⠇⠅⢂⠪⡐⠅⢕⠰⡑⡝⡜⡜⡎⡎⡎⡎⡎⢎⢎⢗⣗⢷
  40. ⢯⣻⣺⣺⣺⢽⢯⢷⣻⣞⣷⢿⣻⣾⢿⣯⣿⡷⣿⣻⣯⡷⣯⣟⣷⡳⣝⢜⢜⠀⡂⠌⡐⢌⢢⢱⠨⡪⠨⠨⠀⠅⠊⢌⠢⡑⡑⢌⠎⡪⢊⢎⢊⠆⡊⡢⢂⢣⢣⣳
  41. ⣳⣳⣳⡳⡽⡽⡯⣟⣾⣺⢽⡯⣷⣟⣿⣳⣿⢽⣯⢿⡷⣟⣷⣻⢾⣝⢮⡪⡪⡀⡂⠌⡌⡪⠢⡃⡇⡕⢕⠅⡅⢅⢑⠄⠅⡊⢌⠢⡑⢌⢢⢱⡰⣕⠒⡀⡊⡆⣗⢷
  42. ⣺⡺⡮⡯⡯⡯⡯⣗⡷⣯⢯⡯⣗⣯⢷⣻⣞⣯⣟⣯⣟⣯⣿⢽⣻⣺⢵⢝⢮⢢⢂⢑⢌⠪⡪⢸⢨⢪⢪⢪⢪⢪⢢⢪⢪⢢⢱⢸⡸⣜⢮⢳⢱⡁⡜⣌⢆⢗⢕⢽
  43. ⢞⢮⢯⢯⢯⢯⢯⣗⡯⣯⢯⣟⣷⣻⡯⣟⡷⣟⣷⢯⡿⣾⡽⣯⡿⣾⢽⢽⡱⡱⡱⢐⠔⡑⠜⡌⡎⡎⡮⡪⡣⣏⢮⡣⡳⣕⢕⢧⢯⡪⠣⡣⢃⢇⢣⢣⢳⣕⡯⣯
  44. ⢝⣝⢽⢝⢽⣝⣗⣗⢯⢯⣟⢾⣺⣳⢯⡯⡿⣽⣞⣯⢿⣳⡿⣽⢯⢿⣽⣳⢝⡎⢌⠢⡑⢌⢪⠸⡸⡸⡸⡜⡮⣪⡳⣝⣕⣗⢽⢽⡕⢌⢪⢰⡱⡴⡵⡽⡽⣮⣻⡺
  45. ⡳⡵⡹⡽⣕⣗⢷⢽⣝⣗⡯⣟⣞⡾⡽⡽⡯⣗⣗⡿⣽⣗⣿⢽⡯⣿⣞⡾⣝⡎⡢⢑⢌⢢⠱⡑⡕⡕⡕⡝⡜⡮⣺⣪⡺⡮⡯⣗⣿⢵⣳⣳⢯⢯⢯⢯⣻⣺⡪⡊
  46. ⡯⡪⡯⣺⡺⡪⣯⣳⣳⡳⣯⣳⡳⣯⣻⢽⢯⣗⡷⡯⣗⡷⡯⡿⡽⣗⣯⢯⣗⣇⢊⠔⠔⡅⡣⡱⡸⡸⡸⡸⡸⡪⡺⣜⢮⢯⢯⡯⣿⢽⣺⣺⢽⢽⣝⣗⢗⠕⡁⡂
  47. ⡯⡺⣪⢺⣪⣻⣺⡺⣮⣻⣺⡺⣽⣺⡺⡽⡽⡮⡯⡯⣗⡯⣿⢽⢯⣗⡯⣟⣞⣎⠐⢌⠪⡂⠑⠌⡆⢇⢇⢇⢇⢏⢮⡪⡫⣯⡳⡯⡯⣟⣞⢮⢯⣳⡳⡕⠅⠅⡀⢂
  48. ⡮⡪⡪⡳⡵⣕⢗⣝⣞⣞⢮⣻⡺⡮⡯⡯⡯⣯⢯⣟⡾⣽⢽⡽⣽⡺⣽⣳⡳⡕⡈⡢⢑⠌⡄⢔⠸⡘⡜⡜⡜⡜⡜⣎⢯⢮⢯⢯⢯⡳⡽⣝⣗⢕⠇⠁⠀⠂⡂⠂`;
  49.  
  50. //====================================================================================================
  51.  
  52. class PORNHUB_RULES {
  53. constructor() {
  54. const { pathname } = window.location;
  55.  
  56. this.IS_MODEL_PAGE = pathname.startsWith('/model/');
  57. this.IS_VIDEO_PAGE = pathname.startsWith('/view_video.php');
  58. this.IS_PLAYLIST_PAGE = pathname.startsWith('/playlist/');
  59.  
  60. this.PAGINATION = document.querySelector('.paginationGated');
  61. this.PAGINATION_LAST = parseInt(document.querySelector('.page_next')?.previousElementSibling.innerText);
  62.  
  63. this.CONTAINER = document.querySelector('ul.videos.row-5-thumbs, ul.videos.nf-videos, ul#singleFeedSection, ul#videoSearchResult, ul#singleFeedSection');
  64.  
  65. this.INTERSECTION_OBSERVABLE = this.CONTAINER && findNextSibling(this.CONTAINER);
  66. }
  67.  
  68. THUMB_URL(thumb) {
  69. return thumb.querySelector('.linkVideoThumb').href;
  70. }
  71.  
  72. GET_THUMBS(html) {
  73. const parent = html.querySelector(this.IS_MODEL_PAGE ? '.videos.row-5-thumbs' : 'ul.videos.nf-videos') || html;
  74. return parent.querySelectorAll('li.videoBox.videoblock, li.videoblock');
  75. }
  76.  
  77. THUMB_IMG_DATA(thumb) {
  78. const img = thumb.querySelector('.js-videoThumb.thumb.js-videoPreview');
  79. const imgSrc = img?.getAttribute('data-mediumthumb') || img?.getAttribute('data-path').replace('{index}', '1');
  80. if (!img?.complete || img.naturalWidth === 0) { return ({});}
  81. return { img, imgSrc };
  82. }
  83.  
  84. THUMB_DATA(thumb) {
  85. const title = thumb.querySelector('span.title').innerText.toLowerCase();
  86. const duration = timeToSeconds(thumb.querySelector('.duration').innerText);
  87. return {
  88. title,
  89. duration,
  90. };
  91. }
  92.  
  93. URL_DATA() {
  94. const { origin, pathname, href } = window.location;
  95. const url = new URL(window.location.href);
  96. const offset = parseInt(url.searchParams.get('page')) || 1;
  97. const iteratable_url = n => {
  98. url.searchParams.set('page', n);
  99. return url.href;
  100. }
  101. return {
  102. offset,
  103. iteratable_url
  104. }
  105. }
  106. }
  107.  
  108. const RULES = new PORNHUB_RULES();
  109.  
  110. //====================================================================================================
  111.  
  112. const { state } = new PersistentState({
  113. filterDurationFrom: 0,
  114. filterDurationTo: 600,
  115. filterDuration: false,
  116. filterExcludeWords: "",
  117. filterExclude: false,
  118. filterIncludeWords: "",
  119. filterInclude: false,
  120. infiniteScrollEnabled: true,
  121. uiEnabled: true,
  122. });
  123.  
  124. const stateLocale = reactive({
  125. pagIndexLast: 1,
  126. pagIndexCur: 1,
  127. });
  128.  
  129. watch([() => state.filterDurationFrom, () => state.filterDurationTo], (a, b) => {
  130. state.filterDurationFrom = parseIntegerOr(a[0], b[0]);
  131. state.filterDurationTo = parseIntegerOr(a[1], b[1]);
  132. if (state.filterDuration) filter_({ filterDuration: true });
  133. });
  134.  
  135. watch(() => state.filterDuration, () => filter_({ filterDuration: true }));
  136.  
  137. watch(() => state.filterExclude, () => filter_({ filterExclude: true }));
  138.  
  139. watch(() => state.filterExcludeWords, () => {
  140. if (state.filterExclude) filter_({ filterExclude: true });
  141. }, { deep: true });
  142.  
  143. watch(() => state.filterInclude, () => filter_({ filterInclude: true }));
  144.  
  145. watch(() => state.filterIncludeWords, () => {
  146. if (state.filterInclude) filter_({ filterInclude: true });
  147. }, { deep: true });
  148.  
  149.  
  150. //====================================================================================================
  151.  
  152. console.log(LOGO);
  153.  
  154. const SCROLL_RESET_DELAY = 350;
  155.  
  156. const { filter_, handleLoadedHTML } = new DataManager(RULES, state);
  157.  
  158. if (window.location.pathname.startsWith('/view_video.php')) {
  159. const containers = Array.from(document.querySelectorAll('li.pcVideoListItem.js-pop.videoBox'))
  160. .reduce((acc, v) => acc.includes(v.parentElement) ? acc : [...acc, v.parentElement], []).slice(2,);
  161. containers.forEach(c => handleLoadedHTML(c, c));
  162. }
  163.  
  164. if (RULES.CONTAINER) {
  165. handleLoadedHTML(RULES.CONTAINER);
  166. }
  167.  
  168. if (window.location.pathname.startsWith('/playlist/')) {
  169. const container = document.querySelector('ul#videoPlaylist');
  170. handleLoadedHTML(container);
  171. watchElementChildrenCount(container, () => {
  172. handleLoadedHTML(container);
  173. });
  174. }
  175.  
  176. if (RULES.PAGINATION) {
  177. const paginationManager = new PaginationManager(state, stateLocale, RULES, handleLoadedHTML, SCROLL_RESET_DELAY);
  178. }
  179.  
  180. const ui = new VueUI(state, stateLocale);