171 lines
5.0 KiB
Go
171 lines
5.0 KiB
Go
package torrent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type QbittorrentClient struct {
|
|
baseURL string
|
|
client *http.Client
|
|
sid string
|
|
}
|
|
|
|
func NewQbittorrentClient(baseURL string) *QbittorrentClient {
|
|
return &QbittorrentClient{
|
|
baseURL: strings.TrimRight(baseURL, "/"),
|
|
client: &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (c *QbittorrentClient) Login(username string, password string) (string, error) {
|
|
log.Trace().Str("url", c.baseURL).Str("username", username).Msg("qbittorrent login attempt")
|
|
|
|
data := url.Values{
|
|
"username": {username},
|
|
"password": {password},
|
|
}
|
|
|
|
start := time.Now()
|
|
resp, err := c.client.PostForm(c.baseURL+"/api/v2/auth/login", data)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("url", c.baseURL).Msg("qbittorrent login request failed")
|
|
return "", fmt.Errorf("login request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
log.Trace().Int("status", resp.StatusCode).Dur("duration", time.Since(start)).Msg("qbittorrent login response")
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("qbittorrent reading login response failed")
|
|
return "", fmt.Errorf("reading login response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode == http.StatusForbidden {
|
|
log.Warn().Msg("qbittorrent login forbidden: too many failed attempts")
|
|
return "", fmt.Errorf("login forbidden: too many failed attempts")
|
|
}
|
|
|
|
if string(body) != "Ok." {
|
|
log.Warn().Str("response", string(body)).Msg("qbittorrent login rejected")
|
|
return "", fmt.Errorf("login failed: %s", string(body))
|
|
}
|
|
|
|
for _, cookie := range resp.Cookies() {
|
|
if cookie.Name == "SID" {
|
|
c.sid = cookie.Value
|
|
log.Info().Str("url", c.baseURL).Msg("qbittorrent login successful")
|
|
log.Trace().Str("sid", c.sid).Msg("qbittorrent session ID acquired")
|
|
return c.sid, nil
|
|
}
|
|
}
|
|
|
|
log.Error().Msg("qbittorrent login succeeded but no SID cookie returned")
|
|
return "", fmt.Errorf("login succeeded but no SID cookie returned")
|
|
}
|
|
|
|
func (c *QbittorrentClient) List() ([]TorrentInfo, error) {
|
|
log.Trace().Msg("qbittorrent listing torrents")
|
|
|
|
req, err := http.NewRequest("GET", c.baseURL+"/api/v2/torrents/info", nil)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("qbittorrent creating list request failed")
|
|
return nil, fmt.Errorf("creating list request: %w", err)
|
|
}
|
|
req.AddCookie(&http.Cookie{Name: "SID", Value: c.sid})
|
|
|
|
start := time.Now()
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("qbittorrent list request failed")
|
|
return nil, fmt.Errorf("list request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
log.Trace().Int("status", resp.StatusCode).Dur("duration", time.Since(start)).Msg("qbittorrent list response")
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
log.Error().Int("status", resp.StatusCode).Msg("qbittorrent list returned non-OK status")
|
|
return nil, fmt.Errorf("list request returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var items []QbittorrentListItem
|
|
if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
|
|
log.Error().Err(err).Msg("qbittorrent decoding list response failed")
|
|
return nil, fmt.Errorf("decoding list response: %w", err)
|
|
}
|
|
|
|
torrents := make([]TorrentInfo, len(items))
|
|
for i, item := range items {
|
|
torrents[i] = item.toTorrentInfo()
|
|
}
|
|
|
|
log.Debug().Int("count", len(torrents)).Msg("qbittorrent torrents listed")
|
|
|
|
return torrents, nil
|
|
}
|
|
|
|
type QbittorrentListItem struct {
|
|
Hash string `json:"hash"`
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
Progress float64 `json:"progress"`
|
|
DlSpeed int64 `json:"dlspeed"`
|
|
UpSpeed int64 `json:"upspeed"`
|
|
NumSeeds int32 `json:"num_seeds"`
|
|
NumLeechs int32 `json:"num_leechs"`
|
|
State string `json:"state"`
|
|
ETA int64 `json:"eta"`
|
|
Ratio float64 `json:"ratio"`
|
|
Category string `json:"category"`
|
|
Tags string `json:"tags"`
|
|
AddedOn int64 `json:"added_on"`
|
|
CompletionOn int64 `json:"completion_on"`
|
|
SavePath string `json:"save_path"`
|
|
ContentPath string `json:"content_path"`
|
|
Downloaded int64 `json:"downloaded"`
|
|
Uploaded int64 `json:"uploaded"`
|
|
Tracker string `json:"tracker"`
|
|
SeedingTime int64 `json:"seeding_time"`
|
|
AmountLeft int64 `json:"amount_left"`
|
|
Availability float64 `json:"availability"`
|
|
}
|
|
|
|
func (t *QbittorrentListItem) toTorrentInfo() TorrentInfo {
|
|
return TorrentInfo{
|
|
Hash: t.Hash,
|
|
Name: t.Name,
|
|
Size: t.Size,
|
|
Progress: t.Progress,
|
|
DlSpeed: t.DlSpeed,
|
|
UpSpeed: t.UpSpeed,
|
|
NumSeeds: t.NumSeeds,
|
|
NumLeechs: t.NumLeechs,
|
|
State: t.State,
|
|
ETA: t.ETA,
|
|
Ratio: t.Ratio,
|
|
Category: t.Category,
|
|
Tags: t.Tags,
|
|
AddedOn: t.AddedOn,
|
|
CompletionOn: t.CompletionOn,
|
|
SavePath: t.SavePath,
|
|
ContentPath: t.ContentPath,
|
|
Downloaded: t.Downloaded,
|
|
Uploaded: t.Uploaded,
|
|
Tracker: t.Tracker,
|
|
SeedingTime: t.SeedingTime,
|
|
AmountLeft: t.AmountLeft,
|
|
Availability: t.Availability,
|
|
}
|
|
}
|