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 // @author troublesis <[email protected]> // @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() { let code = extractMissavCode() // Check for search result header (Point 3) const searchHeader = document.querySelector( 'h1.text-center.text-2xl.text-nord4.mb-6' ) if ( !code && searchHeader && (searchHeader.textContent.includes('的搜寻结果') || searchHeader.textContent.includes('Search results for')) ) { // Extract query from "XYZ 的搜寻结果" or "Search results for XYZ" code = searchHeader.textContent .replace('的搜寻结果', '') .replace('Search results for', '') .trim() } if (!code) return // Point 1: Standard Action Bar (Video detail page) const actionBar = document.querySelector( 'div.flex.flex-wrap.justify-center.py-8.rounded-md.shadow-sm' ) if (actionBar) { 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) }) } // Point 2: Under "Back to Home" button const homeBtn = Array.from( document.querySelectorAll('a.button-primary') ).find( el => el.textContent.includes('返回主页') || (el.href && (el.href.endsWith('/cn') || el.href.endsWith('/en') || el.href.endsWith('/ja'))) ) if (homeBtn) { let wrapper = homeBtn.parentElement.querySelector( '.search-wrapper-missav-home' ) if (!wrapper) { wrapper = document.createElement('div') wrapper.className = 'search-wrapper-missav-home mt-4 flex flex-wrap justify-center' homeBtn.after(wrapper) } wrapper.querySelectorAll('.search-btn').forEach(btn => btn.remove()) getVisibleProviders().forEach(provider => { const btn = createMissavSearchButton(code, provider) btn.style.margin = '4px 6px' wrapper.appendChild(btn) }) } // Point 3: Under Search Header if (searchHeader) { let wrapper = searchHeader.nextElementSibling?.classList.contains( 'search-wrapper-missav-header' ) ? searchHeader.nextElementSibling : null if (!wrapper) { wrapper = document.createElement('div') wrapper.className = 'search-wrapper-missav-header mb-6 flex flex-wrap justify-center' searchHeader.after(wrapper) } wrapper.querySelectorAll('.search-btn').forEach(btn => btn.remove()) getVisibleProviders().forEach(provider => { const btn = createMissavSearchButton(code, provider) btn.style.margin = '4px 6px' 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) }) })()