Files
MeBox/internal/service/aria2_adp.go
T
2026-08-10 21:04:04 +08:00

204 lines
5.7 KiB
Go

// Package service — Aria2 下载适配器。
//
// Aria2Adapter 实现了 DownloadAdapter 接口,通过 Aria2 JSON-RPC API
// 管理下载任务。
package service
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"sync"
"time"
)
var errAria2ListUnavailable = errors.New("aria2 task list unavailable")
// Aria2Adapter 是 Aria2 的 DownloadAdapter 实现。
type Aria2Adapter struct {
mu sync.Mutex
cfg DownloadClientConfig
client *http.Client
idSeq int
}
// NewAria2Adapter 创建新的 Aria2 适配器。
func NewAria2Adapter() *Aria2Adapter {
return &Aria2Adapter{
client: NewInternalHTTPClient(20 * time.Second),
}
}
// AddTorrent 通过 URL 添加种子或磁力链接。
func (a *Aria2Adapter) AddTorrent(ctx context.Context, torrentURL, savePath string) (string, error) {
a.mu.Lock()
defer a.mu.Unlock()
// Aria2 addUri 的参数: [secret, [uris], options]
uris := []string{torrentURL}
options := map[string]string{}
if savePath != "" {
options["dir"] = savePath
}
result, err := a.rpcLocked(ctx, "aria2.addUri", []interface{}{uris, options})
if err != nil {
return "", err
}
var gid string
if err := json.Unmarshal(result, &gid); err != nil {
return "", err
}
return gid, nil
}
// AddMagnet 通过磁力链接添加下载。
func (a *Aria2Adapter) AddMagnet(ctx context.Context, magnet, savePath string) (string, error) {
return a.AddTorrent(ctx, magnet, savePath)
}
// AddTorrentFile submits application-fetched .torrent bytes using
// aria2.addTorrent. The empty URI list matches aria2's RPC signature.
func (a *Aria2Adapter) AddTorrentFile(ctx context.Context, data []byte, _ string, savePath string) (string, error) {
a.mu.Lock()
defer a.mu.Unlock()
options := map[string]string{}
if savePath != "" {
options["dir"] = savePath
}
result, err := a.rpcLocked(ctx, "aria2.addTorrent", []interface{}{
base64.StdEncoding.EncodeToString(data),
[]string{},
options,
})
if err != nil {
return "", err
}
var gid string
if err := json.Unmarshal(result, &gid); err != nil {
return "", err
}
return gid, nil
}
// Pause 暂停下载任务(通过 GID)。
func (a *Aria2Adapter) Pause(ctx context.Context, hash string) error {
a.mu.Lock()
defer a.mu.Unlock()
_, err := a.rpcLocked(ctx, "aria2.pause", []interface{}{hash})
return err
}
// Resume 恢复下载任务(通过 GID)。
func (a *Aria2Adapter) Resume(ctx context.Context, hash string) error {
a.mu.Lock()
defer a.mu.Unlock()
_, err := a.rpcLocked(ctx, "aria2.unpause", []interface{}{hash})
return err
}
// Remove 移除下载任务。
func (a *Aria2Adapter) Remove(ctx context.Context, hash string, deleteFiles bool) error {
a.mu.Lock()
defer a.mu.Unlock()
_, removeErr := a.rpcLocked(ctx, "aria2.remove", []interface{}{hash})
_, resultErr := a.rpcLocked(ctx, "aria2.removeDownloadResult", []interface{}{hash})
if removeErr != nil && resultErr != nil {
return errors.Join(removeErr, resultErr)
}
_ = deleteFiles // aria2 RPC removes the task/result but has no delete-local-data flag.
return nil
}
// List 列出所有活动/等待/已停止的任务。
func (a *Aria2Adapter) List(ctx context.Context, filter string) ([]TorrentInfo, error) {
a.mu.Lock()
defer a.mu.Unlock()
var allResults []TorrentInfo
var listErrs []error
var successfulCalls int
// 获取活动任务
active, err := a.rpcLocked(ctx, "aria2.tellActive", []interface{}{
[]string{"gid", "bittorrent", "files", "totalLength", "completedLength", "downloadSpeed", "uploadSpeed", "status", "dir", "numSeeders", "connections", "errorCode"},
})
if err == nil && active != nil {
successfulCalls++
items := a.parseAria2Items(active)
allResults = append(allResults, items...)
} else if err != nil {
listErrs = append(listErrs, err)
}
// 获取等待中的任务
waiting, err := a.rpcLocked(ctx, "aria2.tellWaiting", []interface{}{
0, 100,
[]string{"gid", "bittorrent", "files", "totalLength", "completedLength", "downloadSpeed", "uploadSpeed", "status", "dir", "numSeeders", "connections", "errorCode"},
})
if err == nil && waiting != nil {
successfulCalls++
items := a.parseAria2Items(waiting)
allResults = append(allResults, items...)
} else if err != nil {
listErrs = append(listErrs, err)
}
// 获取已停止的任务
stopped, err := a.rpcLocked(ctx, "aria2.tellStopped", []interface{}{
0, 100,
[]string{"gid", "bittorrent", "files", "totalLength", "completedLength", "downloadSpeed", "uploadSpeed", "status", "dir", "numSeeders", "connections", "errorCode"},
})
if err == nil && stopped != nil {
successfulCalls++
items := a.parseAria2Items(stopped)
allResults = append(allResults, items...)
} else if err != nil {
listErrs = append(listErrs, err)
}
if successfulCalls == 0 {
return nil, fmt.Errorf("%w: %w", errAria2ListUnavailable, errors.Join(listErrs...))
}
if filter != "" {
filtered := make([]TorrentInfo, 0, len(allResults))
for _, item := range allResults {
if strings.EqualFold(item.State, filter) {
filtered = append(filtered, item)
}
}
return filtered, errors.Join(listErrs...)
}
return allResults, errors.Join(listErrs...)
}
// GetInfo 获取单个任务信息。
func (a *Aria2Adapter) GetInfo(ctx context.Context, hash string) (*TorrentInfo, error) {
a.mu.Lock()
defer a.mu.Unlock()
result, err := a.rpcLocked(ctx, "aria2.tellStatus", []interface{}{
hash,
[]string{"gid", "bittorrent", "files", "totalLength", "completedLength", "downloadSpeed", "uploadSpeed", "status", "dir", "numSeeders", "connections"},
})
if err != nil {
return nil, err
}
var item map[string]interface{}
if err := json.Unmarshal(result, &item); err != nil {
return nil, err
}
info := a.parseSingleItem(item)
if info == nil {
return nil, fmt.Errorf("task %s not found", hash)
}
return info, nil
}