Sleazy Fork is available in English.

SpankBang.com Improved

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

Fra og med 29.10.2024. Se den nyeste version.

  1. // ==UserScript==
  2. // @name SpankBang.com Improved
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.96
  5. // @license MIT
  6. // @description Infinite scroll (optional). 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://cdn.jsdelivr.net/npm/billy-herrington-utils@1.1.8/dist/billy-herrington-utils.umd.js
  12. // @require https://cdn.jsdelivr.net/npm/jabroni-outfit@1.4.9/dist/jabroni-outfit.umd.js
  13. // @require https://update.greasyfork.org/scripts/494204/data-manager.user.js?version=1458190
  14. // @require https://update.greasyfork.org/scripts/494205/pagination-manager.user.js?version=1459738
  15. // @run-at document-idle
  16. // @icon https://www.google.com/s2/favicons?sz=64&domain=spankbang.com
  17. // ==/UserScript==
  18. /* globals $ DataManager PaginationManager */
  19.  
  20. Object.assign(unsafeWindow, { bhutils: window.bhutils });
  21. const { JabroniOutfitStore, defaultStateWithDuration, JabroniOutfitUI, DefaultScheme } = window.jabronioutfit;
  22.  
  23. const LOGO = `
  24. ⡕⡧⡳⡽⣿⣇⠀⢀⠀⠄⠐⡐⠌⡂⡂⠠⠀⠠⠀⠠⠀⠠⡐⡆⡇⣇⢎⢆⠆⠌⢯⡷⡥⡂⡐⠨⣻⣳⢽⢝⣵⡫⣗⢯⣺⢵⢹⡪⡳⣝⢮⡳⣿⣿⣿⣿⣿⣿⣿⣿
  25. ⢎⢞⡜⣞⣿⡯⡆⠀⠄⠐⠀⢐⠡⢊⢐⢀⠈⠀⠄⠁⡀⠡⠸⡸⡪⣪⢺⢸⢱⠡⢑⡝⣟⣧⡂⠅⠪⣻⣎⢯⡺⣪⣗⢯⣺⢪⡣⡯⣝⢼⡪⣞⣽⣿⣿⣿⣿⣿⣿⣿
  26. ⡱⡣⣣⢳⣻⣯⢿⡐⠀⠂⠁⢀⠪⢐⠐⠄⡀⠁⠄⠁⠀⠄⡑⠆⡎⢎⢎⢇⢕⠬⠀⢇⢣⢻⣞⠌⠌⡪⣷⡱⣝⢮⣺⢕⡗⣕⢧⢳⢕⢗⣝⢞⣾⣿⣿⣿⣿⣿⣿⣻
  27. ⡸⡪⣪⡚⣞⣯⡗⣝⡀⠄⠁⡀⠌⡂⠅⡁⠄⠂⠐⠈⠀⢂⢐⢔⢜⢜⢜⢜⢔⢬⢊⠠⠨⠨⣻⣽⢐⠨⡺⣞⢼⢕⣗⡽⣪⢺⢜⢵⡹⣕⢵⡫⣾⣿⣿⣿⣿⣿⢯⣟
  28. ⢎⢇⢇⢧⡫⣿⡞⡜⡄⠀⠄⠀⠐⠄⡁⠀⠄⠐⠀⠂⢁⢐⢔⢕⢕⠱⢱⠱⡱⡱⡱⡅⡂⢁⠪⡿⣎⡢⡘⢽⡪⡧⣳⡝⣜⢎⢗⢇⢯⡪⡧⣻⡺⣿⣿⣿⣿⢯⡻⡮
  29. ⢕⢝⢜⡜⡮⣻⣗⢕⠅⡀⠂⠈⢈⠐⠀⠁⢀⠐⠀⡁⠄⢊⢢⢣⢣⢣⢣⢣⢣⢫⢪⢪⠀⠄⠨⡚⣿⣳⡜⡘⣗⢽⡺⡪⡺⡸⡵⡹⡕⡧⣫⡺⡺⣿⣿⡿⡯⡯⣫⢯
  30. ⢸⢱⢱⢱⢕⢯⣿⢜⠌⢀⠀⠂⠀⠂⠁⠈⠀⠀⠄⠀⠄⢑⠌⡆⢇⢅⠕⡌⡬⡪⡪⡪⠀⠄⢁⠸⡸⣯⡻⣪⢺⡵⡫⡪⣣⢫⡪⡣⡏⣞⢜⢮⣫⣻⡿⣫⢯⢞⣗⢽
  31. ⢪⢪⢪⢪⢎⢞⢾⡇⢕⠄⡆⡪⡘⡔⢔⢀⠐⠀⢀⠁⠠⠀⠑⠜⡌⡖⡕⡕⡕⢕⠑⠀⠠⠐⠀⡀⠕⡳⣻⣜⣕⣯⢣⠣⡣⡣⡣⡫⡪⡪⣎⢧⣧⡳⣝⢮⡳⡽⣜⢵
  32. ⡸⡸⡸⡸⡸⡱⣫⢯⣲⢱⢱⢘⢜⢜⢕⢕⢅⣂⠀⡀⠐⠀⡁⢐⠨⢐⠡⡊⢌⠂⠄⠂⠀⠂⡠⡠⡢⡫⡳⡱⡝⡮⡪⣇⢧⢳⢕⣝⢮⡫⡮⡳⣕⢵⣓⢗⡝⡮⡺⡜
  33. ⠱⡑⡕⡕⡝⡜⣎⢿⣪⢗⡝⡜⡜⡜⡜⢜⢕⢎⢎⢆⢎⢔⢄⠢⡌⢆⢕⠨⡢⢱⢰⢸⢸⣜⢮⢎⢎⢎⢇⢇⢯⢪⢇⡗⣝⢮⢳⢕⡗⣝⢮⡫⡮⣣⡳⡕⣝⢼⢸⢸
  34. ⢜⢸⢨⢪⢪⢪⢪⣻⣺⢯⢎⣇⢧⢧⢧⢧⢯⢾⡵⣯⢾⣼⣜⣜⢜⢜⢜⢜⢜⢜⡜⡮⣗⡯⣟⣎⢎⢆⢇⢕⢕⢇⢗⡝⣜⢮⣳⣟⣾⣷⣷⣿⣮⣎⢎⢎⢎⢎⢎⠎
  35. ⠣⡑⢌⠎⡜⢜⢜⢜⢜⡕⣧⡳⣫⢏⡯⡯⡯⡯⣯⢯⣟⢷⣻⣟⣿⢷⣷⣳⡽⡮⡾⣝⡎⡏⡎⡇⡕⡜⡔⡕⡕⣝⢜⢮⡳⡽⣺⢽⣻⢿⢿⣟⣿⣺⣝⢮⡢⡧⣧⡠
  36. ⠐⠨⢢⢑⢅⢣⢱⢜⢞⢮⡳⣝⢮⣻⣪⢯⢯⡻⣺⢝⡾⣝⢷⢽⣺⢯⢿⢽⣻⣮⡳⣕⢇⢇⢇⢇⠪⡪⡪⡪⡪⣎⢯⢳⣹⡪⡯⣫⢯⣻⢽⣳⣳⣳⢳⣝⢮⡫⡷⣷
  37. ⠀⠨⡂⡅⡖⡵⡹⣕⢏⡧⢯⢮⡳⣣⢷⢽⢕⣯⡳⣏⡯⣞⡽⣳⢽⢽⢽⣝⣗⣗⣟⢮⢳⡱⡱⡱⡑⡕⡜⢜⢜⡜⣎⢧⡣⡯⡺⣝⡵⡯⡯⣞⣞⢮⣳⡳⣝⣞⡽⣺
  38. ⠀⠰⡸⡸⣸⢪⡫⣎⢗⢽⢕⣗⢽⣕⢯⣳⡫⣞⢮⣳⢽⣪⢯⢞⡽⣺⢵⣳⣳⡳⣳⢝⢵⡹⡪⣎⢎⢆⢇⢇⢇⢗⡕⡧⡳⣝⡝⡮⣞⡽⡽⡵⣳⣫⢞⡮⣗⣗⢽⣳
  39. ⠀⠨⢢⢣⢳⡱⣝⢼⢭⢳⢳⢕⣗⢗⡽⣪⢞⡽⣕⢷⢝⡮⣏⡯⣞⣗⢽⡺⡼⣺⢵⡫⡧⣳⢹⢜⡜⡜⡜⢜⢪⢣⡫⣎⢯⡪⣞⣝⢞⢮⢏⡯⣳⣳⣫⢾⢕⣗⢯⣞
  40. ⠀⠀⠱⡱⡱⡕⡧⡳⡕⣏⢗⣝⢮⢳⢝⢮⡳⣝⢮⡳⣝⢮⡳⣝⢮⡺⡵⣫⢯⡳⣳⢝⣞⡜⣎⢇⢎⠎⡪⢊⠎⡎⡎⡮⡺⣜⢮⢮⣫⣫⡳⡽⣕⣗⢵⣫⢯⡺⡵⣳
  41. ⠀⠀⠈⢎⢎⠮⡺⡸⡕⡧⡳⣕⢝⢮⡫⣣⢯⣪⢳⢝⢮⢳⢝⢮⡳⣝⣝⢮⡳⣝⢮⡳⡵⣝⢼⢸⢐⠅⠂⡐⢅⢇⢣⢣⢳⡱⣝⠮⡮⣪⢞⣝⢮⡺⣕⢗⣗⢽⡹⣪
  42. ⠀⠈⠀⠌⡒⡝⡜⣕⢕⢧⢫⡪⡳⡱⣝⢜⢮⡪⣳⢹⡪⣳⢹⢕⢽⡸⣜⢵⢝⢮⢳⢝⢞⢮⡪⡣⡣⡑⢅⢢⠱⡘⡜⡜⣜⢜⠮⡝⡮⡪⡧⡳⡵⣹⡪⡳⡕⡗⣝⢮
  43. ⠀⠐⠀⡁⠌⢎⢎⢎⢮⢪⢎⢮⢪⢳⢱⢝⢜⢎⢮⢣⡫⡪⡎⣗⢕⢧⢳⡱⡝⣎⢗⣝⢕⡗⣝⢼⢸⢨⢢⢃⢇⢣⢣⢣⢣⢳⢹⢪⢎⢗⢝⡜⣎⢮⢪⡳⡹⣪⢺⢸
  44. ⠀⠠⠐⢀⠐⠈⡎⡪⡪⡪⡪⡪⡣⡫⡪⡪⡣⡫⡪⡣⡣⡫⣪⢪⢺⢸⢪⢪⢺⢸⢪⡪⡺⡸⣪⢪⢪⢪⠸⡨⡊⡎⡎⡎⡇⡏⡎⡞⡜⡕⣕⢕⢕⡕⡇⡧⡫⡪⡪⡪
  45. ⢀⠀⠐⠀⠄⠁⠨⡊⡎⡎⡎⡎⡎⡎⡎⡎⡇⡏⡎⡎⡎⡎⡎⡎⡎⡮⡪⡣⡳⡱⡱⡕⡝⣜⢜⢜⢜⢔⢕⢱⠸⡸⡸⡸⡸⡸⡸⡸⡸⡸⡸⡨⡣⡣⡣⡣⡣⡣⡃⡇
  46. ⠀⠀⠈⡀⠂⠐⠀⢑⠜⡌⢎⢪⠪⡪⡪⡪⢪⠪⡪⢪⠪⡪⡪⢪⠪⡊⡎⡜⡌⡎⢎⢎⢎⢎⠎⡎⡪⠢⡑⢌⢪⢘⢔⠱⡡⢣⢃⢇⠕⡕⢅⢇⢣⢱⢑⢕⠸⡐⡱⡘`;
  47.  
  48.  
  49. class SPANKBANG_RULES {
  50. constructor() {
  51. this.PAGINATION = document.querySelector('.paginate-bar, .pagination');
  52. this.PAGINATION_LAST = parseInt(
  53. document.querySelector('.paginate-bar .status span')?.innerText.match(/\d+/)?.[0] ||
  54. document.querySelector('.pagination .next')?.previousElementSibling?.innerText);
  55. this.CONTAINER = document.querySelectorAll('.results .video-list')[0];
  56. this.HAS_VIDEOS = !!this.GET_THUMBS(document.body).length > 0;
  57. }
  58.  
  59. GET_THUMBS(html) {
  60. return Array.from(html.querySelectorAll('.video-item:not(.clear-fix)') || [])
  61. .filter(e => !e.parentElement.hasAttribute('data-disabled-layout-change'));
  62. }
  63.  
  64. THUMB_URL(thumb) {
  65. return thumb.querySelector('.thumb').href;
  66. }
  67.  
  68. THUMB_IMG_DATA(thumb) {
  69. const img = thumb.querySelector('img');
  70. const imgSrc = img.getAttribute('data-src');
  71. img.removeAttribute('data-src');
  72. return { img, imgSrc };
  73. }
  74.  
  75. THUMB_DATA(thumb) {
  76. const title = bhutils.sanitizeStr(thumb.querySelector('.name')?.innerText);
  77. const duration = (parseInt(thumb.querySelector('span.l')?.innerText) || 1) * 60;
  78. return { title, duration };
  79. }
  80.  
  81. URL_DATA() {
  82. const url = new URL(window.location.href);
  83. const offset = parseInt(url.pathname.match(/\/(\d+)\/$/)?.pop()) || 1;
  84. if (!/\/\d+\/$/.test(url.pathname)) url.pathname = `${url.pathname}/${offset}/`;
  85.  
  86. const iteratable_url = n => {
  87. url.pathname = url.pathname.replace(/\/\d+\/$/, `/${n}/`);
  88. return url.href;
  89. };
  90.  
  91. return { offset, iteratable_url };
  92. }
  93. }
  94.  
  95. const RULES = new SPANKBANG_RULES();
  96.  
  97. //====================================================================================================
  98.  
  99.  
  100. function createPreviewElement(src, mount) {
  101. const elem = bhutils.parseDom(`
  102. <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"
  103. id="mp4t_video" tabindex="-1" lang="en" translate="no" role="region" aria-label="Video Player" style="display: none;">
  104. <video id="mp4t_video_html5_api" class="vjs-tech" tabindex="-1" loop="loop" autoplay="autoplay" muted="muted" playsinline="playsinline"></video>
  105. <div class="vjs-poster vjs-hidden" tabindex="-1" aria-disabled="false"></div>
  106. <div class="vjs-text-track-display" translate="yes" aria-live="off" aria-atomic="true">
  107. <div style="position: absolute; inset: 0px; margin: 1.5%;"></div>
  108. </div>
  109. <div class="vjs-loading-spinner" dir="ltr">
  110. <span class="vjs-control-text">Video Player is loading.</span>
  111. </div><button class="vjs-big-play-button" type="button" title="Play Video" aria-disabled="false">
  112. <span class="vjs-icon-placeholder" aria-hidden="true"></span>
  113. <span class="vjs-control-text" aria-live="polite">
  114. </div>`);
  115.  
  116. mount.append(elem);
  117. const video = elem.querySelector('video');
  118. video.src = src;
  119. video.addEventListener('loadeddata', () => {
  120. elem.style.display = 'block';
  121. }, false);
  122.  
  123. const removeElem = () => {
  124. video.removeAttribute('src');
  125. video.load();
  126. elem.remove();
  127. }
  128.  
  129. return { elem, removeElem };
  130. }
  131.  
  132. function animate() {
  133. RULES.GET_THUMBS(document.body)?.forEach(e => { e.querySelector('[data-preview]')?.setAttribute('do-not-animate', ''); });
  134. function handleThumbHover(e) {
  135. if (!(e.target.classList.contains('cover') && e.target.getAttribute('data-preview') && !e.target.hasAttribute('do-not-animate'))) return;
  136. const parent = e.target.parentElement.parentElement;
  137. const videoSrc = e.target.getAttribute('data-preview');
  138. const { removeElem } = createPreviewElement(videoSrc, parent);
  139. parent.addEventListener('mouseleave', removeElem, { once: true });
  140. }
  141.  
  142. document.body.addEventListener('mouseover', handleThumbHover, true);
  143. }
  144.  
  145. //====================================================================================================
  146.  
  147. console.log(LOGO);
  148.  
  149. const SCROLL_RESET_DELAY = 350;
  150.  
  151. const store = new JabroniOutfitStore(defaultStateWithDuration);
  152. const { state, stateLocale } = store;
  153. const { applyFilters, handleLoadedHTML } = new DataManager(RULES, state);
  154. store.subscribe(applyFilters);
  155.  
  156. if (RULES.HAS_VIDEOS) {
  157. animate();
  158. new JabroniOutfitUI(store);
  159. document.querySelectorAll('.video-list').forEach(c => handleLoadedHTML(c, c));
  160. }
  161.  
  162. if (RULES.PAGINATION) {
  163. new PaginationManager(state, stateLocale, RULES, handleLoadedHTML, SCROLL_RESET_DELAY);
  164. }
  165.