mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-08 14:26:37 +08:00
split transmission adapter helpers
This commit is contained in:
@@ -5,32 +5,14 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// transmissionRPCRequest 是 Transmission RPC 请求的通用结构。
|
|
||||||
type transmissionRPCRequest struct {
|
|
||||||
Method string `json:"method"`
|
|
||||||
Arguments map[string]interface{} `json:"arguments"`
|
|
||||||
Tag int `json:"tag,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// transmissionRPCResponse 是 Transmission RPC 响应的通用结构。
|
|
||||||
type transmissionRPCResponse struct {
|
|
||||||
Result string `json:"result"`
|
|
||||||
Arguments map[string]interface{} `json:"arguments"`
|
|
||||||
Tag int `json:"tag"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransmissionAdapter 是 Transmission 的 DownloadAdapter 实现。
|
// TransmissionAdapter 是 Transmission 的 DownloadAdapter 实现。
|
||||||
type TransmissionAdapter struct {
|
type TransmissionAdapter struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -47,115 +29,6 @@ func NewTransmissionAdapter() *TransmissionAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize 配置并初始化 Transmission RPC 连接。
|
|
||||||
func (a *TransmissionAdapter) Initialize(ctx context.Context, cfg DownloadClientConfig) error {
|
|
||||||
a.mu.Lock()
|
|
||||||
defer a.mu.Unlock()
|
|
||||||
endpoint, err := normalizeDownloadClientEndpoint("transmission", cfg.Host)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cfg.Host = endpoint
|
|
||||||
a.cfg = cfg
|
|
||||||
a.sessionID = ""
|
|
||||||
a.tag = 0
|
|
||||||
return a.pingLocked(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping 测试连接。
|
|
||||||
func (a *TransmissionAdapter) Ping(ctx context.Context) error {
|
|
||||||
a.mu.Lock()
|
|
||||||
defer a.mu.Unlock()
|
|
||||||
return a.pingLocked(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// pingLocked 内部 ping 实现(调用者必须持有锁)。
|
|
||||||
func (a *TransmissionAdapter) pingLocked(ctx context.Context) error {
|
|
||||||
rpcURL, err := downloadClientRPCURL("transmission", a.cfg.Host)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req, err := newDownloadClientHTTPRequest(ctx, http.MethodGet, rpcURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if a.cfg.Username != "" {
|
|
||||||
req.SetBasicAuth(a.cfg.Username, a.cfg.Password)
|
|
||||||
}
|
|
||||||
resp, err := a.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
_, _ = io.Copy(io.Discard, resp.Body)
|
|
||||||
if resp.StatusCode == 409 {
|
|
||||||
// 正常:需要 CSRF token
|
|
||||||
a.sessionID = resp.Header.Get("X-Transmission-Session-Id")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
return fmt.Errorf("transmission rpc: %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// rpcLocked 发送 RPC 请求(调用者必须持有锁)。
|
|
||||||
func (a *TransmissionAdapter) rpcLocked(ctx context.Context, method string, args map[string]interface{}) (*transmissionRPCResponse, error) {
|
|
||||||
rpcURL, err := downloadClientRPCURL("transmission", a.cfg.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
a.tag++
|
|
||||||
body, err := json.Marshal(transmissionRPCRequest{
|
|
||||||
Method: method,
|
|
||||||
Arguments: args,
|
|
||||||
Tag: a.tag,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for attempt := 0; attempt < 2; attempt++ {
|
|
||||||
req, err := newDownloadClientHTTPRequest(ctx, http.MethodPost, rpcURL, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
if a.sessionID != "" {
|
|
||||||
req.Header.Set("X-Transmission-Session-Id", a.sessionID)
|
|
||||||
}
|
|
||||||
if a.cfg.Username != "" {
|
|
||||||
req.SetBasicAuth(a.cfg.Username, a.cfg.Password)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := a.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == 409 {
|
|
||||||
a.sessionID = resp.Header.Get("X-Transmission-Session-Id")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
raw, _ := io.ReadAll(resp.Body)
|
|
||||||
return nil, fmt.Errorf("transmission rpc error: %d: %s", resp.StatusCode, string(raw))
|
|
||||||
}
|
|
||||||
|
|
||||||
var result transmissionRPCResponse
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if result.Result != "success" {
|
|
||||||
return nil, fmt.Errorf("transmission rpc result: %s", result.Result)
|
|
||||||
}
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("transmission: failed after CSRF retry")
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTorrent 通过 URL 添加种子。
|
// AddTorrent 通过 URL 添加种子。
|
||||||
func (a *TransmissionAdapter) AddTorrent(ctx context.Context, torrentURL, savePath string) (string, error) {
|
func (a *TransmissionAdapter) AddTorrent(ctx context.Context, torrentURL, savePath string) (string, error) {
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
@@ -326,95 +199,3 @@ func (a *TransmissionAdapter) GetInfo(ctx context.Context, hash string) (*Torren
|
|||||||
}
|
}
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// transmissionStateStr 将 Transmission 状态码转为可读字符串。
|
|
||||||
func transmissionStateStr(status int) string {
|
|
||||||
switch status {
|
|
||||||
case 0:
|
|
||||||
return "stopped"
|
|
||||||
case 1:
|
|
||||||
return "check_pending"
|
|
||||||
case 2:
|
|
||||||
return "checking"
|
|
||||||
case 3:
|
|
||||||
return "download_pending"
|
|
||||||
case 4:
|
|
||||||
return "downloading"
|
|
||||||
case 5:
|
|
||||||
return "seed_pending"
|
|
||||||
case 6:
|
|
||||||
return "seeding"
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// toInt64 安全地将 interface{} 转为 int64。
|
|
||||||
func toInt64(v interface{}) int64 {
|
|
||||||
switch val := v.(type) {
|
|
||||||
case float64:
|
|
||||||
return int64(val)
|
|
||||||
case int:
|
|
||||||
return int64(val)
|
|
||||||
case int64:
|
|
||||||
return val
|
|
||||||
case json.Number:
|
|
||||||
n, _ := val.Int64()
|
|
||||||
return n
|
|
||||||
case string:
|
|
||||||
n, _ := strconv.ParseInt(val, 10, 64)
|
|
||||||
return n
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// toFloat64 安全地将 interface{} 转为 float64。
|
|
||||||
func toFloat64(v interface{}) float64 {
|
|
||||||
switch val := v.(type) {
|
|
||||||
case float64:
|
|
||||||
return val
|
|
||||||
case int:
|
|
||||||
return float64(val)
|
|
||||||
case int64:
|
|
||||||
return float64(val)
|
|
||||||
case json.Number:
|
|
||||||
n, _ := val.Float64()
|
|
||||||
return n
|
|
||||||
case string:
|
|
||||||
n, _ := strconv.ParseFloat(val, 64)
|
|
||||||
return n
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// strVal 安全地提取字符串。
|
|
||||||
func strVal(v interface{}) string {
|
|
||||||
if v == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
s, ok := v.(string)
|
|
||||||
if ok {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%v", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// toJSONLabels 将 Transmission labels 转为逗号分隔字符串。
|
|
||||||
func toJSONLabels(v interface{}) string {
|
|
||||||
if v == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
arr, ok := v.([]interface{})
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
labels := make([]string, 0, len(arr))
|
|
||||||
for _, item := range arr {
|
|
||||||
if s, ok := item.(string); ok {
|
|
||||||
labels = append(labels, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Join(labels, ",")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// transmissionStateStr 将 Transmission 状态码转为可读字符串。
|
||||||
|
func transmissionStateStr(status int) string {
|
||||||
|
switch status {
|
||||||
|
case 0:
|
||||||
|
return "stopped"
|
||||||
|
case 1:
|
||||||
|
return "check_pending"
|
||||||
|
case 2:
|
||||||
|
return "checking"
|
||||||
|
case 3:
|
||||||
|
return "download_pending"
|
||||||
|
case 4:
|
||||||
|
return "downloading"
|
||||||
|
case 5:
|
||||||
|
return "seed_pending"
|
||||||
|
case 6:
|
||||||
|
return "seeding"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toInt64 安全地将 interface{} 转为 int64。
|
||||||
|
func toInt64(v interface{}) int64 {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return int64(val)
|
||||||
|
case int:
|
||||||
|
return int64(val)
|
||||||
|
case int64:
|
||||||
|
return val
|
||||||
|
case json.Number:
|
||||||
|
n, _ := val.Int64()
|
||||||
|
return n
|
||||||
|
case string:
|
||||||
|
n, _ := strconv.ParseInt(val, 10, 64)
|
||||||
|
return n
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toFloat64 安全地将 interface{} 转为 float64。
|
||||||
|
func toFloat64(v interface{}) float64 {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return val
|
||||||
|
case int:
|
||||||
|
return float64(val)
|
||||||
|
case int64:
|
||||||
|
return float64(val)
|
||||||
|
case json.Number:
|
||||||
|
n, _ := val.Float64()
|
||||||
|
return n
|
||||||
|
case string:
|
||||||
|
n, _ := strconv.ParseFloat(val, 64)
|
||||||
|
return n
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// strVal 安全地提取字符串。
|
||||||
|
func strVal(v interface{}) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
s, ok := v.(string)
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// toJSONLabels 将 Transmission labels 转为逗号分隔字符串。
|
||||||
|
func toJSONLabels(v interface{}) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
arr, ok := v.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
labels := make([]string, 0, len(arr))
|
||||||
|
for _, item := range arr {
|
||||||
|
if s, ok := item.(string); ok {
|
||||||
|
labels = append(labels, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(labels, ",")
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// transmissionRPCRequest 是 Transmission RPC 请求的通用结构。
|
||||||
|
type transmissionRPCRequest struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Arguments map[string]interface{} `json:"arguments"`
|
||||||
|
Tag int `json:"tag,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// transmissionRPCResponse 是 Transmission RPC 响应的通用结构。
|
||||||
|
type transmissionRPCResponse struct {
|
||||||
|
Result string `json:"result"`
|
||||||
|
Arguments map[string]interface{} `json:"arguments"`
|
||||||
|
Tag int `json:"tag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize 配置并初始化 Transmission RPC 连接。
|
||||||
|
func (a *TransmissionAdapter) Initialize(ctx context.Context, cfg DownloadClientConfig) error {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
endpoint, err := normalizeDownloadClientEndpoint("transmission", cfg.Host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg.Host = endpoint
|
||||||
|
a.cfg = cfg
|
||||||
|
a.sessionID = ""
|
||||||
|
a.tag = 0
|
||||||
|
return a.pingLocked(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping 测试连接。
|
||||||
|
func (a *TransmissionAdapter) Ping(ctx context.Context) error {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
return a.pingLocked(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pingLocked 内部 ping 实现(调用者必须持有锁)。
|
||||||
|
func (a *TransmissionAdapter) pingLocked(ctx context.Context) error {
|
||||||
|
rpcURL, err := downloadClientRPCURL("transmission", a.cfg.Host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err := newDownloadClientHTTPRequest(ctx, http.MethodGet, rpcURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if a.cfg.Username != "" {
|
||||||
|
req.SetBasicAuth(a.cfg.Username, a.cfg.Password)
|
||||||
|
}
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
|
if resp.StatusCode == 409 {
|
||||||
|
// 正常:需要 CSRF token
|
||||||
|
a.sessionID = resp.Header.Get("X-Transmission-Session-Id")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return fmt.Errorf("transmission rpc: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rpcLocked 发送 RPC 请求(调用者必须持有锁)。
|
||||||
|
func (a *TransmissionAdapter) rpcLocked(ctx context.Context, method string, args map[string]interface{}) (*transmissionRPCResponse, error) {
|
||||||
|
rpcURL, err := downloadClientRPCURL("transmission", a.cfg.Host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tag++
|
||||||
|
body, err := json.Marshal(transmissionRPCRequest{
|
||||||
|
Method: method,
|
||||||
|
Arguments: args,
|
||||||
|
Tag: a.tag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for attempt := 0; attempt < 2; attempt++ {
|
||||||
|
req, err := newDownloadClientHTTPRequest(ctx, http.MethodPost, rpcURL, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if a.sessionID != "" {
|
||||||
|
req.Header.Set("X-Transmission-Session-Id", a.sessionID)
|
||||||
|
}
|
||||||
|
if a.cfg.Username != "" {
|
||||||
|
req.SetBasicAuth(a.cfg.Username, a.cfg.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == 409 {
|
||||||
|
a.sessionID = resp.Header.Get("X-Transmission-Session-Id")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
raw, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("transmission rpc error: %d: %s", resp.StatusCode, string(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result transmissionRPCResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result.Result != "success" {
|
||||||
|
return nil, fmt.Errorf("transmission rpc result: %s", result.Result)
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("transmission: failed after CSRF retry")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user