您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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或关注我们的公众号极客氢云获取最新地址