Greasy Fork

Greasy Fork is available in English.

Get Free Torrents

该插件主要用于抓取指定页面(即 "torrents.php")中的免费种子信息,并将其按照剩余时间从短到长排序后,以表格形式呈现给用户。用户可以一键复制所有展示种子的链接,同时具备筛选功能,允许用户设定自定义时间阈值,仅复制剩余时间超过该阈值的种子链接。此外,插件还支持添加自定义URL参数以扩展功能或满足个性化需求。

目前为 2024-04-23 提交的版本,查看 最新版本

// ==UserScript==
// @name Get Free Torrents
// @namespace http://tampermonkey.net/
// @version 1.0.6
// @description 该插件主要用于抓取指定页面(即 "torrents.php")中的免费种子信息,并将其按照剩余时间从短到长排序后,以表格形式呈现给用户。用户可以一键复制所有展示种子的链接,同时具备筛选功能,允许用户设定自定义时间阈值,仅复制剩余时间超过该阈值的种子链接。此外,插件还支持添加自定义URL参数以扩展功能或满足个性化需求。
// @author 飞天小猪
// @match http*://*/*torrents*.php*
// @match http*://kp.m-team.cc/*
// @match http*://*/*special*.php*
// @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()
})();