// ==UserScript==
// @name Get Free Torrents
// @namespace http://tampermonkey.net/
// @version 1.0.4
// @description 该插件主要用于抓取指定页面(即 "torrents.php")中的免费种子信息,并将其按照剩余时间从短到长排序后,以表格形式呈现给用户。用户可以一键复制所有展示种子的链接,同时具备筛选功能,允许用户设定自定义时间阈值,仅复制剩余时间超过该阈值的种子链接。此外,插件还支持添加自定义URL参数以扩展功能或满足个性化需求。
// @author 飞天小猪
// @match http*://*/*torrents*.php*
// @match http*://kp.m-team.cc/*
// @icon https://gongjux.com/files/3/4453uhm5937m/32/favicon.ico
// @grant none
// @require http://greasyfork.icu/scripts/453166-jquery/code/jquery.js?version=1105525
// @require http://greasyfork.icu/scripts/28502-jquery-ui-v1-11-4/code/jQuery%20UI%20-%20v1114.js?version=187735
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const specialRules = [
{
site: 'https://hhanclub.top',
torrentMethod: (item) => $(item).closest('.torrent-table-sub-info'),
titleMethod: ($tdElement) => $($tdElement.find('a[class*="torrent-info-text-name"]')[0]).text(),
timeMethod: ($tdElement) => {
const dateTimeRegex = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/
const $spansWithTitle = $tdElement.find('span[title]');
return $spansWithTitle.filter(function () {
return dateTimeRegex.test($(this).attr('title'));
}).get().length ? $($spansWithTitle.filter(function () {
return dateTimeRegex.test($(this).attr('title'));
}).get()[0]).attr('title') : 'infinite'
},
urlMethod: ($tdElement) => normalizeUrl(location.origin + '/' + $($tdElement.find('a[href*="download.php"]')[0]).attr('href')),
freeSelector: () => $('[class*="free"]')
},
{
site: 'https://kp.m-team.cc',
torrentMethod: (item) => $(item).closest('tr'),
titleMethod: ($tdElement) => $($tdElement.find('strong')[0]).text(),
timeMethod: ($tdElement) => {
const freeTagParent = $tdElement.find('span.ant-tag:contains("Free")').parent().first()
if (freeTagParent.is('span') && freeTagParent.attr('title').includes('促銷')) {
const title = freeTagParent.attr('title');
const deadlinePattern = /(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/;
const match = title.match(deadlinePattern);
if (match) {
const deadline = match[1]
return deadline
} else {
return 'infinite'
}
} else {
return 'infinite'
}
},
urlMethod: ($tdElement) => {
const url = $($tdElement.find('a[href*="download.php"]')[0]).attr('href')
const regex = /\/detail\/(\d+)/;
const match = url.match(regex);
if (match) {
// 如果匹配成功,match数组的第一个元素是整个匹配的子串,
// 第二个元素(match[1])是第一个捕获组的内容,即所需的ID
return match[1];
} else {
return ''
}
},
freeSelector: () => $('span.ant-tag:contains("Free")')
},
{
site: 'https://amani.top',
torrentMethod: (item) => $($(item)).parentsUntil('table', 'tr'),
titleMethod: ($tdElement) => $($tdElement.find('div[class*="title"]')[0]).text(),
timeMethod: ($tdElement) => {
// const dateTimeRegex = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/
// const $spansWithTitle = $tdElement.find('span[title]');
// return $spansWithTitle.filter(function () {
// return dateTimeRegex.test($(this).attr('title'));
// }).get().length ? $($spansWithTitle.filter(function () {
// return dateTimeRegex.test($(this).attr('title'));
// }).get()[0]).attr('title') : 'infinite'
return 'infinite'
},
urlMethod: ($tdElement) => normalizeUrl(location.origin + '/' + $($tdElement.find('a[title*="下载"]')[0]).attr('href'))
},
]
function selectTDsWithFreeClassAncestors() {
var $matchingTds = $();
// 选择所有class包含"free"的后代元素
const siteInfo = specialRules.find(i => i.site === location.origin)
var $freeElements = siteInfo && siteInfo.freeSelector ? siteInfo.freeSelector().toArray() : $('[class*="free"]').toArray();
// 遍历这些元素,找到它们的所有祖先td元素
$freeElements.forEach((item) => {
let $ancestorsWithClass = null
if (siteInfo) {
$ancestorsWithClass = siteInfo.torrentMethod(item)
} else {
$ancestorsWithClass = $($(item)).parentsUntil('table', 'tr');
}
// 把找到的td元素加入结果集
$matchingTds = $matchingTds.add($ancestorsWithClass);
});
// 返回结果集
return $matchingTds;
}
// 调用函数并进行操作
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 timeSort(data) {
return data.sort((a, b) => {
if (a.time === 'infinite') {
return 1;
} else if (b.time === 'infinite') {
return -1;
} else {
return new Date(a.time) - new Date(b.time);
}
});
}
function calcRestTime(timeStr) {
if (timeStr === 'infinite') {
return { restTime: '无限', days: 9999, hours: 9999, minutes: 9999 };
}
const date = new Date(timeStr);
const now = new Date();
const sub = Math.abs(date.getTime() - now.getTime())
// 转换为天数、小时数、分钟数和秒数
const days = Math.floor(sub / (1000 * 60 * 60 * 24));
const hours = Math.floor((sub % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((sub % (1000 * 60 * 60)) / (1000 * 60));
return {
restTime: `${days > 0 ? days + '天 ' : ''}${hours > 0 ? hours + '小时 ' : ''}${minutes}分`,
days,
hours,
minutes
}
}
function getInfo() {
const siteInfo = specialRules.find(i => i.site === location.origin)
const selectedTds = selectTDsWithFreeClassAncestors().toArray();
const result = selectedTds.map(i => {
const $tdElement = $(i);
const dateTimeRegex = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/;
const item = {
title: '',
time: '',
url: ''
}
if (siteInfo) {
item.title = siteInfo.titleMethod($tdElement)
item.time = siteInfo.timeMethod($tdElement)
item.url = siteInfo.urlMethod($tdElement)
} else {
item.title = $($tdElement.find('a[href*="details.php"]')[0]).attr('title')
const $spansWithTitle = $tdElement.find('span[title]');
const url = $($tdElement.find('a[href*="download.php"]')[0]).attr('href')
item.time = $spansWithTitle.filter(function () {
return dateTimeRegex.test($(this).attr('title'));
}).get().length ? $($spansWithTitle.filter(function () {
return dateTimeRegex.test($(this).attr('title'));
}).get()[0]).attr('title') : 'infinite'
item.url = normalizeUrl(location.origin + '/' + url)
}
return item
})
result.forEach(i => {
const { restTime, days, hours, minutes } = calcRestTime(i.time)
i.restTime = restTime
i.days = days
i.hours = hours
i.minutes = minutes
})
return timeSort(result)
}
// 复制文字到剪贴板
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error('Failed to copy to clipboard: ', err);
}
}
function setButton() {
console.log('setbtn')
// 创建一个新的button元素
var button = document.createElement('button');
button.textContent = '获取信息'; // 设置按钮文字
// 设置按钮的基本样式(包括固定定位与默认透明度)
button.style.cssText = `
position: fixed; /* 或者 absolute,取决于您的布局需求 */
top: 88px; /* 举例位置,您可以自定义 */
right: 10px; /* 举例位置,您可以自定义 */
z-index: 999;
background-color: #007bff;
color: white;
padding: 6px 12px;
border: none;
border-radius: 5px;
cursor: pointer;
opacity: 0.3;
transition: opacity 0.3s ease;
`;
// 添加鼠标悬浮时的透明度变化
button.addEventListener('mouseover', function () {
this.style.opacity = 1;
});
// 添加鼠标离开时的透明度变化
button.addEventListener('mouseout', function () {
this.style.opacity = 0.3;
});
button.addEventListener('click', setTable)
// 将按钮添加到文档中
document.body.appendChild(button);
}
function setTable() {
if (location.origin === 'https://kp.m-team.cc') return alert("该站点支持正在开发中。。。")
const sortedData = getInfo()
const mask = document.createElement('div');
mask.addEventListener('click', (event) => {
// 判断点击的是mask本身还是其子元素
if (event.target === mask) {
document.body.removeChild(mask);
} else {
event.stopPropagation(); // 阻止子元素点击事件向上冒泡到mask
}
});
mask.classList.add('mask');
mask.style.cssText = `
background-color: rgba(0, 0, 0, 0.2);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
`
document.body.appendChild(mask);
const tableWrap = document.createElement('div')
tableWrap.classList.add('table-wrap');
tableWrap.style.cssText = `
max-width: 1000px;
max-height: 900px;
min-height: 500px;
min-width: 500px;
width: 50vw;
height: 70vh;
background-color: #fff;
overflow-y: auto;
padding: 10px;
`
// 创建操作区域
const btnArea = document.createElement('div')
btnArea.style.cssText = `
height: 40px;
margin-bottom: 8px;
box-sizing: border-box;
padding: 4px 0;
`
// 创建关闭按钮
const closeBtn = document.createElement('button')
closeBtn.textContent = '关闭'
closeBtn.style.cssText = `
background-color: #F56C6C;
color: white;
padding: 6px 12px;
border: none;
border-radius: 5px;
cursor: pointer;
margin-right: 10px;
`
closeBtn.addEventListener('click', () => {
document.body.removeChild(mask);
})
// 创建复制链接按钮
const copyLink = document.createElement('button')
copyLink.textContent = '复制下载链接'
copyLink.style.cssText = `
background-color: #007bff;
color: white;
padding: 6px 12px;
border: none;
border-radius: 5px;
cursor: pointer;
margin-right: 10px;
`
const filterInput = document.createElement('input')
filterInput.type = 'number'
filterInput.placeholder = '剩余时间 > ?'
filterInput.style.cssText = `
width: 90px;
margin-right: 10px;
padding: 6px;
border-radius: 4px;
border: 1px solid #2165f9;
`
copyLink.addEventListener('click', () => {
const siteInfo = specialRules.find(i => i.site === location.origin)
const getTorrentType = siteInfo && siteInfo.getTorrentType
if (getTorrentType && getTorrentType === 'remote') {
// 发起网络请求获取链接
} else {
let suffix = ''
if (textArea.value) {
suffix = `?${textArea.value.split('\n').join('&')}`
}
if (filterInput.value) {
const limit = +filterInput.value * 60
console.log(limit)
const filterData = sortedData.filter(i => (i.days * 24 + i.hours) * 60 + i.minutes > limit)
const str = filterData.map(i => i.url + suffix).join('\n')
copyToClipboard(str)
alert(`复制成功 已复制 ${filterData.length} 个种子`)
} else {
const str = sortedData.map(i => i.url + suffix).join('\n')
copyToClipboard(str)
alert(`复制成功 已复制 ${sortedData.length} 个种子`)
}
}
})
// 创建复制cookie按钮
const copyCookie = document.createElement('button')
copyCookie.textContent = '复制Cookie'
copyCookie.style.cssText = `
background-color: #007bff;
color: white;
padding: 6px 12px;
border: none;
border-radius: 5px;
cursor: pointer;
`
copyCookie.addEventListener('click', () => {
const cookie = document.cookie
if (cookie) {
copyToClipboard(cookie)
alert('复制成功')
} else {
alert('Cookie 为空')
}
})
btnArea.appendChild(closeBtn)
btnArea.appendChild(filterInput)
btnArea.appendChild(copyLink)
btnArea.appendChild(copyCookie)
const textAreaWrap = document.createElement('div')
textAreaWrap.style.cssText = `
height: 160px;
width: 100%;
overflow-y: auto;
padding: 10px;
box-sizing: border-box;
`
const textArea = document.createElement('textarea')
textArea.placeholder = '请输入自定义参数 1行一条,格式为 key=value'
textArea.style.cssText = `
width: 100%;
height: 100%;
border: 1px solid #2165f9;
box-sizing: border-box;
padding: 10px;
border-radius: 4px;
`
textAreaWrap.appendChild(textArea)
// 创建表格元素
const table = document.createElement('table');
table.style.cssText = `
width: 100%;
height: 100%;
`;
const lessOnHour = sortedData.filter(i => i.days <= 0 && i.hours <= 0).length
const lessAHalfDay = sortedData.filter(i => i.days <= 0 && i.hours <= 12 && i.hours > 1).length
const total = document.createElement('div')
total.style.fontSize = '16px'
total.style.fontWeight = 'bold'
const num = sortedData.length
const span1 = document.createElement('span')
span1.textContent = '共有 '
const span2 = document.createElement('span')
span2.textContent = num
span2.style.color = '#2165f9'
const span3 = document.createElement('span')
span3.textContent = ' 个免费种子'
const tip1 = document.createElement('span')
const tip2 = document.createElement('span')
const tip3 = document.createElement('span')
const tip4 = document.createElement('span')
const tip5 = document.createElement('span')
tip1.textContent = '小于1小时:'
tip2.textContent = lessOnHour
tip2.style.color = 'red'
tip3.textContent = ' 个,大于1小时小于12小时:'
tip4.textContent = lessAHalfDay
tip4.style.color = 'orange'
tip5.textContent = ' 个'
const tipSpan = document.createElement('span')
tipSpan.style.fontSize = '12px'
tipSpan.style.marginLeft = '20px'
tipSpan.appendChild(tip1)
tipSpan.appendChild(tip2)
tipSpan.appendChild(tip3)
tipSpan.appendChild(tip4)
tipSpan.appendChild(tip5)
total.appendChild(span1)
total.appendChild(span2)
total.appendChild(span3)
total.appendChild(tipSpan)
tableWrap.appendChild(btnArea)
tableWrap.appendChild(total)
tableWrap.appendChild(textAreaWrap)
const tableBox = document.createElement('div');
tableBox.style.cssText = `
height: calc(100% - 232px);
width: 100%;
overflow-y: auto;
`
tableWrap.appendChild(tableBox);
tableBox.appendChild(table);
mask.appendChild(tableWrap);
// 创建表头
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
['序号', '剩余时间', '标题', '下载链接'].forEach(header => {
const th = document.createElement('th');
th.textContent = header;
headerRow.appendChild(th);
th.style.cssText = `
position: sticky;
top: 0;
background-color: white; /* 添加背景颜色以防止内容滚动时穿透 */
z-index: 2;
`
});
thead.appendChild(headerRow);
table.appendChild(thead);
// 创建表体
const tbody = document.createElement('tbody');
sortedData.forEach((item, index) => {
const row = document.createElement('tr');
const indexCell = document.createElement('td');
indexCell.textContent = index + 1;
row.appendChild(indexCell);
const timeCell = document.createElement('td');
timeCell.textContent = item.restTime;
row.appendChild(timeCell);
const titleCell = document.createElement('td');
titleCell.textContent = item.title
row.appendChild(titleCell);
const linkCell = document.createElement('td');
linkCell.textContent = item.url
row.appendChild(linkCell);
if (item.days <= 0 && item.hours <= 0) {
indexCell.style.color = 'red';
timeCell.style.color = 'red';
titleCell.style.color = 'red';
linkCell.style.color = 'red';
} else if (item.days <= 0 && item.hours <= 12) {
indexCell.style.color = 'orange';
timeCell.style.color = 'orange';
titleCell.style.color = 'orange';
linkCell.style.color = 'orange';
} tbody.appendChild(row);
});
table.appendChild(tbody);
}
setButton()
})();