Spaces:
Runtime error
Runtime error
package qbittorrent | |
import ( | |
"bytes" | |
"errors" | |
"io" | |
"mime/multipart" | |
"net/http" | |
"net/http/cookiejar" | |
"net/url" | |
"github.com/alist-org/alist/v3/pkg/utils" | |
) | |
type Client interface { | |
AddFromLink(link string, savePath string, id string) error | |
GetInfo(id string) (TorrentInfo, error) | |
GetFiles(id string) ([]FileInfo, error) | |
Delete(id string, deleteFiles bool) error | |
} | |
type client struct { | |
url *url.URL | |
client http.Client | |
Client | |
} | |
func New(webuiUrl string) (Client, error) { | |
u, err := url.Parse(webuiUrl) | |
if err != nil { | |
return nil, err | |
} | |
jar, err := cookiejar.New(nil) | |
if err != nil { | |
return nil, err | |
} | |
var c = &client{ | |
url: u, | |
client: http.Client{Jar: jar}, | |
} | |
err = c.checkAuthorization() | |
if err != nil { | |
return nil, err | |
} | |
return c, nil | |
} | |
func (c *client) checkAuthorization() error { | |
// check authorization | |
if c.authorized() { | |
return nil | |
} | |
// check authorization after logging in | |
err := c.login() | |
if err != nil { | |
return err | |
} | |
if c.authorized() { | |
return nil | |
} | |
return errors.New("unauthorized qbittorrent url") | |
} | |
func (c *client) authorized() bool { | |
resp, err := c.post("/api/v2/app/version", nil) | |
if err != nil { | |
return false | |
} | |
return resp.StatusCode == 200 // the status code will be 403 if not authorized | |
} | |
func (c *client) login() error { | |
// prepare HTTP request | |
v := url.Values{} | |
v.Set("username", c.url.User.Username()) | |
passwd, _ := c.url.User.Password() | |
v.Set("password", passwd) | |
resp, err := c.post("/api/v2/auth/login", v) | |
if err != nil { | |
return err | |
} | |
// check result | |
body := make([]byte, 2) | |
_, err = resp.Body.Read(body) | |
if err != nil { | |
return err | |
} | |
if string(body) != "Ok" { | |
return errors.New("failed to login into qBittorrent webui with url: " + c.url.String()) | |
} | |
return nil | |
} | |
func (c *client) post(path string, data url.Values) (*http.Response, error) { | |
u := c.url.JoinPath(path) | |
u.User = nil // remove userinfo for requests | |
req, err := http.NewRequest("POST", u.String(), bytes.NewReader([]byte(data.Encode()))) | |
if err != nil { | |
return nil, err | |
} | |
if data != nil { | |
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | |
} | |
resp, err := c.client.Do(req) | |
if err != nil { | |
return nil, err | |
} | |
if resp.Cookies() != nil { | |
c.client.Jar.SetCookies(u, resp.Cookies()) | |
} | |
return resp, nil | |
} | |
func (c *client) AddFromLink(link string, savePath string, id string) error { | |
err := c.checkAuthorization() | |
if err != nil { | |
return err | |
} | |
buf := new(bytes.Buffer) | |
writer := multipart.NewWriter(buf) | |
addField := func(name string, value string) { | |
if err != nil { | |
return | |
} | |
err = writer.WriteField(name, value) | |
} | |
addField("urls", link) | |
addField("savepath", savePath) | |
addField("tags", "alist-"+id) | |
addField("autoTMM", "false") | |
if err != nil { | |
return err | |
} | |
err = writer.Close() | |
if err != nil { | |
return err | |
} | |
u := c.url.JoinPath("/api/v2/torrents/add") | |
u.User = nil // remove userinfo for requests | |
req, err := http.NewRequest("POST", u.String(), buf) | |
if err != nil { | |
return err | |
} | |
req.Header.Add("Content-Type", writer.FormDataContentType()) | |
resp, err := c.client.Do(req) | |
if err != nil { | |
return err | |
} | |
// check result | |
body := make([]byte, 2) | |
_, err = resp.Body.Read(body) | |
if err != nil { | |
return err | |
} | |
if resp.StatusCode != 200 || string(body) != "Ok" { | |
return errors.New("failed to add qBittorrent task: " + link) | |
} | |
return nil | |
} | |
type TorrentStatus string | |
const ( | |
ERROR TorrentStatus = "error" | |
MISSINGFILES TorrentStatus = "missingFiles" | |
UPLOADING TorrentStatus = "uploading" | |
PAUSEDUP TorrentStatus = "pausedUP" | |
QUEUEDUP TorrentStatus = "queuedUP" | |
STALLEDUP TorrentStatus = "stalledUP" | |
CHECKINGUP TorrentStatus = "checkingUP" | |
FORCEDUP TorrentStatus = "forcedUP" | |
ALLOCATING TorrentStatus = "allocating" | |
DOWNLOADING TorrentStatus = "downloading" | |
METADL TorrentStatus = "metaDL" | |
PAUSEDDL TorrentStatus = "pausedDL" | |
QUEUEDDL TorrentStatus = "queuedDL" | |
STALLEDDL TorrentStatus = "stalledDL" | |
CHECKINGDL TorrentStatus = "checkingDL" | |
FORCEDDL TorrentStatus = "forcedDL" | |
CHECKINGRESUMEDATA TorrentStatus = "checkingResumeData" | |
MOVING TorrentStatus = "moving" | |
UNKNOWN TorrentStatus = "unknown" | |
) | |
// https://github.com/DGuang21/PTGo/blob/main/app/client/client_distributer.go | |
type TorrentInfo struct { | |
AddedOn int `json:"added_on"` // 将 torrent 添加到客户端的时间(Unix Epoch) | |
AmountLeft int64 `json:"amount_left"` // 剩余大小(字节) | |
AutoTmm bool `json:"auto_tmm"` // 此 torrent 是否由 Automatic Torrent Management 管理 | |
Availability float64 `json:"availability"` // 当前百分比 | |
Category string `json:"category"` // | |
Completed int64 `json:"completed"` // 完成的传输数据量(字节) | |
CompletionOn int `json:"completion_on"` // Torrent 完成的时间(Unix Epoch) | |
ContentPath string `json:"content_path"` // torrent 内容的绝对路径(多文件 torrent 的根路径,单文件 torrent 的绝对文件路径) | |
DlLimit int `json:"dl_limit"` // Torrent 下载速度限制(字节/秒) | |
Dlspeed int `json:"dlspeed"` // Torrent 下载速度(字节/秒) | |
Downloaded int64 `json:"downloaded"` // 已经下载大小 | |
DownloadedSession int64 `json:"downloaded_session"` // 此会话下载的数据量 | |
Eta int `json:"eta"` // | |
FLPiecePrio bool `json:"f_l_piece_prio"` // 如果第一个最后一块被优先考虑,则为true | |
ForceStart bool `json:"force_start"` // 如果为此 torrent 启用了强制启动,则为true | |
Hash string `json:"hash"` // | |
LastActivity int `json:"last_activity"` // 上次活跃的时间(Unix Epoch) | |
MagnetURI string `json:"magnet_uri"` // 与此 torrent 对应的 Magnet URI | |
MaxRatio float64 `json:"max_ratio"` // 种子/上传停止种子前的最大共享比率 | |
MaxSeedingTime int `json:"max_seeding_time"` // 停止种子种子前的最长种子时间(秒) | |
Name string `json:"name"` // | |
NumComplete int `json:"num_complete"` // | |
NumIncomplete int `json:"num_incomplete"` // | |
NumLeechs int `json:"num_leechs"` // 连接到的 leechers 的数量 | |
NumSeeds int `json:"num_seeds"` // 连接到的种子数 | |
Priority int `json:"priority"` // 速度优先。如果队列被禁用或 torrent 处于种子模式,则返回 -1 | |
Progress float64 `json:"progress"` // 进度 | |
Ratio float64 `json:"ratio"` // Torrent 共享比率 | |
RatioLimit int `json:"ratio_limit"` // | |
SavePath string `json:"save_path"` | |
SeedingTime int `json:"seeding_time"` // Torrent 完成用时(秒) | |
SeedingTimeLimit int `json:"seeding_time_limit"` // max_seeding_time | |
SeenComplete int `json:"seen_complete"` // 上次 torrent 完成的时间 | |
SeqDl bool `json:"seq_dl"` // 如果启用顺序下载,则为true | |
Size int64 `json:"size"` // | |
State TorrentStatus `json:"state"` // 参见https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-list | |
SuperSeeding bool `json:"super_seeding"` // 如果启用超级播种,则为true | |
Tags string `json:"tags"` // Torrent 的逗号连接标签列表 | |
TimeActive int `json:"time_active"` // 总活动时间(秒) | |
TotalSize int64 `json:"total_size"` // 此 torrent 中所有文件的总大小(字节)(包括未选择的文件) | |
Tracker string `json:"tracker"` // 第一个具有工作状态的tracker。如果没有tracker在工作,则返回空字符串。 | |
TrackersCount int `json:"trackers_count"` // | |
UpLimit int `json:"up_limit"` // 上传限制 | |
Uploaded int64 `json:"uploaded"` // 累计上传 | |
UploadedSession int64 `json:"uploaded_session"` // 当前session累计上传 | |
Upspeed int `json:"upspeed"` // 上传速度(字节/秒) | |
} | |
type InfoNotFoundError struct { | |
Id string | |
Err error | |
} | |
func (i InfoNotFoundError) Error() string { | |
return "there should be exactly one task with tag \"alist-" + i.Id + "\"" | |
} | |
func NewInfoNotFoundError(id string) InfoNotFoundError { | |
return InfoNotFoundError{Id: id} | |
} | |
func (c *client) GetInfo(id string) (TorrentInfo, error) { | |
var infos []TorrentInfo | |
err := c.checkAuthorization() | |
if err != nil { | |
return TorrentInfo{}, err | |
} | |
v := url.Values{} | |
v.Set("tag", "alist-"+id) | |
response, err := c.post("/api/v2/torrents/info", v) | |
if err != nil { | |
return TorrentInfo{}, err | |
} | |
body, err := io.ReadAll(response.Body) | |
if err != nil { | |
return TorrentInfo{}, err | |
} | |
err = utils.Json.Unmarshal(body, &infos) | |
if err != nil { | |
return TorrentInfo{}, err | |
} | |
if len(infos) != 1 { | |
return TorrentInfo{}, NewInfoNotFoundError(id) | |
} | |
return infos[0], nil | |
} | |
type FileInfo struct { | |
Index int `json:"index"` | |
Name string `json:"name"` | |
Size int64 `json:"size"` | |
Progress float32 `json:"progress"` | |
Priority int `json:"priority"` | |
IsSeed bool `json:"is_seed"` | |
PieceRange []int `json:"piece_range"` | |
Availability float32 `json:"availability"` | |
} | |
func (c *client) GetFiles(id string) ([]FileInfo, error) { | |
var infos []FileInfo | |
err := c.checkAuthorization() | |
if err != nil { | |
return []FileInfo{}, err | |
} | |
tInfo, err := c.GetInfo(id) | |
if err != nil { | |
return []FileInfo{}, err | |
} | |
v := url.Values{} | |
v.Set("hash", tInfo.Hash) | |
response, err := c.post("/api/v2/torrents/files", v) | |
if err != nil { | |
return []FileInfo{}, err | |
} | |
body, err := io.ReadAll(response.Body) | |
if err != nil { | |
return []FileInfo{}, err | |
} | |
err = utils.Json.Unmarshal(body, &infos) | |
if err != nil { | |
return []FileInfo{}, err | |
} | |
return infos, nil | |
} | |
func (c *client) Delete(id string, deleteFiles bool) error { | |
err := c.checkAuthorization() | |
if err != nil { | |
return err | |
} | |
info, err := c.GetInfo(id) | |
if err != nil { | |
return err | |
} | |
v := url.Values{} | |
v.Set("hashes", info.Hash) | |
if deleteFiles { | |
v.Set("deleteFiles", "true") | |
} else { | |
v.Set("deleteFiles", "false") | |
} | |
response, err := c.post("/api/v2/torrents/delete", v) | |
if err != nil { | |
return err | |
} | |
if response.StatusCode != 200 { | |
return errors.New("failed to delete qbittorrent task") | |
} | |
v = url.Values{} | |
v.Set("tags", "alist-"+id) | |
response, err = c.post("/api/v2/torrents/deleteTags", v) | |
if err != nil { | |
return err | |
} | |
if response.StatusCode != 200 { | |
return errors.New("failed to delete qbittorrent tag") | |
} | |
return nil | |
} | |