IMDBTorrent (userscript)

Adds a nice button to IMDB

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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