// ==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()
})();