Skip to content

feat(func): support ed2k & magnet & torrent offline download#501

Open
PIKACHUIM wants to merge 2 commits intomainfrom
dev-cas
Open

feat(func): support ed2k & magnet & torrent offline download#501
PIKACHUIM wants to merge 2 commits intomainfrom
dev-cas

Conversation

@PIKACHUIM
Copy link
Copy Markdown
Member

@PIKACHUIM PIKACHUIM commented May 7, 2026

Description / 描述

支持BT种子文件/ED2K/磁力链离线下载,支持天翼云CAS秒传

功能支持项

  1. 支持打开 torrent 发起离线任务,支持选择部分文件下载,支持选择保存路径
  2. 支持使用 ed2k://magnet:? 链接发起离线下载任务
  3. 支持驱动生成 torrent 文件(含 SHA-1 piece hash),实现分享种子以供其他用户秒传
  4. 特别的,对于天翼云,在种子中内嵌 CAS 信息,支持天翼云秒传
  5. 转存到天翼云时自动尝试秒传(计算 MD5 + sliceMD5),失败后回退普通上传
  6. 前端增强版离线下载对话框,支持 Tab 切换(链接下载/BT下载)、文件树形选择、CAS 秒传提示
  7. 种子/磁力链/ED2K 场景自动禁用 SimpleHttp 下载工具
  8. 有 CAS 信息且目标为天翼云时,默认直接走 CAS 秒传(不需要选择下载工具)

预期的逻辑

  1. 可以从列表中选择BT文件发起离线下载,要求支持只勾选部分文件,并且需要选择保存路径(默认当前路径)
  2. 可以支持使用 ed2k 和磁力链获取种子/发起离线下载任务,同上
  3. 可以从本地上传 bt 文件并发起离线任务,同上
  4. BT和磁力链ED2K,非天翼云,都要优先尝试秒传
  5. 对于天翼云,如果BT文件包含CAS,则秒传,否则下载到本地再秒传

后端改动

新增文件

文件 说明
pkg/torrent/bencode.go Bencode 编解码实现(BT 协议标准格式)
pkg/torrent/torrent.go Torrent 文件结构定义、编码/解码、x-cas 扩展支持
pkg/torrent/hash_writer.go HashWriter:同时计算 MD5(CAS秒传)和 SHA-1(BT piece hash)
pkg/torrent/generate.go 通用 torrent 生成函数(不绑定特定驱动)
drivers/189pc/torrent.go 189PC 驱动:torrent 生成、CAS 秒传(新版 initMultiUpload 接口)、CAS 注入/提取
drivers/189/torrent.go 189 驱动:torrent 生成、CAS 秒传
server/handles/torrent.go Torrent API 接口(解析/上传解析/秒传/生成)

修改文件

文件 说明
drivers/189pc/meta.go 添加 GenerateTorrent 配置选项
drivers/189pc/utils.go StreamUpload 中集成 SHA-1 计算,上传完成后异步生成并上传 torrent 文件
drivers/189/util.go newUpload 中集成 SHA-1 计算,上传完成后异步生成 torrent
internal/offline_download/tool/transfer.go 转存时自动尝试天翼云秒传 + 异步生成 torrent
server/router.go 注册 torrent 相关 API 路由

新增 API

方法 路径 功能
POST /api/fs/torrent/parse 解析 Base64 编码的 torrent,返回文件列表/CAS 信息
POST /api/fs/torrent/upload_parse 文件上传方式解析 torrent
POST /api/fs/torrent/rapid_upload 从 torrent CAS 信息秒传到天翼云(使用新版 initMultiUpload 接口)
POST /api/fs/torrent/generate 为指定路径文件生成 torrent

前端改动

新增文件

文件 说明
src/types/torrent.ts Torrent 相关 TypeScript 类型定义
src/pages/home/toolbar/OfflineDownloadEnhanced.tsx 增强版离线下载对话框(Tab切换/秒传/工具选择)
src/pages/home/toolbar/TorrentFileList.tsx Torrent 文件列表树形展示与选择组件

修改文件

文件 说明
src/types/index.ts 导出 torrent 类型
src/utils/api.ts 新增 torrentParse/torrentUploadParse/torrentRapidUpload API
src/utils/bus.ts 添加 torrent_parsed 事件类型
src/pages/home/toolbar/operations.ts 添加 offline_download_torrent 操作图标
src/pages/home/toolbar/Toolbar.tsx 替换为 OfflineDownloadEnhanced 组件
src/pages/home/folder/context-menu.tsx 为 .torrent 文件添加"离线下载"右键菜单
src/lang/en/home.json 添加离线下载增强相关 i18n 键值

BT文件逻辑

graph TD
    A[上传文件] --> B[按 10MB 分片]
    B --> C[每片计算 MD5<br/>用于天翼云秒传]
    B --> D[每片计算 SHA-1<br/>用于 BT piece hash]
    C --> E[fileMD5 + sliceMD5]
    D --> F[pieces 字段]
    E --> G[x-cas 扩展<br/>存储在 info 外部]
    F --> H[info 字典]
    G --> I[生成 .torrent 文件]
    H --> I
    
    J[收到 .torrent] --> K{有 x-cas?}
    K -->|是| L[提取 fileMD5 + sliceMD5]
    L --> M[调用秒传接口]
    M --> N[文件恢复成功]
    K -->|否| O[需要下载后计算 MD5]
    O --> P[注入 CAS 信息到 torrent]
Loading

离线下载完整流程

graph TD
    A[用户上传/选择 .torrent] --> B[解析 torrent]
    B --> C{有 CAS 信息?}
    C -->|是| D[直接 CAS 秒传<br/>不需要选择下载工具]
    D --> E{秒传成功?}
    E -->|是| F[完成 ✅]
    E -->|否| G[提示失败<br/>用户决定下一步]
    C -->|否| H[选择下载工具]
    H --> I[提交离线下载]
    I --> J[aria2/qBit 下载]
    J --> K[下载完成]
    K --> L{目标是天翼云?}
    L -->|是| M[计算 MD5 尝试秒传]
    M --> N{秒传成功?}
    N -->|是| F
    N -->|否| O[普通上传]
    O --> F
    L -->|否| O
Loading

CAS 秒传接口说明

秒传使用新版 initMultiUpload 接口(同时传 fileMd5 + sliceMd5 + sliceSize),与正常上传使用的接口完全一致,确保能匹配到通过新版接口上传的文件。旧版 createUploadFile.action 接口作为回退方案。

设计要点

  • piece size = 10MB:BT 分片与天翼云 CAS 分片完全对齐
  • x-cas 扩展放在 info 外部:不影响 info_hash,普通 BT 客户端忽略该扩展
  • md5sum 使用 BT 标准字段:最大兼容性
  • 通用 torrent 生成pkg/torrent 包不依赖任何驱动,所有存储均可使用
  • ED2K 自动路由:aria2 不支持 ED2K 协议,自动路由到迅雷系工具处理
  • CAS 秒传优先:有 CAS 信息时默认走秒传路径,隐藏下载工具选择器
  • 新版接口秒传:使用 initMultiUpload + commitMultiUploadFile 接口,兼容新旧上传方式

Motivation and Context / 背景

实现 BT 种子生态与网盘秒传的融合,使得:

  1. 天翼云驱动生成的 BT 种子可直接用于天翼云秒传
  2. 其他 BT 种子可下载后秒传,再写回 CAS 信息支持后续秒传
  3. 魔改 BT 客户端可利用天翼云驱动的种子通过天翼云服务器加速下载

How Has This Been Tested? / 测试

  • 天翼云 PC 驱动上传文件并勾选 generate_torrent,验证 .torrent 文件生成并上传到同目录
  • 解析生成的 .torrent 文件,验证包含正确的 x-cas 扩展信息
  • 使用包含 CAS 信息的 .torrent 文件在天翼云进行秒传
  • 前端选择 .torrent 文件右键离线下载,验证文件列表展示和部分选择
  • 前端输入磁力链/ed2k 链接,验证 SimpleHttp 选项被禁用
  • 转存文件到天翼云时,验证自动尝试秒传逻辑
  • 有 CAS 信息时,验证默认走秒传路径且隐藏下载工具选择器
  • 验证秒传使用新版 initMultiUpload 接口能正确匹配已上传的文件

Checklist / 检查清单

  • I have formatted my code with go fmt or prettier.
    我已使用 go fmtprettier 格式化提交的代码。
  • I have added appropriate labels to this PR (or mentioned needed labels in the description if lacking permissions).
    我已为此 PR 添加了适当的标签(如无权限或需要的标签不存在,请在描述中说明,管理员将后续处理)。
  • I have requested review from relevant code authors using the "Request review" feature when applicable.
    我已在适当情况下使用"Request review"功能请求相关代码作者进行审查。
  • I have read the CONTRIBUTING document.
    我已阅读 CONTRIBUTING 文档。
  • I have updated the repository accordingly (If it's needed).
    我已相应更新了相关仓库(若适用)。

Copy link
Copy Markdown
Member

@jyxjjj jyxjjj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plan 别提交

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the frontend offline download workflow to support torrent files, magnet links, and ED2K links, aligning the UI with newly introduced backend torrent APIs (parse / rapid-upload) and adding a richer “BT Download” flow (file list display, save-path selection, tool filtering).

Changes:

  • Added torrent-related frontend types and API helpers (parse/upload-parse/rapid-upload).
  • Introduced an enhanced offline download modal with Link/BT tabs and a torrent file tree selector UI.
  • Added a .torrent context-menu entry to parse a torrent and open the enhanced modal, plus related i18n and toolbar wiring.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/utils/bus.ts Adds torrent_parsed event payload typing for opening the enhanced modal with parsed torrent data.
src/utils/api.ts Adds torrent parse/upload-parse/rapid-upload API wrappers.
src/types/torrent.ts Introduces TS interfaces for torrent parse/rapid-upload responses.
src/types/index.ts Re-exports torrent types.
src/pages/home/toolbar/TorrentFileList.tsx New torrent file tree component with checkbox selection support.
src/pages/home/toolbar/OfflineDownloadEnhanced.tsx New enhanced offline download modal (tabs, torrent parsing, CAS rapid upload, tool filtering).
src/pages/home/toolbar/Toolbar.tsx Switches modal registration from old OfflineDownload to OfflineDownloadEnhanced.
src/pages/home/toolbar/operations.ts Adds offline_download_torrent operation/icon for context menu display.
src/pages/home/folder/context-menu.tsx Adds .torrent right-click “offline download” entry that downloads + parses torrent and opens modal.
src/lang/en/home.json Adds i18n strings for the enhanced offline download UI and new menu entry.
.codebuddy/plan/offline-download-enhancement/task-item.md Planning doc for implementation steps.
.codebuddy/plan/offline-download-enhancement/requirements.md Requirements doc describing expected behavior and acceptance criteria.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/utils/api.ts Outdated
Comment on lines +299 to +301
return r.post("/fs/torrent/upload_parse", formData, {
headers: { "Content-Type": "multipart/form-data" },
})
Comment on lines +258 to +267
if (e.dataTransfer?.files.length) {
for (const file of e.dataTransfer.files) {
if (file.name.toLowerCase().endsWith(".torrent")) {
handleTorrentFile(file)
return
}
}
// 如果在链接 Tab 中拖入 torrent,转换为磁力链
if (activeTab() === "link") {
const values: string[] = []
Comment on lines +139 to +141
// 当有 CAS 信息时,默认使用天翼云秒传(不需要 aria2)
const shouldUseCasRapidUpload = createMemo(() => {
return activeTab() === "bt" && !!torrentInfo()?.has_cas
Comment on lines +340 to +363
// 秒传失败,提示用户
notificationService.show({
status: "warning",
title: t(
"home.toolbar.offline_download_enhanced.cas_rapid_upload_failed",
),
description: resp.message,
})
}
} catch (err) {
// 秒传异常,提示用户
notificationService.show({
status: "danger",
title: t(
"home.toolbar.offline_download_enhanced.cas_rapid_upload_failed",
),
description: String(err),
})
} finally {
setRapidUploading(false)
}
// CAS 秒传失败后不自动回退,直接返回让用户决定
return
}
Comment on lines +377 to +388
// 正常离线下载:将 torrent 转为磁力链提交
const buffer = Uint8Array.from(atob(torrentData()), (c) =>
c.charCodeAt(0),
)
const magnetUrl = toMagnetUrl(buffer)
const resp = await offlineDownload(
savePath(),
[magnetUrl],
tool(),
deletePolicy(),
)
handleRespWithNotifySuccess(resp, () => {
if (data.announce) {
params.tr = utf8Decode(data.announce)
}
return `magnet:?xt=urn:btih:${infoHash}&${new URLSearchParams(params).toString()}`
Comment on lines +93 to +101
hidden={() => {
return (
isShare() ||
!oneChecked() ||
selectedObjs().some((o) => o.is_dir) ||
!selectedObjs().every((o) =>
o.name.toLowerCase().endsWith(".torrent"),
)
)
Comment on lines +197 to +212
const isChecked = createMemo(() => {
if (!props.node.isDir) {
return props.selectedFiles.includes(props.node.fileIndex!)
}
const indices = getAllFileIndices(props.node)
return indices.every((i) => props.selectedFiles.includes(i))
})

const isIndeterminate = createMemo(() => {
if (!props.node.isDir) return false
const indices = getAllFileIndices(props.node)
const checkedCount = indices.filter((i) =>
props.selectedFiles.includes(i),
).length
return checkedCount > 0 && checkedCount < indices.length
})
@PIKACHUIM
Copy link
Copy Markdown
Member Author

@copilot apply changes based on the comments in this thread

- Fix torrentUploadParse: remove explicit Content-Type header to allow Axios to set boundary automatically
- Fix toMagnetUrl: remove trailing '&' when no extra params
- Fix handleDrop: branch by activeTab() - BT tab parses torrent, Link tab converts to magnet
- Fix shouldUseCasRapidUpload: add casRapidUploadFailed state to enable fallback after CAS failure
- Fix CAS fallback: after rapid upload failure, set casRapidUploadFailed flag so user can proceed with normal offline download
- Fix file selection: add informational note that partial selection is not yet supported by backend
- Fix context menu: add userCan('offline_download') && objStore.write permission checks for .torrent menu item
- Optimize TorrentFileList: use Set for O(1) selection lookups and cache directory descendant indices
- Add new i18n keys: cas_failed_fallback_hint, file_selection_hint

Agent-Logs-Url: https://github.com/OpenListTeam/OpenList-Frontend/sessions/075a5707-3520-4dea-ada1-b54e89521fb4
Copy link
Copy Markdown
Contributor

Copilot AI commented May 8, 2026

@copilot apply changes based on the comments in this thread

Applied all changes from the review thread in commit 4625d7e:

  1. api.ts: Removed explicit Content-Type: multipart/form-data header from torrentUploadParse so Axios sets the boundary automatically.
  2. toMagnetUrl: Fixed trailing & — now only appends params when non-empty.
  3. handleDrop: Branched by activeTab() — BT tab parses the .torrent file; Link tab converts it to a magnet URL and appends to the textarea.
  4. shouldUseCasRapidUpload: Added casRapidUploadFailed state. After a CAS failure, the flag is set and the tool selector becomes visible, allowing the user to fall back to normal offline download.
  5. CAS fallback: After rapid upload failure, shows a warning hint "CAS failed — you can now choose a download tool and proceed normally." The download no longer silently returns.
  6. File selection: Added an informational note that partial file selection is not yet supported by the backend. Fixed the submit button's disabled to only require a parsed torrent (not a minimum selection count).
  7. Context menu: Added !userCan("offline_download") || !objStore.write to the .torrent menu item's hidden condition.
  8. TorrentFileList: Used a Set for O(1) membership checks in isChecked/isIndeterminate, and cached each directory node's descendant indices during tree building.

Plan 别提交

The .codebuddy directory is already in .gitignore (added in the original commit) and no plan file was committed to the repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants