Get Free Torrents

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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