Streamtrooper

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

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

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

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

您需要先安裝使用者腳本管理器擴充功能,如 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'] );

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>`);
}

async function filterAdsAndOpen(urlPath, badDomains=[]) {
  let domain = await getVidsrcDomain(badDomains, urlPath);
  if (domain=='' || badDomains.length > 10) return failureMessage(badDomains);
  let url = `https://${domain}/${urlPath}`;
  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(urlPath, 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
function createVidsrcButton( url, text) {
  const button = document.createElement('button');
  button.className = 'vidsrc-button';
  button.textContent = text;
  button.onclick = () => filterAdsAndOpen(  url);
  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') {
    const movieUrl = `/embed/movie?imdb=${imdbId}`;
    container.appendChild(document.createElement('br'));
    container.appendChild(createVidsrcButton(movieUrl, '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') {
    const movieUrl = `/embed/movie?tmdb=${tmdbId}`;
    container.appendChild(document.createElement('br'));
    container.appendChild(createVidsrcButton(movieUrl, '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);
    const url = `/embed/tv?imdb=${imdbId}&season=${season}&episode=${episode}`;
    element.appendChild(createVidsrcButton(url, `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 url = `/embed/tv?tmdb=${tmdbId}&season=${seasonNumber}&episode=${episodeNumber}`;
      const button = createVidsrcButton(url, `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(badDomains=[], url= 'movies/latest/page-1.json') {
   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: `https://${domain}/${url}`,
          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'); 
      }
    });
  });
}