IMDB info + .torrent from magnet

Show IMDB info on torrent sites and add .torrent download links for magnets

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name          IMDB info + .torrent from magnet
// @version       4.1.1
// @description   Show IMDB info on torrent sites and add .torrent download links for magnets
// @copyright 2025, quantavil (https://openuserjs.org/users/quantavil)
// @license MIT
// @namespace     hossam6236-fixed
// @run-at        document-idl

// Use @match (recommended) or @include per preference
// @match         http*://*torrent*.*/*
// @match         http*://*pirate*bay*.*/*
// @match         http*://*tpb*.*/*
// @match         http*://*isohunt*.*/*
// @match         http*://*1337x*.*/*
// @match         http*://*rarbg*.*/*
// @match         http*://*zooqle*.*/*
// @match         http*://*torlock*.*/*
// @match         http*://*eztv*.*/*
// @match         http*://*toorgle*.*/*
// @match         http*://*demonoid*.*/*
// @match         http*://*kickass*.*/*
// @match         http*://*kat*.*/*
// @match         http*://*.imdb.*/*

// Grants for cross-origin requests via GM APIs
// @grant         GM_xmlhttpRequest
// @grant         GM.xmlHttpRequest
// @grant         GM_getValue
// @grant         GM_setValue
// @grant         GM_deleteValue

// Domains allowed for GM requests (Tampermonkey uses @connect)
// @connect       omdbapi.com
// @connect       m.media-amazon.com
// @connect       ia.media-imdb.com
// @connect       itorrents.org
// @connect       torrage.info
// @connect       btcache.me
// ==/UserScript==

(() => {
  // ========== CONFIG ==========
  // IMPORTANT: Replace with a valid personal OMDb API key.
  // Get a key and use it via &apikey=KEY with t= and optional y= as per OMDb docs.
  const OMDB_API_KEY = "YOUR_API_KEY";

  const POSTER_PLACEHOLDER =
    "https://ia.media-imdb.com/images/G/01/imdb/images/nopicture/large/film-184890147._CB379391879_.png";

  // Prefer a resilient reference to the GM request function across TM variants
  const gmXhr = (typeof GM_xmlhttpRequest === "function")
    ? GM_xmlhttpRequest
    : (typeof GM !== "undefined" && typeof GM.xmlHttpRequest === "function" ? GM.xmlHttpRequest : null);

  if (!gmXhr) {
    console.error("[IMDB info] No GM request API available. Check @grant for GM_xmlhttpRequest or GM.xmlHttpRequest.");
    return;
  }

  const CACHE_PREFIX = "imdbinfo-cache-v1:";
  const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days

  // Cross-platform storage wrappers (GM.* / GM_* / localStorage)
  const gmGet = async (key, defVal = null) => {
    try {
      if (typeof GM !== "undefined" && typeof GM.getValue === "function") return await GM.getValue(key, defVal);
      if (typeof GM_getValue === "function") return GM_getValue(key, defVal);
      const raw = localStorage.getItem(key);
      return raw == null ? defVal : raw;
    } catch {
      return defVal;
    }
  };
  const gmSet = async (key, value) => {
    try {
      if (typeof GM !== "undefined" && typeof GM.setValue === "function") return await GM.setValue(key, value);
      if (typeof GM_setValue === "function") return GM_setValue(key, value);
      localStorage.setItem(key, value);
    } catch {}
  };
  const gmDel = async (key) => {
    try {
      if (typeof GM !== "undefined" && typeof GM.deleteValue === "function") return await GM.deleteValue(key);
      if (typeof GM_deleteValue === "function") return GM_deleteValue(key);
      localStorage.removeItem(key);
    } catch {}
  };

  const norm = (s) => (s || "").toString().trim().toLowerCase();
  const cacheKeyFor = (title, year) => `${CACHE_PREFIX}${norm(title)}|${norm(year || "")}`;

  async function getCachedMovie(title, year) {
    const key = cacheKeyFor(title, year);
    const raw = await gmGet(key, null);
    if (!raw) return null;
    let payload = null;
    try {
      payload = typeof raw === "string" ? JSON.parse(raw) : raw;
    } catch {
      return null;
    }
    if (!payload || !payload.data || !payload.ts) return null;
    if (Date.now() - payload.ts > CACHE_TTL_MS) {
      await gmDel(key); // expire
      return null;
    }
    return payload.data;
  }

  async function setCachedMovie(title, year, data) {
    // Only cache valid responses
    if (!data || data.Response !== "True") return;
    const key = cacheKeyFor(title, year);
    const payload = JSON.stringify({ ts: Date.now(), data });
    await gmSet(key, payload);
  }

  const STYLE = `
    .imdb-download-link::before { content: '⇩'; }
    .title_wrapper .imdb-download-link { font-size: .5em; }
    a.movie-preview { display: inline-block !important; cursor: pointer; }
    .movie-preview-starter {
      display: inline-block; position: fixed; opacity: 0.85; top: 0; right: 0; z-index: 10000; text-align: center;
    }
    .movie-preview-starter--button {
      display: inline-block; cursor: pointer; margin: 7px; padding: 7px; font-size: 12pt; font-family: Tahoma, Arial; border-radius: 5px;
    }
    .movie-preview-box {
      position: fixed; z-index:9999; width:475px; height:283px; top: calc(50vh - 150px); left: 50vw;
      display: flex; color: #000; background-color: white; border: 3px solid #222; border-radius: 5px; overflow: hidden;
      opacity: 0; visibility: hidden; transition: all 0.5s ease-in-out;
    }
    .movie-preview-box.visible { opacity: 1; visibility: visible; }
    .movie-preview-box *, .movie-preview-unique-list > * { font-size: 10pt; font-family: Tahoma, Arial; line-height: initial; }
    .movie-preview-box.no-trailer .preview--info--trailer { display: none; }
    .torrent-download-links { opacity: 0.8; font-size: 90%; position: absolute; display: none; }
    .assisted-torrent-link:hover .torrent-download-links { display: inline-block; }
    .movie-preview-unique-list {
      width: 50%; max-width: 400px; max-height: 200px; margin: auto; overflow: auto; text-align: left; padding: 5px; line-height: 15px;
      color: #000; background-color: white; border: 3px solid #222; border-radius: 5px;
    }
    .movie-preview-unique-list > * { margin: 2px; }
    .movie-preview-unique-list a { border: 0; }
    .movie-preview-unique-list a:hover { border: 0; text-decoration: underline; }
    a.movie-preview.highlight { background-color: rgba(255, 231, 58, 0.59); }
    .movie-preview-enhancement { display: inline-block !important; max-width: 30px; min-width: 30px; font-size: 85%; margin:0 4px 0 0; }
    .movie-preview-enhancement.remarkable { font-weight: bold; }
    .movie-preview-enhancement.starred-1::after { content: "★"; color: #DD0000; }
    .movie-preview-enhancement.starred-2::after { content: "★"; color: #660000; }
    .movie-preview-enhancement.starred-3::after { content: "★"; }
    .movie-preview-enhancement.starred-4::after { content: "☆"; }
    .preview--poster { flex-shrink: 0; width: 200px; height: 283px; }
    .preview--poster--img { cursor: pointer; width: 100%; height: 100%; }
    .preview--info { text-align:left; padding:3px; height:277px; overflow:auto; display:inline-block; }
    .preview--info--title { text-align:center; font-size:125%; font-weight:bold; }
    .preview--info--trailer { color: #369; cursor: pointer; display: inline-block; }
    .preview--info--trailer:hover { text-decoration: underline; }
    .preview--info--trailer::before { content: '('; }
    .preview--info--trailer::after { content: '), '; }
    .preview--info--imdb-rating, .preview--info--imdb-votes { font-weight: bold; }
  `;

  const appendStyle = (css) => {
    const n = document.createElement("style");
    n.type = "text/css";
    n.textContent = css;
    document.head.append(n);
  };

  const fetchSafe = (url) => new Promise((resolve, reject) => {
    gmXhr({
      url,
      method: "GET",
      onload: (res) => resolve(res.responseText),
      onerror: reject
    });
  });

  const setImgSrcBypassingAdBlock = (imageNode, src) => {
    imageNode.src = src;
    let blobUrl = null;
    imageNode.onerror = () => {
      if (!imageNode.src || !/^https?:/i.test(imageNode.src)) return;
      gmXhr({
        url: imageNode.src,
        method: "GET",
        responseType: "blob",
        onload: (data) => {
          const reader = new FileReader();
          reader.onloadend = () => {
            if (blobUrl) URL.revokeObjectURL(blobUrl);
            blobUrl = reader.result;
            imageNode.src = blobUrl;
            const old = imageNode;
            const clone = old.cloneNode();
            clone.style = "";
            old.replaceWith(clone);
          };
          reader.readAsDataURL(data.response);
        },
        onerror: () => {
          imageNode.src = POSTER_PLACEHOLDER;
        }
      });
    };
  };

  // Helpers
  const getTorrentSearchURLFromMovieTitle = (title) =>
    `https://thepiratebay.org/search/${encodeURIComponent(title)}/0/99/0`;

  const getMovieHashFromTitleAndYear = (title, year = "") =>
    `${title}_${year}`.trim().replace(/[^a-zA-Z0-9]+/g, "-");

  const isHostnameIMDB = (h) => h.endsWith("imdb.com");
  const isHostnamePirateBay = (h) => /.*(pirate.*bay|tpb).*/.test(h);

  // Robust 1337x/document fallback: use document.title and try to carve out year if present
  function getBestTitleYearFallback() {
    const h1 = document.querySelector("h1");
    let base = (h1 && h1.textContent) ? h1.textContent : document.title || "";
    base = base.replace(/\s*[-|–]\s*1337x.*$/i, "").trim();
    // Replace dots with spaces, strip common quality tags
    base = base.replace(/\./g, " ").replace(/\b(720p|1080p|2160p|480p|webrip|web-dl|bluray|bdrip|hdrip|x264|x265|hevc|aac|dts|yts|mkv|mp4)\b/ig, " ");
    // Extract year if present
    const ym = base.match(/\b(19|20)\d{2}\b/);
    if (ym) {
      const year = ym[0];
      const title = base.slice(0, ym.index).trim();
      return { title, year };
    }
    return { title: base.trim(), year: "" };
  }

  // OMDb load by title/year (t & y)
  async function loadMovie(title, year) {
    // 1) Try cache first
    const cached = await getCachedMovie(title, year);
    if (cached) return cached;

    // 2) Fetch from OMDb
    const url = `https://www.omdbapi.com/?apikey=${encodeURIComponent(OMDB_API_KEY)}&t=${encodeURIComponent(title)}${year ? `&y=${encodeURIComponent(year)}` : ""}&plot=full&r=json`;
    try {
      const txt = await fetchSafe(url);
      const obj = JSON.parse(txt);
      // 3) Cache only successful responses
      if (obj && obj.Response === "True") {
        await setCachedMovie(title, year, obj);
        return obj;
      }
      // No caching for failed or missing data — will retry next time
      return { Error: obj && obj.Error ? obj.Error : "Not found", Title: title, Year: year || "" };
    } catch (e) {
      // No caching for errors — will retry next time
      return { Error: e + "", Title: title, Year: year || "" };
    }
  }

  function extractRankingMetrics(m) {
    const awards = m.Awards || "";
    const _cap = (text, re, idx = 1) => {
      const r = text.match(re);
      return r ? r[idx] : "";
    };
    const reg_wins = /([0-9]+) win(s|)/i;
    const reg_noms = /([0-9]+) nomination(s|)/i;
    const reg_wins_sig = /Won ([0-9]+) Oscar(s|)/i;
    const reg_noms_sig = /Nominated for ([0-9]+) Oscar(s|)/i;
    return {
      rating: parseFloat(m.imdbRating) || 0,
      votes: parseFloat((m.imdbVotes || "0").replace(/,/g, "")) || 0,
      wins: parseInt(_cap(awards, reg_wins, 1)) || 0,
      noms: parseInt(_cap(awards, reg_noms, 1)) || 0,
      wins_sig: parseInt(_cap(awards, reg_wins_sig, 1)) || 0,
      noms_sig: parseInt(_cap(awards, reg_noms_sig, 1)) || 0,
      awards_text: awards.toLowerCase(),
    };
  }

  function assessMovieRankings(m) {
    const rm = extractRankingMetrics(m);
    const { rating, votes, wins_sig, wins, noms_sig, noms } = rm;
    const isRemarkable = rating >= 7.0 && votes > 50000;
    let starredDegree;
    if ((wins_sig >= 1 || noms_sig >= 2) && (wins >= 5 || noms >= 10)) starredDegree = 1;
    else if (wins >= 10 || (noms_sig >= 1 && noms >= 5) || (rating > 8.0 && votes > 50000)) starredDegree = 2;
    else if (wins >= 5 || noms >= 10 || noms_sig >= 1 || votes > 150000) starredDegree = 3;
    else if (wins + noms > 1) starredDegree = 4;
    let significancePercentage = 1.0;
    if (rating <= 5.0 || votes <= 1000) {
      significancePercentage = Math.max(0.15, Math.min(rating / 10, votes / 1000));
    } else if (m.imdbRating == "N/A" || m.imdbVotes == "N/A") {
      significancePercentage = 0.15;
    }
    return { isRemarkable, starredDegree, significancePercentage, rankingMetrics: rm };
  }

  function initPreviewNode() {
    const previewNode = document.createElement("div");
    previewNode.className = "movie-preview-box";
    previewNode.insertAdjacentHTML("beforeend", `
      <div class="preview--poster">
        <img class="preview--poster--img" src="${POSTER_PLACEHOLDER}">
      </div>
      <div class="preview--info">
        <div class="preview--info--title">
          <a href="" target="_blank">
            <span class="title">Title</span> (<span class="year">Year</span>)
          </a>
        </div>
        <div class="preview--info--trailer" title="Play trailer" data-trailer-url="">▶</div>
        <span class="preview--info--imdb-rating">-</span><span style="color:grey;">/10</span>
        (<span class="preview--info--imdb-votes">-</span> votes),
        <span class="preview--info--imdb-metascore">-</span> Metascore
        <br /><u>Awards</u>: <span class="preview--info--awards">N/A</span>
        <br /><u>Genre</u>: <span class="preview--info--genre">-</span>
        <br /><u>Released</u>: <span class="preview--info--released">-</span>
        <br /><u>Box Office</u>: <span class="preview--info--boxofficegross">N/A</span>
        <br /><u>Rated</u>: <span class="preview--info--mpaa-rating">-</span>,
        <u>Runtime</u>: <span class="preview--info--runtime">-</span>
        <br /><u>Actors</u>: <span class="preview--info--actors">-</span>
        <br /><u>Director</u>: <span class="preview--info--director">-</span>
        <br /><u>Plot</u>: <span class="preview--info--plot">-</span>
      </div>
    `);

    const posterImg = previewNode.querySelector(".preview--poster--img");
    if (posterImg) {
      posterImg.addEventListener("click", (e) => {
        e.preventDefault();
        const poster = posterImg.getAttribute("src") || "";
        if (!poster || poster === POSTER_PLACEHOLDER || !/^https?:/i.test(poster)) return;
        window.open(poster, "", "width=600,height=600");
      });
    }

    const trailerBtn = previewNode.querySelector(".preview--info--trailer");
    if (trailerBtn) {
      trailerBtn.addEventListener("click", (e) => {
        e.preventDefault();
        const url = trailerBtn.getAttribute("data-trailer-url");
        if (url) window.open(url, "", "width=900,height=500");
      });
    }

    previewNode.hiding = 0;
    previewNode.show = () => {
      if (previewNode.hiding) clearTimeout(previewNode.hiding);
      previewNode.hiding = 0;
      previewNode.classList.add("visible");
    };
    previewNode.hide = () => {
      if (previewNode.hiding) clearTimeout(previewNode.hiding);
      previewNode.hiding = setTimeout(() => {
        previewNode.classList.remove("visible");
        previewNode.hiding = 0;
      }, 800);
    };

    previewNode.setMovie = (m) => {
      const tA = previewNode.querySelector(".preview--info--title > a");
      if (tA) tA.setAttribute("href", m.imdbID ? `https://www.imdb.com/title/${m.imdbID}` : "#");

      const tS = previewNode.querySelector(".preview--info--title .title");
      if (tS) tS.textContent = m.Title || "-";

      const yS = previewNode.querySelector(".preview--info--title .year");
      if (yS) yS.textContent = m.Year || "-";

      const pImg = previewNode.querySelector(".preview--poster--img");
      if (pImg) setImgSrcBypassingAdBlock(pImg, m.Poster || POSTER_PLACEHOLDER);

      if (!m.Trailer) previewNode.classList.add("no-trailer"); else previewNode.classList.remove("no-trailer");
      const tr = previewNode.querySelector(".preview--info--trailer");
      if (tr) tr.setAttribute("data-trailer-url", m.Trailer || "");

      const r = previewNode.querySelector(".preview--info--imdb-rating");
      if (r) r.textContent = m.imdbRating || "-";

      const v = previewNode.querySelector(".preview--info--imdb-votes");
      if (v) v.textContent = m.imdbVotes || "-";

      const ms = previewNode.querySelector(".preview--info--imdb-metascore");
      if (ms) ms.textContent = m.Metascore || "-";

      const rel = previewNode.querySelector(".preview--info--released");
      if (rel) rel.textContent = m.Released || "-";

      const bo = previewNode.querySelector(".preview--info--boxofficegross");
      if (bo) bo.textContent = m.BoxOffice || "N/A";

      const g = previewNode.querySelector(".preview--info--genre");
      if (g) g.textContent = m.Genre || "-";

      const rr = previewNode.querySelector(".preview--info--mpaa-rating");
      if (rr) rr.textContent = m.Rated || "-";

      const rt = previewNode.querySelector(".preview--info--runtime");
      if (rt) rt.textContent = m.Runtime || "-";

      const aw = previewNode.querySelector(".preview--info--awards");
      if (aw) aw.innerHTML = (m.Awards || "N/A")
        .replace("Oscars.", "<b>Oscars</b>.")
        .replace("Oscar.", "<b>Oscar</b>.")
        .replace("Another ", "<br />Another ");

      const ac = previewNode.querySelector(".preview--info--actors");
      if (ac) ac.textContent = m.Actors || "-";

      const dr = previewNode.querySelector(".preview--info--director");
      if (dr) dr.textContent = m.Director || "-";

      const pl = previewNode.querySelector(".preview--info--plot");
      if (pl) pl.textContent = m.Plot || "-";
    };

    previewNode.addEventListener("mouseover", previewNode.show);
    previewNode.addEventListener("mouseout", previewNode.hide);
    return previewNode;
  }

  function updateLinkNodesWithMovieData(nodes, movie, onOver, onOut) {
    nodes.forEach((linkNode) => {
      if (!movie || movie.Error) return;
      linkNode.addEventListener("mouseover", () => onOver(movie));
      linkNode.addEventListener("mouseout", () => onOut(movie));

      const { isRemarkable, starredDegree, significancePercentage, rankingMetrics: { awards_text } } = assessMovieRankings(movie);

      const enh = document.createElement("a");
      enh.classList.add("movie-preview-enhancement");
      if (isRemarkable) enh.classList.add("remarkable");
      if (starredDegree) enh.classList.add(`starred-${starredDegree}`);
      enh.href = movie.imdbID ? `https://www.imdb.com/title/${movie.imdbID}` : "#";
      enh.target = "_blank";
      enh.title = `${movie.imdbVotes || "-"} votes - ${movie.Runtime || "-"} - Rated ${movie.Rated || "-"} - Awards: ${awards_text || "-"}`;
      enh.textContent = movie.imdbRating || "-";
      enh.style.opacity = String(significancePercentage);
      if (linkNode.parentNode) {
        linkNode.parentNode.insertBefore(enh, linkNode);
      }
    });
  }

  function applyImdbDomUpdate() {
    const nodes = document.querySelectorAll(
      "div.titleBar > div.title_wrapper > h1, td.titleColumn, div.lister-item-content .lister-item-header, div.title > a.title-grid, td.overview-top > h4 > a"
    );
    for (const n of nodes) {
      if (n.hasAttribute("with-download-link")) continue;
      n.setAttribute("with-download-link", "true");
      let movieTitle = n.textContent || "";
      movieTitle = movieTitle.replace(/\s+/g, " ").trim();
      const a = document.createElement("a");
      a.classList.add("imdb-download-link");
      a.href = getTorrentSearchURLFromMovieTitle(movieTitle);
      n.append(a);
    }
  }

  function getMovieTitleAndYearFromLinkNode(linkNode) {
    let text = (linkNode.textContent || "").toLowerCase();
    // strip punctuation and common quality tags
    text = text.replace(/[.,()]/g, " ")
               .replace(/\b(1080p|720p|2160p|480p|webrip|web-dl|bluray|bdrip|hdrip|x264|x265|hevc|aac|dts|yts|mkv|mp4)\b/ig, " ");
    const reYear = /\b(19|20)\d{2}\b/;
    const reSeries = /S[0-9]{2}E[0-9]{2}|[0-9]{1}x[0-9]{2}/i;

    const yM = text.match(reYear);
    if (yM) {
      const year = yM[0];
      const title = text.slice(0, yM.index).trim();
      if (title) return { title, year };
    } else if (reSeries.test(text)) {
      const idx = text.search(reSeries);
      const title = text.slice(0, idx).trim();
      if (title) return { title, year: "-" };
    }
    return { title: null, year: null };
  }

  function cleanupPorn(node) {
    const s = (node.innerHTML || "").toLowerCase();
    if (s.includes("xxx") || s.includes("porn")) node.outerHTML = "";
  }

  async function main() {
    appendStyle(STYLE);

    const hostname = window.location.hostname;
    if (isHostnameIMDB(hostname)) {
      applyImdbDomUpdate();
      return;
    }

    // Starter button
    const starter = document.createElement("form");
    starter.className = "movie-preview-starter";
    starter.insertAdjacentHTML("beforeend", `<button class="movie-preview-starter--button"> load IMDb info </button>`);
    starter.addEventListener("submit", async (e) => {
      e.preventDefault();

      const preview = initPreviewNode();
      const movies = new Map(); // key: hash, val: {title, year, hash, promise}

      // Scan all anchors for titles and magnets
      document.querySelectorAll("a").forEach((a) => {
        const href = a.getAttribute("href") || "";
        const hashMatch = /(^\/|^magnet\:\?xt\=urn\:btih\:)([a-zA-Z0-9]{40})/i.exec(href);
        cleanupPorn(a);

        if (hashMatch) {
          const hash = hashMatch[2].toUpperCase();
          const assist = document.createElement("div");
          assist.className = "torrent-download-links";
          assist.insertAdjacentHTML("beforeend", `
            <a target="_blank" href="https://torrage.info/torrent.php?h=${hash}" style="display:inline-block;padding:0 5px;background-color:#748DAB;text-align:center;">t1</a>
            <a target="_blank" href="https://www.btcache.me/torrent/${hash}" style="display:inline-block;padding:0 5px;background-color:#748DAB;text-align:center;">t2</a>
            <a target="_blank" href="https://itorrents.org/torrent/${hash}.torrent" style="display:inline-block;padding:0 5px;background-color:#748DAB;text-align:center;">t3</a>
          `);
          const parent = a.parentNode;
          if (parent && parent.nodeType === 1) {
            parent.classList.add("assisted-torrent-link");
            parent.append(assist);
          }
        }

        const { title, year } = getMovieTitleAndYearFromLinkNode(a);
        if (title && (year || year === "-")) {
          const h = getMovieHashFromTitleAndYear(title, year);
          a.classList.add("movie-preview");
          a.dataset.movieHash = h;
          if (!movies.has(h)) {
            movies.set(h, { title, year, hash: h, promise: loadMovie(title, year === "-" ? "" : year) });
          }
        }
      });

      // 1337x/detail fallback: if nothing parsed from anchors, try document title/h1
      if (movies.size === 0) {
        const fb = getBestTitleYearFallback();
        if (fb.title) {
          const h = getMovieHashFromTitleAndYear(fb.title, fb.year || "");
          movies.set(h, { title: fb.title, year: fb.year || "", hash: h, promise: loadMovie(fb.title, fb.year || "") });
        }
      }

      const onOver = (m) => { preview.setMovie(m); preview.show(); };
      const onOut = () => { preview.hide(); };

      for (const mv of movies.values()) {
        mv.promise.then((data) => {
          const nodes = document.querySelectorAll(`.movie-preview[data-movie-hash="${mv.hash}"]`);
          updateLinkNodesWithMovieData(Array.from(nodes), data, onOver, onOut);
        }).catch((err) => console.error("[IMDB info] movie error:", mv.hash, err));
      }

      starter.remove();
      // Unique list
      const list = document.createElement("div");
      list.className = "movie-preview-unique-list";
      for (const mv of movies.values()) {
        const row = document.createElement("div");
        const l = document.createElement("a");
        l.className = "movie-preview";
        l.dataset.movieHash = mv.hash;
        l.textContent = mv.hash;
        l.addEventListener("click", () => {
          document.querySelectorAll(".movie-preview").forEach(el => el.classList.remove("highlight"));
          document.querySelectorAll(`.movie-preview[data-movie-hash="${mv.hash}"]`).forEach(el => el.classList.add("highlight"));
        });
        mv.promise.then((md) => {
          if (md && md.Title) l.textContent = `${md.Title} (${md.Year || "-"})`;
        }).catch(() => {});
        row.append(l);
        list.append(row);
      }

      document.body.prepend(list);
      document.body.append(preview);
      window.addEventListener("beforeunload", () => {
        if (preview.hiding) clearTimeout(preview.hiding);
      });
    });

    document.body.prepend(starter);

    if (isHostnamePirateBay(hostname)) {
      const mainContent = document.querySelector("#main-content");
      if (mainContent) {
        mainContent.style.marginLeft = "0";
        mainContent.style.marginRight = "0";
      }
    }
  }

  main();
})();