IMDBTorrent (userscript)

Adds a nice button to IMDB

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址