Search Button Enhancer

Add search buttons to Exoticaz and MissAV pages

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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