您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
该插件主要用于抓取指定页面(即 "torrents.php")中的免费种子信息,并将其按照剩余时间从短到长排序后,以表格形式呈现给用户。用户可以一键复制所有展示种子的链接,同时具备筛选功能,允许用户设定自定义时间阈值,仅复制剩余时间超过该阈值的种子链接。此外,插件还支持添加自定义URL参数以扩展功能或满足个性化需求。
// ==UserScript== // @name Get Free Torrents // @namespace http://tampermonkey.net/ // @version 1.0.9 // @description 该插件主要用于抓取指定页面(即 "torrents.php")中的免费种子信息,并将其按照剩余时间从短到长排序后,以表格形式呈现给用户。用户可以一键复制所有展示种子的链接,同时具备筛选功能,允许用户设定自定义时间阈值,仅复制剩余时间超过该阈值的种子链接。此外,插件还支持添加自定义URL参数以扩展功能或满足个性化需求。 // @author 飞天小猪 // @match http*://*/*torrents*.php* // @match http*://kp.m-team.cc/* // @match http*://*/*special*.php* // @match https://hhanclub.top/rescue.php* // @icon https://gongjux.com/files/3/4453uhm5937m/32/favicon.ico // @grant none // @require https://greasyfork.org/scripts/453166-jquery/code/jquery.js?version=1105525 // @require https://greasyfork.org/scripts/28502-jquery-ui-v1-11-4/code/jQuery%20UI%20-%20v1114.js?version=187735 // @license MIT // ==/UserScript== // ----------------规则---------------- const specialRules = [ { site: 'https://hhanclub.top', torrentMethod: () => $('.torrent-table-sub-info'), rowMethod: (item) => $(item).find('.torrent-table-for-spider-info'), urlMethod: (item) => { return normalizeUrl(location.origin + '/' + $(item.parent().find('a[href*="download.php"]')[0]).attr('href')) }, freeMethod: (item) => item.find('[class*="free"]').length > 0, titleMethod: (item) => $(item.find('a[class*="torrent-info-text-name"]')[0]).text(), sizeMethod: (item) => { const sizeStr = $($(item).find('.torrent-info-text-size')[0]).text().trim().split(' ').join('') const size = convertToBytes(sizeStr) return { sizeStr, size } }, timeMethod: (item) => { const dateTimeRegex = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/; const spansWithTitle = item.find('span[title]'); const spanArr = spansWithTitle.filter(function () { return dateTimeRegex.test($(this).attr('title')); }).get() let time = '' if (spanArr.length > 0) { time = $(spanArr[0]).attr('title') } else { time = 'infinite' } return time }, dlStateMethod: (item) => { // console.log(item, 'dlStateMethod') const seeding = item.find('div[title^="seeding "]') const activity = item.find('div[title^="activity "]') const inactivity = item.find('div[title^="inactivity "]') if (seeding.length || activity.length || inactivity.length) { return 'isDownloaded' } else { return 'unknown' } }, }, { site: 'default', torrentMethod: () => $('.torrents>tbody>tr'), rowMethod: (item) => $(item).find('table'), urlMethod: (item) => normalizeUrl(location.origin + '/' + $(item.find('a[href*="download.php"]')[0]).attr('href')), freeMethod: (item) => item.find('[class*="free"]').length > 0, titleMethod: (item) => $(item.find('a[href*="details.php"]')[0]).attr('title'), sizeMethod: (item) => { const sizeUnit = `td:contains('KB'),td:contains('MB'),td:contains('GB'),td:contains('TB')` const sizeTdArr = $(item).find(sizeUnit).filter(function () { const text = $(this).text().trim() const sizeReg = /^[-+]?[0-9]*\.?[0-9]+[KMGTP]B$/ return sizeReg.test(text) }) const sizeStr = $(sizeTdArr[0]).text().trim() const size = convertToBytes(sizeStr) return { sizeStr, size } }, timeMethod: (item) => { const dateTimeRegex = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/; const spansWithTitle = item.find('span[title]'); const spanArr = spansWithTitle.filter(function () { return dateTimeRegex.test($(this).attr('title')); }).get() let time = '' if (spanArr.length > 0) { time = $(spanArr[0]).attr('title') } else { time = 'infinite' } return time }, dlStateMethod: (item) => { const seeding = item.find('div[title^="seeding "]') const activity = item.find('div[title^="activity "]') const inactivity = item.find('div[title^="inactivity "]') if (seeding.length || activity.length || inactivity.length) { return 'isDownloaded' } else { return 'unknown' } } } ] // ----------------初始化数据---------------- const originData = [] let filterData = [] const queryParams = { isFree: '1', sortBy: '1', sort: '1', dlState: 'unknown' } // 调用函数并进行操作 function normalizeUrl(url) { const httpPattern = /^(https?|ftp):\/\/[^/]+/; // 匹配http、https或ftp开头的URL部分 const matchedUrl = url.match(httpPattern); if (matchedUrl) { // 获取URL部分之后的子串 const remainingStr = url.slice(matchedUrl[0].length); // 替换剩余部分中的双斜杠为单斜杠 const fixedRemainingStr = remainingStr.replace(/\/{2,}/g, '/'); // 将处理过的剩余部分与原始URL部分拼接 return matchedUrl[0] + fixedRemainingStr; } else { // 如果字符串不以http(s)://开头,直接替换整个字符串中的双斜杠为单斜杠 return url.replace(/\/{2,}/g, '/'); } } // ----------------工具方法---------------- // 格式化文件大小 function convertToBytes(sizeString) { const units = { 'B': 1, 'KB': 1024, 'MB': 1024 * 1024, 'GB': 1024 * 1024 * 1024, 'TB': 1024 * 1024 * 1024 * 1024 }; // 使用正则表达式匹配数字和单位 const match = sizeString.match(/^(\d+(\.\d+)?)([A-Za-z]{2,3})$/); if (!match) { throw new Error('Invalid size format'); } // 提取数字和单位 const [_, numberStr, , unit] = match; const number = parseFloat(numberStr); const unitInBytes = units[unit.toUpperCase()] || units['B']; // 转换为Bytes return number * unitInBytes; } // 格式化时间 function formatTime(timestemp) { const now = new Date().getTime() let sub = timestemp - now let hours = Math.floor(sub / (1000 * 60 * 60)); // 计算小时数 sub %= (1000 * 60 * 60); // 剩余毫秒数转为分钟计算 let minutes = Math.floor(sub / (1000 * 60)); // 计算分钟数 sub %= (1000 * 60); // 剩余毫秒数转为秒计算 let seconds = Math.floor(sub / 1000); // 计算秒数 const hoursStr = hours > 0 ? `${hours}小时` : '' const minutesStr = minutes > 0 ? `${minutes}分` : '' const secondsStr = seconds > 0 ? `${seconds}秒` : '' const restTime = `${hoursStr}${minutesStr}` const color = '' return { restTime, color } } // 复制内容至剪贴板 async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); } catch (err) { console.error('Failed to copy to clipboard: ', err); } } // ----------------绑定事件---------------- // 全屏or退出全屏 function screenModal() { $('#fpModal').hasClass('full-screen') ? $('#fpModal').removeClass('full-screen') : $('#fpModal').addClass('full-screen') } // 关闭模态窗 function closeModal() { console.log('-- getFreeTorrents closeModal --') $('#fpMask').hide() $('#fpModal').hide() originData.length = 0 filterData.length = 0 } // 打开模态窗 function showModal() { console.log('-- getFreeTorrents showModal --') // 整理数据 cleanData() // 根据初始化参数显示数据 setData(queryParams) $('#fpMask').css('display', 'flex').show() $('#fpModal').show() } // 免费下拉变化事件 function freeChange() { console.log('-- getFreeTorrents freeChange --') const isFree = $('#fpSelectorFree').val() const sortBy = $('#fpSelectorSortBy').val() const sort = $('#fpSelectorSort').val() const dlState = $('#fpSelectorDlState').val() setData({ isFree, sortBy, sort, dlState }) } // 排序依据下拉变化事件 function sortByChange() { console.log('-- getFreeTorrents sortByChange --') const isFree = $('#fpSelectorFree').val() const sortBy = $('#fpSelectorSortBy').val() const sort = $('#fpSelectorSort').val() const dlState = $('#fpSelectorDlState').val() setData({ isFree, sortBy, sort, dlState }) } // 排序顺序下拉变化事件 function sortChange() { console.log('-- getFreeTorrents sortChange --') const isFree = $('#fpSelectorFree').val() const sortBy = $('#fpSelectorSortBy').val() const sort = $('#fpSelectorSort').val() const dlState = $('#fpSelectorDlState').val() setData({ isFree, sortBy, sort, dlState }) } // 是否下载过下拉变化事件 function dlStateChange() { console.log('-- getFreeTorrents dlStateChange --') const isFree = $('#fpSelectorFree').val() const sortBy = $('#fpSelectorSortBy').val() const sort = $('#fpSelectorSort').val() const dlState = $('#fpSelectorDlState').val() console.log(dlState) setData({ isFree, sortBy, sort, dlState }) } // 复制Cookie function copyCookie() { console.log('-- getFreeTorrents copyCookie --') const cookie = document.cookie console.log(cookie) if (cookie) { copyToClipboard(cookie) alert('复制成功') } else { alert('Cookie 为空') } } // 复制种子链接 function copyTorrent() { console.log('-- getFreeTorrents copyTorrent --') const timelimit = $('#fpTimeLimit').val() || 0 const params = $('#fpParams').val() let suffix = '' if (params) { suffix = '&' + params.split('\n').join('&') } let torrentstr = '' console.log(filterData) const limitData = filterData.filter(i => { const now = new Date().getTime() return ((i.timestemp - now) / (1000 * 60 * 60)) > parseInt(timelimit) || !i.timestemp }) console.log(limitData.length) limitData.forEach(i => { torrentstr += `${i.downloadUrl}${suffix}\n` }) copyToClipboard(torrentstr) alert(`成功复制 ${limitData.length} 个种子`) } // ----------------数据方法---------------- function cleanData() { console.log('-- getFreeTorrents cleanData --') const siteInfo = specialRules.find(i => i.site === location.origin) || specialRules.find(i => i.site === 'default') console.log('-- set siteInfo --' + siteInfo.site) // 获取所有行信息 // 获取行信息种的种子名称、种子id、下载地址、下载进度、是否为免费种、剩余免费时间 const temp = siteInfo.torrentMethod() temp.each(function () { const res = siteInfo.rowMethod(this) // 判断是否为有效的行数据 if (res.length >= 1) { const el = res[0] // console.log(el) const isFree = siteInfo.freeMethod($(el)) const temp = { title: siteInfo.titleMethod($(el)), isFree, downloadUrl: siteInfo.urlMethod($(el)), } const that = this if (isFree) { temp.time = siteInfo.timeMethod($(el)) } else { temp.time = null } if (temp.time && temp.time !== 'infinite') { temp.timestemp = new Date(temp.time).getTime() const timeInfo = formatTime(temp.timestemp) temp.restTime = timeInfo.restTime temp.color = timeInfo.color } else if (temp.time === 'infinite') { temp.timestemp = Infinity temp.color = 'green' } else { temp.timestemp = null temp.color = 'red' } temp.dlState = siteInfo.dlStateMethod($(el)) const sizeInfo = siteInfo.sizeMethod(that) temp.sizeStr = sizeInfo.sizeStr temp.size = sizeInfo.size originData.push(temp) } }) const freeLength = originData.filter(i => i.isFree).length const undownloadLength = originData.filter(i => i.dlState === 'unknown').length const lessThen12 = originData.filter(i => { const now = new Date().getTime() return i.isFree && ((i.timestemp - now) / (1000 * 60 * 60) < 12) }).length const lessThen24 = originData.filter(i => { const now = new Date().getTime() return i.isFree && ((i.timestemp - now) / (1000 * 60 * 60) < 24) }).length - lessThen12 const infoDomStr = ` 当前页面共有种子:<span style="margin-right: 8px">${originData.length}个</span> <span style="margin-right: 8px;background-color:#f3f0ff">未下载种子:${undownloadLength}个</span> <span style="margin-right: 8px;color: #67C23A;">免费种子:${freeLength}个</span> <span style="margin-right: 8px;color: #F56C6C;">免费种子<12h:${lessThen12}个</span> <span style="color: #E6A23C;">免费种子<24h:${lessThen24}个</span>` $('#fpInfo').html(infoDomStr) } // 获取展示数据 function setData(queryParams) { const { isFree, sortBy, sort, dlState, timeLimit } = queryParams const freeFilterData = originData.filter(i => isFree === '1' ? i.isFree : isFree === '0' ? !i.isFree : true) const dlStateMap = { all: (i) => true, unknown: (i) => i.dlState === 'unknown', isDownloaded: (i) => i.dlState !== 'unknown' } const dlStateFilterData = freeFilterData.filter(dlStateMap[dlState]) const sortByMap = { '1': (a, b) => { let aTime = a.timestemp === 'infinite' ? Infinity : a.timestemp let bTime = b.timestemp === 'infinite' ? Infinity : b.timestemp return aTime - bTime }, '2': (a, b) => a.size - b.size } const sortData = dlStateFilterData.sort(sortByMap[sortBy]) const desSortData = Array.from(new Set(sortData)).reverse() const result = sort === '1' ? sortData : desSortData filterData = result let domStr = `` result.forEach((i, index) => { const now = new Date().getTime() let color = (i.isFree && (i.timestemp - now) / (1000 * 60 * 60) < 24) ? '#E6A23C' : '#333' color = (i.isFree && (i.timestemp - now) / (1000 * 60 * 60) < 12) ? '#F56C6C' : color domStr += ` <tr class="${i.dlState === 'unknown' ? 'fp-undownload' : ''}"> <td style="color:${color}">${index + 1}</td> <td style="color:${color}">${i.title}</td> <td style="color:${color}">${i.isFree ? i.restTime : ''}</td> <td style="color:${color}">${i.sizeStr}</td> <td style="color:${color}">${i.dlState === 'unknown' ? '未下载' : '下载过'}</td> <td style="color:${color}">${i.downloadUrl}</td> </tr>` }) $('#fpTableBody').html(domStr) } // 注册事件 function bindEvent() { $('#fpMenuButton').bind('click', showModal) $('#fpClose').bind('click', closeModal) $('#fpScreen').bind('click', screenModal) $('#fpSelectorFree').bind('change', freeChange) $('#fpSelectorSortBy').bind('change', sortByChange) $('#fpSelectorSort').bind('change', sortChange) $('#fpSelectorDlState').bind('change', dlStateChange) $('#fpCopyCookie').bind('click', copyCookie) $('#fpCopyTorrent').bind('click', copyTorrent) } // ----------------初始化页面---------------- // 初始化样式 function initStyle() { const style = ` <style> .fp-button { color: #fff; background-color: #9278ff; border: none; padding: 4px 10px; border-radius: 4px; cursor: pointer; } .fp-menu-button { position: fixed; right: 20px; top: 140px; z-index: 1000001; opacity: .3; transition: opacity .3s; } .fp-menu-button:hover { opacity: 1; } .fp-modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1000002; background-color: rgba(0, 0, 0, .5); display: none; justify-content: center; align-items: center; } .fp-modal { height: 80vh; width: 70vw; min-width: 800px; max-width: 1400px; min-height: 600px; max-width: 1200px; background-color: #fff; box-sizing: border-box; border-radius: 8px; overflow: hidden; } .fp-modal.full-screen { height: 100vh; width: 100vw; } .fp-modal-header { width: 100%; background-color: #9278ff; padding: 8px; display: flex; justify-content: space-between; box-sizing: border-box; color: #fff; font-size: 12px; } .fp-modal-header-title { display: flex; align-items: center; } .fp-icon-wrap { display: flex; } .fp-modal-header-close { cursor: pointer; font-size: 20px; margin-left: 12px; } .fp-modal-content { padding: 8px; font-size: 12px; height: calc(100% - 60px); overflow: auto; } .fp-modal-control { display: flex; } .fp-select { display: flex; flex-wrap: nowrap; } .fp-select-item { display: flex; align-items: center; margin-right: 8px; } .fp-select-dom select { border: 1px solid #9278ff; width: 100px; padding: 2px 8px; border-radius: 4px; outline: none; } .fp-time-control { margin-top: 12px; display: flex; justify-content: space-between; align-items: center; } .fp-time-limit input { border: 1px solid #9278ff; width: 140px; padding: 2px 8px; border-radius: 4px; outline: none; padding: 4px; } .fp-info { margin-left: 8px; } .fp-btn-group { display: flex; button { margin-left: 8px; } } .fp-params-wrap { margin: 12px 0; width: 100%; height: 200px; box-sizing: border-box; padding: 0px; } .fp-params { box-sizing: border-box; height: 100%; width: 100%; border: 1px solid #9278ff; border-radius: 4px; padding: 4px 8px; outline: none; } .fp-table { width: 100%; border-collapse: collapse; border: 1px solid #9278ff; } .fp-table, .fp-table td, .fp-table th { border: 1px solid #9278ff; background-color: #fff; } .fp-table td { padding: 4px; } .fp-undownload td { background-color: #f3f0ff !important; } </style> ` $('head').append(style) } // 初始化DOM元素 function initDom() { const dom = ` <button id="fpMenuButton" class="fp-button fp-menu-button">🐷 获取信息 🐷</button> <div id="fpMask" class="fp-modal-mask"> <div id="fpModal" class="fp-modal"> <div class="fp-modal-header"> <div class="fp-modal-header-title">Get Free Torrents By 飞天小猪</div> <div class="fp-icon-wrap"> <div id="fpScreen" class="fp-modal-header-close" title="全屏/退出全屏">▣</div> <div id="fpClose" class="fp-modal-header-close" title="关闭">✖</div> </div> </div> <div class="fp-modal-content"> <div class="fp-modal-control"> <div class="fp-select"> <div class="fp-select-item"> <div class="fp-select-label">种子促销:</div> <div class="fp-select-dom"> <select name="" id="fpSelectorFree" value="0"> <option value="1">是</option> <option value="0">否</option> <option value="all">全部</option> </select> </div> </div> <div class="fp-select-item"> <div class="fp-select-label">排序依据:</div> <div class="fp-select-dom"> <select name="" id="fpSelectorSortBy" value="0"> <option value="1">剩余时间</option> <option value="2">种子体积</option> </select> </div> </div> <div class="fp-select-item"> <div class="fp-select-label">排序方式:</div> <div class="fp-select-dom"> <select name="" id="fpSelectorSort" value=""> <option value="1">正序</option> <option value="2">倒序</option> </select> </div> </div> <div class="fp-select-item"> <div class="fp-select-label">下载状态:</div> <div class="fp-select-dom"> <select name="" id="fpSelectorDlState" value=""> <option value="unknown">未下载</option> <option value="isDownloaded">下载过</option> <option value="all">全部</option> </select> </div> </div> </div> </div> <div class="fp-time-control"> <div class="fp-time-limit"> <input id="fpTimeLimit" type="text" placeholder="剩余时间>?(hour)"> </div> <div id="fpInfo" class="fp-info"></div> <div class="fp-btn-group"> <button id="fpCopyCookie" class="fp-button">复制Cookie</button> <button id="fpCopyTorrent" class="fp-button">复制种子链接</button> </div> </div> <div id="fpParamsWrap" class="fp-params-wrap"> <textarea name="" id="fpParams" class="fp-params" placeholder="请输入自定义参数 1行一条,格式为 key=value"></textarea> </div> <div class="fp-table-wrap"> <table id="fpTable" class="fp-table"> <thead> <th>序号</th> <th>种子名称</th> <th>免费剩余时间</th> <th>体积</th> <th>下载状态</th> <th>下载链接</th> </thead> <tbody id="fpTableBody"> </tbody> </table> </div> </div> </div> </div>` $('body').append(dom) } (function() { 'use strict'; initStyle() initDom() bindEvent() })();