// ==UserScript==
// @name SpankBang.com Improved
// @namespace http://tampermonkey.net/
// @version 1.4
// @license MIT
// @description Infinite scroll (optional). Lazy loading. Filter by duration, include/exclude phrases
// @author smartacephale
// @match https://*.spankbang.com/*
// @grant GM_addStyle
// @require https://unpkg.com/[email protected]/dist/vue.global.prod.js
// @require https://update.greasyfork.org/scripts/494206/utils.user.js
// @require data:, let tempVue = unsafeWindow.Vue; unsafeWindow.Vue = Vue; const { ref, watch, reactive, createApp } = Vue;
// @require https://update.greasyfork.org/scripts/494207/persistent-state.user.js
// @require https://update.greasyfork.org/scripts/494204/data-manager.user.js
// @require https://update.greasyfork.org/scripts/494205/pagination-manager.user.js
// @require https://update.greasyfork.org/scripts/494203/vue-ui.user.js
// @run-at document-idle
// @icon https://www.google.com/s2/favicons?sz=64&domain=spankbang.com
// ==/UserScript==
/* globals jQuery, $, Vue, createApp, watch, reactive,
timeToSeconds, parseDOM, parseIntegerOr, fetchHtml, stringToWords, Observer
LazyImgLoader, PersistentState, DataManager, PaginationManager, VueUI */
const LOGO = `
⡕⡧⡳⡽⣿⣇⠀⢀⠀⠄⠐⡐⠌⡂⡂⠠⠀⠠⠀⠠⠀⠠⡐⡆⡇⣇⢎⢆⠆⠌⢯⡷⡥⡂⡐⠨⣻⣳⢽⢝⣵⡫⣗⢯⣺⢵⢹⡪⡳⣝⢮⡳⣿⣿⣿⣿⣿⣿⣿⣿
⢎⢞⡜⣞⣿⡯⡆⠀⠄⠐⠀⢐⠡⢊⢐⢀⠈⠀⠄⠁⡀⠡⠸⡸⡪⣪⢺⢸⢱⠡⢑⡝⣟⣧⡂⠅⠪⣻⣎⢯⡺⣪⣗⢯⣺⢪⡣⡯⣝⢼⡪⣞⣽⣿⣿⣿⣿⣿⣿⣿
⡱⡣⣣⢳⣻⣯⢿⡐⠀⠂⠁⢀⠪⢐⠐⠄⡀⠁⠄⠁⠀⠄⡑⠆⡎⢎⢎⢇⢕⠬⠀⢇⢣⢻⣞⠌⠌⡪⣷⡱⣝⢮⣺⢕⡗⣕⢧⢳⢕⢗⣝⢞⣾⣿⣿⣿⣿⣿⣿⣻
⡸⡪⣪⡚⣞⣯⡗⣝⡀⠄⠁⡀⠌⡂⠅⡁⠄⠂⠐⠈⠀⢂⢐⢔⢜⢜⢜⢜⢔⢬⢊⠠⠨⠨⣻⣽⢐⠨⡺⣞⢼⢕⣗⡽⣪⢺⢜⢵⡹⣕⢵⡫⣾⣿⣿⣿⣿⣿⢯⣟
⢎⢇⢇⢧⡫⣿⡞⡜⡄⠀⠄⠀⠐⠄⡁⠀⠄⠐⠀⠂⢁⢐⢔⢕⢕⠱⢱⠱⡱⡱⡱⡅⡂⢁⠪⡿⣎⡢⡘⢽⡪⡧⣳⡝⣜⢎⢗⢇⢯⡪⡧⣻⡺⣿⣿⣿⣿⢯⡻⡮
⢕⢝⢜⡜⡮⣻⣗⢕⠅⡀⠂⠈⢈⠐⠀⠁⢀⠐⠀⡁⠄⢊⢢⢣⢣⢣⢣⢣⢣⢫⢪⢪⠀⠄⠨⡚⣿⣳⡜⡘⣗⢽⡺⡪⡺⡸⡵⡹⡕⡧⣫⡺⡺⣿⣿⡿⡯⡯⣫⢯
⢸⢱⢱⢱⢕⢯⣿⢜⠌⢀⠀⠂⠀⠂⠁⠈⠀⠀⠄⠀⠄⢑⠌⡆⢇⢅⠕⡌⡬⡪⡪⡪⠀⠄⢁⠸⡸⣯⡻⣪⢺⡵⡫⡪⣣⢫⡪⡣⡏⣞⢜⢮⣫⣻⡿⣫⢯⢞⣗⢽
⢪⢪⢪⢪⢎⢞⢾⡇⢕⠄⡆⡪⡘⡔⢔⢀⠐⠀⢀⠁⠠⠀⠑⠜⡌⡖⡕⡕⡕⢕⠑⠀⠠⠐⠀⡀⠕⡳⣻⣜⣕⣯⢣⠣⡣⡣⡣⡫⡪⡪⣎⢧⣧⡳⣝⢮⡳⡽⣜⢵
⡸⡸⡸⡸⡸⡱⣫⢯⣲⢱⢱⢘⢜⢜⢕⢕⢅⣂⠀⡀⠐⠀⡁⢐⠨⢐⠡⡊⢌⠂⠄⠂⠀⠂⡠⡠⡢⡫⡳⡱⡝⡮⡪⣇⢧⢳⢕⣝⢮⡫⡮⡳⣕⢵⣓⢗⡝⡮⡺⡜
⠱⡑⡕⡕⡝⡜⣎⢿⣪⢗⡝⡜⡜⡜⡜⢜⢕⢎⢎⢆⢎⢔⢄⠢⡌⢆⢕⠨⡢⢱⢰⢸⢸⣜⢮⢎⢎⢎⢇⢇⢯⢪⢇⡗⣝⢮⢳⢕⡗⣝⢮⡫⡮⣣⡳⡕⣝⢼⢸⢸
⢜⢸⢨⢪⢪⢪⢪⣻⣺⢯⢎⣇⢧⢧⢧⢧⢯⢾⡵⣯⢾⣼⣜⣜⢜⢜⢜⢜⢜⢜⡜⡮⣗⡯⣟⣎⢎⢆⢇⢕⢕⢇⢗⡝⣜⢮⣳⣟⣾⣷⣷⣿⣮⣎⢎⢎⢎⢎⢎⠎
⠣⡑⢌⠎⡜⢜⢜⢜⢜⡕⣧⡳⣫⢏⡯⡯⡯⡯⣯⢯⣟⢷⣻⣟⣿⢷⣷⣳⡽⡮⡾⣝⡎⡏⡎⡇⡕⡜⡔⡕⡕⣝⢜⢮⡳⡽⣺⢽⣻⢿⢿⣟⣿⣺⣝⢮⡢⡧⣧⡠
⠐⠨⢢⢑⢅⢣⢱⢜⢞⢮⡳⣝⢮⣻⣪⢯⢯⡻⣺⢝⡾⣝⢷⢽⣺⢯⢿⢽⣻⣮⡳⣕⢇⢇⢇⢇⠪⡪⡪⡪⡪⣎⢯⢳⣹⡪⡯⣫⢯⣻⢽⣳⣳⣳⢳⣝⢮⡫⡷⣷
⠀⠨⡂⡅⡖⡵⡹⣕⢏⡧⢯⢮⡳⣣⢷⢽⢕⣯⡳⣏⡯⣞⡽⣳⢽⢽⢽⣝⣗⣗⣟⢮⢳⡱⡱⡱⡑⡕⡜⢜⢜⡜⣎⢧⡣⡯⡺⣝⡵⡯⡯⣞⣞⢮⣳⡳⣝⣞⡽⣺
⠀⠰⡸⡸⣸⢪⡫⣎⢗⢽⢕⣗⢽⣕⢯⣳⡫⣞⢮⣳⢽⣪⢯⢞⡽⣺⢵⣳⣳⡳⣳⢝⢵⡹⡪⣎⢎⢆⢇⢇⢇⢗⡕⡧⡳⣝⡝⡮⣞⡽⡽⡵⣳⣫⢞⡮⣗⣗⢽⣳
⠀⠨⢢⢣⢳⡱⣝⢼⢭⢳⢳⢕⣗⢗⡽⣪⢞⡽⣕⢷⢝⡮⣏⡯⣞⣗⢽⡺⡼⣺⢵⡫⡧⣳⢹⢜⡜⡜⡜⢜⢪⢣⡫⣎⢯⡪⣞⣝⢞⢮⢏⡯⣳⣳⣫⢾⢕⣗⢯⣞
⠀⠀⠱⡱⡱⡕⡧⡳⡕⣏⢗⣝⢮⢳⢝⢮⡳⣝⢮⡳⣝⢮⡳⣝⢮⡺⡵⣫⢯⡳⣳⢝⣞⡜⣎⢇⢎⠎⡪⢊⠎⡎⡎⡮⡺⣜⢮⢮⣫⣫⡳⡽⣕⣗⢵⣫⢯⡺⡵⣳
⠀⠀⠈⢎⢎⠮⡺⡸⡕⡧⡳⣕⢝⢮⡫⣣⢯⣪⢳⢝⢮⢳⢝⢮⡳⣝⣝⢮⡳⣝⢮⡳⡵⣝⢼⢸⢐⠅⠂⡐⢅⢇⢣⢣⢳⡱⣝⠮⡮⣪⢞⣝⢮⡺⣕⢗⣗⢽⡹⣪
⠀⠈⠀⠌⡒⡝⡜⣕⢕⢧⢫⡪⡳⡱⣝⢜⢮⡪⣳⢹⡪⣳⢹⢕⢽⡸⣜⢵⢝⢮⢳⢝⢞⢮⡪⡣⡣⡑⢅⢢⠱⡘⡜⡜⣜⢜⠮⡝⡮⡪⡧⡳⡵⣹⡪⡳⡕⡗⣝⢮
⠀⠐⠀⡁⠌⢎⢎⢎⢮⢪⢎⢮⢪⢳⢱⢝⢜⢎⢮⢣⡫⡪⡎⣗⢕⢧⢳⡱⡝⣎⢗⣝⢕⡗⣝⢼⢸⢨⢢⢃⢇⢣⢣⢣⢣⢳⢹⢪⢎⢗⢝⡜⣎⢮⢪⡳⡹⣪⢺⢸
⠀⠠⠐⢀⠐⠈⡎⡪⡪⡪⡪⡪⡣⡫⡪⡪⡣⡫⡪⡣⡣⡫⣪⢪⢺⢸⢪⢪⢺⢸⢪⡪⡺⡸⣪⢪⢪⢪⠸⡨⡊⡎⡎⡎⡇⡏⡎⡞⡜⡕⣕⢕⢕⡕⡇⡧⡫⡪⡪⡪
⢀⠀⠐⠀⠄⠁⠨⡊⡎⡎⡎⡎⡎⡎⡎⡎⡇⡏⡎⡎⡎⡎⡎⡎⡎⡮⡪⡣⡳⡱⡱⡕⡝⣜⢜⢜⢜⢔⢕⢱⠸⡸⡸⡸⡸⡸⡸⡸⡸⡸⡸⡨⡣⡣⡣⡣⡣⡣⡃⡇
⠀⠀⠈⡀⠂⠐⠀⢑⠜⡌⢎⢪⠪⡪⡪⡪⢪⠪⡪⢪⠪⡪⡪⢪⠪⡊⡎⡜⡌⡎⢎⢎⢎⢎⠎⡎⡪⠢⡑⢌⢪⢘⢔⠱⡡⢣⢃⢇⠕⡕⢅⢇⢣⢱⢑⢕⠸⡐⡱⡘`;
//====================================================================================================
class SPANKBANG_RULES {
constructor() {
this.PAGINATION = document.querySelector('.paginate-bar') || document.querySelector('.pagination');
this.PAGINATION_LAST = parseInt(
document.querySelector('.paginate-bar .status span')?.innerText.match(/\d+/)?.[0] ||
document.querySelector('.pagination .next')?.previousElementSibling?.innerText);
this.CONTAINER = document.querySelectorAll('.results .video-list')[0];
this.HAS_VIDEOS = document.querySelector('.video-list');
}
GET_THUMBS(html) {
return html.querySelectorAll('.video-item:not(.clear-fix)');
}
THUMB_URL(thumb) {
return thumb.querySelector('.thumb').href;
}
THUMB_IMG_DATA(thumb) {
const img = thumb.querySelector('img');
const imgSrc = img.getAttribute('data-src');
img.removeAttribute('data-src');
return { img, imgSrc };
}
THUMB_DATA(thumb) {
const title = (thumb.querySelector('span.n') || thumb.querySelector('a.n')).innerText.toLowerCase();
const duration = (parseInt(thumb.querySelector('span.l')?.innerText) || 1) * 60;
return {
title,
duration
}
}
URL_DATA() {
const { href, pathname, search, origin } = window.location;
const mres = pathname.split(/\/(\d+)\/?$/);
const basePathname = mres[0];
const offset = parseInt(mres[1]) || 1;
const iteratable_url = n => `${origin}${basePathname}/${n}/${search}`;
return {
offset,
iteratable_url
};
}
}
const RULES = new SPANKBANG_RULES();
//====================================================================================================
const { state } = new PersistentState({
filterDurationFrom: 0,
filterDurationTo: 600,
filterDuration: false,
filterExcludeWords: "",
filterExclude: false,
filterIncludeWords: "",
filterInclude: false,
infiniteScrollEnabled: true,
uiEnabled: true,
});
const stateLocale = reactive({
pagIndexLast: 1,
pagIndexCur: 1,
});
watch([() => state.filterDurationFrom, () => state.filterDurationTo], (a, b) => {
state.filterDurationFrom = parseIntegerOr(a[0], b[0]);
state.filterDurationTo = parseIntegerOr(a[1], b[1]);
if (state.filterDuration) filter_({ filterDuration: true });
});
watch(() => state.filterDuration, () => filter_({ filterDuration: true }));
watch(() => state.filterExclude, () => filter_({ filterExclude: true }));
watch(() => state.filterExcludeWords, () => {
if (state.filterExclude) filter_({ filterExclude: true });
}, { deep: true });
watch(() => state.filterInclude, () => filter_({ filterInclude: true }));
watch(() => state.filterIncludeWords, () => {
if (state.filterInclude) filter_({ filterInclude: true });
}, { deep: true });
//====================================================================================================
function createPreviewElement(src, mount) {
const elem = parseDOM(`
<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"
id="mp4t_video" tabindex="-1" lang="en" translate="no" role="region" aria-label="Video Player" style="display: none;">
<video id="mp4t_video_html5_api" class="vjs-tech" tabindex="-1" loop="loop" autoplay="autoplay" muted="muted" playsinline="playsinline"></video>
<div class="vjs-poster vjs-hidden" tabindex="-1" aria-disabled="false"></div>
<div class="vjs-text-track-display" translate="yes" aria-live="off" aria-atomic="true">
<div style="position: absolute; inset: 0px; margin: 1.5%;"></div>
</div>
<div class="vjs-loading-spinner" dir="ltr">
<span class="vjs-control-text">Video Player is loading.</span>
</div><button class="vjs-big-play-button" type="button" title="Play Video" aria-disabled="false">
<span class="vjs-icon-placeholder" aria-hidden="true"></span>
<span class="vjs-control-text" aria-live="polite">
</div>`);
mount.append(elem);
const video = elem.querySelector('video');
video.src = src;
video.addEventListener('loadeddata', () => {
elem.style.display = 'block';
}, false);
return {
elem,
removeElem: () => {
video.removeAttribute('src');
video.load();
elem.remove();
}
};
}
function animate() {
function handleThumbHover(e) {
if (!(e.target.classList.contains('cover') && e.target.getAttribute('data-preview'))) return;
const videoSrc = e.target.getAttribute('data-preview');
const { elem, removeElem } = createPreviewElement(videoSrc, e.target.parentElement.parentElement);
e.target.parentElement.parentElement.addEventListener('mouseleave', removeElem, { once: true });
}
if (RULES.PAGINATION || document.querySelectorAll('.video-list').length > 0) {
(RULES.CONTAINER || document.body).addEventListener('mouseover', handleThumbHover);
}
}
//====================================================================================================
console.log(LOGO);
const SCROLL_RESET_DELAY = 350;
const { filter_, handleLoadedHTML } = new DataManager(RULES, state);
if (RULES.HAS_VIDEOS) {
animate();
const ui = new VueUI(state, stateLocale);
for (const list of document.querySelectorAll('.video-list')) {
handleLoadedHTML(list);
}
}
if (RULES.PAGINATION) {
const paginationManager = new PaginationManager(state, stateLocale, RULES, handleLoadedHTML, SCROLL_RESET_DELAY);
}