mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 03:36:37 +08:00
42ef19b7e5
Fixes #65
192 lines
6.1 KiB
Go
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
|
|
}
|