Search Button Enhancer

Add search buttons to Exoticaz and MissAV pages

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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