feat: support qbittorrent (close #3087 in #3333)

* 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>
This commit is contained in:
kdxcxs
2023-02-14 15:20:45 +08:00
committed by GitHub
parent 46b2ed2507
commit c28168c970
14 changed files with 930 additions and 237 deletions

View File

@ -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)
}

View File

@ -5,6 +5,7 @@ import (
"github.com/alist-org/alist/v3/internal/aria2"
"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/server/common"
"github.com/gin-gonic/gin"
@ -19,9 +20,19 @@ type TaskInfo struct {
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{
ID: strconv.FormatUint(task.ID, 10),
ID: k2Str(task.ID),
Name: task.Name,
State: task.GetState(),
Status: task.GetStatus(),
@ -30,183 +41,68 @@ func getTaskInfoUint(task *task.Task[uint64]) TaskInfo {
}
}
func getTaskInfoStr(task *task.Task[string]) 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 {
func getTaskInfos[K comparable](tasks []*task.Task[K], k2Str K2Str[K]) []TaskInfo {
var infos []TaskInfo
for _, t := range tasks {
infos = append(infos, getTaskInfoUint(t))
infos = append(infos, getTaskInfo(t, k2Str))
}
return infos
}
func getTaskInfosStr(tasks []*task.Task[string]) []TaskInfo {
var infos []TaskInfo
for _, t := range tasks {
infos = append(infos, getTaskInfoStr(t))
}
return infos
type Str2K[K comparable] func(str string) (K, error)
func str2Uint64K(str string) (uint64, error) {
return strconv.ParseUint(str, 10, 64)
}
func UndoneDownTask(c *gin.Context) {
common.SuccessResp(c, getTaskInfosStr(aria2.DownTaskManager.ListUndone()))
func str2StrK(str string) (string, error) {
return str, nil
}
func DoneDownTask(c *gin.Context) {
common.SuccessResp(c, getTaskInfosStr(aria2.DownTaskManager.ListDone()))
}
func CancelDownTask(c *gin.Context) {
tid := c.Query("tid")
if err := aria2.DownTaskManager.Cancel(tid); err != nil {
common.ErrorResp(c, err, 500)
} else {
func taskRoute[K comparable](g *gin.RouterGroup, manager *task.Manager[K], k2Str K2Str[K], str2K Str2K[K]) {
g.GET("/undone", func(c *gin.Context) {
common.SuccessResp(c, getTaskInfos(manager.ListUndone(), k2Str))
})
g.GET("/done", func(c *gin.Context) {
common.SuccessResp(c, getTaskInfos(manager.ListDone(), k2Str))
})
g.POST("/cancel", func(c *gin.Context) {
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)
}
})
}
func DeleteDownTask(c *gin.Context) {
tid := c.Query("tid")
if err := aria2.DownTaskManager.Remove(tid); err != nil {
common.ErrorResp(c, err, 500)
} else {
common.SuccessResp(c)
}
}
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)
func SetupTaskRoute(g *gin.RouterGroup) {
taskRoute(g.Group("/aria2_down"), aria2.DownTaskManager, strK2Str, str2StrK)
taskRoute(g.Group("/aria2_transfer"), aria2.TransferTaskManager, uint64K2Str, str2Uint64K)
taskRoute(g.Group("/upload"), fs.UploadTaskManager, uint64K2Str, str2Uint64K)
taskRoute(g.Group("/copy"), fs.CopyTaskManager, uint64K2Str, str2Uint64K)
taskRoute(g.Group("/qbit_down"), qbittorrent.DownTaskManager, strK2Str, str2StrK)
taskRoute(g.Group("/qbit_transfer"), qbittorrent.TransferTaskManager, uint64K2Str, str2Uint64K)
}

View File

@ -89,28 +89,10 @@ func admin(g *gin.RouterGroup) {
setting.POST("/delete", handles.DeleteSetting)
setting.POST("/reset_token", handles.ResetToken)
setting.POST("/set_aria2", handles.SetAria2)
setting.POST("/set_qbittorrent", handles.SetQbittorrent)
task := g.Group("/task")
task.GET("/down/undone", handles.UndoneDownTask)
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)
handles.SetupTaskRoute(task)
ms := g.Group("/message")
ms.POST("/get", message.HttpInstance.GetHandle)
@ -139,6 +121,7 @@ func _fs(g *gin.RouterGroup) {
g.PUT("/form", middlewares.FsUp, handles.FsForm)
g.POST("/link", middlewares.AuthAdmin, handles.Link)
g.POST("/add_aria2", handles.AddAria2)
g.POST("/add_qbit", handles.AddQbittorrent)
}
func Cors(r *gin.Engine) {