// ==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 // @downloadURL https://update.greasyfork.icu/scripts/490514/Get%20Free%20Torrents.user.js // @updateURL https://update.greasyfork.icu/scripts/490514/Get%20Free%20Torrents.meta.js // ==/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 = ` 当前页面共有种子:${originData.length}个 未下载种子:${undownloadLength}个 免费种子:${freeLength}个 免费种子<12h:${lessThen12}个 免费种子<24h:${lessThen24}个` $('#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 += `