您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
search torrents from multi sites and render back to the movie page
// ==UserScript== // @name 豆瓣电影种子搜索 douban_movie_torrent_search // @namespace https://github.com/ned42 // @version 0.3 // @description search torrents from multi sites and render back to the movie page // @author ned42 // @match https://movie.douban.com/subject/* // @grant GM.xmlHttpRequest // @grant GM_addStyle // @grant GM_registerMenuCommand // @connect bt4gprx.com // @license MIT // ==/UserScript== (async function () { 'use strict'; if (window.location.pathname.split('/')[3] !== '') return; // exclude subpage // 全局配置 const CONFIG = { CACHE: { PREFIX: 'torrent_search_cache', EXPIRY_TIME: 2 * 60 * 60 * 1000, // 2 hours }, UI: { ROOT_ID: 'torrent-list', TABLE: { MAX_VISIBLE_ROWS: 10, }, DOUBAN: { INTEREST_SECTION: '#interest_sect_level', }, }, NETWORK: { TIMEOUT: 5000, // 5 seconds }, }; // 缓存模块 const cacheModule = { CACHE_PREFIX: CONFIG.CACHE.PREFIX, CACHE_KEY: `${CONFIG.CACHE.PREFIX}-${window.location.pathname.split('/')[2]}`, // pageid _getCacheStore: function () { const cacheString = localStorage.getItem(this.CACHE_KEY); if (cacheString) { try { return JSON.parse(cacheString); } catch (e) { console.error(`Error parsing cache for '${this.CACHE_KEY}':`, e); } } this._saveCacheStore({}); return {}; }, _saveCacheStore: function (cacheStore) { try { cacheStore.expiryTimeStamp = new Date().getTime() + CONFIG.CACHE.EXPIRY_TIME; localStorage.setItem(this.CACHE_KEY, JSON.stringify(cacheStore)); } catch (e) { console.error(`Error saving cache for '${this.CACHE_KEY}':`, e); } }, cleanAllCache: function (force = false) { const isExpired = (timeStamp) => { new Date().getTime() > timeStamp; }; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key.startsWith(cacheModule.CACHE_PREFIX)) { let cacheStore; try { cacheStore = JSON.parse(localStorage.getItem(key)); } catch (e) { console.error(`Error parsing cache for '${key}':`, e); localStorage.removeItem(key); continue; } if (force || isExpired(cacheStore.expiryTimeStamp)) { localStorage.removeItem(key); } } } }, setCache: function (title, data) { const cacheStore = this._getCacheStore(); cacheStore[title] = data; this._saveCacheStore(cacheStore); }, getCache: function (title) { const cacheStore = this._getCacheStore(); if (Object.prototype.hasOwnProperty.call(cacheStore, title)) { return cacheStore[title]; } return null; }, }; // 通用模块 const utils = { /** * this certain function is basically where the whole original script idea came from * now is only used for linking imdb to douban.com * and in memory of RARBG.com, a torrent site which supports imdb for searching */ getIMDbId: function () { const IMDb_tag = Array.from(document.querySelectorAll('span.pl')).filter( (node) => node.textContent === 'IMDb:' )[0]; let imdbId = IMDb_tag ? IMDb_tag.nextSibling.data.trim() : ''; if (/(tt[0-9]*)/.test(imdbId)) { const imdbLink = `https://www.imdb.com/title/${imdbId}`; const imdbSpan = document.createElement('span'); imdbSpan.innerHTML = `${IMDb_tag.outerHTML} <span><a target="_blank" href="${imdbLink}">${imdbId}</a></span>`; IMDb_tag.nextSibling.remove(); IMDb_tag.replaceWith(imdbSpan); return imdbId; } }, // 获取电影主标题、副标题和年份,返回对象 getTitleInfo: function () { const title_text = document.querySelector('h1').innerText.trim(); const en_regex = /(\s[A-Za-z0-9\s'.:,&-]*)(?=\s\(\d{4}\))/; const year_regex = /\s\((\d{4})\)/; const yearMatch = title_text.match(year_regex); const titleYear = yearMatch ? yearMatch[1] : ''; // 主标题处理逻辑 - 优先使用英文作为主标题 "zh en (year)" let mainTitle = title_text.substring(0, title_text.search(' ')); let en_match = title_text.match(en_regex); if (en_match) { en_match = en_match[1].replace(/Season \d/, '').trim(); if (en_match.length) { mainTitle = en_match; // 如果有英文标题,使用英文作为主标题 } } // 副标题仅从ExtraTitle_tag获取,仅包含英文标题 const ExtraTitle_tag = Array.from(document.querySelectorAll('span.pl')).filter( (node) => node.textContent === '又名:' ); let extraTitles = []; let aliasArr = ExtraTitle_tag[0] ? ExtraTitle_tag[0].nextSibling.data.split('/') : null; if (aliasArr) { const alias_regex = /^[A-Za-z0-9\s'.:,&-]+$/; extraTitles = extraTitles.concat( aliasArr.map((t) => t.trim().replace(/\u200e/g, '')).filter((a) => alias_regex.test(a)) ); } const uniqueTitles = [...new Set([mainTitle, ...extraTitles])]; // 返回所有可用标题和年份 return { allTitles: uniqueTitles, yearForSearch: titleYear }; }, // 封装GM.xmlHttpRequest用于异步获取URL内容,处理超时和错误 gmFetch: async function (url) { return new Promise((resolve, reject) => { console.log('正在获取:', url); let settled = false; GM.xmlHttpRequest({ method: 'GET', timeout: CONFIG.NETWORK.TIMEOUT, url: url, onload: (response) => { if (settled) return; settled = true; // no check cos bt4g returns 404 for no result if (response.status) { try { const doc = new DOMParser().parseFromString(response.responseText, 'text/html'); resolve(doc); } catch (parseError) { console.error('解析HTML失败:', parseError, 'URL:', url); reject(new Error(`解析HTML失败: ${parseError.message}`)); } } else { console.error('HTTP请求失败:', response.status, response.statusText, 'URL:', url); reject(new Error(`HTTP请求失败: ${response.status} ${response.statusText}`)); } }, onerror: (error) => { if (settled) return; settled = true; console.error('GM.xmlHttpRequest 错误:', error, 'URL:', url); reject(new Error(`GM.xmlHttpRequest 错误: ${error.statusText || '未知错误'}`)); }, ontimeout: () => { if (settled) return; settled = true; console.warn('GM.xmlHttpRequest 请求超时:', url); resolve(new Error(`GM.xmlHttpRequest 请求超时: ${url}`)); }, }); }); }, // 从指定站点配置和标题获取种子数据,处理各种错误情况并返回结果或错误对象数组 _fetchSiteData: async function (siteConfig, searchKey) { const fullUrl = siteConfig.host + siteConfig.url(searchKey); try { const response = await this.gmFetch(fullUrl); if (response instanceof Error) { return [ { name: `[网络错误] ${siteConfig.host} - ${searchKey} (点击查看详情)`, link: fullUrl, isError: true, errorType: 'gmFetchResolvedError', }, ]; } const table = response.querySelector(siteConfig.torrent_list); // 获取种子列表的DOM元素 const info = []; // 存储解析出的种子信息 if (table) { let rows = Array.from(table.children); if (siteConfig.torrent_list.includes('table')) rows = rows.slice(1); // 跳过表头 rows.forEach((row) => { const rowData = { name: row.querySelector(siteConfig.info.name)?.textContent.trim(), link: siteConfig.host + row.querySelector(siteConfig.info.link)?.getAttribute('href'), size: row.querySelector(siteConfig.info.size)?.textContent.trim(), seeder: row.querySelector(siteConfig.info.upload)?.textContent.trim(), leecher: row.querySelector(siteConfig.info.download)?.textContent.trim(), isError: false, }; if (rowData.size && rowData.name) info.push(rowData); // 确保关键信息存在 }); } else { console.info(`站点 ${siteConfig.host} 未找到种子列表 标题: ${searchKey}, URL: ${fullUrl}`); throw new Error(`未找到种子`); } return info; } catch (error) { // 捕获 gmFetch reject 的错误 (如HTTP错误、HTML解析错误) return [ { name: `[获取异常] ${siteConfig.host} - ${searchKey}: ${error.message}`, link: fullUrl, isError: true, errorType: 'fetchException', }, ]; } }, // 准备列表用的标题数据,获取缓存或发起新检索 _prepareTitleData: async function (title) { render.renderMessage(`正在搜索 ${title} 的种子...`); let validResults = cacheModule.getCache(title); if (validResults) { return render._renderArrayResults(title); // 直接渲染缓存结果 } else { if (appState.titlePending[title] == true) return; // 防止重复请求 validResults = []; // init on new title appState.titlePending[title] = true; // 遍历所有站点配置 const sitePromises = SEARCH_SITES_CONFIGS.map(async (site) => { const siteResults = await utils._fetchSiteData(site, title + ' ' + appState.titleYear); validResults = validResults.concat(siteResults.slice(0, 10)); // 合并各站前10个结果 cacheModule.setCache(title, validResults); render._renderArrayResults(title); // 每个站点返回后都渲染 }); await Promise.all(sitePromises); appState.titlePending[title] = false; validResults = validResults.filter((a) => !a.isError); cacheModule.setCache(title, validResults); } }, }; // 页面渲染模块 const render = { uiElements: { rootContainer: null, // <div id="torrent-list"></div> menuContainer: null, // #torrent-list > h2 tableContainer: null, // <div class="res_table_wrap"></div> }, // 初始化界面菜单 initializeContainer: function () { const existingList = document.querySelector(`#${CONFIG.UI.ROOT_ID}`); if (existingList) existingList.remove(); // 创建根容器 this.uiElements.rootContainer = document.createElement('div'); this.uiElements.rootContainer.id = CONFIG.UI.ROOT_ID; // 创建菜单容器 this.uiElements.menuContainer = document.createElement('h2'); let titleSwitchHtml = ''; if (appState.allTitles.length > 1) { titleSwitchHtml = `<span class="pl"> ( `; appState.allTitles.forEach((title, i) => { const isActive = title === appState.currentTitle ? ' active' : ''; const separator = i > 0 ? ' / ' : ''; titleSwitchHtml += `${separator}<a href="#" data-title-name="${title}" class="title-switch${isActive}">${title}</a>`; }); titleSwitchHtml += ' ) </span>'; } this.uiElements.menuContainer.innerHTML = `<i>可用资源</i> · · · · · ·${titleSwitchHtml}`; this._bindTitleSwitchEvents(); // 绑定标题切换事件 // 创建表格容器 this.uiElements.tableContainer = document.createElement('div'); this.uiElements.tableContainer.className = 'res_table_wrap'; this._bindTableMaskEvents(); // 绑定表格mask监听 // 组装容器,插入页面 this.uiElements.rootContainer.appendChild(this.uiElements.menuContainer); this.uiElements.rootContainer.appendChild(this.uiElements.tableContainer); document .querySelector(CONFIG.UI.DOUBAN.INTEREST_SECTION) .insertAdjacentElement('beforebegin', this.uiElements.rootContainer); utils._prepareTitleData(appState.currentTitle); // 初始化后查询第一个标题的数据 }, // 渲染消息 renderMessage: function (message, isLoading = true) { const anchorId = isLoading ? 'loading' : ''; const messageTypeClass = isLoading ? 'loading-message' : 'final-message'; const anchorClass = `lnk-sharing ${messageTypeClass}`; this.uiElements.tableContainer.innerHTML = `<a class="${anchorClass}" ${anchorId ? `id="${anchorId}"` : ''}>${message}</a>`; if (!isLoading) { const finalmessageElement = this.uiElements.tableContainer.querySelector( `.${messageTypeClass.replace(' ', '.')}` ); // 点击后重置缓存 finalmessageElement.onclick = () => { cacheModule.setCache(appState.currentTitle, null); utils._prepareTitleData(appState.currentTitle); }; } }, // 渲染结果列表 _renderArrayResults: function (title) { if (title != appState.currentTitle) return; this.uiElements.tableContainer.replaceChildren(); // 错误结果后置 const resList = cacheModule.getCache(title); const errorResults = resList.filter((node) => node.isError); const validResults = resList.filter((node) => !node.isError); const sortedResList = validResults.concat(errorResults); // 构建表格内容行,处理特殊字符 const formatNumberWithK = (numb) => (Number(numb) >= 1000 ? (numb / 1000).toFixed(1) + 'k' : numb); const nodeNameFormatter = (str) => (str ? str.replace('【', '[').replace('】', ']').normalize('NFKC') : ''); const allRowsArr = sortedResList.map((node) => { if (node.isError) { return `<td>⚠️</td><td><a class="error-item" href="${node.link}" target="_blank">${node.name}</a></td><td></td>`; } else { node.seeder = formatNumberWithK(node.seeder); node.leecher = formatNumberWithK(node.leecher); node.name = nodeNameFormatter(node.name); return `<td>${node.seeder || '0'}-${node.leecher || '0'}</td><td><a target="_blank" href="${node.link}" title="${node.name}">${node.name}</a></td><td>${node.size}</td>`; } }); // 表格遮罩处理,默认只显示前10条 const showCount = CONFIG.UI.TABLE.MAX_VISIBLE_ROWS; const totalRows = allRowsArr.length; if (totalRows > 0) { let tableHtml = '<table class="res_table">'; allRowsArr.forEach((rowData, index) => { const isHidden = index >= showCount ? ' hidden-row' : ''; tableHtml += `<tr class="table-row${isHidden}">${rowData}</tr>`; }); tableHtml += '</table>'; let maskHtml = ''; if (totalRows > showCount) { tableHtml = tableHtml.replace('class="res_table"', 'class="res_table res_table-collapsed"'); maskHtml = '<div class="res_table_mask"></div>'; } this.uiElements.tableContainer.innerHTML = `${tableHtml}${maskHtml}`; } else { if (!appState.titlePending[title]) { this.renderMessage('无有效结果或错误信息可供显示', false); } } }, // 菜单标题切换事件监听 _bindTitleSwitchEvents: function () { this.uiElements.menuContainer.addEventListener('click', (e) => { const titleSwitchElement = e.target.closest('.title-switch'); if (!titleSwitchElement) return; e.preventDefault(); const titleName = titleSwitchElement.dataset.titleName; if (titleName === appState.currentTitle) return; // 更新 UI 状态 this.uiElements.menuContainer .querySelectorAll('.title-switch') .forEach((link) => link.classList.remove('active')); titleSwitchElement.classList.add('active'); // 更新当前标题及内容 appState.setCurrentTitle(titleName); utils._prepareTitleData(titleName); }); }, // 表格mask事件监听 _bindTableMaskEvents: function () { this.uiElements.tableContainer.addEventListener( 'mouseenter', function (e) { const mask = e.target.closest('.res_table_mask'); if (!mask) return; const table = mask.parentNode.childNodes[0]; const hiddenRows = table.querySelectorAll('.hidden-row'); hiddenRows.forEach((row) => (row.style.display = 'table-row')); table.classList.replace('res_table-collapsed', 'res_table-expanded'); mask.style.opacity = '0'; mask.style.pointerEvents = 'none'; }, true ); this.uiElements.tableContainer.addEventListener( 'mouseleave', function (e) { const wrap = e.target.closest('.res_table_wrap'); if (!wrap) return; if (e.relatedTarget && wrap.contains(e.relatedTarget)) { return; // 鼠标仍在table内,不执行收起 } const table = wrap.querySelector('.res_table'); const mask = wrap.querySelector('.res_table_mask'); if (table && mask && table.classList.contains('res_table-expanded')) { const hiddenRows = table.querySelectorAll('.hidden-row'); hiddenRows.forEach((row) => (row.style.display = 'none')); table.classList.replace('res_table-expanded', 'res_table-collapsed'); mask.style.opacity = '1'; mask.style.pointerEvents = 'auto'; } }, true ); }, }; // 检索站点配置 const SEARCH_SITES_CONFIGS = [ { host: 'https://byr.pt/', url: (title) => `torrents.php?search=${encodeURIComponent(title)}&cat408=1&cat401=1&incldead=0&spstate=0&inclbookmarked=0&search_area=0&search_mode=0&sort=7&type=desc`, torrent_list: 'table.torrents tbody', info: { name: 'td.rowfollow > table.torrentname a[title]', link: 'td.rowfollow > table.torrentname a[title]', size: 'td:nth-child(6)', upload: 'td:nth-child(7)', download: 'td:nth-child(8)', }, }, { host: 'https://pt.btschool.club/', url: (title) => `torrents.php?incldead=1&spstate=0&inclbookmarked=0&search=${encodeURIComponent(title)}&search_area=0&search_mode=0&sort=7&type=desc`, torrent_list: 'table.torrents tbody', info: { name: 'td.rowfollow > table.torrentname a[title]', link: 'td.rowfollow > table.torrentname a[title]', size: 'td:nth-child(5)', upload: 'td:nth-child(6) a', download: 'td:nth-child(7)', }, }, { host: 'https://bt4gprx.com', url: (title) => `/search?q=${encodeURIComponent(title)}&orderby=seeders&p=1`, torrent_list: '.list-group', // css tags how the original site display torrent info: { name: 'a[title]', link: 'a[title]', size: 'b.cpill', upload: '#seeders', download: '#leechers', }, }, ]; // 页面式样 GM_addStyle(` #torrent-list { display: inline-block; width: 100%; overflow: hidden; } #torrent-list h2 { margin: 12px 0 12px 0; } .title-switch.active { color: #111; background: none; cursor: auto; } .res_table_wrap { position: relative; width: 95%; } .res_table { width: 100%; table-layout: fixed; } .res_table tr.table-row.hidden-row { display: none; } .res_table td { padding: 3px 2px; text-align: left; } .res_table td:first-child, .res_table td:last-child { width: 65px; white-space: nowrap; text-align: center; color: #666666; } .res_table td:last-child { text-align: right; } .res_table td:nth-child(2) { /* 自动宽度 */ } .res_table td a { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .res_table td a.error-item { color: #888888; text-decoration: underline; } .res_table_mask { position: absolute; left: 0; right: 0; bottom: 0; height: 20%; background: linear-gradient( to bottom, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.85) 100% ); pointer-events: auto; opacity: 1; transition: opacity 0.3s; } .res_table_wrap > a.lnk-sharing { display: block; padding: 5px; text-align: center; color: #888888; cursor: pointer; } @keyframes hourglass { 0% { transform: rotate(0deg); } 40% { transform: rotate(180deg); } 50% { transform: rotate(180deg); } 90% { transform: rotate(360deg); } 100% { transform: rotate(360deg); } } .res_table_wrap > a#loading::after { /* 加载状态图标 */ content: " ⏳"; display: inline-block; margin-left: 5px; animation: hourglass 2s ease-in-out infinite; } .res_table_wrap > a.final-message::after { /* 最终消息状态图标 */ content: " 😢"; display: inline-block; margin-left: 5px; } `); // 主逻辑 const appState = { currentTitle: null, titleYear: null, allTitles: [], titlePending: {}, init() { const { allTitles, yearForSearch } = utils.getTitleInfo(); this.allTitles = allTitles; this.titleYear = yearForSearch; this.currentTitle = this.allTitles[0]; this.allTitles.forEach((title) => { this.titlePending[title] = false; }); utils.getIMDbId(); // eggs }, setCurrentTitle(title) { this.currentTitle = title; }, }; appState.init(); cacheModule.cleanAllCache(); // 清理所有过期缓存 render.initializeContainer(); // 初始化菜单 // 注册(不可用)GM菜单,提供手动清除缓存功能 GM_registerMenuCommand( '清理种子缓存', function () { cacheModule.cleanAllCache(true); render.renderMessage('缓存已清理,点击进行重试', false); }, 'c' ); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址