Search Button Enhancer

Add search buttons to Exoticaz and MissAV pages

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Search Button Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.9
// @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.dataset.provider = provider.name // used by the hide-panel toggles
    btn.setAttribute('data-provider', provider.name) // explicit attr for iOS compat
    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]

    // Set up hide toggle first (creates the button in the card-header)
    handleExoticazHideToggle()

    // Place search buttons in card-header: after hide btn, before bookmark btn
    const bookmarkBtn = document.querySelector(
      'button[title="Bookmark this Torrent"]'
    )
    if (!bookmarkBtn) return

    const headerContainer = bookmarkBtn.parentElement
    if (!headerContainer) return

    let searchWrapper = headerContainer.querySelector('.search-wrapper-header')
    if (!searchWrapper) {
      searchWrapper = document.createElement('span')
      searchWrapper.className = 'search-wrapper-header'
      searchWrapper.style.marginRight = '6px'
      // Insert after hide button (before bookmark button)
      headerContainer.insertBefore(searchWrapper, bookmarkBtn)
    }

    addSearchButtons(searchWrapper, code, false)
  }

  function handleExoticazHideToggle() {
    // Only run once
    if (document.getElementById('exz-hide-toggle-btn')) return

    const bookmarkBtn = document.querySelector(
      'button[title="Bookmark this Torrent"]'
    )
    if (!bookmarkBtn) return

    const mainCard = bookmarkBtn.closest('.card.mb-3')

    // ── Hideable item definitions ────────────────────────────────────────────
    const HIDE_ITEMS = [
      {
        id: 'exz-item-navbar',
        label: 'Navigation Bar',
        getEls: () => {
          const el =
            document.querySelector('nav.navbar.navbar-expand-lg.fixed-top') ||
            document.querySelector('.navbar.navbar-expand-lg.fixed-top')
          return el ? [el] : []
        },
      },
      {
        id: 'exz-item-ratiobar',
        label: 'Ratio Bar',
        getEls: () => {
          const el = document.querySelector('.ratio-bar.mb-1')
          return el ? [el] : []
        },
      },
      {
        id: 'exz-item-titleheader',
        label: 'Title Header',
        getEls: () => {
          const el = document.querySelector('h1.h4')?.closest('.card-header')
          return el ? [el] : []
        },
      },
      {
        id: 'exz-item-suggestedits',
        label: 'Suggest Edits Btn',
        getEls: () => {
          const el = document.querySelector('button[title="Suggest Changes"]')
          return el ? [el] : []
        },
      },
      {
        id: 'exz-item-reportbtn',
        label: 'Report Btn',
        getEls: () => {
          const el = document.querySelector(
            'button[title="Report this torrent"]'
          )
          return el ? [el] : []
        },
      },
      {
        id: 'exz-item-cardbody',
        label: 'Torrent Details',
        getEls: () => {
          const el = mainCard?.querySelector(':scope > .card-body')
          return el ? [el] : []
        },
      },
      {
        id: 'exz-item-screenshotsheader',
        label: 'Screenshots Header',
        getEls: () => {
          const el = document
            .querySelector('[data-target="#screenshots"], [href="#screenshots"]')
            ?.closest('.card-header')
          return el ? [el] : []
        },
      },
    ]

    // ── State: load from localStorage, default all-hidden ───────────────────
    const STORAGE_KEY = 'exz-hide-state'
    const state = {}
    let savedState = {}
    try { savedState = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') } catch (e) {}
    HIDE_ITEMS.forEach(item => {
      state[item.id] = item.id in savedState ? savedState[item.id] : true
    })

    function saveState() {
      try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) } catch (e) {}
    }

    function applyItem(item) {
      item.getEls().forEach(el => {
        el.style.display = state[item.id] ? 'none' : ''
      })
    }
    function applyAll() {
      HIDE_ITEMS.forEach(applyItem)
    }

    // Apply saved/default state immediately on page load
    applyAll()

    // ── Build popup panel ────────────────────────────────────────────────────
    const panel = document.createElement('div')
    panel.id = 'exz-hide-panel'
    panel.style.cssText = [
      'display:none',
      'position:fixed',
      'z-index:99999',
      'background:#fff',
      'border:1px solid #d0d0d0',
      'border-radius:7px',
      'box-shadow:0 6px 20px rgba(0,0,0,0.18)',
      'padding:10px 14px 12px',
      'min-width:280px',
      'font-size:13px',
      'font-family:inherit',
    ].join(';')

    // Header row: title + Hide All / Show All
    const headerRow = document.createElement('div')
    headerRow.style.cssText =
      'display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;padding-bottom:7px;border-bottom:1px solid #ebebeb'

    const panelTitle = document.createElement('span')
    panelTitle.textContent = 'Visibility'
    panelTitle.style.cssText =
      'font-weight:700;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:#888'

    const actionsDiv = document.createElement('div')

    function makeActionBtn(label, bg) {
      const b = document.createElement('button')
      b.type = 'button'
      b.textContent = label
      b.style.cssText = [
        `background:${bg}`,
        'color:#fff',
        'border:none',
        'border-radius:4px',
        'padding:2px 9px',
        'font-size:11px',
        'cursor:pointer',
        'margin-left:4px',
      ].join(';')
      return b
    }

    const hideAllBtn = makeActionBtn('Hide All', '#6c757d')
    const showAllBtn = makeActionBtn('Show All', '#17a2b8')

    hideAllBtn.onclick = () => {
      HIDE_ITEMS.forEach(item => (state[item.id] = true))
      applyAll()
      saveState()
      syncCheckboxes()
    }
    showAllBtn.onclick = () => {
      HIDE_ITEMS.forEach(item => (state[item.id] = false))
      applyAll()
      saveState()
      syncCheckboxes()
    }

    actionsDiv.appendChild(hideAllBtn)
    actionsDiv.appendChild(showAllBtn)
    headerRow.appendChild(panelTitle)
    headerRow.appendChild(actionsDiv)
    panel.appendChild(headerRow)

    // 2-column checkbox grid
    const grid = document.createElement('div')
    grid.style.cssText =
      'display:grid;grid-template-columns:1fr 1fr;gap:2px 12px'

    const checkboxEls = {}

    HIDE_ITEMS.forEach(item => {
      const lbl = document.createElement('label')
      lbl.style.cssText =
        'display:flex;align-items:center;gap:6px;cursor:pointer;padding:4px 2px;user-select:none;color:#333'

      const cb = document.createElement('input')
      cb.type = 'checkbox'
      cb.checked = state[item.id]
      cb.style.cssText = 'cursor:pointer;width:13px;height:13px;flex-shrink:0'
      cb.addEventListener('change', () => {
        state[item.id] = cb.checked
        applyItem(item)
        saveState()
      })
      checkboxEls[item.id] = cb

      const txt = document.createElement('span')
      txt.textContent = item.label
      txt.style.cssText = 'font-size:12px;line-height:1.3'

      lbl.appendChild(cb)
      lbl.appendChild(txt)
      grid.appendChild(lbl)
    })

    panel.appendChild(grid)

    // ── Search Buttons section ───────────────────────────────────────────────
    const visibleProviders = getVisibleProviders()

    if (visibleProviders.length) {
      const divider = document.createElement('div')
      divider.style.cssText =
        'border-top:1px solid #ebebeb;margin:10px 0 8px'
      panel.appendChild(divider)

      const searchSectionHeader = document.createElement('div')
      searchSectionHeader.style.cssText =
        'display:flex;justify-content:space-between;align-items:center;margin-bottom:6px'

      const searchTitle = document.createElement('span')
      searchTitle.textContent = 'Search Buttons'
      searchTitle.style.cssText =
        'font-weight:700;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:#888'

      const searchActionsDiv = document.createElement('div')
      const searchHideAllBtn = makeActionBtn('Hide All', '#6c757d')
      const searchShowAllBtn = makeActionBtn('Show All', '#17a2b8')
      searchActionsDiv.appendChild(searchHideAllBtn)
      searchActionsDiv.appendChild(searchShowAllBtn)

      searchSectionHeader.appendChild(searchTitle)
      searchSectionHeader.appendChild(searchActionsDiv)
      panel.appendChild(searchSectionHeader)

      const searchGrid = document.createElement('div')
      searchGrid.style.cssText =
        'display:grid;grid-template-columns:1fr 1fr;gap:2px 12px'

      // State: checked = shown; load from localStorage, default all on
      const SEARCH_STORAGE_KEY = 'exz-search-state'
      const searchState = {}
      const searchCheckboxEls = {}
      let savedSearchState = {}
      try { savedSearchState = JSON.parse(localStorage.getItem(SEARCH_STORAGE_KEY) || '{}') } catch (e) {}
      visibleProviders.forEach(p => {
        searchState[p.name] = p.name in savedSearchState ? savedSearchState[p.name] : true
      })

      function saveSearchState() {
        try { localStorage.setItem(SEARCH_STORAGE_KEY, JSON.stringify(searchState)) } catch (e) {}
      }

      // iOS-safe: match by data-provider attr OR text content fallback
      function applySearchItem(name) {
        let el = document.querySelector(
          `.search-wrapper-header [data-provider="${name}"]`
        )
        if (!el) {
          // Fallback: match by text content (for iOS where attr query may fail)
          document
            .querySelectorAll('.search-wrapper-header .search-btn')
            .forEach(candidate => {
              if (candidate.textContent.trim().includes(name)) el = candidate
            })
        }
        if (el) el.style.display = searchState[name] ? '' : 'none'
      }

      // Apply saved search state AFTER search buttons are in the DOM
      // (addSearchButtons() runs after this function returns, so defer by one tick)
      setTimeout(() => {
        visibleProviders.forEach(p => applySearchItem(p.name))
      }, 0)

      visibleProviders.forEach(p => {
        const lbl = document.createElement('label')
        lbl.style.cssText =
          'display:flex;align-items:center;gap:6px;cursor:pointer;padding:4px 2px;user-select:none;color:#333'

        const cb = document.createElement('input')
        cb.type = 'checkbox'
        cb.checked = searchState[p.name]
        cb.style.cssText = 'cursor:pointer;width:13px;height:13px;flex-shrink:0'
        cb.addEventListener('change', () => {
          searchState[p.name] = cb.checked
          applySearchItem(p.name)
          saveSearchState()
        })
        searchCheckboxEls[p.name] = cb

        const txt = document.createElement('span')
        txt.textContent = p.name
        txt.style.cssText = 'font-size:12px;line-height:1.3'

        lbl.appendChild(cb)
        lbl.appendChild(txt)
        searchGrid.appendChild(lbl)
      })

      panel.appendChild(searchGrid)

      function syncSearchCheckboxes() {
        visibleProviders.forEach(p => {
          if (searchCheckboxEls[p.name])
            searchCheckboxEls[p.name].checked = searchState[p.name]
        })
      }

      searchHideAllBtn.onclick = () => {
        visibleProviders.forEach(p => (searchState[p.name] = false))
        visibleProviders.forEach(p => applySearchItem(p.name))
        saveSearchState()
        syncSearchCheckboxes()
      }
      searchShowAllBtn.onclick = () => {
        visibleProviders.forEach(p => (searchState[p.name] = true))
        visibleProviders.forEach(p => applySearchItem(p.name))
        saveSearchState()
        syncSearchCheckboxes()
      }
    }

    document.body.appendChild(panel)

    function syncCheckboxes() {
      HIDE_ITEMS.forEach(item => {
        if (checkboxEls[item.id]) checkboxEls[item.id].checked = state[item.id]
      })
    }

    // ── Toggle button (always labeled "Hide") ────────────────────────────────
    const btn = document.createElement('button')
    btn.id = 'exz-hide-toggle-btn'
    btn.type = 'button'
    btn.className = 'btn btn-secondary btn-sm'
    btn.style.marginRight = '6px'
    btn.textContent = 'Hide'

    btn.addEventListener('click', e => {
      e.stopPropagation()
      const open = panel.style.display !== 'none'
      if (open) {
        panel.style.display = 'none'
      } else {
        const rect = btn.getBoundingClientRect()
        panel.style.top = rect.bottom + 4 + 'px'
        panel.style.left = rect.left + 'px'
        panel.style.display = 'block'
      }
    })

    // Close panel when clicking outside
    document.addEventListener('click', e => {
      if (!panel.contains(e.target) && e.target !== btn) {
        panel.style.display = 'none'
      }
    })

    // Insert to the LEFT of the bookmark button
    bookmarkBtn.parentElement.insertBefore(btn, bookmarkBtn)
  }

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