Streamtrooper

Streamtrooper automatically adds VidSrc links to IMDb and TMDb pages for easy access to streaming content.

目前為 2025-11-13 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        Streamtrooper
// @version     V1.0.0
// @author      Amit
// @match       https://www.imdb.com/*
// @match       https://www.themoviedb.org/*
// @icon        https://www.videolan.org/favicon.ico
// @run-at      document-end
// @connect     ipapi.co
// @grant       GM_registerMenuCommand
// @grant       GM_addStyle
// @grant       GM_openInTab
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_xmlhttpRequest
// @connect     *
// @copyright   2025, amit (https://openuserjs.org/users/amit)
// @require     https://update.greasyfork.org/scripts/528923/1599357/MonkeyConfig%20Mod.js
// @license     CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
// @license     GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
// @description  Streamtrooper automatically adds VidSrc links to IMDb and TMDb pages for easy access to streaming content.
// @namespace 4mit
// ==/UserScript==

/**
 * Streamtrooper is a userscript that automatically adds VidSrc links to IMDb and TMDb pages.
 * the links are added to the page as buttons that open in a new tab.
 * the script uses the VidSrc API to get the links and adds them to the page.
 * The URLs are of the form: https://vidsrc/embed/movie?imdb=<imdb_id>
 * or https://vidsrc/embed/tv?imdb=<imdb_id>&season=<season>&episode=<episode>
 * where <imdb_id> is the IMDb ID of the movie or TV show, and
 * <season> and <episode> are the season and episode numbers for TV shows.
 * For TMDb, the URLs are of the form:
 * https://vidsrc/embed/movie?tmdb=<tmdb_id>
 * or https://vidsrc/embed/tv?tmdb=<tmdb_id>&season=<season>&episode=<episode>
 * where <tmdb_id> is the TMDb ID of the movie or TV show.
 */

'use strict';
// Default VidSrc domain
let vidsrcDomain = GM_getValue('vidsrcDomain', '');
let domainLastCheckedTime = GM_getValue('domainLastCheckedTime', 0);

let countryCode = "";
// Available VidSrc domains
const availableDomains = GM_getValue ('availableDomains', ['vidsrc.pm', 'vidsrc.xyz', 'vidsrc.net',
'vidsrc-embed.ru', 'vidsrc-embed.su' , 'vidsrcme.su ','vidsrc.in',  'vsrc.su', 'player.vidplus.to'] );

GM_registerMenuCommand(`Edit Domains`, () => {
  const message = 'Enter new line VidSrc domains:';
  document.body.insertAdjacentHTML('beforeend',
    `<div id="vidsrc-domain-editor" style="position:fixed;top:10%;left:50%;transform:translateX(-50%);background-color:#fff;padding:20px;box-shadow:0 0 10px rgba(0,0,0,0.5);z-index:10000;">
      <h3>Edit VidSrc Domains</h3>
      <textarea id="vidsrc-domains-textarea" rows="10" cols="50">${availableDomains.join('\n')}</textarea><br>
      <button id="vidsrc-save-domains">Save</button>
      <button id="vidsrc-cancel-domains">Cancel</button>
    </div>`);

  document.getElementById('vidsrc-save-domains').onclick = () => {
    const textareaValue = document.getElementById('vidsrc-domains-textarea').value;
    const newDomains = textareaValue.split('\n').map(domain => domain.trim()).filter(domain => domain !== '');
    GM_setValue('availableDomains', newDomains);
    alert('VidSrc domains updated. Please reload the page for changes to take effect.');
    document.getElementById('vidsrc-domain-editor').remove();
  };

  document.getElementById('vidsrc-cancel-domains').onclick = () => {
    document.getElementById('vidsrc-domain-editor').remove();
  };
  
});

// Add custom styles for buttons
GM_addStyle(`
  .vidsrc-button {
    background-color: #4CAF50;
    border: none;
    color: white;
    padding: 10px 20px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    font-size: 14px;
    margin: 4px 2px;
    cursor: pointer;
    border-radius: 4px;
    font-weight: bold;
  }
  .vidsrc-button:hover {
    background-color: #45a049;
  }
  .vidsrc-container {
    margin: 10px 0;
    padding: 10px;
    background-color: #f9f9f9;
    border-left: 4px solid #4CAF50;
  }
`);

// Function to extract IMDb ID from URL
function getImdbId() {
  const match = window.location.href.match(/\/title\/(tt\d+)/);
  return match ? match[1] : null;
}

// Function to extract TMDb ID from URL
function getTmdbId() {
  const match = window.location.href.match(/\/(movie|tv)\/(\d+)/);
  return match ? match[2] : null;
}

// Function to get content type (movie or tv)
function getContentType() {
  if (window.location.hostname.includes('imdb.com')) {
    // Check if it's a TV show or movie on IMDb
    // TV URLs are of the form https://www.imdb.com/title/<id>/episodes/
    // movie URLs are of the form https://www.imdb.com/title/<id>/
    return window.location.pathname.includes('/episodes/') ? 'tv' : 'movie';
  }
  else if (window.location.hostname.includes('themoviedb.org')) {
    return window.location.pathname.includes('/tv/') ? 'tv' : 'movie';
  }
  return 'movie';
}


function failureMessage(badDomains) {
  document.body.insertAdjacentHTML('afterbegin',
    `<div style="position:fixed;top:0;left:0;width:100%;background-color:#FFFF00;color:#000;padding:10px;text-align:center;z-index:10000;">
      Streamtrooper: Unable to find a working source after multiple attempts Checked ${badDomains}.
    </div>`);
}

function makeURLPath(domain, opts) {
  if (domain.startsWith('player.vidplus')) {
    if (opts.type === 'movie') {
      return `https://${domain}/embed/movie/${opts.movie}`;
    } else if (opts.type === 'tv' && opts.season && opts.episode) {
      return `https://${domain}/embed/tv/${opts.movie}/${opts.season}/${opts.episode}`;
    }
  }
  if (opts.type === 'movie') {
    urlPath = `/embed/movie?${opts.source || 'imdb'}=${opts.movie}`;
  } else if (opts.type === 'tv' && opts.season && opts.episode) {
    urlPath = `/embed/tv?${opts.source || 'imdb'}=${opts.movie}&season=${opts.season}&episode=${opts.episode}`;
  }
  return `https://${domain}/${urlPath}`;
}

async function filterAdsAndOpen(opts, badDomains=[]) {
  let domain = await getVidsrcDomain(opts, badDomains );
  if (domain=='' || badDomains.length > 10) return failureMessage(badDomains);
  let url = makeURLPath(domain, opts);
  console.log('Streamtrooper: Filtering ads for URL:', url);
  try {
    const response = await new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: 'GET',
        url: url,
        headers: {
          "User-Agent": "Mozilla/5.0",
          "Referer": url
        },
        onload: resolve,
        onerror: reject
      });
    });
    console.log('Streamtrooper: Received response from embed page.');
    console.log('Streamtrooper: Response status:', response.status);
    if (response.status >=400 && response.status < 600) {   
      console.warn(`Streamtrooper: Access forbidden or gone (status: ${response.status}).`);
      console.warn(`Streamtrooper: We need to change the domain from ${badDomains.concat([domain])}.`);
      return filterAdsAndOpen(opts, badDomains.concat([domain]));
    }
    if (response.status >= 200 && response.status < 300) {
      console.log('Streamtrooper: Response text:', response.responseText.substring(0, 200) + '...');
      const parser = new DOMParser();
      const doc = parser.parseFromString(response.responseText, 'text/html');
      const iframe = doc.querySelector('iframe');

      if (iframe && iframe.src) {
        // The iframe src might be a relative URL, so resolve it against the original URL.
        const iframeUrl = new URL(iframe.src, url).href;
        launchUrlInNewTab(iframeUrl);
      } else {
        console.warn('Streamtrooper: No iframe found on the page, opening original URL.');
        launchUrlInNewTab(url);
      }
    } else {
      console.error(`Streamtrooper: Failed to fetch embed page. Status: ${response.status}. Opening original URL.`);
      launchUrlInNewTab(url);
    }
  } catch (error) {
    console.error('Streamtrooper: Error in filterAds function:', error);
    launchUrlInNewTab(url); // Fallback to original URL on error
  }
}

function launchUrlInNewTab(url) {
  GM_openInTab(url, { active: true, insert: true });
}

// Function to create VidSrc button
/**
 * Creates a VidSrc button for movies or episodes.
 * @param {Object} opts - Options object
 * @param {string} opts.movie - IMDb or TMDb ID
 * @param {string} [opts.type] - 'movie' or 'tv'
 * @param {number} [opts.season] - Season number (for TV)
 * @param {number} [opts.episode] - Episode number (for TV)
 * @param {string} [opts.source] - 'imdb' or 'tmdb'
 * @param {string} text - Button text
 */
function createVidsrcButton(opts, text) {
  const button = document.createElement('button');
  button.className = 'vidsrc-button';
  button.textContent = text;
  button.onclick = () => filterAdsAndOpen(opts);
  return button;
}

// Function to add VidSrc links to IMDb pages
/**
 * Adds IMDb-based video source links to the current page as a floating container.
 * 
 * Retrieves the IMDb ID from the current page and creates appropriate video links
 * based on content type (movie or TV series). 
 * 
 * @async
 * @function addImdbLinks
 * @returns {Promise<void>} Resolves when the floating container is added to the page
 * 
 * @description
 * - Removes any existing floating button before creating a new one
 * - Creates a fixed-position floating container in the top-right corner
 * - For movies: immediately adds a direct watch link
 * - For TV series: delays 5 seconds before adding episode links to allow page elements to load
 * - Container includes styling for visibility and user experience
 */
async function addImdbLinks() {
  const imdbId = getImdbId();
  if (!imdbId) return;

  const contentType = getContentType();

  // Remove any existing floating button
  const existingButton = document.querySelector('.vidsrc-floating-container');
  if (existingButton) {
    existingButton.remove();
  }

  const container = document.createElement('div');
  container.className = 'vidsrc-floating-container';
  container.style.cssText = `
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 9999;
    background: white;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    padding: 10px;
  `;
  countryCode = await getCountryCode();
  container.appendChild(document.createTextNode(`Your Country: ${countryCode.toUpperCase()}`));
  document.body.appendChild(container);
  

  if (contentType === 'movie') {
    container.appendChild(document.createElement('br'));
    container.appendChild(createVidsrcButton({ movie: imdbId, type: 'movie', source: 'imdb' }, 'Watch'));
  }
  else {
    setTimeout(() => {
      addEpisodeLinks(container);
    }, 5000); // Delay to allow episode elements to load
  }

  document.body.appendChild(container);
}

// Function to add VidSrc links to TMDb pages
async function addTmdbLinks() {
  const tmdbId = getTmdbId();
  if (!tmdbId) return;

  const contentType = getContentType();

  // Remove any existing floating button
  const existingButton = document.querySelector('.vidsrc-floating-container');
  if (existingButton) {
    existingButton.remove();
  }

  const container = document.createElement('div');
  container.className = 'vidsrc-floating-container';
  container.style.cssText = `
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 9999;
    background: white;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    padding: 10px;
  `;

  countryCode = await getCountryCode();
  container.appendChild(document.createTextNode(`Your Country: ${countryCode.toUpperCase()}`));
  document.body.appendChild(container);
    
  

  if (contentType === 'movie') {
    container.appendChild(document.createElement('br'));
    container.appendChild(createVidsrcButton({ movie: tmdbId, type: 'movie', source: 'tmdb' }, 'Watch'));
  }
  else {
    addTmdbEpisodeLinks();
  }

  document.body.appendChild(container);
}

// Main execution
function init() {
  if (window.location.hostname.includes('imdb.com')) {
    addImdbLinks();
  }
  else if (window.location.hostname.includes('themoviedb.org')) {
    addTmdbLinks();
  }
}

// Run on page load and handle dynamic content
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
}
else {
  init();
}

// Handle navigation changes (for single-page applications)
let lastUrl = location.href;
new MutationObserver(() => {
  const url = location.href;
  if (url !== lastUrl) {
    lastUrl = url;
    setTimeout(init, 1000); // Delay to allow page to load
  }
}).observe(document, {
  subtree: true,
  childList: true
});

async function addEpisodeLinks(container) {
  const imdbId = getImdbId();
  if (!imdbId) return;
  // We search for all dives like:<div class="ipc-title__text ipc-title__text--reduced">S3.E5 ∙ A Tale of Two Topas</div>
  const episodeElements = document.querySelectorAll('div.ipc-title__text.ipc-title__text--reduced');
  if (episodeElements.length === 0) return;

  episodeElements.forEach(async (element, index) => {
    const episodeText = element.textContent.trim();
    const [season, episode] = episodeText.match(/S(\d+)\.E(\d+)/).slice(1);
    element.appendChild(createVidsrcButton({ movie: imdbId, type: 'tv', season: season, episode: episode, source: 'imdb' }, `Watch ${episodeText}`));
  });
}

async function addTmdbEpisodeLinks() {
  // Episode pages are of the form https://www.themoviedb.org/tv/<id>/season/<s>
  // We search for all a like this and relace the href with the VidSrc link
  // <a class="no_click open" data-episode-id="66ab5088c1afc0fa87c007f2" data-episode-number="3" data-season-number="2" href="/tv/93405/season/2/episode/3" title="Squid Game: Season 2 (2024): Episode 3 - 001"> <img src="/assets/2/v4/static_cache/down_arrow_silver-caf7b40a340dbc2be34990af9cc5ecaf479f81e59b37ff57f1d6241fe26c026d.svg"> Expand </a>
  const tmdbId = getTmdbId();
  if (!tmdbId) return;
  const episodeLinks = document.querySelectorAll('a.no_click.open');
  episodeLinks.forEach(async link => {
    const seasonNumber = link.getAttribute('data-season-number');
    const episodeNumber = link.getAttribute('data-episode-number');
    if (seasonNumber && episodeNumber && link.textContent.includes('Expand')) {
      const button = createVidsrcButton({ movie: tmdbId, type: 'tv', season: seasonNumber, episode: episodeNumber, source: 'tmdb' }, `Watch S${seasonNumber}.E${episodeNumber}`);
      // replace the link with the button
      link.replaceWith(button);
    }
  });

}


function showCheckingDomainMessage(domain) {
  const existingMessage = document.querySelector('.vidsrc-domain-checking-message');
  if (existingMessage) {
    existingMessage.textContent = `Checking domain: ${domain}...`;
    if (domain === '')  existingMessage.remove();
    return;
  }
  const message = document.createElement('div');
  message.className = 'vidsrc-domain-checking-message';
  message.style.cssText = `
    position: fixed;
    bottom: 20px;
    right: 20px;
    z-index: 9999;
    background: #ffeb3b;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    padding: 10px;
    font-weight: bold;
  `;
  message.textContent = `Checking domain: ${domain}...`;
  document.body.appendChild(message);
} 
 

async function getVidsrcDomain(opts,badDomains=[]) {
   for (const domain of availableDomains) {
    console.log(`Streamtrooper: Trying domain ${domain}...`);
    if (badDomains.includes(domain)  ) continue;
    try {
      showCheckingDomainMessage(domain);
      const response = await new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: 'GET',
          url: makeURLPath(domain, opts),
          onload: function (response) {
            if (response.status === 200) {
              resolve(response);
            } else {
              reject(new Error('Non-200 status'));
            }
          },
          onerror: function (error) {
            reject(error);
          }
        });
      });
      console.log(`Streamtrooper: Domain ${domain} is reachable.`);
      vidsrcDomain = domain;
      GM_setValue('vidsrcDomain', vidsrcDomain);
      GM_setValue('domainLastCheckedTime', Date.now());
      return domain;  
      break;
    } catch (error) {
      console.warn(`Domain ${domain} is not reachable.`);
      badDomains.push(domain);
    }
  }
  showCheckingDomainMessage('');
  return '';
}
    

async function getCountryCode() {
  return new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      method: 'GET',
      url: 'https://ipapi.co/json/',
      onload: function (response) {
        if (response.status === 200) {
        const data = JSON.parse(response.responseText);
        resolve(data.country_code.toLowerCase());
        }
        else {
        console.error('Failed to fetch country code:', response.statusText);
        resolve('Unknown'); 
        }
      },
      onerror: function (error) {
        console.error('Error fetching country code:', error);
        resolve('Unknown'); 
      }
    });
  });
}