mirror of https://github.com/Xhofe/alist
* feat(qbittorrent): authorization and logging in support * feat(qbittorrent/client): support `AddFromLink` * refactor(qbittorrent/client): check authorization when getting a new client * feat(qbittorrent/client): support `GetInfo` * test(qbittorrent/client): update test cases * feat(qbittorrent): init qbittorrent client on bootstrap * feat(qbittorrent): support setting webui url via gin * feat(qbittorrent/client): support deleting * feat(qbittorrent/client): parse `TorrentStatus` enum when unmarshalling json in `GetInfo()` * feat(qbittorrent/client): support getting files by id * feat(qbittorrent): support adding qbittorrent tasks via gin * refactor(qbittorrent/client): return a `Client` interface in `New()` instead of `*client` * refactor: task handle * chore: fix typo * chore: change path --------- Co-authored-by: Andy Hsu <i@nn.ci>pull/3398/head
parent
46b2ed2507
commit
c28168c970
|
@ -29,6 +29,7 @@ the address is defined in config file`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Init()
|
Init()
|
||||||
bootstrap.InitAria2()
|
bootstrap.InitAria2()
|
||||||
|
bootstrap.InitQbittorrent()
|
||||||
bootstrap.LoadStorages()
|
bootstrap.LoadStorages()
|
||||||
if !flags.Debug && !flags.Dev {
|
if !flags.Debug && !flags.Dev {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
|
|
@ -154,6 +154,9 @@ func InitialSettings() []model.SettingItem {
|
||||||
{Key: conf.GithubClientId, Value: "", Type: conf.TypeString, Group: model.GITHUB, Flag: model.PRIVATE},
|
{Key: conf.GithubClientId, Value: "", Type: conf.TypeString, Group: model.GITHUB, Flag: model.PRIVATE},
|
||||||
{Key: conf.GithubClientSecrets, Value: "", Type: conf.TypeString, Group: model.GITHUB, Flag: model.PRIVATE},
|
{Key: conf.GithubClientSecrets, Value: "", Type: conf.TypeString, Group: model.GITHUB, Flag: model.PRIVATE},
|
||||||
{Key: conf.GithubLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GITHUB, Flag: model.PUBLIC},
|
{Key: conf.GithubLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GITHUB, Flag: model.PUBLIC},
|
||||||
|
|
||||||
|
// qbittorrent settings
|
||||||
|
{Key: conf.QbittorrentUrl, Value: "http://admin:adminadmin@localhost:8080/", Type: conf.TypeString, Group: model.QBITTORRENT, Flag: model.PRIVATE},
|
||||||
}
|
}
|
||||||
if flags.Dev {
|
if flags.Dev {
|
||||||
initialSettingItems = append(initialSettingItems, []model.SettingItem{
|
initialSettingItems = append(initialSettingItems, []model.SettingItem{
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/internal/qbittorrent"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitQbittorrent() {
|
||||||
|
go func() {
|
||||||
|
err := qbittorrent.InitClient()
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Infof("qbittorrent not ready.")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
|
@ -1,33 +1,33 @@
|
||||||
package conf
|
package conf
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeString = "string"
|
TypeString = "string"
|
||||||
TypeSelect = "select"
|
TypeSelect = "select"
|
||||||
TypeBool = "bool"
|
TypeBool = "bool"
|
||||||
TypeText = "text"
|
TypeText = "text"
|
||||||
TypeNumber = "number"
|
TypeNumber = "number"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// site
|
// site
|
||||||
VERSION = "version"
|
VERSION = "version"
|
||||||
SiteTitle = "site_title"
|
SiteTitle = "site_title"
|
||||||
Announcement = "announcement"
|
Announcement = "announcement"
|
||||||
AllowIndexed = "allow_indexed"
|
AllowIndexed = "allow_indexed"
|
||||||
|
|
||||||
Logo = "logo"
|
Logo = "logo"
|
||||||
Favicon = "favicon"
|
Favicon = "favicon"
|
||||||
MainColor = "main_color"
|
MainColor = "main_color"
|
||||||
|
|
||||||
// preview
|
// preview
|
||||||
TextTypes = "text_types"
|
TextTypes = "text_types"
|
||||||
AudioTypes = "audio_types"
|
AudioTypes = "audio_types"
|
||||||
VideoTypes = "video_types"
|
VideoTypes = "video_types"
|
||||||
ImageTypes = "image_types"
|
ImageTypes = "image_types"
|
||||||
ProxyTypes = "proxy_types"
|
ProxyTypes = "proxy_types"
|
||||||
ProxyIgnoreHeaders = "proxy_ignore_headers"
|
ProxyIgnoreHeaders = "proxy_ignore_headers"
|
||||||
AudioAutoplay = "audio_autoplay"
|
AudioAutoplay = "audio_autoplay"
|
||||||
VideoAutoplay = "video_autoplay"
|
VideoAutoplay = "video_autoplay"
|
||||||
|
|
||||||
// global
|
// global
|
||||||
HideFiles = "hide_files"
|
HideFiles = "hide_files"
|
||||||
|
@ -46,26 +46,29 @@ const (
|
||||||
IgnorePaths = "ignore_paths"
|
IgnorePaths = "ignore_paths"
|
||||||
MaxIndexDepth = "max_index_depth"
|
MaxIndexDepth = "max_index_depth"
|
||||||
|
|
||||||
// aria2
|
// aria2
|
||||||
Aria2Uri = "aria2_uri"
|
Aria2Uri = "aria2_uri"
|
||||||
Aria2Secret = "aria2_secret"
|
Aria2Secret = "aria2_secret"
|
||||||
|
|
||||||
// single
|
// single
|
||||||
Token = "token"
|
Token = "token"
|
||||||
IndexProgress = "index_progress"
|
IndexProgress = "index_progress"
|
||||||
|
|
||||||
//Github
|
//Github
|
||||||
GithubClientId = "github_client_id"
|
GithubClientId = "github_client_id"
|
||||||
GithubClientSecrets = "github_client_secrets"
|
GithubClientSecrets = "github_client_secrets"
|
||||||
GithubLoginEnabled = "github_login_enabled"
|
GithubLoginEnabled = "github_login_enabled"
|
||||||
|
|
||||||
|
// qbittorrent
|
||||||
|
QbittorrentUrl = "qbittorrent_url"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UNKNOWN = iota
|
UNKNOWN = iota
|
||||||
FOLDER
|
FOLDER
|
||||||
//OFFICE
|
//OFFICE
|
||||||
VIDEO
|
VIDEO
|
||||||
AUDIO
|
AUDIO
|
||||||
TEXT
|
TEXT
|
||||||
IMAGE
|
IMAGE
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,6 +9,7 @@ const (
|
||||||
ARIA2
|
ARIA2
|
||||||
INDEX
|
INDEX
|
||||||
GITHUB
|
GITHUB
|
||||||
|
QBITTORRENT
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -20,16 +20,17 @@ type User struct {
|
||||||
Role int `json:"role"` // user's role
|
Role int `json:"role"` // user's role
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
// Determine permissions by bit
|
// Determine permissions by bit
|
||||||
// 0: can see hidden files
|
// 0: can see hidden files
|
||||||
// 1: can access without password
|
// 1: can access without password
|
||||||
// 2: can add aria2 tasks
|
// 2: can add aria2 tasks
|
||||||
// 3: can mkdir and upload
|
// 3: can mkdir and upload
|
||||||
// 4: can rename
|
// 4: can rename
|
||||||
// 5: can move
|
// 5: can move
|
||||||
// 6: can copy
|
// 6: can copy
|
||||||
// 7: can remove
|
// 7: can remove
|
||||||
// 8: webdav read
|
// 8: webdav read
|
||||||
// 9: webdav write
|
// 9: webdav write
|
||||||
|
// 10: can add qbittorrent tasks
|
||||||
Permission int32 `json:"permission"`
|
Permission int32 `json:"permission"`
|
||||||
OtpSecret string `json:"-"`
|
OtpSecret string `json:"-"`
|
||||||
GithubID int `json:"github_id"`
|
GithubID int `json:"github_id"`
|
||||||
|
@ -93,6 +94,10 @@ func (u User) CanWebdavManage() bool {
|
||||||
return u.IsAdmin() || (u.Permission>>9)&1 == 1
|
return u.IsAdmin() || (u.Permission>>9)&1 == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u User) CanAddQbittorrentTasks() bool {
|
||||||
|
return u.IsAdmin() || (u.Permission>>10)&1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
func (u User) JoinPath(reqPath string) (string, error) {
|
func (u User) JoinPath(reqPath string) (string, error) {
|
||||||
return utils.JoinBasePath(u.BasePath, reqPath)
|
return utils.JoinBasePath(u.BasePath, reqPath)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package qbittorrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/task"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddURL(ctx context.Context, url string, dstDirPath string) error {
|
||||||
|
// check storage
|
||||||
|
storage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithMessage(err, "failed get storage")
|
||||||
|
}
|
||||||
|
// check is it could upload
|
||||||
|
if storage.Config().NoUpload {
|
||||||
|
return errors.WithStack(errs.UploadNotSupported)
|
||||||
|
}
|
||||||
|
// check path is valid
|
||||||
|
obj, err := op.Get(ctx, storage, dstDirActualPath)
|
||||||
|
if err != nil {
|
||||||
|
if !errs.IsObjectNotFound(err) {
|
||||||
|
return errors.WithMessage(err, "failed get object")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !obj.IsDir() {
|
||||||
|
// can't add to a file
|
||||||
|
return errors.WithStack(errs.NotFolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// call qbittorrent
|
||||||
|
id := uuid.NewString()
|
||||||
|
tempDir := filepath.Join(conf.Conf.TempDir, "qbittorrent", id)
|
||||||
|
err = qbclient.AddFromLink(url, tempDir, id)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to add url %s", url)
|
||||||
|
}
|
||||||
|
DownTaskManager.Submit(task.WithCancelCtx(&task.Task[string]{
|
||||||
|
ID: id,
|
||||||
|
Name: fmt.Sprintf("download %s to [%s](%s)", url, storage.GetStorage().MountPath, dstDirActualPath),
|
||||||
|
Func: func(tsk *task.Task[string]) error {
|
||||||
|
m := &Monitor{
|
||||||
|
tsk: tsk,
|
||||||
|
tempDir: tempDir,
|
||||||
|
dstDirPath: dstDirPath,
|
||||||
|
}
|
||||||
|
return m.Loop()
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,337 @@
|
||||||
|
package qbittorrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
AddFromLink(link string, savePath string, id string) error
|
||||||
|
GetInfo(id string) (TorrentInfo, error)
|
||||||
|
GetFiles(id string) ([]FileInfo, error)
|
||||||
|
Delete(id string) 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)
|
||||||
|
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 int `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"` // 上传速度(字节/秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
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{}, errors.New("there should be exactly one task with tag \"alist-" + 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) 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)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
package qbittorrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogin(t *testing.T) {
|
||||||
|
// test logging in with wrong password
|
||||||
|
u, err := url.Parse("http://admin:admin@127.0.0.1:8080/")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
var c = &client{
|
||||||
|
url: u,
|
||||||
|
client: http.Client{Jar: jar},
|
||||||
|
}
|
||||||
|
err = c.login()
|
||||||
|
if err == nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test logging in with correct password
|
||||||
|
u, err = url.Parse("http://admin:adminadmin@127.0.0.1:8080/")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
c.url = u
|
||||||
|
err = c.login()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// in this test, the `Bypass authentication for clients on localhost` option in qBittorrent webui should be disabled
|
||||||
|
func TestAuthorized(t *testing.T) {
|
||||||
|
// init client
|
||||||
|
u, err := url.Parse("http://admin:adminadmin@127.0.0.1:8080/")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
var c = &client{
|
||||||
|
url: u,
|
||||||
|
client: http.Client{Jar: jar},
|
||||||
|
}
|
||||||
|
|
||||||
|
// test without logging in, which should be unauthorized
|
||||||
|
authorized := c.authorized()
|
||||||
|
if authorized {
|
||||||
|
t.Error("Should not be authorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// test after logging in
|
||||||
|
err = c.login()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
authorized = c.authorized()
|
||||||
|
if !authorized {
|
||||||
|
t.Error("Should be authorized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
_, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
_, err = New("http://admin:wrong_password@127.0.0.1:8080/")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Should get an error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdd(t *testing.T) {
|
||||||
|
// init client
|
||||||
|
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test add
|
||||||
|
err = c.login()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
err = c.AddFromLink(
|
||||||
|
"https://releases.ubuntu.com/22.04/ubuntu-22.04.1-desktop-amd64.iso.torrent",
|
||||||
|
"D:\\qBittorrentDownload\\alist",
|
||||||
|
"uuid-1",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
err = c.AddFromLink(
|
||||||
|
"magnet:?xt=urn:btih:375ae3280cd80a8e9d7212e11dfaf7c45069dd35&dn=archlinux-2023.02.01-x86_64.iso",
|
||||||
|
"D:\\qBittorrentDownload\\alist",
|
||||||
|
"uuid-2",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetInfo(t *testing.T) {
|
||||||
|
// init client
|
||||||
|
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
_, err = c.GetInfo("uuid-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFiles(t *testing.T) {
|
||||||
|
// init client
|
||||||
|
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
files, err := c.GetFiles("uuid-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if len(files) != 1 {
|
||||||
|
t.Error("should have exactly one file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
// init client
|
||||||
|
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
err = c.Delete("uuid-2")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
package qbittorrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/task"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Monitor struct {
|
||||||
|
tsk *task.Task[string]
|
||||||
|
tempDir string
|
||||||
|
dstDirPath string
|
||||||
|
finish chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Monitor) Loop() error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
completed bool
|
||||||
|
)
|
||||||
|
m.finish = make(chan struct{})
|
||||||
|
outer:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.tsk.Ctx.Done():
|
||||||
|
err = qbclient.Delete(m.tsk.ID)
|
||||||
|
return err
|
||||||
|
case <-time.After(time.Second * 2):
|
||||||
|
completed, err = m.update()
|
||||||
|
if completed {
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.tsk.SetStatus("qbittorrent download completed, transferring")
|
||||||
|
<-m.finish
|
||||||
|
m.tsk.SetStatus("completed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Monitor) update() (bool, error) {
|
||||||
|
info, err := qbclient.GetInfo(m.tsk.ID)
|
||||||
|
if err != nil {
|
||||||
|
m.tsk.SetStatus("qbittorrent " + string(info.State))
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
progress := float64(info.Completed) / float64(info.Size) * 100
|
||||||
|
m.tsk.SetProgress(int(progress))
|
||||||
|
switch info.State {
|
||||||
|
case UPLOADING:
|
||||||
|
case PAUSEDUP:
|
||||||
|
case QUEUEDUP:
|
||||||
|
case STALLEDUP:
|
||||||
|
case FORCEDUP:
|
||||||
|
case CHECKINGUP:
|
||||||
|
err = m.complete()
|
||||||
|
return true, errors.WithMessage(err, "failed to transfer file")
|
||||||
|
case ALLOCATING:
|
||||||
|
case DOWNLOADING:
|
||||||
|
case METADL:
|
||||||
|
case PAUSEDDL:
|
||||||
|
case QUEUEDDL:
|
||||||
|
case STALLEDDL:
|
||||||
|
case CHECKINGDL:
|
||||||
|
case FORCEDDL:
|
||||||
|
case CHECKINGRESUMEDATA:
|
||||||
|
case MOVING:
|
||||||
|
case UNKNOWN: // or maybe should return an error for UNKNOWN?
|
||||||
|
m.tsk.SetStatus("qbittorrent downloading")
|
||||||
|
return false, nil
|
||||||
|
case ERROR:
|
||||||
|
case MISSINGFILES:
|
||||||
|
return true, errors.Errorf("failed to download %s, error: %s", m.tsk.ID, info.State)
|
||||||
|
}
|
||||||
|
return true, errors.New("unknown error occurred downloading qbittorrent") // should never happen
|
||||||
|
}
|
||||||
|
|
||||||
|
var TransferTaskManager = task.NewTaskManager(3, func(k *uint64) {
|
||||||
|
atomic.AddUint64(k, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
func (m *Monitor) complete() error {
|
||||||
|
// check dstDir again
|
||||||
|
storage, dstDirActualPath, err := op.GetStorageAndActualPath(m.dstDirPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithMessage(err, "failed get storage")
|
||||||
|
}
|
||||||
|
// get files
|
||||||
|
files, err := qbclient.GetFiles(m.tsk.ID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to get files of %s", m.tsk.ID)
|
||||||
|
}
|
||||||
|
log.Debugf("files len: %d", len(files))
|
||||||
|
// upload files
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(files))
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
err := os.RemoveAll(m.tempDir)
|
||||||
|
m.finish <- struct{}{}
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to remove qbittorrent temp dir: %+v", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for _, file := range files {
|
||||||
|
filePath := filepath.Join(m.tempDir, file.Name)
|
||||||
|
TransferTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
|
||||||
|
Name: fmt.Sprintf("transfer %s to [%s](%s)", filePath, storage.GetStorage().MountPath, dstDirActualPath),
|
||||||
|
Func: func(tsk *task.Task[uint64]) error {
|
||||||
|
defer wg.Done()
|
||||||
|
size := file.Size
|
||||||
|
mimetype := utils.GetMimeType(filePath)
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to open file %s", filePath)
|
||||||
|
}
|
||||||
|
stream := &model.FileStream{
|
||||||
|
Obj: &model.Object{
|
||||||
|
Name: path.Base(filePath),
|
||||||
|
Size: size,
|
||||||
|
Modified: time.Now(),
|
||||||
|
IsFolder: false,
|
||||||
|
},
|
||||||
|
ReadCloser: f,
|
||||||
|
Mimetype: mimetype,
|
||||||
|
}
|
||||||
|
newDistDir := filepath.Join(dstDirActualPath, file.Name)
|
||||||
|
return op.Put(tsk.Ctx, storage, newDistDir, stream, tsk.SetProgress)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package qbittorrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
|
"github.com/alist-org/alist/v3/internal/setting"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DownTaskManager = task.NewTaskManager[string](3)
|
||||||
|
var qbclient Client
|
||||||
|
|
||||||
|
func InitClient() error {
|
||||||
|
var err error
|
||||||
|
qbclient = nil
|
||||||
|
|
||||||
|
url := setting.GetStr(conf.QbittorrentUrl)
|
||||||
|
qbclient, err = New(url)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsQbittorrentReady() bool {
|
||||||
|
return qbclient != nil
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package handles
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
|
"github.com/alist-org/alist/v3/internal/qbittorrent"
|
||||||
|
"github.com/alist-org/alist/v3/server/common"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SetQbittorrentReq struct {
|
||||||
|
Url string `json:"url" form:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetQbittorrent(c *gin.Context) {
|
||||||
|
var req SetQbittorrentReq
|
||||||
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
|
common.ErrorResp(c, err, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := []model.SettingItem{
|
||||||
|
{Key: conf.QbittorrentUrl, Value: req.Url, Type: conf.TypeString, Group: model.QBITTORRENT, Flag: model.PRIVATE},
|
||||||
|
}
|
||||||
|
if err := op.SaveSettingItems(items); err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := qbittorrent.InitClient(); err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.SuccessResp(c, "ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddQbittorrentReq struct {
|
||||||
|
Urls []string `json:"urls"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddQbittorrent(c *gin.Context) {
|
||||||
|
user := c.MustGet("user").(*model.User)
|
||||||
|
if !user.CanAddQbittorrentTasks() {
|
||||||
|
common.ErrorStrResp(c, "permission denied", 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !qbittorrent.IsQbittorrentReady() {
|
||||||
|
common.ErrorStrResp(c, "qbittorrent not ready", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req AddQbittorrentReq
|
||||||
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
|
common.ErrorResp(c, err, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reqPath, err := user.JoinPath(req.Path)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, url := range req.Urls {
|
||||||
|
err := qbittorrent.AddURL(c, url, reqPath)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
common.SuccessResp(c)
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/aria2"
|
"github.com/alist-org/alist/v3/internal/aria2"
|
||||||
"github.com/alist-org/alist/v3/internal/fs"
|
"github.com/alist-org/alist/v3/internal/fs"
|
||||||
|
"github.com/alist-org/alist/v3/internal/qbittorrent"
|
||||||
"github.com/alist-org/alist/v3/pkg/task"
|
"github.com/alist-org/alist/v3/pkg/task"
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
"github.com/alist-org/alist/v3/server/common"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
@ -19,9 +20,19 @@ type TaskInfo struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTaskInfoUint(task *task.Task[uint64]) TaskInfo {
|
type K2Str[K comparable] func(k K) string
|
||||||
|
|
||||||
|
func uint64K2Str(k uint64) string {
|
||||||
|
return strconv.FormatUint(k, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func strK2Str(str string) string {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTaskInfo[K comparable](task *task.Task[K], k2Str K2Str[K]) TaskInfo {
|
||||||
return TaskInfo{
|
return TaskInfo{
|
||||||
ID: strconv.FormatUint(task.ID, 10),
|
ID: k2Str(task.ID),
|
||||||
Name: task.Name,
|
Name: task.Name,
|
||||||
State: task.GetState(),
|
State: task.GetState(),
|
||||||
Status: task.GetStatus(),
|
Status: task.GetStatus(),
|
||||||
|
@ -30,183 +41,68 @@ func getTaskInfoUint(task *task.Task[uint64]) TaskInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTaskInfoStr(task *task.Task[string]) TaskInfo {
|
func getTaskInfos[K comparable](tasks []*task.Task[K], k2Str K2Str[K]) []TaskInfo {
|
||||||
return TaskInfo{
|
|
||||||
ID: task.ID,
|
|
||||||
Name: task.Name,
|
|
||||||
State: task.GetState(),
|
|
||||||
Status: task.GetStatus(),
|
|
||||||
Progress: task.GetProgress(),
|
|
||||||
Error: task.GetErrMsg(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTaskInfosUint(tasks []*task.Task[uint64]) []TaskInfo {
|
|
||||||
var infos []TaskInfo
|
var infos []TaskInfo
|
||||||
for _, t := range tasks {
|
for _, t := range tasks {
|
||||||
infos = append(infos, getTaskInfoUint(t))
|
infos = append(infos, getTaskInfo(t, k2Str))
|
||||||
}
|
}
|
||||||
return infos
|
return infos
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTaskInfosStr(tasks []*task.Task[string]) []TaskInfo {
|
type Str2K[K comparable] func(str string) (K, error)
|
||||||
var infos []TaskInfo
|
|
||||||
for _, t := range tasks {
|
func str2Uint64K(str string) (uint64, error) {
|
||||||
infos = append(infos, getTaskInfoStr(t))
|
return strconv.ParseUint(str, 10, 64)
|
||||||
}
|
|
||||||
return infos
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func UndoneDownTask(c *gin.Context) {
|
func str2StrK(str string) (string, error) {
|
||||||
common.SuccessResp(c, getTaskInfosStr(aria2.DownTaskManager.ListUndone()))
|
return str, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DoneDownTask(c *gin.Context) {
|
func taskRoute[K comparable](g *gin.RouterGroup, manager *task.Manager[K], k2Str K2Str[K], str2K Str2K[K]) {
|
||||||
common.SuccessResp(c, getTaskInfosStr(aria2.DownTaskManager.ListDone()))
|
g.GET("/undone", func(c *gin.Context) {
|
||||||
}
|
common.SuccessResp(c, getTaskInfos(manager.ListUndone(), k2Str))
|
||||||
|
})
|
||||||
func CancelDownTask(c *gin.Context) {
|
g.GET("/done", func(c *gin.Context) {
|
||||||
tid := c.Query("tid")
|
common.SuccessResp(c, getTaskInfos(manager.ListDone(), k2Str))
|
||||||
if err := aria2.DownTaskManager.Cancel(tid); err != nil {
|
})
|
||||||
common.ErrorResp(c, err, 500)
|
g.POST("/cancel", func(c *gin.Context) {
|
||||||
} else {
|
tid := c.Query("tid")
|
||||||
|
id, err := str2K(tid)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := manager.Cancel(id); err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
} else {
|
||||||
|
common.SuccessResp(c)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
g.POST("/delete", func(c *gin.Context) {
|
||||||
|
tid := c.Query("tid")
|
||||||
|
id, err := str2K(tid)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := manager.Remove(id); err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
} else {
|
||||||
|
common.SuccessResp(c)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
g.POST("/clear_done", func(c *gin.Context) {
|
||||||
|
manager.ClearDone()
|
||||||
common.SuccessResp(c)
|
common.SuccessResp(c)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteDownTask(c *gin.Context) {
|
func SetupTaskRoute(g *gin.RouterGroup) {
|
||||||
tid := c.Query("tid")
|
taskRoute(g.Group("/aria2_down"), aria2.DownTaskManager, strK2Str, str2StrK)
|
||||||
if err := aria2.DownTaskManager.Remove(tid); err != nil {
|
taskRoute(g.Group("/aria2_transfer"), aria2.TransferTaskManager, uint64K2Str, str2Uint64K)
|
||||||
common.ErrorResp(c, err, 500)
|
taskRoute(g.Group("/upload"), fs.UploadTaskManager, uint64K2Str, str2Uint64K)
|
||||||
} else {
|
taskRoute(g.Group("/copy"), fs.CopyTaskManager, uint64K2Str, str2Uint64K)
|
||||||
common.SuccessResp(c)
|
taskRoute(g.Group("/qbit_down"), qbittorrent.DownTaskManager, strK2Str, str2StrK)
|
||||||
}
|
taskRoute(g.Group("/qbit_transfer"), qbittorrent.TransferTaskManager, uint64K2Str, str2Uint64K)
|
||||||
}
|
|
||||||
|
|
||||||
func ClearDoneDownTasks(c *gin.Context) {
|
|
||||||
aria2.DownTaskManager.ClearDone()
|
|
||||||
common.SuccessResp(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UndoneTransferTask(c *gin.Context) {
|
|
||||||
common.SuccessResp(c, getTaskInfosUint(aria2.TransferTaskManager.ListUndone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func DoneTransferTask(c *gin.Context) {
|
|
||||||
common.SuccessResp(c, getTaskInfosUint(aria2.TransferTaskManager.ListDone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func CancelTransferTask(c *gin.Context) {
|
|
||||||
id := c.Query("tid")
|
|
||||||
tid, err := strconv.ParseUint(id, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := aria2.TransferTaskManager.Cancel(tid); err != nil {
|
|
||||||
common.ErrorResp(c, err, 500)
|
|
||||||
} else {
|
|
||||||
common.SuccessResp(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteTransferTask(c *gin.Context) {
|
|
||||||
id := c.Query("tid")
|
|
||||||
tid, err := strconv.ParseUint(id, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := aria2.TransferTaskManager.Remove(tid); err != nil {
|
|
||||||
common.ErrorResp(c, err, 500)
|
|
||||||
} else {
|
|
||||||
common.SuccessResp(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClearDoneTransferTasks(c *gin.Context) {
|
|
||||||
aria2.TransferTaskManager.ClearDone()
|
|
||||||
common.SuccessResp(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UndoneUploadTask(c *gin.Context) {
|
|
||||||
common.SuccessResp(c, getTaskInfosUint(fs.UploadTaskManager.ListUndone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func DoneUploadTask(c *gin.Context) {
|
|
||||||
common.SuccessResp(c, getTaskInfosUint(fs.UploadTaskManager.ListDone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func CancelUploadTask(c *gin.Context) {
|
|
||||||
id := c.Query("tid")
|
|
||||||
tid, err := strconv.ParseUint(id, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := fs.UploadTaskManager.Cancel(tid); err != nil {
|
|
||||||
common.ErrorResp(c, err, 500)
|
|
||||||
} else {
|
|
||||||
common.SuccessResp(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteUploadTask(c *gin.Context) {
|
|
||||||
id := c.Query("tid")
|
|
||||||
tid, err := strconv.ParseUint(id, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := fs.UploadTaskManager.Remove(tid); err != nil {
|
|
||||||
common.ErrorResp(c, err, 500)
|
|
||||||
} else {
|
|
||||||
common.SuccessResp(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClearDoneUploadTasks(c *gin.Context) {
|
|
||||||
fs.UploadTaskManager.ClearDone()
|
|
||||||
common.SuccessResp(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UndoneCopyTask(c *gin.Context) {
|
|
||||||
common.SuccessResp(c, getTaskInfosUint(fs.CopyTaskManager.ListUndone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func DoneCopyTask(c *gin.Context) {
|
|
||||||
common.SuccessResp(c, getTaskInfosUint(fs.CopyTaskManager.ListDone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func CancelCopyTask(c *gin.Context) {
|
|
||||||
id := c.Query("tid")
|
|
||||||
tid, err := strconv.ParseUint(id, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := fs.CopyTaskManager.Cancel(tid); err != nil {
|
|
||||||
common.ErrorResp(c, err, 500)
|
|
||||||
} else {
|
|
||||||
common.SuccessResp(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteCopyTask(c *gin.Context) {
|
|
||||||
id := c.Query("tid")
|
|
||||||
tid, err := strconv.ParseUint(id, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := fs.CopyTaskManager.Remove(tid); err != nil {
|
|
||||||
common.ErrorResp(c, err, 500)
|
|
||||||
} else {
|
|
||||||
common.SuccessResp(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClearDoneCopyTasks(c *gin.Context) {
|
|
||||||
fs.CopyTaskManager.ClearDone()
|
|
||||||
common.SuccessResp(c)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,28 +89,10 @@ func admin(g *gin.RouterGroup) {
|
||||||
setting.POST("/delete", handles.DeleteSetting)
|
setting.POST("/delete", handles.DeleteSetting)
|
||||||
setting.POST("/reset_token", handles.ResetToken)
|
setting.POST("/reset_token", handles.ResetToken)
|
||||||
setting.POST("/set_aria2", handles.SetAria2)
|
setting.POST("/set_aria2", handles.SetAria2)
|
||||||
|
setting.POST("/set_qbittorrent", handles.SetQbittorrent)
|
||||||
|
|
||||||
task := g.Group("/task")
|
task := g.Group("/task")
|
||||||
task.GET("/down/undone", handles.UndoneDownTask)
|
handles.SetupTaskRoute(task)
|
||||||
task.GET("/down/done", handles.DoneDownTask)
|
|
||||||
task.POST("/down/cancel", handles.CancelDownTask)
|
|
||||||
task.POST("/down/delete", handles.DeleteDownTask)
|
|
||||||
task.POST("/down/clear_done", handles.ClearDoneDownTasks)
|
|
||||||
task.GET("/transfer/undone", handles.UndoneTransferTask)
|
|
||||||
task.GET("/transfer/done", handles.DoneTransferTask)
|
|
||||||
task.POST("/transfer/cancel", handles.CancelTransferTask)
|
|
||||||
task.POST("/transfer/delete", handles.DeleteTransferTask)
|
|
||||||
task.POST("/transfer/clear_done", handles.ClearDoneTransferTasks)
|
|
||||||
task.GET("/upload/undone", handles.UndoneUploadTask)
|
|
||||||
task.GET("/upload/done", handles.DoneUploadTask)
|
|
||||||
task.POST("/upload/cancel", handles.CancelUploadTask)
|
|
||||||
task.POST("/upload/delete", handles.DeleteUploadTask)
|
|
||||||
task.POST("/upload/clear_done", handles.ClearDoneUploadTasks)
|
|
||||||
task.GET("/copy/undone", handles.UndoneCopyTask)
|
|
||||||
task.GET("/copy/done", handles.DoneCopyTask)
|
|
||||||
task.POST("/copy/cancel", handles.CancelCopyTask)
|
|
||||||
task.POST("/copy/delete", handles.DeleteCopyTask)
|
|
||||||
task.POST("/copy/clear_done", handles.ClearDoneCopyTasks)
|
|
||||||
|
|
||||||
ms := g.Group("/message")
|
ms := g.Group("/message")
|
||||||
ms.POST("/get", message.HttpInstance.GetHandle)
|
ms.POST("/get", message.HttpInstance.GetHandle)
|
||||||
|
@ -139,6 +121,7 @@ func _fs(g *gin.RouterGroup) {
|
||||||
g.PUT("/form", middlewares.FsUp, handles.FsForm)
|
g.PUT("/form", middlewares.FsUp, handles.FsForm)
|
||||||
g.POST("/link", middlewares.AuthAdmin, handles.Link)
|
g.POST("/link", middlewares.AuthAdmin, handles.Link)
|
||||||
g.POST("/add_aria2", handles.AddAria2)
|
g.POST("/add_aria2", handles.AddAria2)
|
||||||
|
g.POST("/add_qbit", handles.AddQbittorrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Cors(r *gin.Engine) {
|
func Cors(r *gin.Engine) {
|
||||||
|
|
Loading…
Reference in New Issue