mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-01 03:56:38 +08:00
202 lines
5.5 KiB
Go
202 lines
5.5 KiB
Go
// Package service — Transmission 下载适配器。
|
|
//
|
|
// TransmissionAdapter 实现了 DownloadAdapter 接口,通过 Transmission RPC API
|
|
// 管理下载任务。
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// TransmissionAdapter 是 Transmission 的 DownloadAdapter 实现。
|
|
type TransmissionAdapter struct {
|
|
mu sync.Mutex
|
|
cfg DownloadClientConfig
|
|
client *http.Client
|
|
tag int
|
|
sessionID string
|
|
}
|
|
|
|
// NewTransmissionAdapter 创建新的 Transmission 适配器。
|
|
func NewTransmissionAdapter() *TransmissionAdapter {
|
|
return &TransmissionAdapter{
|
|
client: NewInternalHTTPClient(20 * time.Second),
|
|
}
|
|
}
|
|
|
|
// AddTorrent 通过 URL 添加种子。
|
|
func (a *TransmissionAdapter) AddTorrent(ctx context.Context, torrentURL, savePath string) (string, error) {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
args := map[string]interface{}{"filename": torrentURL}
|
|
if savePath != "" {
|
|
args["download-dir"] = savePath
|
|
}
|
|
resp, err := a.rpcLocked(ctx, "torrent-add", args)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if added, ok := resp.Arguments["torrent-added"].(map[string]interface{}); ok {
|
|
if hashStr, ok := added["hashString"].(string); ok {
|
|
return hashStr, nil
|
|
}
|
|
}
|
|
if dup, ok := resp.Arguments["torrent-duplicate"].(map[string]interface{}); ok {
|
|
if hashStr, ok := dup["hashString"].(string); ok {
|
|
return hashStr, nil
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
// AddMagnet 通过磁力链接添加种子。
|
|
func (a *TransmissionAdapter) AddMagnet(ctx context.Context, magnet, savePath string) (string, error) {
|
|
return a.AddTorrent(ctx, magnet, savePath)
|
|
}
|
|
|
|
// Pause 暂停种子。
|
|
func (a *TransmissionAdapter) Pause(ctx context.Context, hash string) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
_, err := a.rpcLocked(ctx, "torrent-stop", map[string]interface{}{
|
|
"ids": []string{hash},
|
|
})
|
|
return err
|
|
}
|
|
|
|
// Resume 恢复种子。
|
|
func (a *TransmissionAdapter) Resume(ctx context.Context, hash string) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
_, err := a.rpcLocked(ctx, "torrent-start", map[string]interface{}{
|
|
"ids": []string{hash},
|
|
})
|
|
return err
|
|
}
|
|
|
|
// Remove 删除种子。
|
|
func (a *TransmissionAdapter) Remove(ctx context.Context, hash string, deleteFiles bool) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
_, err := a.rpcLocked(ctx, "torrent-remove", map[string]interface{}{
|
|
"ids": []string{hash},
|
|
"delete-local-data": deleteFiles,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// List 列出种子。
|
|
func (a *TransmissionAdapter) List(ctx context.Context, filter string) ([]TorrentInfo, error) {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
args := map[string]interface{}{
|
|
"fields": []string{
|
|
"hashString", "name", "totalSize", "percentDone",
|
|
"rateDownload", "rateUpload", "status", "downloadDir",
|
|
"peersSendingToUs", "peersGettingFromUs", "addedDate",
|
|
"labels", "isStalled",
|
|
},
|
|
}
|
|
resp, err := a.rpcLocked(ctx, "torrent-get", args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
torrentsRaw, ok := resp.Arguments["torrents"].([]interface{})
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
result := make([]TorrentInfo, 0, len(torrentsRaw))
|
|
for _, tr := range torrentsRaw {
|
|
t, ok := tr.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
hash, _ := t["hashString"].(string)
|
|
name, _ := t["name"].(string)
|
|
size := toInt64(t["totalSize"])
|
|
progress := toFloat64(t["percentDone"])
|
|
dlSpeed := toInt64(t["rateDownload"])
|
|
upSpeed := toInt64(t["rateUpload"])
|
|
savePath, _ := t["downloadDir"].(string)
|
|
numSeeds := int(toInt64(t["peersSendingToUs"]))
|
|
numLeechs := int(toInt64(t["peersGettingFromUs"]))
|
|
addedOn := int64(toFloat64(t["addedDate"]))
|
|
|
|
// Transmission 状态码转字符串
|
|
status := int(toFloat64(t["status"]))
|
|
state := transmissionStateStr(status)
|
|
|
|
// 过滤
|
|
if filter != "" && !strings.EqualFold(state, filter) {
|
|
continue
|
|
}
|
|
|
|
result = append(result, TorrentInfo{
|
|
Hash: hash,
|
|
Name: name,
|
|
Size: size,
|
|
Progress: progress * 100,
|
|
DLSpeed: dlSpeed,
|
|
UPSpeed: upSpeed,
|
|
State: state,
|
|
SavePath: savePath,
|
|
NumSeeds: numSeeds,
|
|
NumLeechs: numLeechs,
|
|
AddedOn: time.Unix(addedOn, 0),
|
|
Tags: toJSONLabels(t["labels"]),
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// GetInfo 获取单个种子信息。
|
|
func (a *TransmissionAdapter) GetInfo(ctx context.Context, hash string) (*TorrentInfo, error) {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
args := map[string]interface{}{
|
|
"ids": []string{hash},
|
|
"fields": []string{
|
|
"hashString", "name", "totalSize", "percentDone",
|
|
"rateDownload", "rateUpload", "status", "downloadDir",
|
|
"peersSendingToUs", "peersGettingFromUs", "addedDate", "labels",
|
|
},
|
|
}
|
|
resp, err := a.rpcLocked(ctx, "torrent-get", args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
torrentsRaw, ok := resp.Arguments["torrents"].([]interface{})
|
|
if !ok || len(torrentsRaw) == 0 {
|
|
return nil, fmt.Errorf("torrent %s not found", hash)
|
|
}
|
|
t, ok := torrentsRaw[0].(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("torrent %s: invalid response", hash)
|
|
}
|
|
|
|
status := int(toFloat64(t["status"]))
|
|
info := &TorrentInfo{
|
|
Hash: hash,
|
|
Name: strVal(t["name"]),
|
|
Size: toInt64(t["totalSize"]),
|
|
Progress: toFloat64(t["percentDone"]) * 100,
|
|
DLSpeed: toInt64(t["rateDownload"]),
|
|
UPSpeed: toInt64(t["rateUpload"]),
|
|
State: transmissionStateStr(status),
|
|
SavePath: strVal(t["downloadDir"]),
|
|
NumSeeds: int(toInt64(t["peersSendingToUs"])),
|
|
NumLeechs: int(toInt64(t["peersGettingFromUs"])),
|
|
AddedOn: time.Unix(int64(toFloat64(t["addedDate"])), 0),
|
|
Tags: toJSONLabels(t["labels"]),
|
|
}
|
|
return info, nil
|
|
}
|