Exoticaz Search Button Enhancer

Add search buttons to Exoticaz torrent list and detail pages

// ==UserScript==
// @name         Exoticaz Search Button Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Add search buttons to Exoticaz torrent list and detail pages
// @match        https://exoticaz.to/torrents*
// @match        https://exoticaz.to/torrent/*
// @grant        none
// @license      MIT
// ==/UserScript==
(function () {
  'use strict';

  // Configuration: Add or modify search providers here
  const SEARCH_PROVIDERS = [
    {
      name: 'NetFlav',
      icon: '',
      url: 'https://netflav.com/search?type=title&keyword=',
      className: 'btn-warning'
    },
    {
      name: 'JavMost',
      icon: '',
      url: 'https://www5.javmost.com/search/',
      className: 'btn-warning'
    },
    // Add more providers here:
    // {
    //   name: 'Google',
    //   icon: '🌐',
    //   url: 'https://www.google.com/search?q=',
    //   className: 'btn-info'
    // }
  ];

  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(`${provider.url}${encodeURIComponent(code)}`, '_blank');
      };
    } else {
      // For detail page buttons (links)
      btn.href = `${provider.url}${encodeURIComponent(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
    SEARCH_PROVIDERS.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 init() {
    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']
      });
    }
  }

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