Sleazy Fork is available in English.

SpankBang.com Improved

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

Version au 08/05/2024. Voir la dernière version.

  1. // ==UserScript==
  2. // @name SpankBang.com Improved
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.6
  5. // @license MIT
  6. // @description Infinite scroll (optional). Lazy loading. Filter by duration, include/exclude phrases
  7. // @author smartacephale
  8. // @supportURL https://github.com/smartacephale/sleazy-fork
  9. // @match https://*.spankbang.com/*
  10. // @grant GM_addStyle
  11. // @require https://unpkg.com/vue@3.4.21/dist/vue.global.prod.js
  12. // @require https://update.greasyfork.org/scripts/494206/utils.user.js
  13. // @require data:, let tempVue = unsafeWindow.Vue; unsafeWindow.Vue = Vue; const { ref, watch, reactive, createApp } = Vue;
  14. // @require https://update.greasyfork.org/scripts/494207/persistent-state.user.js
  15. // @require https://update.greasyfork.org/scripts/494204/data-manager.user.js
  16. // @require https://update.greasyfork.org/scripts/494205/pagination-manager.user.js
  17. // @require https://update.greasyfork.org/scripts/494203/vue-ui.user.js
  18. // @run-at document-idle
  19. // @icon https://www.google.com/s2/favicons?sz=64&domain=spankbang.com
  20. // ==/UserScript==
  21. /* globals jQuery, $, Vue, createApp, watch, reactive, DefaultState,
  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. class SPANKBANG_RULES {
  52. constructor() {
  53. this.PAGINATION = document.querySelector('.paginate-bar') || document.querySelector('.pagination');
  54. this.PAGINATION_LAST = parseInt(
  55. document.querySelector('.paginate-bar .status span')?.innerText.match(/\d+/)?.[0] ||
  56. document.querySelector('.pagination .next')?.previousElementSibling?.innerText);
  57. this.CONTAINER = document.querySelectorAll('.results .video-list')[0];
  58. this.HAS_VIDEOS = document.querySelector('.video-list');
  59. }
  60.  
  61. GET_THUMBS(html) {
  62. return html.querySelectorAll('.video-item:not(.clear-fix)');
  63. }
  64.  
  65. THUMB_URL(thumb) {
  66. return thumb.querySelector('.thumb').href;
  67. }
  68.  
  69. THUMB_IMG_DATA(thumb) {
  70. const img = thumb.querySelector('img');
  71. const imgSrc = img.getAttribute('data-src');
  72. img.removeAttribute('data-src');
  73. return { img, imgSrc };
  74. }
  75.  
  76. THUMB_DATA(thumb) {
  77. const title = (thumb.querySelector('span.n') || thumb.querySelector('a.n')).innerText.toLowerCase();
  78. const duration = (parseInt(thumb.querySelector('span.l')?.innerText) || 1) * 60;
  79. return {
  80. title,
  81. duration
  82. }
  83. }
  84.  
  85. URL_DATA() {
  86. const { href, pathname, search, origin } = window.location;
  87. const mres = pathname.split(/\/(\d+)\/?$/);
  88. const basePathname = mres[0];
  89. const offset = parseInt(mres[1]) || 1;
  90. const iteratable_url = n => `${origin}${basePathname}/${n}/${search}`;
  91. return {
  92. offset,
  93. iteratable_url
  94. };
  95. }
  96. }
  97.  
  98. const RULES = new SPANKBANG_RULES();
  99.  
  100. //====================================================================================================
  101.  
  102. function createPreviewElement(src, mount) {
  103. const elem = parseDOM(`
  104. <div class="video-js vjs-controls-disabled vjs-touch-enabled vjs-workinghover vjs-v7 vjs-user-active vjs-playing vjs-has-started mp4t_video-dimensions"
  105. id="mp4t_video" tabindex="-1" lang="en" translate="no" role="region" aria-label="Video Player" style="display: none;">
  106. <video id="mp4t_video_html5_api" class="vjs-tech" tabindex="-1" loop="loop" autoplay="autoplay" muted="muted" playsinline="playsinline"></video>
  107. <div class="vjs-poster vjs-hidden" tabindex="-1" aria-disabled="false"></div>
  108. <div class="vjs-text-track-display" translate="yes" aria-live="off" aria-atomic="true">
  109. <div style="position: absolute; inset: 0px; margin: 1.5%;"></div>
  110. </div>
  111. <div class="vjs-loading-spinner" dir="ltr">
  112. <span class="vjs-control-text">Video Player is loading.</span>
  113. </div><button class="vjs-big-play-button" type="button" title="Play Video" aria-disabled="false">
  114. <span class="vjs-icon-placeholder" aria-hidden="true"></span>
  115. <span class="vjs-control-text" aria-live="polite">
  116. </div>`);
  117.  
  118. mount.append(elem);
  119. const video = elem.querySelector('video');
  120. video.src = src;
  121. video.addEventListener('loadeddata', () => {
  122. elem.style.display = 'block';
  123. }, false);
  124.  
  125. return {
  126. elem,
  127. removeElem: () => {
  128. video.removeAttribute('src');
  129. video.load();
  130. elem.remove();
  131. }
  132. };
  133. }
  134.  
  135. function animate() {
  136. function handleThumbHover(e) {
  137. if (!(e.target.classList.contains('cover') && e.target.getAttribute('data-preview'))) return;
  138. const videoSrc = e.target.getAttribute('data-preview');
  139. const { elem, removeElem } = createPreviewElement(videoSrc, e.target.parentElement.parentElement);
  140. e.target.parentElement.parentElement.addEventListener('mouseleave', removeElem, { once: true });
  141. }
  142.  
  143. if (RULES.PAGINATION || document.querySelectorAll('.video-list').length > 0) {
  144. (RULES.CONTAINER || document.body).addEventListener('mouseover', handleThumbHover);
  145. }
  146. }
  147.  
  148. //====================================================================================================
  149.  
  150. console.log(LOGO);
  151.  
  152. const SCROLL_RESET_DELAY = 350;
  153.  
  154. const defaultState = new DefaultState();
  155. const { state, stateLocale } = defaultState;
  156. const { filter_, handleLoadedHTML } = new DataManager(RULES, state);
  157. defaultState.setWatchers(filter_);
  158.  
  159. if (RULES.HAS_VIDEOS) {
  160. animate();
  161. const ui = new VueUI(state, stateLocale);
  162. document.querySelectorAll('.video-list').forEach(c => {
  163. handleLoadedHTML(c, c);
  164. });
  165. }
  166.  
  167. if (RULES.PAGINATION) {
  168. const paginationManager = new PaginationManager(state, stateLocale, RULES, handleLoadedHTML, SCROLL_RESET_DELAY);
  169. }
  170.