mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-02 20:26:36 +08:00
42ef19b7e5
Fixes #65
347 lines
9.2 KiB
Go
347 lines
9.2 KiB
Go
// Package service — 下载管理器,管理多个下载客户端适配器。
|
|
//
|
|
// DownloadManager 提供多客户端分发能力,支持运行时热插拔。
|
|
// 调用方通过 GetDefault() 或 GetClient(id) 获取适配器来执行下载操作。
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
|
)
|
|
|
|
// DownloadManager 管理多个下载客户端适配器实例。
|
|
type DownloadManager struct {
|
|
log *zap.Logger
|
|
repo *repository.Container
|
|
crypto *CryptoService
|
|
|
|
mu sync.RWMutex
|
|
clients map[string]DownloadAdapter // clientID -> adapter
|
|
configs map[string]DownloadClientConfig
|
|
models map[string]model.DownloadClient
|
|
order []string
|
|
}
|
|
|
|
// NewDownloadManager 创建新的下载管理器。
|
|
func NewDownloadManager(log *zap.Logger, repo *repository.Container, crypto *CryptoService) *DownloadManager {
|
|
return &DownloadManager{
|
|
log: log,
|
|
repo: repo,
|
|
crypto: crypto,
|
|
clients: make(map[string]DownloadAdapter),
|
|
configs: make(map[string]DownloadClientConfig),
|
|
models: make(map[string]model.DownloadClient),
|
|
}
|
|
}
|
|
|
|
// LoadAll 从数据库加载所有已启用的客户端并初始化适配器。
|
|
func (m *DownloadManager) LoadAll(ctx context.Context) error {
|
|
dbClients, err := m.repo.DownloadClient.ListEnabled(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := m.ensureEnabledDefault(ctx, dbClients); err != nil {
|
|
return err
|
|
}
|
|
|
|
clients := make(map[string]DownloadAdapter, len(dbClients))
|
|
configs := make(map[string]DownloadClientConfig, len(dbClients))
|
|
models := make(map[string]model.DownloadClient, len(dbClients))
|
|
order := make([]string, 0, len(dbClients))
|
|
|
|
for _, dc := range dbClients {
|
|
adapter, cfg, ok := m.initializeClient(ctx, dc)
|
|
if !ok {
|
|
continue
|
|
}
|
|
clients[dc.ID] = adapter
|
|
configs[dc.ID] = cfg
|
|
models[dc.ID] = dc
|
|
order = append(order, dc.ID)
|
|
m.log.Info("download client registered",
|
|
zap.String("id", dc.ID),
|
|
zap.String("name", dc.Name),
|
|
zap.String("type", dc.Type),
|
|
)
|
|
}
|
|
m.mu.Lock()
|
|
m.clients = clients
|
|
m.configs = configs
|
|
m.models = models
|
|
m.order = order
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (m *DownloadManager) ensureEnabledDefault(ctx context.Context, clients []model.DownloadClient) error {
|
|
if len(clients) == 0 {
|
|
return nil
|
|
}
|
|
for i := range clients {
|
|
if clients[i].IsDefault {
|
|
return nil
|
|
}
|
|
}
|
|
if err := m.repo.DownloadClient.SetDefault(ctx, clients[0].ID); err != nil {
|
|
return err
|
|
}
|
|
clients[0].IsDefault = true
|
|
return nil
|
|
}
|
|
|
|
func (m *DownloadManager) initializeClient(ctx context.Context, dc model.DownloadClient) (DownloadAdapter, DownloadClientConfig, bool) {
|
|
cfg, err := m.buildConfig(&dc)
|
|
if err != nil {
|
|
m.log.Warn("failed to build config for download client",
|
|
zap.String("id", dc.ID),
|
|
zap.String("name", dc.Name),
|
|
zap.Error(err))
|
|
return nil, DownloadClientConfig{}, false
|
|
}
|
|
adapter := AdapterFactory(dc.Type)
|
|
if adapter == nil {
|
|
m.log.Warn("unknown download client type",
|
|
zap.String("type", dc.Type),
|
|
zap.String("id", dc.ID))
|
|
return nil, DownloadClientConfig{}, false
|
|
}
|
|
if initErr := adapter.Initialize(ctx, cfg); initErr != nil {
|
|
// Register configured clients even when the external process is still
|
|
// starting; each operation can reconnect once it becomes reachable.
|
|
m.log.Warn("download client init failed; registered for lazy reconnect",
|
|
zap.String("id", dc.ID),
|
|
zap.String("name", dc.Name),
|
|
zap.Error(initErr))
|
|
}
|
|
return adapter, cfg, true
|
|
}
|
|
|
|
// GetDefault 返回默认下载客户端适配器。
|
|
// 如果没有设置默认客户端,返回第一个可用的客户端。
|
|
func (m *DownloadManager) GetDefault(_ context.Context) (*model.DownloadClient, DownloadAdapter, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
for _, id := range m.order {
|
|
client := m.models[id]
|
|
if client.IsDefault {
|
|
if adapter, ok := m.clients[id]; ok {
|
|
copy := client
|
|
return ©, adapter, nil
|
|
}
|
|
}
|
|
}
|
|
for _, id := range m.order {
|
|
if adapter, ok := m.clients[id]; ok {
|
|
client := m.models[id]
|
|
copy := client
|
|
return ©, adapter, nil
|
|
}
|
|
}
|
|
|
|
return nil, nil, errors.New("no download client available")
|
|
}
|
|
|
|
// GetClient 返回指定 ID 的下载客户端适配器。
|
|
func (m *DownloadManager) GetClient(id string) (DownloadAdapter, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
adapter, ok := m.clients[id]
|
|
if !ok {
|
|
return nil, errors.New("download client not found or not initialized")
|
|
}
|
|
return adapter, nil
|
|
}
|
|
|
|
type managedDownloadTarget struct {
|
|
client model.DownloadClient
|
|
adapter DownloadAdapter
|
|
}
|
|
|
|
func (m *DownloadManager) getTarget(id string) (managedDownloadTarget, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
adapter, ok := m.clients[id]
|
|
if !ok {
|
|
return managedDownloadTarget{}, errors.New("download client not found or not initialized")
|
|
}
|
|
client, ok := m.models[id]
|
|
if !ok {
|
|
return managedDownloadTarget{}, errors.New("download client metadata not found")
|
|
}
|
|
return managedDownloadTarget{client: client, adapter: adapter}, nil
|
|
}
|
|
|
|
func (m *DownloadManager) targets() []managedDownloadTarget {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
out := make([]managedDownloadTarget, 0, len(m.order))
|
|
for _, id := range m.order {
|
|
adapter, ok := m.clients[id]
|
|
if !ok {
|
|
continue
|
|
}
|
|
client, ok := m.models[id]
|
|
if !ok {
|
|
continue
|
|
}
|
|
out = append(out, managedDownloadTarget{client: client, adapter: adapter})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (m *DownloadManager) hasClients() bool {
|
|
if m == nil {
|
|
return false
|
|
}
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return len(m.clients) > 0
|
|
}
|
|
|
|
// AddClient 动态添加并初始化一个下载客户端。
|
|
func (m *DownloadManager) AddClient(ctx context.Context, dc *model.DownloadClient) error {
|
|
cfg, err := m.buildConfig(dc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
adapter := AdapterFactory(dc.Type)
|
|
if adapter == nil {
|
|
return errors.New("unknown download client type: " + dc.Type)
|
|
}
|
|
|
|
if err := adapter.Initialize(ctx, cfg); err != nil {
|
|
return err
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for i, current := range m.order {
|
|
if current == dc.ID {
|
|
m.order = append(m.order[:i], m.order[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
m.clients[dc.ID] = adapter
|
|
m.configs[dc.ID] = cfg
|
|
m.models[dc.ID] = *dc
|
|
m.order = append(m.order, dc.ID)
|
|
return nil
|
|
}
|
|
|
|
// RemoveClient 移除一个下载客户端(停止适配器,不删除数据库记录)。
|
|
func (m *DownloadManager) RemoveClient(id string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.clients, id)
|
|
delete(m.configs, id)
|
|
delete(m.models, id)
|
|
for i, current := range m.order {
|
|
if current == id {
|
|
m.order = append(m.order[:i], m.order[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// UpdateClient 更新已有客户端的配置并重新初始化。
|
|
func (m *DownloadManager) UpdateClient(ctx context.Context, dc *model.DownloadClient) error {
|
|
m.RemoveClient(dc.ID)
|
|
return m.AddClient(ctx, dc)
|
|
}
|
|
|
|
// TestConnection 测试客户端连接。
|
|
func (m *DownloadManager) TestConnection(ctx context.Context, dc *model.DownloadClient) error {
|
|
cfg, err := m.buildConfig(dc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
adapter := AdapterFactory(dc.Type)
|
|
if adapter == nil {
|
|
return errors.New("unknown download client type: " + dc.Type)
|
|
}
|
|
|
|
return adapter.Initialize(ctx, cfg)
|
|
}
|
|
|
|
// ListAll 获取所有已加载客户端的种子列表。
|
|
func (m *DownloadManager) ListAll(ctx context.Context, filter string) (map[string][]TorrentInfo, error) {
|
|
result := make(map[string][]TorrentInfo)
|
|
var listErrs []error
|
|
for _, target := range m.targets() {
|
|
list, err := target.adapter.List(ctx, filter)
|
|
if err != nil {
|
|
m.log.Warn("failed to list torrents from client",
|
|
zap.String("id", target.client.ID),
|
|
zap.Error(err),
|
|
)
|
|
listErrs = append(listErrs, fmt.Errorf("%s (%s): %w", target.client.Name, target.client.Type, err))
|
|
}
|
|
if len(list) > 0 || err == nil {
|
|
result[target.client.ID] = list
|
|
}
|
|
}
|
|
return result, errors.Join(listErrs...)
|
|
}
|
|
|
|
// GetAdapterTypes 返回支持的下载客户端类型列表。
|
|
func (m *DownloadManager) GetAdapterTypes() []AdapterTypeInfo {
|
|
return []AdapterTypeInfo{
|
|
{Type: "qbittorrent", Name: "qBittorrent", Description: "qBittorrent WebUI API (v2)"},
|
|
{Type: "transmission", Name: "Transmission", Description: "Transmission RPC API"},
|
|
{Type: "aria2", Name: "Aria2", Description: "Aria2 JSON-RPC API"},
|
|
}
|
|
}
|
|
|
|
// AdapterTypeInfo 描述下载客户端类型信息。
|
|
type AdapterTypeInfo struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// buildConfig 从数据库模型构建适配器配置。
|
|
func (m *DownloadManager) buildConfig(dc *model.DownloadClient) (DownloadClientConfig, error) {
|
|
password := dc.Password
|
|
if m.crypto != nil && password != "" {
|
|
password = m.crypto.Decrypt(password)
|
|
}
|
|
host, err := normalizeDownloadClientEndpoint(dc.Type, dc.Host)
|
|
if err != nil {
|
|
return DownloadClientConfig{}, err
|
|
}
|
|
|
|
cfg := DownloadClientConfig{
|
|
Host: host,
|
|
Username: dc.Username,
|
|
Password: password,
|
|
}
|
|
|
|
// 解析 Extra JSON 配置
|
|
if dc.Extra != "" {
|
|
extraStr := dc.Extra
|
|
if m.crypto != nil {
|
|
extraStr = m.crypto.Decrypt(extraStr)
|
|
}
|
|
var extra map[string]string
|
|
if err := json.Unmarshal([]byte(extraStr), &extra); err == nil {
|
|
cfg.Extra = extra
|
|
}
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|