fix(downloads): support Transmission and aria2 end to end

Fixes #65
This commit is contained in:
ShukeBta
2026-08-10 21:04:04 +08:00
parent c0d743d89d
commit 42ef19b7e5
43 changed files with 2350 additions and 480 deletions
+79 -8
View File
@@ -7,6 +7,7 @@ package service
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"mime/multipart"
@@ -61,6 +62,12 @@ func (a *QBitAdapter) Ping(ctx context.Context) error {
// AddTorrent 通过 URL 添加种子。
func (a *QBitAdapter) AddTorrent(ctx context.Context, torrentURL, savePath string) (string, error) {
return a.AddTorrentWithCategory(ctx, torrentURL, savePath, "")
}
// AddTorrentWithCategory submits a URL or magnet while preserving the
// qBittorrent category selected by MediaStationGo's classification rules.
func (a *QBitAdapter) AddTorrentWithCategory(ctx context.Context, torrentURL, savePath, category string) (string, error) {
a.mu.Lock()
defer a.mu.Unlock()
if err := a.ensureAuthLocked(ctx); err != nil {
@@ -70,10 +77,49 @@ func (a *QBitAdapter) AddTorrent(ctx context.Context, torrentURL, savePath strin
body := &bytes.Buffer{}
w := multipart.NewWriter(body)
_ = w.WriteField("urls", torrentURL)
return a.addTorrentMultipartLocked(ctx, body, w, savePath, category, torrentURLInfoHash(torrentURL))
}
// AddMagnet 通过磁力链接添加种子。
func (a *QBitAdapter) AddMagnet(ctx context.Context, magnet, savePath string) (string, error) {
return a.AddTorrent(ctx, magnet, savePath)
}
// AddTorrentFile uploads .torrent bytes as multipart/form-data.
func (a *QBitAdapter) AddTorrentFile(ctx context.Context, data []byte, name, savePath string) (string, error) {
return a.AddTorrentFileWithCategory(ctx, data, name, savePath, "")
}
// AddTorrentFileWithCategory uploads .torrent bytes and preserves the
// qBittorrent category selected by the caller.
func (a *QBitAdapter) AddTorrentFileWithCategory(ctx context.Context, data []byte, name, savePath, category string) (string, error) {
a.mu.Lock()
defer a.mu.Unlock()
if err := a.ensureAuthLocked(ctx); err != nil {
return "", err
}
body := &bytes.Buffer{}
w := multipart.NewWriter(body)
part, err := w.CreateFormFile("torrents", name)
if err != nil {
return "", err
}
if _, err := part.Write(data); err != nil {
return "", err
}
return a.addTorrentMultipartLocked(ctx, body, w, savePath, category, torrentInfoHash(data))
}
func (a *QBitAdapter) addTorrentMultipartLocked(ctx context.Context, body *bytes.Buffer, w *multipart.Writer, savePath, category, externalID string) (string, error) {
if savePath != "" {
_ = w.WriteField("savepath", savePath)
}
_ = w.Close()
if category != "" {
_ = w.WriteField("category", category)
}
if err := w.Close(); err != nil {
return "", err
}
baseURL := strings.TrimRight(a.cfg.Host, "/")
req, err := newDownloadClientHTTPRequest(ctx, http.MethodPost,
@@ -95,14 +141,9 @@ func (a *QBitAdapter) AddTorrent(ctx context.Context, torrentURL, savePath strin
return "", fmt.Errorf("qbittorrent add torrent: %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
}
if strings.EqualFold(strings.TrimSpace(string(raw)), "Fails.") {
return "", fmt.Errorf("qbittorrent add torrent: rejected by downloader")
return externalID, errors.New("qbittorrent add torrent: rejected by downloader")
}
return "", nil
}
// AddMagnet 通过磁力链接添加种子。
func (a *QBitAdapter) AddMagnet(ctx context.Context, magnet, savePath string) (string, error) {
return a.AddTorrent(ctx, magnet, savePath)
return externalID, nil
}
// Pause 暂停种子。
@@ -193,6 +234,36 @@ func (a *QBitAdapter) Remove(ctx context.Context, hash string, deleteFiles bool)
return nil
}
func (a *QBitAdapter) Relocate(ctx context.Context, hash, location string) error {
a.mu.Lock()
defer a.mu.Unlock()
if err := a.ensureAuthLocked(ctx); err != nil {
return err
}
baseURL := strings.TrimRight(a.cfg.Host, "/")
form := url.Values{}
form.Set("hashes", hash)
form.Set("location", location)
req, err := newDownloadClientHTTPRequest(ctx, http.MethodPost,
baseURL+"/api/v2/torrents/setLocation", strings.NewReader(form.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Referer", baseURL)
req.Header.Set("Origin", baseURL)
resp, err := a.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
return fmt.Errorf("qbittorrent setLocation: %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
return nil
}
// loginLocked 执行登录(调用者必须持有锁)。
func (a *QBitAdapter) loginLocked(ctx context.Context) error {
if a.cfg.Host == "" {