构建专业的 M-Team 刷流量自动化工具:TypeScript 实战指南
2025年12月26日4 次阅读0 人喜欢
TypeScriptBitTorrent自动化青龙面板M-TeamqBittorrent
构建专业的 M-Team 刷流量自动化工具:TypeScript 实战指南
本文介绍如何使用 TypeScript 构建一个功能完整的 BitTorrent 刷流自动化工具,集成 M-Team API 和 qBittorrent。
前言
在 PT(Private Tracker)社区中,"刷流"是一个常见需求:自动下载免费种子,完成下载后做种以获取上传流量。本文将介绍一个基于 TypeScript 的专业刷流工具,它具备以下特点:
- ? 自动化: 定时任务,全自动运行
- ? 智能筛选: 基于多种条件过滤种子
- ? 数据持久化: 记录已处理种子,避免重复下载
- ? 资源管理: 自动清理低流行度种子,防止磁盘爆满
- ⚡ 优雅设计: 模块化架构,易于扩展和维护
系统架构
整体设计
┌─────────────────────────────────────────────────────────┐
│ M-Team 刷流工具主类 │
│ MTeamBrushTool │
└─────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ MTeamClient │ │ QBittorrent │ │ DataManager │
│ (API封装) │ │ (下载器) │ │ (数据管理) │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
▼ ▼ ▼
yeast.js @ctrl/qbittorrent fs/json
核心组件
- MTeamClient: 封装 M-Team API,支持 RSS 和搜索
- QBittorrentClient: 封装 qBittorrent Web API
- DataManager: 持久化存储处理记录
- MTeamBrushTool: 业务逻辑编排
技术实现详解
1. 环境配置管理
使用统一的配置管理,支持环境变量和构造函数参数:
typescript
// libs/utils.ts
export function getMTeamBrushConfig(): MTeamBrushConfig {
return {
diskSpaceLimitGb: parseFloat(getEnvSilent('MTEAM_DISK_SPACE_LIMIT_GB', '80')),
maxTorrentSizeGb: parseFloat(getEnvSilent('MTEAM_MAX_TORRENT_SIZE_GB', '30')),
minTorrentSizeGb: parseFloat(getEnvSilent('MTEAM_MIN_TORRENT_SIZE_GB', '1')),
seedFreeTimeHours: parseInt(getEnvSilent('MTEAM_SEED_FREE_TIME_HOURS', '8'), 10),
publishBeforeHours: parseInt(getEnvSilent('MTEAM_PUBLISH_BEFORE_HOURS', '24'), 10),
lsRatioMin: parseFloat(getEnvSilent('MTEAM_LS_RATIO_MIN', '1.0')),
maxUnfinishedDownloads: parseInt(getEnvSilent('MTEAM_MAX_UNFINISHED_DOWNLOADS', '100'), 10),
apiDelayMin: parseFloat(getEnvSilent('MTEAM_API_DELAY_MIN', '1.5')),
apiDelayMax: parseFloat(getEnvSilent('MTEAM_API_DELAY_MAX', '3.5')),
dataFilePath: getEnvSilent('MTEAM_DATA_FILE_PATH', './tmp/mteam_brush_data.json'),
qbCategory: getEnvSilent('MTEAM_QB_CATEGORY', '刷流'),
qbTags: (getEnvSilent('MTEAM_QB_TAGS', '刷流') || '刷流').split(',').map(t => t.trim()),
};
}
2. M-Team API 封装
基于 yeast.js 库进行二次封装,支持 RSS 订阅和 API 搜索:
typescript
// libs/mteam.ts
export class MTeamClient {
private yeast?: Yeast;
private rssUrl?: string;
private useIPv6: boolean;
// 从 RSS 获取种子
async getRSSFeed(): Promise<RSSItem[]> {
if (!this.rssUrl) return [];
const response = await axios.get(this.rssUrl, { timeout: 45000 });
const parser = new XMLParser({ ignoreAttributes: false });
const xmlData = parser.parse(response.data);
// 解析 XML 并转换为统一格式
return this.parseRSSItems(xmlData);
}
// 获取种子详情
async getTorrentDetail(torrentId: string): Promise<TorrentDetail | null> {
if (!this.yeast) return null;
const data = await this.yeast.seed.detail(torrentId, { origin: 'm-team' });
return {
id: torrentId,
name: data.name || '',
size: parseInt(data.size) || 0,
discount: data.status?.discount || 'UNKNOWN',
discountEndTime: data.status?.discountEndTime || null,
seeders: parseInt(data.status?.seeders) || 0,
leechers: parseInt(data.status?.leechers) || 0,
createdDate: data.createdDate,
};
}
// 获取下载链接
async getTorrentDownloadUrl(torrentId: string): Promise<string | null> {
if (!this.yeast) return null;
const tokenUrlPart = await this.yeast.seed.genDlToken(torrentId);
const [baseUrl, paramsStr] = tokenUrlPart.split('?');
const params = new URLSearchParams(paramsStr || '');
// 支持 IPv6 配置
params.set('useHttps', 'true');
params.set('type', this.useIPv6 ? 'ipv6' : 'ipv4');
return `${baseUrl}?${params.toString()}`;
}
}
3. qBittorrent 客户端封装
基于 @ctrl/qbittorrent 库,提供标准化的 API:
typescript
// libs/qbittorrent.ts
export class QBittorrentClient {
private client: QBittorrent;
// 添加种子(支持 URL 和文件)
async addTorrentUrl(url: string, options: AddTorrentOptions = {}): Promise<boolean> {
const magnetOptions: any = {
category: options.category,
tags: options.tags,
paused: options.paused ? 'true' : 'false',
};
// 不传递 savepath 时使用分类默认路径
if (options.savepath) {
magnetOptions.savepath = options.savepath;
}
const result = await (this.client as any).addMagnet(url, magnetOptions);
return result === 'Ok.' || result === true;
}
// 获取标准化的种子列表
async getTorrents(filter: { filter?: string; category?: string } = {}): Promise<NormalizedTorrent[]> {
const result = await this.client.listTorrents(filter);
return result.map(torrent => ({
id: torrent.hash,
name: torrent.name,
progress: torrent.progress || 0,
ratio: torrent.ratio || 0,
dateAdded: torrent.added_on ? new Date(torrent.added_on * 1000).toISOString() : '',
downloadSpeed: torrent.dlspeed || 0,
// ... 更多字段
}));
}
// 获取磁盘空间(多接口 fallback)
async getAppPreferences(): Promise<QBAppPreferences> {
// 优先使用 /api/v2/sync/maindata 接口
let freeSpace = await this.getDiskSpaceFromMainData();
// 失败时使用 getAllData
if (freeSpace === null) {
const allData = await this.client.getAllData();
freeSpace = allData?.server_state?.free_space_on_disk ?? null;
}
return {
...preferences,
free_space_on_disk: freeSpace,
};
}
}
4. 数据持久化
使用 JSON 文件存储已处理种子记录,支持自动清理:
typescript
class DataManager {
private maxRecords: number = 1000;
private maxAgeDays: number = 30;
loadProcessedTorrents(): ProcessedTorrent[] {
if (!fs.existsSync(this.filePath)) return [];
const data = fs.readFileSync(this.filePath, 'utf-8');
const records = JSON.parse(data);
// 清理过期记录
return this.cleanOldRecords(records);
}
private cleanOldRecords(records: ProcessedTorrent[]): ProcessedTorrent[] {
const now = dayjs();
return records.filter(record => {
// 保留成功添加的记录
if (record.status === 'added_to_qb') return true;
// 其他记录检查时间
const recordTime = dayjs(record.time);
const daysDiff = now.diff(recordTime, 'day');
return daysDiff <= this.maxAgeDays;
});
}
}
5. 核心业务逻辑
种子筛选流程
typescript
async run(): Promise<void> {
// 1. 初始化检查
await this.init();
// 2. 清理低流行度种子
await this.cleanLowPopularityTorrents();
// 3. 检查可能过期的种子
await this.checkExpiringTorrents();
// 4. 检查未完成任务数
const unfinishedCount = await this.checkUnfinishedDownloads();
if (unfinishedCount > this.config.maxUnfinishedDownloads) {
return;
}
// 5. 检查磁盘空间
const diskSpace = await this.getDiskSpace();
if (diskSpace <= spaceLimit) {
return;
}
// 6. 获取种子列表(RSS 优先)
let torrents = await this.getTorrents();
// 7. 逐个处理种子
for (const item of torrents) {
// 检查是否已处理
if (this.isProcessed(item.id)) continue;
// 检查发布时间
if (!this.checkPublishTime(item.pubDate)) continue;
// 获取详情
const detail = await this.mteam.getTorrentDetail(item.id);
if (!detail) continue;
// 多重筛选条件
if (!this.checkSize(detail.size)) continue;
if (!['FREE', '_2X_FREE'].includes(detail.discount)) continue;
if (!this.checkFreeTime(detail.discountEndTime)) continue;
if (detail.seeders <= 0) continue;
if (detail.leechers / detail.seeders < this.config.lsRatioMin) continue;
// 生成重命名
const renameName = generateRenameName(item.id, item, detail);
// 添加到 qBittorrent
const success = await this.qb.addTorrentUrl(downloadUrl, {
category: this.config.qbCategory,
tags: this.config.qbTags.join(','),
});
if (success) {
this.markProcessed(item.id, 'added_to_qb', detail.name, detail.size, renameName, detail.discountEndTime);
}
}
// 8. 保存数据并输出总结
this.dataManager.saveProcessedTorrents(this.processedTorrents);
this.printSummary();
}
低流行度种子清理
typescript
private async cleanLowPopularityTorrents(): Promise<void> {
const torrents = await this.qb.getTorrents({ category: this.config.qbCategory });
const toDelete: string[] = [];
for (const torrent of torrents) {
const popularity = torrent.popularity ?? torrent.ratio ?? 0;
const daysSinceAdded = now.diff(dayjs(torrent.dateAdded), 'day');
// 流行度 < 1 且添加超过 2 天的种子将被删除
if (popularity < 1 && daysSinceAdded > 2) {
toDelete.push(torrent.id);
}
}
if (toDelete.length > 0) {
await this.qb.deleteTorrents(toDelete, true);
}
}
过期种子预测
typescript
private async checkExpiringTorrents(): Promise<void> {
const torrents = await this.qb.getTorrents({
category: this.config.qbCategory,
filter: 'downloading'
});
const toPause: string[] = [];
for (const torrent of torrents) {
// 从名称提取 ID
const torrentId = torrent.name.match(/^\[(\d+)\]/)?.[1];
const record = this.processedTorrents.find(t => t.id === torrentId);
if (!record?.discountEndTime) continue;
const discountEndTime = dayjs(record.discountEndTime);
const progress = torrent.progress;
const downloadSpeed = torrent.downloadSpeed;
const remainingBytes = torrent.totalSize - torrent.totalDownloaded;
// 估算完成时间
let estimatedCompletionTime: dayjs.Dayjs | null = null;
if (downloadSpeed > 0) {
const remainingSeconds = remainingBytes / downloadSpeed;
estimatedCompletionTime = dayjs().add(remainingSeconds, 'second');
}
// 如果预计无法在免费期内完成,暂停下载
if (estimatedCompletionTime && estimatedCompletionTime.isAfter(discountEndTime)) {
toPause.push(torrent.id);
}
}
if (toPause.length > 0) {
await this.qb.pauseTorrents(toPause);
}
}
环境变量配置
M-Team 配置
bash
# API 认证(二选一)
MTEAM_AUTHORIZATION=your_api_token
MTEAM_API_KEY=your_api_key
# RSS 订阅(推荐)
MTEAM_RSS_URL=https://kp.m-team.cc/rss.php?auth=your_token
# 网络配置
MTEAM_USE_IPV6=false
MTEAM_API_HOST=https://api.m-team.cc
qBittorrent 配置
bash
QB_HOST=http://localhost:8080
QB_USERNAME=admin
QB_PASSWORD=adminadmin
刷流策略配置
bash
# 磁盘空间限制(GB)
MTEAM_DISK_SPACE_LIMIT_GB=80
# 种子大小限制(GB)
MTEAM_MIN_TORRENT_SIZE_GB=1
MTEAM_MAX_TORRENT_SIZE_GB=30
# 免费时长要求(小时)
MTEAM_SEED_FREE_TIME_HOURS=8
# 发布时间限制(小时)
MTEAM_PUBLISH_BEFORE_HOURS=24
# L/S 比例要求
MTEAM_LS_RATIO_MIN=1.0
# 最大未完成任务数
MTEAM_MAX_UNFINISHED_DOWNLOADS=100
# API 延迟(秒)
MTEAM_API_DELAY_MIN=1.5
MTEAM_API_DELAY_MAX=3.5
# 数据文件路径
MTEAM_DATA_FILE_PATH=./tmp/mteam_brush_data.json
# qBittorrent 分类和标签
MTEAM_QB_CATEGORY=刷流
MTEAM_QB_TAGS=刷流,自动
青龙面板集成
定时任务配置
bash
# 每 2 分钟运行一次
0 0/2 * * * ?
# 每天凌晨运行
0 0 0 * * ?
# 工作日运行
0 0 0 * * 1-5
青龙环境变量设置
在青龙面板的环境变量中添加上述所有配置。
运行示例
bash
# 安装依赖
pnpm install
# 设置环境变量
export MTEAM_AUTHORIZATION=your_token
export QB_HOST=http://localhost:8080
# ... 其他配置
# 运行脚本
pnpm tsx src/m-team-brush.ts
# 或使用青龙面板定时任务
输出示例
============================================================
? M-Team 刷流工具
============================================================
⚙️ 正在初始化 M-Team 刷流工具...
✅ qBittorrent 版本: 4.5.0
? 已加载 15 条历史记录
? 开始检查需要清理的低流行度种子...
? 刷流分类共有 8 个种子
[123456] 流行度: 0.50, 添加时间: 2025-12-20 10:00:00 (6 天前)
❌ 将删除: [123456] Example.Torrent.2025.1080p
?️ 准备删除 1 个低流行度种子...
✅ 已成功删除 1 个种子
⏰ 开始检查可能过期的种子...
? 正在下载的种子数: 2
⚠️ 种子 [789012] Example.Torrent.2025.720p 可能无法在免费期内完成
进度: 45.20%
下载速度: 5.20 MB/s
剩余时间: 12.50 小时
免费剩余: 8.20 小时
⏸️ 准备暂停 1 个可能过期的种子...
✅ 已成功暂停 1 个种子
? qBittorrent 中未完成的任务数: 5
? 当前磁盘剩余空间: 150.50 GB
? 空间下限: 80.00 GB
? 正在从 RSS 订阅源获取种子...
✅ 从 RSS 获取到 25 个种子
开始处理 25 个种子...
⏭️ 种子 123456 已处理过,跳过
⏰ 种子 234567 发布时间过早,跳过
? 种子 345678 RSS大小 (50.50 GB) 不符合要求,跳过
? 处理种子: [Movie] Example Movie 2025 1080p BluRay
ID: 456789
大小: 15.50 GB
优惠: FREE
做种: 8 | 下载: 12
免费到期: 2025-12-27 08:00:00 (剩余 15.5 小时)
✅ 条件满足,准备下载
?️ 重命名为: [456789]_Example.Movie.2025.1080p.BluRay
✅ 已成功添加到 qBittorrent
? 处理种子: [TV] Example TV Show S01 1080p
ID: 567890
大小: 8.20 GB
优惠: _2X_FREE
做种: 5 | 下载: 8
免费到期: 2025-12-27 12:00:00 (剩余 19.5 小时)
✅ 条件满足,准备下载
?️ 重命名为: [567890]_Example.TV.Show.S01.1080p
✅ 已成功添加到 qBittorrent
⚠️ 磁盘空间已达下限,停止添加更多种子
============================================================
? 运行总结
============================================================
✅ 成功添加: 2 个种子
⏱️ 总耗时: 45.23 秒
? 已添加的种子:
? [456789] [Movie] Example Movie 2025 1080p BluRay
重命名: [456789]_Example.Movie.2025.1080p.BluRay
大小: 15.50 GB | 优惠: FREE | L/S: 12/8 = 1.50
免费到期: 2025-12-27 08:00:00 (剩余 15.5 小时)
? [567890] [TV] Example TV Show S01 1080p
重命名: [567890]_Example.TV.Show.S01.1080p
大小: 8.20 GB | 优惠: _2X_FREE | L/S: 8/5 = 1.60
免费到期: 2025-12-27 12:00:00 (剩余 19.5 小时)
============================================================
设计亮点
1. 多层防护机制
- ✅ 磁盘空间检查
- ✅ 未完成任务数限制
- ✅ 种子大小过滤
- ✅ 免费时长要求
- ✅ L/S 比例筛选
- ✅ 发布时间限制
2. 智能清理策略
- 自动删除低流行度种子(popularity < 1)
- 预测并暂停可能过期的下载
- 自动清理历史记录(30 天)
3. 优雅的错误处理
- 多接口 fallback(RSS → API)
- 数据损坏自动备份
- 网络请求超时处理
- 详细的错误日志
4. 可扩展架构
- 模块化设计,易于替换组件
- 统一的配置管理
- 标准化的数据接口
- 完整的类型定义
最佳实践
1. 安全配置
bash
# 使用 .env 文件管理敏感信息
MTEAM_AUTHORIZATION=your_secret_token
QB_PASSWORD=your_secure_password
# 设置文件权限
chmod 600 .env
2. 监控和告警
typescript
// 可以集成通知系统
if (successfullyAdded.length > 0) {
await sendNotification(`成功添加 ${successfullyAdded.length} 个种子`);
}
3. 性能优化
- 使用 RSS 优先(减少 API 调用)
- 合理的延迟配置(避免被封)
- 批量操作减少请求次数
总结
这个刷流工具展示了如何使用 TypeScript 构建一个生产级的自动化工具。它不仅解决了实际需求,还体现了良好的软件工程实践:
- 类型安全: 完整的 TypeScript 类型定义
- 模块化: 清晰的职责分离
- 可配置: 灵活的环境变量配置
- 可维护: 详细的注释和文档
- 健壮性: 多层防护和错误处理
通过这个项目,你可以学习到:
- 如何封装第三方 API
- 如何设计可扩展的架构
- 如何处理异步操作和错误
- 如何实现数据持久化
- 如何编写生产级的 TypeScript 代码
希望这篇文章对你有所帮助!如果有任何问题,欢迎交流讨论。
项目地址: GitHub
文档: docs/m-team-brush-tech-blog.md