Streamtrooper

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

اعتبارا من 05-11-2025. شاهد أحدث إصدار.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل 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'); 
      }
    });
  });
}