构建专业的 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

核心组件

  1. MTeamClient: 封装 M-Team API,支持 RSS 和搜索
  2. QBittorrentClient: 封装 qBittorrent Web API
  3. DataManager: 持久化存储处理记录
  4. 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

加载评论中...
构建专业的 M-Team 刷流量自动化工具:TypeScript 实战指南 | 博客