IMDBTorrent (userscript)

Adds a nice button to IMDB

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.

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

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

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

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

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

// ==UserScript==
// @name         IMDBTorrent (userscript)
// @namespace    https://github.com/knobthree/IMDBTorrent
// @version      1.0.1
// @description  Adds a nice button to IMDB
// @match        https://www.imdb.com/*
// @run-at       document-idle
// @grant        GM_addStyle
// @grant        GM.xmlHttpRequest
// @grant        GM_xmlhttpRequest
// @connect      apibay.org
// @license      MIT
// ==/UserScript==

/* === Inject original styles.css verbatim === */
(function(){
  const css = `
/* Our block that sits at the top of the right CTA column */
#imdbh-rightcol-block.imdbh-rightcol-block{
  display: block;
  margin: 0 0 10px 0;
}

/* The injected button; neutral style that fits IMDb spacing */
#imdbh-open-btn.imdbh-btn{
  width: 100%;
  display: inline-block;
  padding: 10px 14px;
  font: inherit;
  border: 1px solid #ccc;
  border-radius: 10px;
  background: #f5f5f5;
  cursor: pointer;
}
#imdbh-open-btn.imdbh-btn:hover{
  background: #eee;
}

/* Popup */
#imdb-helper-popup{
  position: fixed;
  right: 16px;
  bottom: 16px;
  width: 780px;
  max-width: calc(100vw - 32px);
  max-height: 60vh;
  display: none;
  background: #111;
  color: #eee;
  border: 1px solid #333;
  border-radius: 12px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.4);
  overflow: hidden;
  z-index: 2147483000;
}

#imdb-helper-popup .imdbh-header{
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 12px;
  background: #1a1a1a;
  border-bottom: 1px solid #222;
  font-weight: 600;
}

#imdb-helper-popup .imdbh-close{
  background: transparent;
  color: #aaa;
  border: none;
  font-size: 20px;
  cursor: pointer;
  line-height: 1;
}
#imdb-helper-popup .imdbh-close:hover{ color: #fff; }

#imdb-helper-popup .imdbh-body{
  padding: 12px;
  overflow: auto;
  max-height: calc(60vh - 44px);
}
`;
  (typeof GM_addStyle === "function"
    ? GM_addStyle(css)
    : (function(){ const s=document.createElement("style"); s.textContent=css; document.head.appendChild(s); })());
})();

/* === Original content.js (unchanged except where strictly necessary) === */
(function () {
  const BLOCK_ID = "imdbh-rightcol-block";
  const BTN_ID   = "imdbh-open-btn";
  const PANEL_ID = "imdb-helper-popup";
  const CLOSE_ID = "imdb-helper-popup-close";

  function getTargetContainer(){
    const el = document.querySelector("[data-testid='reviewContent-all-reviews']");
    if(!el)return;
    return el.parentElement;
  }

  function ensureInjected(){
    if (document.getElementById(BLOCK_ID)) return;

    const container = getTargetContainer();
    if (!container) return;

    const block = document.createElement("div");
    block.id = BLOCK_ID;
    block.className = "imdbh-rightcol-block";

    const btn = document.createElement("button");
    btn.id = BTN_ID;
    btn.type = "button";
    btn.className = "imdbh-btn";
    btn.textContent = "Torrent";
    btn.addEventListener("click", (e) => {
      e.preventDefault();
      e.stopPropagation();
      openPopup();
    });

    block.appendChild(btn);

    // Insert as the FIRST child of the parent-parent container
    container.insertBefore(block, container.firstElementChild || null);
  }

  // ----- Popup (same as before) -----
  function ensurePopup() {
    let panel = document.getElementById(PANEL_ID);
    if (panel) return panel;

    panel = document.createElement("div");
    panel.id = PANEL_ID;
    panel.innerHTML = `
      <div class="imdbh-header">
        <span>IMDBTorrent</span>
        <button id="${CLOSE_ID}" class="imdbh-close" aria-label="Close">×</button>
      </div>
      <div class="imdbh-body">

      </div>
    `;
    document.body.appendChild(panel);

    document.getElementById(CLOSE_ID).addEventListener("click", () => panel.remove());
    return panel;
  }

  function getCurrentMovieName(){
    var el = document.querySelector("[data-testid='hero__primary-text']");
    if(!el)return;
    return el.innerText;
  }

  function openPopup() {
    const panel = ensurePopup();
    panel.style.display = "block";

    let movieName = getCurrentMovieName();
    if(!movieName){
      log("Error: Cannot get name of movie. Are you looking at page of one?");
      return;
    }

    TorrentSearch.search(movieName, "[class='imdbh-body']");
  }

  function boot(){
    ensureInjected();

    // Re-run when IMDb updates the DOM or navigates SPA-style
    const mo = new MutationObserver(() => ensureInjected());
    mo.observe(document.documentElement, { childList: true, subtree: true });

    const _push = history.pushState;
    history.pushState = function(){
      const r = _push.apply(this, arguments);
      setTimeout(ensureInjected, 300);
      return r;
    };
    window.addEventListener("popstate", () => setTimeout(ensureInjected, 300));

    // EXTENSION-ONLY MESSAGE HANDLER REMOVED (not applicable in userscript)
    // chrome.runtime.onMessage.addListener((msg) => {
    //   if (msg?.t === "torrentThisMovie") torrentThisMovie();
    // });
  }

  function torrentThisMovie() {
    openPopup();
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot);
  } else {
    boot();
  }

  const log = (msg) => {
    const el = document.querySelector("[class='imdbh-body']");
    const line = document.createElement("div");
    line.textContent = msg;
    el.appendChild(line);
  };

})();

/* === The rest of original content.js (unchanged) === */

const currentPageImdbId = (window.location.href.match(/\/title\/(tt\d+)/) || [])[1];

const TorrentSearch = (() => {
  const BASE_URL = "https://apibay.org/";
  const TRACKERS = [
    "udp://tracker.opentrackr.org:1337",
    "udp://open.stealth.si:80/announce",
    "udp://tracker.torrent.eu.org:451/announce",
    "udp://tracker.bittor.pw:1337/announce",
    "udp://public.popcorn-tracker.org:6969/announce",
    "udp://tracker.dler.org:6969/announce",
    "udp://exodus.desync.com:6969",
    "udp://open.demonii.com:1337/announce"
  ];

  const magnetFrom = (infoHash, name) => {
    const dn = encodeURIComponent(name || "");
    const tr = TRACKERS.map(t => "&tr=" + encodeURIComponent(t)).join("");
    return `magnet:?xt=urn:btih:${infoHash}&dn=${dn}${tr}`;
  };

  const getLogEl = (selector) => {
    const el = document.querySelector(selector);
    if (!el) throw new Error(`Log element not found for selector: ${selector}`);
    return el;
  };

  const log = (el, msg) => {
    const line = document.createElement("div");
    line.textContent = msg;
    el.appendChild(line);
  };

  const renderResults = (logEl, results) => {
    //sort so that results with matching IMDB id are first as long as results have over x seeds.
    //Below that treshold just don't care, to avoid situations where dead "more valid" torrents are only shown
    results.sort((a,b) => {
      const aPrio = (a.seeders > 10 && a.imdb == currentPageImdbId) ? 1 : 0;
      const bPrio = (b.seeders > 10 && b.imdb == currentPageImdbId) ? 1 : 0;

      if(aPrio !== bPrio) {
        return bPrio - aPrio;
      }

      return b.seeds - a.seeds;
    });


    results.slice(0, Math.min(5, results.length)).forEach(r => {
      const sizeGB = (Number(r.size) || 0) / (1024 ** 3);
      const seeds = r.seeders ?? 0;

      const a = document.createElement("a");
      a.href = r.MagnetLink;
      a.target = "_blank";
      a.rel = "noopener noreferrer";
      a.style.display = "grid";
      a.style.gridTemplateColumns = "7rem 7rem 1fr";
      a.style.gap = "0.75rem";
      a.style.alignItems = "baseline";
      a.style.padding = "4px 6px";
      a.style.textDecoration = "none";
      a.style.color = "inherit";

      a.onmouseover = () => a.style.background = "rgba(0,0,0,.06)";
      a.onmouseout  = () => a.style.background = "";

      const size = document.createElement("span");
      size.textContent = sizeGB.toFixed(1) + " GB";
      size.style.fontFamily = "monospace";

      const seedsEl = document.createElement("span");
      seedsEl.textContent = "Seeds: " + seeds;
      seedsEl.style.fontFamily = "monospace";

      const name = document.createElement("span");
      name.textContent = r.name;

      a.appendChild(size);
      a.appendChild(seedsEl);
      a.appendChild(name);

      logEl.appendChild(a);
    });
  };

  async function search(query, logSelector) {
    const logEl = getLogEl(logSelector);

    const openTPBLink = document.createElement("a");
    openTPBLink.href = `https://thepiratebay.org/search.php?q=${encodeURIComponent(query)}`;
    openTPBLink.target = "_blank";
    openTPBLink.textContent = 'Open search page';
    logEl.appendChild(openTPBLink);

    log(logEl, `[TorrentSearch] Query: "${query}"`);

    const fullUrl = `${BASE_URL}q.php?q=${encodeURIComponent(query)}`;
    log(logEl, `[TorrentSearch] GET ${fullUrl}`);

    let json;
    try {
      const res = await fetchExt(fullUrl, { method: "GET" });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const text = await res.body;
      console.log(text);
      // apibay sometimes returns "null" for no results
      json = text && text.trim() !== "null" ? JSON.parse(text) : [];
    } catch (e) {
      log(logEl, `[TorrentSearch] ERROR: ${e.message}`);
      throw e;
    }

    log(logEl, `[TorrentSearch] Results: ${json.length}`);

    // Normalize types similar to your C# class + add magnet
    const results = json.map(r => ({
      id: parseInt(r.id, 10),
      name: r.name,
      info_hash: r.info_hash,
      leechers: parseInt(r.leechers, 10),
      seeders: parseInt(r.seeders, 10),
      num_files: parseInt(r.num_files, 10),
      size: parseInt(r.size, 10),
      username: r.username,
      added: parseInt(r.added, 10),
      status: r.status,
      category: parseInt(r.category, 10),
      imdb: r.imdb,
      get MagnetLink(){ return magnetFrom(this.info_hash, this.name); }
    }));

    renderResults(logEl,results);

    return results;
  }

  return { search, magnetFrom };
})();

/* === Minimal, required replacement for background fetch bridge === */
// const fetchExt = (url, opts) => chrome.runtime.sendMessage({ t: "fetch", url, opts });
// (Above is extension-only; replaced with GM.xmlHttpRequest below.)

const fetchExt = (url, opts = {}) => {
  const req = (typeof GM !== "undefined" && GM.xmlHttpRequest)
            || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest);
  if (!req) return Promise.reject(new Error("GM.xmlHttpRequest not available"));

  return new Promise((resolve, reject) => {
    req({
      url,
      method: (opts.method || "GET"),
      headers: opts.headers || {},
      data: opts.body,
      timeout: 30000,
      onload: (res) => {
        resolve({
          ok: (res.status >= 200 && res.status < 300),
          status: res.status,
          body: res.responseText
        });
      },
      onerror: (e) => reject(e?.error || e),
      ontimeout: () => reject(new Error("Request timed out"))
    });
  });
};