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

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


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)
}
}
}
}
}
}
}