Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/pkg/qbittorrent/client.go
1560 views
1
package qbittorrent
2
3
import (
4
"bytes"
5
"errors"
6
"io"
7
"mime/multipart"
8
"net/http"
9
"net/http/cookiejar"
10
"net/url"
11
12
"github.com/alist-org/alist/v3/pkg/utils"
13
)
14
15
type Client interface {
16
AddFromLink(link string, savePath string, id string) error
17
GetInfo(id string) (TorrentInfo, error)
18
GetFiles(id string) ([]FileInfo, error)
19
Delete(id string, deleteFiles bool) error
20
}
21
22
type client struct {
23
url *url.URL
24
client http.Client
25
Client
26
}
27
28
func New(webuiUrl string) (Client, error) {
29
u, err := url.Parse(webuiUrl)
30
if err != nil {
31
return nil, err
32
}
33
34
jar, err := cookiejar.New(nil)
35
if err != nil {
36
return nil, err
37
}
38
var c = &client{
39
url: u,
40
client: http.Client{Jar: jar},
41
}
42
43
err = c.checkAuthorization()
44
if err != nil {
45
return nil, err
46
}
47
return c, nil
48
}
49
50
func (c *client) checkAuthorization() error {
51
// check authorization
52
if c.authorized() {
53
return nil
54
}
55
56
// check authorization after logging in
57
err := c.login()
58
if err != nil {
59
return err
60
}
61
if c.authorized() {
62
return nil
63
}
64
return errors.New("unauthorized qbittorrent url")
65
}
66
67
func (c *client) authorized() bool {
68
resp, err := c.post("/api/v2/app/version", nil)
69
if err != nil {
70
return false
71
}
72
return resp.StatusCode == 200 // the status code will be 403 if not authorized
73
}
74
75
func (c *client) login() error {
76
// prepare HTTP request
77
v := url.Values{}
78
v.Set("username", c.url.User.Username())
79
passwd, _ := c.url.User.Password()
80
v.Set("password", passwd)
81
resp, err := c.post("/api/v2/auth/login", v)
82
if err != nil {
83
return err
84
}
85
86
// check result
87
body := make([]byte, 2)
88
_, err = resp.Body.Read(body)
89
if err != nil {
90
return err
91
}
92
if string(body) != "Ok" {
93
return errors.New("failed to login into qBittorrent webui with url: " + c.url.String())
94
}
95
return nil
96
}
97
98
func (c *client) post(path string, data url.Values) (*http.Response, error) {
99
u := c.url.JoinPath(path)
100
u.User = nil // remove userinfo for requests
101
102
req, err := http.NewRequest("POST", u.String(), bytes.NewReader([]byte(data.Encode())))
103
if err != nil {
104
return nil, err
105
}
106
if data != nil {
107
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
108
}
109
110
resp, err := c.client.Do(req)
111
if err != nil {
112
return nil, err
113
}
114
if resp.Cookies() != nil {
115
c.client.Jar.SetCookies(u, resp.Cookies())
116
}
117
return resp, nil
118
}
119
120
func (c *client) AddFromLink(link string, savePath string, id string) error {
121
err := c.checkAuthorization()
122
if err != nil {
123
return err
124
}
125
126
buf := new(bytes.Buffer)
127
writer := multipart.NewWriter(buf)
128
129
addField := func(name string, value string) {
130
if err != nil {
131
return
132
}
133
err = writer.WriteField(name, value)
134
}
135
addField("urls", link)
136
addField("savepath", savePath)
137
addField("tags", "alist-"+id)
138
addField("autoTMM", "false")
139
if err != nil {
140
return err
141
}
142
143
err = writer.Close()
144
if err != nil {
145
return err
146
}
147
148
u := c.url.JoinPath("/api/v2/torrents/add")
149
u.User = nil // remove userinfo for requests
150
req, err := http.NewRequest("POST", u.String(), buf)
151
if err != nil {
152
return err
153
}
154
req.Header.Add("Content-Type", writer.FormDataContentType())
155
156
resp, err := c.client.Do(req)
157
if err != nil {
158
return err
159
}
160
161
// check result
162
body := make([]byte, 2)
163
_, err = resp.Body.Read(body)
164
if err != nil {
165
return err
166
}
167
if resp.StatusCode != 200 || string(body) != "Ok" {
168
return errors.New("failed to add qBittorrent task: " + link)
169
}
170
return nil
171
}
172
173
type TorrentStatus string
174
175
const (
176
ERROR TorrentStatus = "error"
177
MISSINGFILES TorrentStatus = "missingFiles"
178
UPLOADING TorrentStatus = "uploading"
179
PAUSEDUP TorrentStatus = "pausedUP"
180
QUEUEDUP TorrentStatus = "queuedUP"
181
STALLEDUP TorrentStatus = "stalledUP"
182
CHECKINGUP TorrentStatus = "checkingUP"
183
FORCEDUP TorrentStatus = "forcedUP"
184
ALLOCATING TorrentStatus = "allocating"
185
DOWNLOADING TorrentStatus = "downloading"
186
METADL TorrentStatus = "metaDL"
187
PAUSEDDL TorrentStatus = "pausedDL"
188
QUEUEDDL TorrentStatus = "queuedDL"
189
STALLEDDL TorrentStatus = "stalledDL"
190
CHECKINGDL TorrentStatus = "checkingDL"
191
FORCEDDL TorrentStatus = "forcedDL"
192
CHECKINGRESUMEDATA TorrentStatus = "checkingResumeData"
193
MOVING TorrentStatus = "moving"
194
UNKNOWN TorrentStatus = "unknown"
195
)
196
197
// https://github.com/DGuang21/PTGo/blob/main/app/client/client_distributer.go
198
type TorrentInfo struct {
199
AddedOn int `json:"added_on"` // 将 torrent 添加到客户端的时间(Unix Epoch)
200
AmountLeft int64 `json:"amount_left"` // 剩余大小(字节)
201
AutoTmm bool `json:"auto_tmm"` // 此 torrent 是否由 Automatic Torrent Management 管理
202
Availability float64 `json:"availability"` // 当前百分比
203
Category string `json:"category"` //
204
Completed int64 `json:"completed"` // 完成的传输数据量(字节)
205
CompletionOn int `json:"completion_on"` // Torrent 完成的时间(Unix Epoch)
206
ContentPath string `json:"content_path"` // torrent 内容的绝对路径(多文件 torrent 的根路径,单文件 torrent 的绝对文件路径)
207
DlLimit int `json:"dl_limit"` // Torrent 下载速度限制(字节/秒)
208
Dlspeed int `json:"dlspeed"` // Torrent 下载速度(字节/秒)
209
Downloaded int64 `json:"downloaded"` // 已经下载大小
210
DownloadedSession int64 `json:"downloaded_session"` // 此会话下载的数据量
211
Eta int `json:"eta"` //
212
FLPiecePrio bool `json:"f_l_piece_prio"` // 如果第一个最后一块被优先考虑,则为true
213
ForceStart bool `json:"force_start"` // 如果为此 torrent 启用了强制启动,则为true
214
Hash string `json:"hash"` //
215
LastActivity int `json:"last_activity"` // 上次活跃的时间(Unix Epoch)
216
MagnetURI string `json:"magnet_uri"` // 与此 torrent 对应的 Magnet URI
217
MaxRatio float64 `json:"max_ratio"` // 种子/上传停止种子前的最大共享比率
218
MaxSeedingTime int `json:"max_seeding_time"` // 停止种子种子前的最长种子时间(秒)
219
Name string `json:"name"` //
220
NumComplete int `json:"num_complete"` //
221
NumIncomplete int `json:"num_incomplete"` //
222
NumLeechs int `json:"num_leechs"` // 连接到的 leechers 的数量
223
NumSeeds int `json:"num_seeds"` // 连接到的种子数
224
Priority int `json:"priority"` // 速度优先。如果队列被禁用或 torrent 处于种子模式,则返回 -1
225
Progress float64 `json:"progress"` // 进度
226
Ratio float64 `json:"ratio"` // Torrent 共享比率
227
RatioLimit int `json:"ratio_limit"` //
228
SavePath string `json:"save_path"`
229
SeedingTime int `json:"seeding_time"` // Torrent 完成用时(秒)
230
SeedingTimeLimit int `json:"seeding_time_limit"` // max_seeding_time
231
SeenComplete int `json:"seen_complete"` // 上次 torrent 完成的时间
232
SeqDl bool `json:"seq_dl"` // 如果启用顺序下载,则为true
233
Size int64 `json:"size"` //
234
State TorrentStatus `json:"state"` // 参见https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-list
235
SuperSeeding bool `json:"super_seeding"` // 如果启用超级播种,则为true
236
Tags string `json:"tags"` // Torrent 的逗号连接标签列表
237
TimeActive int `json:"time_active"` // 总活动时间(秒)
238
TotalSize int64 `json:"total_size"` // 此 torrent 中所有文件的总大小(字节)(包括未选择的文件)
239
Tracker string `json:"tracker"` // 第一个具有工作状态的tracker。如果没有tracker在工作,则返回空字符串。
240
TrackersCount int `json:"trackers_count"` //
241
UpLimit int `json:"up_limit"` // 上传限制
242
Uploaded int64 `json:"uploaded"` // 累计上传
243
UploadedSession int64 `json:"uploaded_session"` // 当前session累计上传
244
Upspeed int `json:"upspeed"` // 上传速度(字节/秒)
245
}
246
247
type InfoNotFoundError struct {
248
Id string
249
Err error
250
}
251
252
func (i InfoNotFoundError) Error() string {
253
return "there should be exactly one task with tag \"alist-" + i.Id + "\""
254
}
255
256
func NewInfoNotFoundError(id string) InfoNotFoundError {
257
return InfoNotFoundError{Id: id}
258
}
259
260
func (c *client) GetInfo(id string) (TorrentInfo, error) {
261
var infos []TorrentInfo
262
263
err := c.checkAuthorization()
264
if err != nil {
265
return TorrentInfo{}, err
266
}
267
268
v := url.Values{}
269
v.Set("tag", "alist-"+id)
270
response, err := c.post("/api/v2/torrents/info", v)
271
if err != nil {
272
return TorrentInfo{}, err
273
}
274
275
body, err := io.ReadAll(response.Body)
276
if err != nil {
277
return TorrentInfo{}, err
278
}
279
err = utils.Json.Unmarshal(body, &infos)
280
if err != nil {
281
return TorrentInfo{}, err
282
}
283
if len(infos) != 1 {
284
return TorrentInfo{}, NewInfoNotFoundError(id)
285
}
286
return infos[0], nil
287
}
288
289
type FileInfo struct {
290
Index int `json:"index"`
291
Name string `json:"name"`
292
Size int64 `json:"size"`
293
Progress float32 `json:"progress"`
294
Priority int `json:"priority"`
295
IsSeed bool `json:"is_seed"`
296
PieceRange []int `json:"piece_range"`
297
Availability float32 `json:"availability"`
298
}
299
300
func (c *client) GetFiles(id string) ([]FileInfo, error) {
301
var infos []FileInfo
302
303
err := c.checkAuthorization()
304
if err != nil {
305
return []FileInfo{}, err
306
}
307
308
tInfo, err := c.GetInfo(id)
309
if err != nil {
310
return []FileInfo{}, err
311
}
312
313
v := url.Values{}
314
v.Set("hash", tInfo.Hash)
315
response, err := c.post("/api/v2/torrents/files", v)
316
if err != nil {
317
return []FileInfo{}, err
318
}
319
320
body, err := io.ReadAll(response.Body)
321
if err != nil {
322
return []FileInfo{}, err
323
}
324
err = utils.Json.Unmarshal(body, &infos)
325
if err != nil {
326
return []FileInfo{}, err
327
}
328
return infos, nil
329
}
330
331
func (c *client) Delete(id string, deleteFiles bool) error {
332
err := c.checkAuthorization()
333
if err != nil {
334
return err
335
}
336
337
info, err := c.GetInfo(id)
338
if err != nil {
339
return err
340
}
341
v := url.Values{}
342
v.Set("hashes", info.Hash)
343
if deleteFiles {
344
v.Set("deleteFiles", "true")
345
} else {
346
v.Set("deleteFiles", "false")
347
}
348
response, err := c.post("/api/v2/torrents/delete", v)
349
if err != nil {
350
return err
351
}
352
if response.StatusCode != 200 {
353
return errors.New("failed to delete qbittorrent task")
354
}
355
356
v = url.Values{}
357
v.Set("tags", "alist-"+id)
358
response, err = c.post("/api/v2/torrents/deleteTags", v)
359
if err != nil {
360
return err
361
}
362
if response.StatusCode != 200 {
363
return errors.New("failed to delete qbittorrent tag")
364
}
365
return nil
366
}
367
368