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

192 lines
6.1 KiB
Go

package service
import (
"context"
"encoding/base32"
"encoding/hex"
"errors"
"fmt"
"net/url"
"strings"
)
const legacyQBitDownloadClientID = "legacy-qbittorrent"
type downloadTarget struct {
clientID string
typ string
adapter DownloadAdapter
legacyQB bool
}
func (d *DownloadService) listLiveTorrents(ctx context.Context, filter string) ([]QBitTorrent, error) {
if d != nil && d.manager != nil && d.manager.hasClients() {
var live []QBitTorrent
var listErrs []error
for _, target := range d.manager.targets() {
items, err := target.adapter.List(ctx, filter)
if err != nil {
listErrs = append(listErrs, fmt.Errorf("%s (%s): %w", target.client.Name, target.client.Type, err))
}
for _, item := range items {
torrent := TorrentInfoToQBit(item)
torrent.ClientID = target.client.ID
torrent.Source = target.client.Type
live = append(live, torrent)
}
}
return live, errors.Join(listErrs...)
}
if d != nil && d.qb != nil && d.qb.IsConfigured() {
live, err := d.qb.List(ctx, filter)
for i := range live {
live[i].ClientID = legacyQBitDownloadClientID
live[i].Source = "qbittorrent"
live[i].Progress = float32(normalizedTorrentProgress(float64(live[i].Progress)))
live[i].State = canonicalTorrentState(live[i].State, float64(live[i].Progress))
}
return live, err
}
return nil, errors.New("no download client available")
}
func torrentURLInfoHash(raw string) string {
parsed, err := url.Parse(strings.TrimSpace(raw))
if err != nil || !strings.EqualFold(parsed.Scheme, "magnet") {
return ""
}
for _, xt := range parsed.Query()["xt"] {
const prefix = "urn:btih:"
if strings.HasPrefix(strings.ToLower(xt), prefix) {
return normalizeTorrentInfoHash(xt[len(prefix):])
}
}
return ""
}
func normalizeTorrentInfoHash(value string) string {
value = strings.TrimSpace(value)
if len(value) == 40 {
if _, err := hex.DecodeString(value); err == nil {
return strings.ToLower(value)
}
}
if len(value) == 32 {
decoded, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ToUpper(value))
if err == nil && len(decoded) == 20 {
return hex.EncodeToString(decoded)
}
}
return strings.ToLower(value)
}
func (d *DownloadService) defaultDownloadTarget(ctx context.Context) (downloadTarget, error) {
if d != nil && d.manager != nil {
client, adapter, err := d.manager.GetDefault(ctx)
if err == nil && client != nil && adapter != nil {
return downloadTarget{clientID: client.ID, typ: client.Type, adapter: adapter}, nil
}
}
if d != nil && d.qb != nil && d.qb.IsConfigured() {
return downloadTarget{clientID: legacyQBitDownloadClientID, typ: "qbittorrent", legacyQB: true}, nil
}
return downloadTarget{}, errors.New("no default downloader configured: 请在下载客户端中配置并启用默认下载器")
}
func (d *DownloadService) downloadTargetByID(ctx context.Context, clientID string) (downloadTarget, error) {
clientID = strings.TrimSpace(clientID)
if clientID == "" {
return d.defaultDownloadTarget(ctx)
}
if clientID == legacyQBitDownloadClientID {
if d != nil && d.qb != nil && d.qb.IsConfigured() {
return downloadTarget{clientID: legacyQBitDownloadClientID, typ: "qbittorrent", legacyQB: true}, nil
}
return downloadTarget{}, errors.New("legacy qbittorrent is not configured")
}
if d != nil && d.manager != nil {
target, err := d.manager.getTarget(clientID)
if err == nil {
return downloadTarget{clientID: target.client.ID, typ: target.client.Type, adapter: target.adapter}, nil
}
}
return downloadTarget{}, errors.New("download client not found or disabled")
}
func (d *DownloadService) resolveOperationClientID(ctx context.Context, externalID, requestedClientID string) (string, string, error) {
requestedClientID = strings.TrimSpace(requestedClientID)
live, listErr := d.listLiveTorrents(ctx, "")
if clientID, name, err, resolved := resolveLiveOperationClient(live, externalID, requestedClientID); resolved {
return clientID, name, err
}
if requestedClientID != "" {
return requestedClientID, "", nil
}
if clientID, err := d.persistedOperationClientID(ctx, externalID); clientID != "" || err != nil {
return clientID, "", err
}
if clientID, err := d.singleAvailableOperationClientID(); clientID != "" || err != nil {
return clientID, "", err
}
if d != nil && d.qb != nil && d.qb.IsConfigured() {
return legacyQBitDownloadClientID, "", nil
}
if listErr != nil {
return "", "", listErr
}
return "", "", errors.New("download client not found")
}
func resolveLiveOperationClient(live []QBitTorrent, externalID, requestedClientID string) (string, string, error, bool) {
var matched []QBitTorrent
for _, torrent := range live {
if requestedClientID != "" && torrent.ClientID != requestedClientID {
continue
}
if strings.EqualFold(strings.TrimSpace(torrent.Hash), strings.TrimSpace(externalID)) {
matched = append(matched, torrent)
}
}
if len(matched) == 1 {
return matched[0].ClientID, matched[0].Name, nil, true
}
if len(matched) > 1 && requestedClientID == "" {
return "", "", errors.New("multiple download clients contain this task; client_id is required"), true
}
return "", "", nil, false
}
func (d *DownloadService) persistedOperationClientID(ctx context.Context, externalID string) (string, error) {
if d != nil && d.repo != nil && d.repo.Download != nil {
rows, err := d.repo.Download.List(ctx)
if err != nil {
return "", err
}
var clientID string
for _, row := range rows {
if !strings.EqualFold(strings.TrimSpace(row.ExternalID), strings.TrimSpace(externalID)) || strings.TrimSpace(row.DownloadClientID) == "" {
continue
}
if clientID != "" && clientID != row.DownloadClientID {
return "", errors.New("multiple download clients contain this task; client_id is required")
}
clientID = row.DownloadClientID
}
return clientID, nil
}
return "", nil
}
func (d *DownloadService) singleAvailableOperationClientID() (string, error) {
if d != nil && d.manager != nil {
targets := d.manager.targets()
if len(targets) == 1 {
return targets[0].client.ID, nil
}
if len(targets) > 1 {
return "", errors.New("client_id is required when multiple download clients are enabled")
}
}
return "", nil
}