SpankBang.com Improved

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

Stan na 07-08-2024. Zobacz najnowsza wersja.

  1. // ==UserScript==
  2. // @name SpankBang.com Improved
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.7
  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://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/menu-ui.user.js?version=1423690
  18. // @run-at document-idle
  19. // @icon https://www.google.com/s2/favicons?sz=64&domain=spankbang.com
  20. // ==/UserScript==
  21. /* globals DefaultState parseDOM DataManager PaginationManager VueUI sanitizeStr */
  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 html.querySelectorAll('.video-item:not(.clear-fix)');
  61. }
  62.  
  63. THUMB_URL(thumb) {
  64. return thumb.querySelector('.thumb').href;
  65. }
  66.  
  67. THUMB_IMG_DATA(thumb) {
  68. const img = thumb.querySelector('img');
  69. const imgSrc = img.getAttribute('data-src');
  70. img.removeAttribute('data-src');
  71. return { img, imgSrc };
  72. }
  73.  
  74. THUMB_DATA(thumb) {
  75. const title = sanitizeStr(thumb.querySelector('span.n, a.n')?.innerText);
  76. const duration = (parseInt(thumb.querySelector('span.l')?.innerText) || 1) * 60;
  77. return { title, duration };
  78. }
  79.  
  80. URL_DATA() {
  81. const url = new URL(window.location.href);
  82. const offset = parseInt(url.pathname.match(/\/(\d+)\/$/)?.pop()) || 1;
  83. if (!/\/\d+\/$/.test(url.pathname)) url.pathname = `${url.pathname}/${offset}/`;
  84.  
  85. const iteratable_url = n => {
  86. url.pathname = url.pathname.replace(/\/\d+\/$/, `/${n}/`);
  87. return url.href;
  88. };
  89.  
  90. return { offset, iteratable_url };
  91. }
  92. }
  93.  
  94. const RULES = new SPANKBANG_RULES();
  95.  
  96. //====================================================================================================
  97.  
  98. function createPreviewElement(src, mount) {
  99. const elem = parseDOM(`
  100. <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"
  101. id="mp4t_video" tabindex="-1" lang="en" translate="no" role="region" aria-label="Video Player" style="display: none;">
  102. <video id="mp4t_video_html5_api" class="vjs-tech" tabindex="-1" loop="loop" autoplay="autoplay" muted="muted" playsinline="playsinline"></video>
  103. <div class="vjs-poster vjs-hidden" tabindex="-1" aria-disabled="false"></div>
  104. <div class="vjs-text-track-display" translate="yes" aria-live="off" aria-atomic="true">
  105. <div style="position: absolute; inset: 0px; margin: 1.5%;"></div>
  106. </div>
  107. <div class="vjs-loading-spinner" dir="ltr">
  108. <span class="vjs-control-text">Video Player is loading.</span>
  109. </div><button class="vjs-big-play-button" type="button" title="Play Video" aria-disabled="false">
  110. <span class="vjs-icon-placeholder" aria-hidden="true"></span>
  111. <span class="vjs-control-text" aria-live="polite">
  112. </div>`);
  113.  
  114. mount.append(elem);
  115. const video = elem.querySelector('video');
  116. video.src = src;
  117. video.addEventListener('loadeddata', () => {
  118. elem.style.display = 'block';
  119. }, false);
  120.  
  121. return {
  122. elem,
  123. removeElem: () => {
  124. video.removeAttribute('src');
  125. video.load();
  126. elem.remove();
  127. }
  128. };
  129. }
  130.  
  131. function animate() {
  132. function handleThumbHover(e) {
  133. if (!(e.target.classList.contains('cover') && e.target.getAttribute('data-preview'))) return;
  134. const videoSrc = e.target.getAttribute('data-preview');
  135. const { removeElem } = createPreviewElement(videoSrc, e.target.parentElement.parentElement);
  136. e.target.parentElement.parentElement.addEventListener('mouseleave', removeElem, { once: true });
  137. }
  138.  
  139. document.body.addEventListener('mouseover', handleThumbHover);
  140. }
  141.  
  142. //====================================================================================================
  143.  
  144. console.log(LOGO);
  145.  
  146. const SCROLL_RESET_DELAY = 350;
  147.  
  148. const defaultState = new DefaultState();
  149. const { state, stateLocale } = defaultState;
  150. const { filter_, handleLoadedHTML } = new DataManager(RULES, state);
  151. defaultState.setWatchers(filter_);
  152.  
  153. if (RULES.HAS_VIDEOS) {
  154. animate();
  155. const ui = new VueUI(state, stateLocale);
  156. document.querySelectorAll('.video-list').forEach(c => {
  157. handleLoadedHTML(c, c);
  158. });
  159. }
  160.  
  161. if (RULES.PAGINATION) {
  162. const paginationManager = new PaginationManager(state, stateLocale, RULES, handleLoadedHTML, SCROLL_RESET_DELAY);
  163. }
  164.