天空vt刷流脚本

_

功能是1小时后开始下载或者前一小时限速一小时后不限速,分享率达标自动删除,空间不足时自动删种
根据大佬们的建议,已支持自动调整限速
initialUploadLimit设置为0为暂停模式,大于0是限速模式,具体值可以自己摸索,建议不要低于250
感谢各位大佬捧场

qb设置取消勾选为所有文件预分配磁盘空间

1769509467-480918-image.png

vt设置rss任务,勾选添加种子时暂停,分类设置为SKY

1769509340-630875-image.png

vt设置定时


cron:* * * * *
脚本:

async () => {
  const moment = require('moment')
  const util = require('../libs/util')
  const clients = global.runningClient

  const CONFIG = {
    category: 'sky',                       // 分类
    resumeDelay: 1 * 60 * 60,              // 初始等待时长 (1小时)
    maxRatio: 3.1,                         // 目标分享率 (扣除初始量)
    minFreeSpace: 12 * 1024 * 1024 * 1024, // 触发清理的空间阈值
    panicSpace: 5 * 1024 * 1024 * 1024,    // 恐慌空间 (无视大部分保护)
    gracePeriod: 10 * 60,                  // 暂停模式特有的保护时间
    initialUploadLimit: 500,               // 初始限速 (KB/s),0 为暂停
    standardUploadLimit: 0                 // 标准限速 (KB/s),0 为无限制
  }

  // 判定种子是否被站点删除/封禁
  const checkIsInvalid = (torrent) => {
    const { trackerStatus } = torrent
    if (!trackerStatus) return false
    const deletedMessages = [
      "torrent banned",
      "Torrent not exists",
      "torrent not registered with this tracker",
      "unregistered torrent",
      "Invalid Torrent:"
    ]
    const trackerMessage = trackerStatus.toLowerCase()
    return deletedMessages.some(msg => trackerMessage.includes(msg.toLowerCase()))
  }

  // 获取扣除等待期上传量后的统计数据
  const getEffectiveStats = async (torrent) => {
    const boundaryTime = torrent.addedTime + CONFIG.resumeDelay
    const now = moment().unix()
    if (now <= boundaryTime) return { effectiveUpload: 0, effectiveRatio: 0 }

    const record = await util.getRecord(
      'SELECT upload FROM torrent_flow WHERE hash = ? AND time >= ? ORDER BY time ASC LIMIT 1',
      [torrent.hash, boundaryTime]
    )
    const uploadAtBoundary = record ? record.upload : 0
    const effectiveUpload = Math.max(0, torrent.uploaded - uploadAtBoundary)
    const effectiveRatio = torrent.size > 0 ? (effectiveUpload / torrent.size) : 0
    return { effectiveUpload, effectiveRatio }
  }

  for (const clientId of Object.keys(clients)) {
    const client = clients[clientId]
    const maindata = client.maindata
    const serverState = maindata?.serverState || maindata?.server_state
    if (!maindata || !maindata.torrents || !serverState) continue

    const torrents = maindata.torrents
    const now = moment().unix()
    const currentFreeSpace = serverState.free_space_on_disk || 0
    const isPanic = currentFreeSpace < CONFIG.panicSpace
    
    // 筛选并预计算
    const skyTorrents = []
    for (const t of torrents) {
      if ((t.category || '').toLowerCase() === CONFIG.category) {
        const stats = await getEffectiveStats(t)
        t.effectiveRatio = stats.effectiveRatio
        t.effectiveUpload = stats.effectiveUpload
        t.actualSize = (t.size || 0) * (t.progress || 0) // 实际磁盘占用
        t.isInvalid = checkIsInvalid(t)                  // 站点失效标记
        skyTorrents.push(t)
      }
    }

    const gracefulDelete = async (torrent, reason) => {
      try {
        if (client.reannounceTorrent) {
          await client.reannounceTorrent(torrent)
          await new Promise(r => setTimeout(r, 2000))
        }
        logger.info(`[脚本] ${reason}: ${torrent.name} | 有效Ratio: ${torrent.effectiveRatio.toFixed(3)} | 状态: ${torrent.state}`)
        await client.deleteTorrent(torrent, { alias: '脚本自动' })
        return true
      } catch (e) {
        logger.error(`[脚本] 删除失败: ${e.message}`)
        return false
      }
    }

    // ================= 达标删除 =================
    let freedSpace = 0
    for (const torrent of skyTorrents) {
      if (torrent.effectiveRatio >= CONFIG.maxRatio) {
        if (await gracefulDelete(torrent, ' 达标删除')) {
          freedSpace += torrent.actualSize
        }
      }
    }

    // 计算达标删除后的虚拟空间
    const virtualFreeSpace = currentFreeSpace + freedSpace

    // ================= 空间清理 =================
    if (virtualFreeSpace < CONFIG.minFreeSpace) {
      const candidateList = []
      
      for (const t of skyTorrents) {
        if (t.effectiveRatio >= CONFIG.maxRatio) continue 

        const isError = t.state.toLowerCase().includes('error')
        // 失效种子和常规报错优先进入候选
        if (t.isInvalid || isError) {
          candidateList.push({ torrent: t })
          continue
        }

        const have = (t.progress > 0)
        const timeActive = now - t.addedTime
        const isUnderDelay = timeActive < CONFIG.resumeDelay
        const isUnderGrace = (CONFIG.initialUploadLimit === 0)
          ? (timeActive < (CONFIG.resumeDelay + CONFIG.gracePeriod))
          : false

        if (isPanic) {
          if (isUnderDelay && !have) continue
        } else {
          if (isUnderDelay) continue
          if (isUnderGrace) continue
        }

        candidateList.push({ torrent: t })
      }

      // 排序逻辑
      candidateList.sort((a, b) => {
        const tA = a.torrent
        const tB = b.torrent

        // 站点已删种子绝对优先
        if (tA.isInvalid !== tB.isInvalid) return tA.isInvalid ? -1 : 1

        // 常规报错次优先
        const aErr = tA.state.toLowerCase().includes('error')
        const bErr = tB.state.toLowerCase().includes('error')
        if (aErr !== bErr) return aErr ? -1 : 1

        // 恐慌模式先删除占用大的
        if (isPanic) {
          return tB.actualSize - tA.actualSize
        }

        // 正常模式:先看上传速度差异
        const upDiff = tA.upspeed - tB.upspeed
        if (Math.abs(upDiff) > 10 * 1024) {
          return upDiff
        }

        // 速度差不多(<=10KB/s)时,删除占用大的
        const sizeDiff = tB.actualSize - tA.actualSize
        if (sizeDiff !== 0) return sizeDiff

        // 保护下载中的种子
        if (tA.progress === 1 && tB.progress < 1) return -1
        if (tB.progress === 1 && tA.progress < 1) return 1

        return 0
      })

      if (candidateList.length > 0) {
        const mode = isPanic ? " [恐慌清理]" : " [空间清理]"
        await gracefulDelete(candidateList[0].torrent, mode)
      }
    }

    // ================= 恢复与限速逻辑 =================
    if (currentFreeSpace > CONFIG.panicSpace) {
      const pausedStates = ['pausedDL', 'pausedUP', 'Stopped', 'stopped']
      
      for (const torrent of skyTorrents) {
        const isUnderDelay = (now - torrent.addedTime < CONFIG.resumeDelay)

        const currentUpLimit = torrent.originProp ? torrent.originProp.up_limit : 0
        const initialLimitByte = CONFIG.initialUploadLimit * 1024
        const standardLimitByte = CONFIG.standardUploadLimit * 1024

        if (CONFIG.initialUploadLimit > 0) {
          if (isUnderDelay) {
            if (pausedStates.includes(torrent.state)) {
              await client.resumeTorrent(torrent.hash)
            }
            if (currentUpLimit !== initialLimitByte) {
              await client.setSpeedLimit(torrent.hash, 'upload', initialLimitByte)
            }
          } else {
            if (currentUpLimit !== standardLimitByte) {
              await client.setSpeedLimit(torrent.hash, 'upload', standardLimitByte)
            }
          }
        } else {
          if (!isUnderDelay) {
            if (currentUpLimit !== standardLimitByte) {
              await client.setSpeedLimit(torrent.hash, 'upload', standardLimitByte)
            }
            if (pausedStates.includes(torrent.state)) {
              await client.resumeTorrent(torrent.hash)
            }
          }
        }
      }
    }
  }
}

WEBUI的Mediainfo/BDinfo截图工具 2026-05-14