Sleazy Fork is available in English.
Add search buttons to Exoticaz and MissAV pages
// ==UserScript==
// @name Search Button Enhancer
// @namespace http://tampermonkey.net/
// @version 1.8
// @description Add search buttons to Exoticaz and MissAV pages
// @match https://exoticaz.to/torrents*
// @match https://exoticaz.to/torrent/*
// @match https://missav.ws/*
// @match https://missav.live/*
// @match https://javdb.com/*
// @match https://www.javdb.com/*
// @match https://www5.javmost.com/*
// @match https://javbus.com/*
// @match https://www.javbus.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// Configuration: Add or modify search providers here
const SEARCH_PROVIDERS = [
{
name: 'Exoticaz',
icon: '',
url: 'https://exoticaz.to/torrents?in=1&search=%s#jump',
className: 'btn-warning',
hostPattern: /(^|\.)exoticaz\.to$/
},
{
name: 'NetFlav',
icon: '',
url: 'https://netflav.com/search?type=title&keyword=',
className: 'btn-warning'
},
{
name: 'JavDB',
icon: '',
url: 'https://JavDB.com/search?f=all&q=%s&sb=0#query#jump&locale=zh#jump',
className: 'btn-warning',
hostPattern: /(^|\.)javdb\.com$/i
},
{
name: 'JavMost',
icon: '',
url: 'https://www5.javmost.com/tag/%s#jump',
className: 'btn-warning',
hostPattern: /(^|\.)javmost\.com$/i
},
{
name: 'JavBus',
icon: '',
url: 'https://www.JavBus.com/search/%s#query#jump',
className: 'btn-warning',
hostPattern: /(^|\.)javbus\.com$/i
},
{
name: 'Missav',
icon: '',
url: 'https://missav.ws/dm18/cn/%s#jump',
className: 'btn-warning',
hostPattern: /(^|\.)missav\.(ws|live)$/i
},
// Add more providers here:
// {
// name: 'Google',
// icon: '🌐',
// url: 'https://www.google.com/search?q=',
// className: 'btn-info'
// }
];
function buildProviderUrl(provider, code) {
const encodedCode = encodeURIComponent(code);
return provider.url.includes('%s')
? provider.url.replace('%s', encodedCode)
: `${provider.url}${encodedCode}`;
}
function getVisibleProviders() {
const host = location.hostname;
return SEARCH_PROVIDERS.filter(provider => {
if (!provider.hostPattern) return true;
return !provider.hostPattern.test(host);
});
}
function createSearchButton(code, provider, isListPage = false) {
const btn = document.createElement(isListPage ? 'button' : 'a');
btn.textContent = `${provider.icon} ${provider.name}`;
btn.className = `btn btn-xs ${provider.className} search-btn`;
btn.style.marginLeft = '6px';
btn.style.marginTop = isListPage ? '4px' : '0';
if (isListPage) {
// For list page buttons
btn.style.padding = '2px 6px';
btn.style.fontSize = '12px';
btn.style.border = '1px solid #ccc';
btn.style.borderRadius = '4px';
btn.style.cursor = 'pointer';
btn.onclick = (e) => {
e.stopPropagation();
window.open(buildProviderUrl(provider, code), '_blank');
};
} else {
// For detail page buttons (links)
btn.href = buildProviderUrl(provider, code);
btn.target = '_blank';
}
return btn;
}
function addSearchButtons(container, code, isListPage = false) {
// Remove existing search buttons to avoid duplicates
container.querySelectorAll('.search-btn').forEach(btn => btn.remove());
// Add all configured search buttons
getVisibleProviders().forEach(provider => {
const searchBtn = createSearchButton(code, provider, isListPage);
container.appendChild(searchBtn);
});
}
function handleDetailPage() {
const titleEl = document.querySelector('h1.h4');
if (!titleEl) return;
const match = titleEl.textContent.match(/\[([^\]]+)\]/);
if (!match) return;
const code = match[1];
// Look for the "Download as Text File" button
const txtBtn = Array.from(document.querySelectorAll('a.btn'))
.find(el => el.textContent.includes('Download as Text'));
if (!txtBtn) return;
const container = txtBtn.parentElement;
if (!container) return;
// Create a wrapper for search buttons if it doesn't exist
let searchWrapper = container.querySelector('.search-wrapper');
if (!searchWrapper) {
searchWrapper = document.createElement('span');
searchWrapper.className = 'search-wrapper';
container.insertBefore(searchWrapper, txtBtn.nextSibling);
}
addSearchButtons(searchWrapper, code, false);
}
function handleListPage() {
// Use requestAnimationFrame to avoid blocking the main thread
const processInBatches = () => {
const torrentLinks = document.querySelectorAll('a.torrent-link:not([data-search-processed])');
const batchSize = 10; // Process 10 items at a time
for (let i = 0; i < Math.min(batchSize, torrentLinks.length); i++) {
const link = torrentLinks[i];
link.setAttribute('data-search-processed', 'true');
const title = link.getAttribute('title') || link.textContent;
const match = title.match(/\[([^\]]+)\]/);
if (!match) continue;
const code = match[1];
const row = link.closest('tr');
if (!row) continue;
const actionTd = row.querySelector('td > .align-top')?.parentElement;
const alignBottom = actionTd?.querySelector('.align-bottom');
if (!alignBottom) continue;
// Create a wrapper for search buttons if it doesn't exist
let searchWrapper = alignBottom.querySelector('.search-wrapper');
if (!searchWrapper) {
searchWrapper = document.createElement('div');
searchWrapper.className = 'search-wrapper';
searchWrapper.style.marginTop = '4px';
alignBottom.appendChild(searchWrapper);
}
addSearchButtons(searchWrapper, code, true);
}
// If there are more items to process, schedule the next batch
if (torrentLinks.length > batchSize) {
requestAnimationFrame(processInBatches);
}
};
requestAnimationFrame(processInBatches);
}
function extractMissavCode() {
const segments = location.pathname.split('/').filter(Boolean);
if (segments.length === 0) return null;
const slug = segments[segments.length - 1];
if (!slug) return null;
// Example: juq-609-uncensored-leak -> juq-609
const match = slug.match(/[a-z]{2,8}-\d{2,6}/i);
return match ? match[0].toLowerCase() : null;
}
function createMissavSearchButton(code, provider) {
const link = document.createElement('a');
link.href = buildProviderUrl(provider, code);
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.className = 'inline-flex items-center whitespace-nowrap text-sm leading-4 font-medium focus:outline-none text-nord4 hover:text-nord6 search-btn';
link.style.marginLeft = '12px';
link.textContent = provider.name;
return link;
}
function handleMissavPage() {
const code = extractMissavCode();
if (!code) return;
const actionBar = document.querySelector('div.flex.flex-wrap.justify-center.py-8.rounded-md.shadow-sm');
if (!actionBar) return;
let wrapper = actionBar.querySelector('.search-wrapper-missav');
if (!wrapper) {
wrapper = document.createElement('span');
wrapper.className = 'search-wrapper-missav inline-flex items-center';
actionBar.appendChild(wrapper);
}
wrapper.querySelectorAll('.search-btn').forEach(btn => btn.remove());
getVisibleProviders().forEach(provider => {
const btn = createMissavSearchButton(code, provider);
wrapper.appendChild(btn);
});
}
function shouldAutoJump() {
return /(^|#)jump($|#|&)/i.test(location.hash);
}
function getFirstResultLinkByHost() {
const selectorsByHost = [
{
host: /(^|\.)exoticaz\.to$/i,
selectors: [
'a.torrent-link[href*="/torrent/"]'
]
},
{
host: /(^|\.)javdb\.com$/i,
selectors: [
'.movie-list a.box[href]',
'a.box[href*="/v/"]',
'a[href*="/v/"]'
]
},
{
host: /(^|\.)javmost\.com$/i,
selectors: [
'.post a[href]',
'.entry-title a[href]',
'article a[href]'
]
},
{
host: /(^|\.)javbus\.com$/i,
selectors: [
'a.movie-box[href]',
'a[href*="/uncensored/"]',
'a[href*="/search/"]'
]
},
{
host: /(^|\.)missav\.(ws|live)$/i,
selectors: [
'a[href*="/dm"]',
'a[href*="/cn/"]'
]
}
];
const hostRule = selectorsByHost.find(rule => rule.host.test(location.hostname));
if (!hostRule) return null;
for (const selector of hostRule.selectors) {
const link = document.querySelector(selector);
if (!link) continue;
const href = link.getAttribute('href') || '';
if (!href || href.startsWith('#') || href.startsWith('javascript:')) continue;
if (link.closest('header, nav, footer')) continue;
return link;
}
return null;
}
function handleAutoJump() {
if (!shouldAutoJump()) return;
let attempts = 0;
const maxAttempts = 20;
const jumpTimer = setInterval(() => {
attempts += 1;
const firstLink = getFirstResultLinkByHost();
if (!firstLink) {
if (attempts >= maxAttempts) clearInterval(jumpTimer);
return;
}
clearInterval(jumpTimer);
const destination = firstLink.href;
if (!destination || destination === location.href) return;
location.assign(destination);
}, 250);
}
function init() {
const isExoticaz = location.hostname === 'exoticaz.to';
const isMissav = /(^|\.)missav\.(ws|live)$/.test(location.hostname);
handleAutoJump();
if (isExoticaz) {
const isDetailPage = /https:\/\/exoticaz\.to\/torrent\/\d+/.test(location.href);
if (isDetailPage) {
handleDetailPage();
} else {
handleListPage();
// Throttled observer to prevent excessive function calls
let observerTimeout;
const throttledHandleListPage = () => {
clearTimeout(observerTimeout);
observerTimeout = setTimeout(handleListPage, 250); // Wait 250ms before processing
};
const observer = new MutationObserver(throttledHandleListPage);
observer.observe(document.body, {
childList: true,
subtree: true,
// Only observe specific changes to reduce overhead
attributeFilter: ['class', 'data-search-processed']
});
}
} else if (isMissav) {
handleMissavPage();
let observerTimeout;
const throttledHandleMissavPage = () => {
clearTimeout(observerTimeout);
observerTimeout = setTimeout(handleMissavPage, 250);
};
const observer = new MutationObserver(throttledHandleMissavPage);
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}
// Use both DOMContentLoaded and load events for better reliability
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
// Small delay to ensure page is fully rendered
setTimeout(init, 100);
});
} else {
setTimeout(init, 100);
}
// Backup initialization on window load
window.addEventListener('load', () => {
setTimeout(init, 200);
});
})();