XHamster Improved

Infinite scroll. Filter by duration, include/exclude phrases. Automatically expand more videos on video page

ของเมื่อวันที่ 21-07-2024 ดู เวอร์ชันล่าสุด

  1. // ==UserScript==
  2. // @name XHamster Improved
  3. // @namespace http://tampermonkey.net/
  4. // @license MIT
  5. // @version 2.2.3
  6. // @description Infinite scroll. Filter by duration, include/exclude phrases. Automatically expand more videos on video page
  7. // @author smartacephale
  8. // @supportURL https://github.com/smartacephale/sleazy-fork
  9. // @match https://*.xhamster.com/*
  10. // @match https://*.xhamster.desi/*
  11. // @exclude https://xhamster.com/embed/*
  12. // @icon https://www.google.com/s2/favicons?sz=64&domain=xhamster.com
  13. // @grant unsafeWindow
  14. // @grant GM_addStyle
  15. // @require https://unpkg.com/vue@3.4.21/dist/vue.global.prod.js
  16. // @require https://update.greasyfork.org/scripts/494206/utils.user.js
  17. // @require data:, let tempVue = unsafeWindow.Vue; unsafeWindow.Vue = Vue; const { ref, watch, reactive, createApp } = Vue;
  18. // @require https://update.greasyfork.org/scripts/494207/persistent-state.user.js
  19. // @require https://update.greasyfork.org/scripts/494204/data-manager.user.js?version=1398472
  20. // @require https://update.greasyfork.org/scripts/494205/pagination-manager.user.js
  21. // @require https://update.greasyfork.org/scripts/494203/menu-ui.user.js
  22. // @run-at document-idle
  23. // ==/UserScript==
  24. /* globals getAllUniqueParents watchElementChildrenCount timeToSeconds Observer DefaultState DataManager PaginationManager VueUI */
  25.  
  26. if (!/^(\w{2}.)?xhamster.(com|desi)/.test(window.location.host)) return;
  27.  
  28. const LOGO = `
  29. ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡘⢲⣃⢖⡚⡴⢣⡞⠰⠁⡀⠀⠀⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  30. ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⢮⡵⣫⣝⡳⠏⢠⢃⡐⠁⡘⢀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  31. ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⡀⠀⡀⠀⠀⠀⠀⢀⠐⡀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠔⡉⢃⡉⠓⡈⢃⠊⡐⠡⡐⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  32. ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠠⠐⠀⠄⠐⠀⡐⠀⠠⠁⡀⢁⠀⠂⠄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠂⡔⢢⡐⢣⡜⢤⠣⡜⡰⢀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  33. ⠀⠀⠀⠀⠀⠀⠀⠠⠐⠀⠠⠐⡈⠄⡈⠐⡀⠌⢀⠐⠀⠄⡈⢂⠔⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠦⡙⣌⠣⡜⢣⡚⡥⣛⠴⣡⠃⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  34. ⠀⠀⠀⠀⠀⠐⠈⢀⠐⠈⡐⠠⠐⠠⢀⠡⠐⡀⠂⠌⡐⠠⢐⠡⢈⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠈⡐⠠⣉⠖⡱⢌⠳⣌⢣⡑⠦⠡⢏⡴⣉⠂⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  35. ⠀⠀⠀⠀⠈⠀⠐⠀⠠⢁⠀⢂⠁⠂⠄⠂⡁⠄⢁⠂⠄⡁⢢⠘⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠈⠆⢀⠰⣀⠣⢔⡩⢜⢪⡱⢂⠇⡌⢣⠁⡞⣰⠡⢂⠀⠀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  36. ⠀⠀⠀⠀⠀⠁⠂⠈⠀⠀⠈⡀⠌⠐⠈⠐⠀⠈⠄⢀⠂⠐⡀⢊⠡⠀⠀⠀⠀⠀⠀⠀⠀⠠⡈⢆⡹⠀⢂⢅⠢⣍⠒⡜⣬⠣⣕⠫⡜⢠⠃⢰⠩⡖⡑⢂⠀⡀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  37. ⠀⠀⠀⠀⠀⠀⠀⢀⠀⠈⠀⠄⠀⠄⠂⢀⠈⠀⠀⠀⠀⠁⡀⠣⢘⠀⠀⠀⠀⠀⠀⠀⠄⡑⠌⢢⠐⡡⢌⡊⠵⡨⠝⢲⡤⡛⣌⠳⣈⠆⠡⢈⠳⣌⡑⢂⠠⠀⠂⠀⢀⠀⠀⢄⡀⡠⠀⠄⠀⠀
  38. ⠀⠀⠀⠀⠀⠀⠀⡀⠀⢈⠀⠂⠁⡀⠠⠀⡀⢈⢄⡒⡔⣢⢔⢣⢆⢆⠤⣠⢀⡀⣀⠬⣰⠜⣎⢧⢯⡵⣫⣜⣳⣙⣎⢧⡒⡥⢌⡱⢀⠂⠁⢈⠱⡰⢌⠂⠤⢑⡈⢅⠢⢌⡑⢆⠱⣡⠋⡔⠀⠀
  39. ⠀⠀⠀⠀⠀⠀⠀⢀⠐⠀⢀⠂⢀⠀⡄⢒⡌⠶⣘⠶⣙⢦⣛⢮⡞⣎⡗⣦⠓⡴⣡⢟⣵⡻⣽⡞⣷⢯⣗⣻⢶⣫⡞⣶⡹⣜⣣⢖⡡⢆⡀⢀⠃⠖⡨⠐⣈⠦⡘⢤⠣⠜⡌⣌⠢⢱⡘⠄⠐⠈
  40. ⠀⠀⠀⠀⠀⠀⠈⠀⠀⠐⠀⢀⠢⣘⠰⣃⠞⣵⢫⡟⣽⣺⢽⣮⡝⡾⡼⣉⢾⣱⢯⣟⡾⣽⣳⢿⣽⣻⢾⣽⣳⢯⣽⣳⢯⣳⢳⣎⡗⢮⡐⢆⡉⢒⠡⠀⢖⡰⢉⢆⠣⣍⠲⣀⠣⣁⠎⠄⠀⢀
  41. ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢆⡱⢌⡳⢭⡞⣭⢷⣻⢷⣯⣟⡾⣽⣳⡱⣝⡮⣟⣽⢾⣻⣽⣟⣯⣷⣿⣻⣾⣽⣻⡾⣽⢯⣟⡷⣞⡽⢮⣝⠲⣌⠢⢁⠂⠥⢊⡕⣊⠱⣂⠓⡄⠣⡔⡈⠀⠀⢀
  42. ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣍⠲⣘⢮⡱⣏⡾⣽⢯⣟⣿⣾⡽⣿⡳⢧⡳⣝⡾⣽⢯⣿⣟⣯⣿⣿⣻⣾⣟⣷⣿⢯⣿⢯⣿⢾⣽⣻⣞⡷⣎⢿⡰⣃⠆⡈⢰⢣⡘⢤⠓⣌⢣⠐⡡⢒⠀⠁⠀⢈
  43. ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡘⠤⣃⠳⣎⣳⣭⢿⣽⣻⡾⣿⣟⣿⢻⣝⡣⡝⣮⢿⣽⣻⣯⣿⣿⣿⣿⣿⣿⣿⣿⣻⣿⢿⣻⣯⣿⣞⣷⣻⣼⣛⡮⣵⢣⡚⢄⠘⢆⡙⢦⡙⢤⠃⢎⠡⠆⡌⠀⠀⠠
  44. ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⡴⣉⠗⣎⢷⣚⡿⣞⣷⣻⢷⡯⣝⠳⣪⠵⡹⣜⣻⢾⣽⣻⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣟⣷⡿⣾⣳⣟⣞⣳⡽⣲⢣⡝⢢⠌⡠⢈⠂⠝⣢⠹⣌⠱⢌⠄⠀⠁⠀
  45. ⢣⠜⡄⢦⢀⠤⢂⠔⠀⠑⡰⢡⡛⣬⢳⢯⡽⣻⡼⢯⣳⠽⡌⢯⡔⣫⢷⡸⣝⠿⣞⡿⣯⣿⣿⡿⣿⣿⢿⣿⣽⣷⣿⣿⣻⣽⡷⣟⡾⣝⣧⢻⡱⢇⡞⡡⢎⠔⡡⢌⡐⠠⠑⠌⡱⢈⠤⠈⠄⣀
  46. ⣂⠎⡰⢢⠍⢆⠃⢠⢩⡔⣮⠱⡸⣌⢗⣫⢞⡵⣻⢏⡧⣏⡽⣌⠲⣭⢖⡧⣫⢟⡽⣻⡽⣷⣻⢿⣻⣽⡿⣯⣿⢯⣷⣿⣻⣽⣻⣽⣛⠷⣎⠷⣙⠮⣜⡱⣊⡜⣡⠒⣌⠱⠈⠄⠡⢂⠒⡌⢰⠀
  47. ⢍⡶⣙⢦⡙⢆⠈⣴⢣⡿⣜⡷⣥⠊⢞⡰⢫⡜⣣⠏⠶⣡⢛⡼⣛⠶⡹⢶⡙⣎⠷⣹⣝⣳⢯⣟⣯⢷⣻⢯⣟⡿⣽⣞⣯⢷⡻⢮⡹⡹⣌⠯⣕⡫⢖⣱⢱⡘⠤⠋⣄⢢⢡⠊⡔⣈⠱⡌⢃⠜
  48. ⡟⡴⣋⠶⣙⡎⢲⣭⢿⣽⢻⡼⣣⠙⠢⣅⠣⣜⠰⣉⠖⡡⢉⠖⡥⢋⡓⢧⡙⢦⡙⠰⢪⠕⣏⠾⣜⠯⣝⢯⡞⣽⢣⠟⣬⣳⣟⣿⣳⢷⡩⢞⡱⣭⢋⡖⠣⡌⣥⢫⡔⣣⢎⡱⠒⡄⢆⠘⡂⠥
  49. ⢿⣱⣏⡖⣣⠜⣱⢮⡷⣫⣟⡵⡃⢀⡁⠆⠓⡌⠳⠌⢎⠡⠃⠎⠐⢩⢌⣃⠹⣂⠡⠓⠤⢊⠜⡸⢌⡳⣍⠶⣙⢦⡛⣞⣳⣟⡾⣷⣟⣧⢻⣭⣓⡌⢧⡜⢧⡹⣆⡳⣙⢦⢋⠴⡉⠔⡈⢰⡉⢇
  50. ⢯⠷⣞⣳⢬⠸⣭⢷⡻⣵⢺⡝⣁⢾⡸⣝⢶⡲⣕⠶⣎⢷⣫⢞⡽⡬⣤⢌⣣⣘⢤⡥⣆⡖⡤⣥⣀⡑⠨⠚⠥⢎⢵⣫⢷⣯⣿⣟⣾⡭⢷⣞⣧⡻⣥⢛⢧⣛⡴⢣⡍⢦⡉⢆⢁⣂⢠⢃⡜⢤
  51. ⣳⢻⡜⣣⢎⡱⣏⢾⡳⣭⢷⡚⢬⡷⣻⡼⣧⣟⢮⣟⢮⣳⣝⡮⢷⡽⢮⣟⡶⣭⢷⣻⡼⣝⣳⡵⣫⢽⡻⢶⣞⣤⠸⣝⣯⡷⣿⣻⢾⣽⣻⡾⣽⣳⢎⡝⡎⠶⣩⠇⣜⡰⡜⣸⠲⡌⣆⠣⡜⢢
  52. ⡷⣋⠾⣥⢫⡴⢫⢧⡛⣭⠾⡽⣣⠻⣵⢻⡵⣞⣟⡾⣏⡷⡾⣽⣛⣾⢻⣼⢻⣭⢷⣳⢟⣭⢷⡻⣝⣾⡹⢷⣎⢯⣽⣻⣞⣿⣳⢯⣟⡾⣣⣟⣷⡻⢎⡰⣉⠳⣤⢛⠴⡣⣕⢣⠣⡝⢤⠓⣍⠆
  53. ⣭⠳⣙⠶⡩⢎⠅⡻⣜⢦⣋⠷⣩⠗⣎⢯⣽⢫⡾⡽⣽⣹⢟⣵⡻⢮⡟⣾⣹⢮⡟⣽⠾⣭⡟⣽⣫⢶⡛⢯⡴⣻⣞⡷⣯⢷⢫⢟⣼⣳⢓⡾⡱⡝⣢⠕⣎⡳⢬⣋⢞⡱⡜⢪⡕⡜⢢⡙⢤⡉
  54. ⢦⡙⡜⢢⠓⣌⢚⡱⣌⠳⣎⠳⣍⡛⣜⡘⡮⢏⡷⣻⢵⣫⢟⡼⣹⠯⣝⢶⢫⡗⣯⠝⢯⠳⡝⢣⢍⠲⣙⠮⡵⣛⡼⡹⢎⢇⡫⡞⡵⣃⠷⣜⢣⡝⢦⡛⡴⡙⢦⡉⢦⠓⣌⠣⠜⡨⢅⡘⠤⣈
  55. ⢆⠰⢈⠆⠱⣀⠣⢒⠌⡓⡌⠳⢄⠹⢤⠓⡘⢭⠲⡱⢎⠶⣩⠚⡥⢛⡌⢎⡱⠚⣄⢋⠆⡣⢉⠖⡨⠣⢍⠚⠥⢓⠬⡑⢎⠲⠱⣙⠲⣉⠞⣌⠣⢎⡱⢜⠲⣉⠦⡙⢦⡙⠤⢓⡘⣐⠢⢌⡐⢀
  56. ⢀⠂⠡⢈⠐⠠⠑⣈⠢⠑⡈⢁⠊⡐⠂⠉⠜⡀⠣⠑⠊⠆⡅⢋⠔⠡⠘⠠⠂⠑⠀⡈⠀⠁⡀⢀⠀⢁⠀⠌⠠⢁⠂⠡⢈⠰⠁⢂⠡⠐⠈⠄⡉⢂⠱⢈⠱⢈⠒⠩⡐⠌⡑⠌⠰⢀⠃⠂⠄⢃
  57. ⢀⠈⠄⠂⠈⠄⠡⠀⠄⠁⡀⠂⠀⠀⠀⠀⠀⠀⠁⠈⡐⠠⠀⠂⠐⠠⠀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠂⠐⠠⠈⠐⢀⠂⠐⠈⡐⠀⡈⠐⠈⠀`;
  58.  
  59. class XHAMSTER_RULES {
  60. constructor() {
  61. this.IS_VIDEO_PAGE = /^\/videos|moments\//.test(window.location.pathname);
  62. this.PAGINATION = document.querySelector('.prev-next-list, .test-pager');
  63. this.PAGINATION_LAST = parseInt(Array.from((this.PAGINATION || document).querySelectorAll('.page-button-link, .xh-paginator-button')).pop()?.innerText);
  64. this.CONTAINER = Array.from(document.querySelectorAll('.thumb-list')).pop();
  65. }
  66.  
  67. GET_THUMBS(html) {
  68. return html.querySelectorAll('.thumb-list__item:not([data-video-type])');
  69. }
  70.  
  71. THUMB_URL(thumb) {
  72. return thumb.firstElementChild.href;
  73. }
  74.  
  75. THUMB_IMG_DATA(thumb) {
  76. if (stateLocale.pagIndexCur === 1) return ({});
  77. const img = thumb.querySelector('img[loading]');
  78. if (img) img.removeAttribute('loading');
  79. if (!img?.complete || img.naturalWidth === 0) return ({});
  80. return { img, imgSrc: img.src }
  81. }
  82.  
  83. THUMB_DATA(thumb) {
  84. const title = thumb.querySelector('.video-thumb-info__name').innerText.toLowerCase();
  85. const duration = timeToSeconds(thumb.querySelector('.thumb-image-container__duration').innerText);
  86. return {
  87. title,
  88. duration,
  89. };
  90. }
  91.  
  92. URL_DATA() {
  93. const { origin, pathname, href } = window.location;
  94.  
  95. let offset = 1;
  96. let iteratable_url = () => "";
  97.  
  98. if (/^\/search\//.test(pathname)) {
  99. const url = new URL(href);
  100. offset = parseInt(url.searchParams.get('page')) || 1;
  101. iteratable_url = n => {
  102. url.searchParams.set('page', n);
  103. return url.href;
  104. };
  105. } else {
  106. const mres = location.pathname.split(/\/(\d+)$/);
  107. const base = mres[0];
  108. if (mres[1]) offset = parseInt(mres[1]);
  109. iteratable_url = n => `${origin}${base}/${n}`;
  110. }
  111.  
  112. return {
  113. offset,
  114. iteratable_url
  115. }
  116. }
  117. }
  118.  
  119. const RULES = new XHAMSTER_RULES();
  120.  
  121. //====================================================================================================
  122.  
  123. function expandMoreVideoPage() {
  124. const getExpandButton = () => document.querySelector('button[data-role="show-more-next"]');
  125. const observer = new Observer((target) => {
  126. target.click();
  127. });
  128. observer.observe(getExpandButton());
  129. }
  130.  
  131. //====================================================================================================
  132.  
  133. function createPreviewVideoElement(src, mount) {
  134. const video = document.createElement('video');
  135. video.playsinline = true;
  136. video.autoplay = true;
  137. video.loop = true;
  138. video.classList.add('thumb-image-container__video');
  139. video.src = src;
  140. video.addEventListener('loadeddata', () => {
  141. mount.before(video);
  142. }, false);
  143. return {
  144. video,
  145. removeVideo: () => {
  146. video.removeAttribute('src');
  147. video.load();
  148. video.remove();
  149. }
  150. };
  151. }
  152.  
  153. function handleThumbHover(e) {
  154. if (!e.target.classList.contains('thumb-image-container__image')) return;
  155. const videoSrc = e.target.parentElement.getAttribute('data-previewvideo');
  156. const { video, removeVideo } = createPreviewVideoElement(videoSrc, e.target);
  157. e.target.parentElement.parentElement.addEventListener('mouseleave', removeVideo, { once: true });
  158. }
  159.  
  160. function animate() {
  161. document.body.addEventListener('mouseover', handleThumbHover);
  162. }
  163.  
  164. //====================================================================================================
  165.  
  166. function parseInPLace() {
  167. const containers = getAllUniqueParents(RULES.GET_THUMBS(document.body));
  168. containers.forEach(c => handleLoadedHTML(c, c, false, false));
  169. }
  170.  
  171. function route() {
  172. animate();
  173.  
  174. if (RULES.IS_VIDEO_PAGE) {
  175. expandMoreVideoPage();
  176. watchElementChildrenCount(RULES.CONTAINER, (_, count) => {
  177. setTimeout(parseInPLace, 1800);
  178. });
  179. }
  180.  
  181. if (RULES.PAGINATION) {
  182. const paginationManager = new PaginationManager(state, stateLocale, RULES, handleLoadedHTML, SCROLL_RESET_DELAY);
  183. }
  184.  
  185. parseInPLace();
  186.  
  187. const ui = new VueUI(state, stateLocale);
  188. }
  189.  
  190. //====================================================================================================
  191.  
  192. const SCROLL_RESET_DELAY = 350;
  193.  
  194. const defaultState = new DefaultState();
  195. const { state, stateLocale } = defaultState;
  196. const { filter_, handleLoadedHTML } = new DataManager(RULES, state);
  197. defaultState.setWatchers(filter_);
  198.  
  199. console.log(LOGO);
  200. route();