Search Button Enhancer

Add search buttons to Exoticaz and MissAV pages

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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)
  })
})()