Skip to content

Commit

Permalink
feat(thunder): add offline download tool (#7673)
Browse files Browse the repository at this point in the history
* feat(thunder): add offline download tool

* fix(thunder): improve error handling and parse file size in status response

---------

Co-authored-by: Andy Hsu <i@nn.ci>
  • Loading branch information
Lanfei and xhofe authored Dec 25, 2024
1 parent 48916cd commit 42243b1
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 0 deletions.
61 changes: 61 additions & 0 deletions drivers/thunder/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"strconv"
"strings"

"github.com/alist-org/alist/v3/drivers/base"
Expand Down Expand Up @@ -522,3 +523,63 @@ func (xc *XunLeiCommon) IsLogin() bool {
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
return err == nil
}

// 离线下载文件
func (xc *XunLeiCommon) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {
var resp OfflineDownloadResp
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"kind": FILE,
"name": fileName,
"parent_id": parentDir.GetID(),
"upload_type": UPLOAD_TYPE_URL,
"url": base.Json{
"url": fileUrl,
},
})
}, &resp)

if err != nil {
return nil, err
}

return &resp.Task, err
}

/*
获取离线下载任务列表
*/
func (xc *XunLeiCommon) OfflineList(ctx context.Context, nextPageToken string) ([]OfflineTask, error) {
res := make([]OfflineTask, 0)

var resp OfflineListResp
_, err := xc.Request(TASK_API_URL, http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx).
SetQueryParams(map[string]string{
"type": "offline",
"limit": "10000",
"page_token": nextPageToken,
})
}, &resp)

if err != nil {
return nil, fmt.Errorf("failed to get offline list: %w", err)
}
res = append(res, resp.Tasks...)
return res, nil
}

func (xc *XunLeiCommon) DeleteOfflineTasks(ctx context.Context, taskIDs []string, deleteFiles bool) error {
_, err := xc.Request(TASK_API_URL, http.MethodDelete, func(req *resty.Request) {
req.SetContext(ctx).
SetQueryParams(map[string]string{
"task_ids": strings.Join(taskIDs, ","),
"delete_files": strconv.FormatBool(deleteFiles),
})
}, nil)
if err != nil {
return fmt.Errorf("failed to delete tasks %v: %w", taskIDs, err)
}
return nil
}
47 changes: 47 additions & 0 deletions drivers/thunder/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,50 @@ type UploadTaskResponse struct {

File Files `json:"file"`
}

// 添加离线下载响应
type OfflineDownloadResp struct {
File *string `json:"file"`
Task OfflineTask `json:"task"`
UploadType string `json:"upload_type"`
URL struct {
Kind string `json:"kind"`
} `json:"url"`
}

// 离线下载列表
type OfflineListResp struct {
ExpiresIn int64 `json:"expires_in"`
NextPageToken string `json:"next_page_token"`
Tasks []OfflineTask `json:"tasks"`
}

// offlineTask
type OfflineTask struct {
Callback string `json:"callback"`
CreatedTime string `json:"created_time"`
FileID string `json:"file_id"`
FileName string `json:"file_name"`
FileSize string `json:"file_size"`
IconLink string `json:"icon_link"`
ID string `json:"id"`
Kind string `json:"kind"`
Message string `json:"message"`
Name string `json:"name"`
Params Params `json:"params"`
Phase string `json:"phase"` // PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING
Progress int64 `json:"progress"`
Space string `json:"space"`
StatusSize int64 `json:"status_size"`
Statuses []string `json:"statuses"`
ThirdTaskID string `json:"third_task_id"`
Type string `json:"type"`
UpdatedTime string `json:"updated_time"`
UserID string `json:"user_id"`
}

type Params struct {
FolderType string `json:"folder_type"`
PredictSpeed string `json:"predict_speed"`
PredictType string `json:"predict_type"`
}
1 change: 1 addition & 0 deletions drivers/thunder/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
const (
API_URL = "https://api-pan.xunlei.com/drive/v1"
FILE_API_URL = API_URL + "/files"
TASK_API_URL = API_URL + "/tasks"
XLUSER_API_URL = "https://xluser-ssl.xunlei.com/v1"
)

Expand Down
1 change: 1 addition & 0 deletions internal/offline_download/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ import (
_ "github.com/alist-org/alist/v3/internal/offline_download/http"
_ "github.com/alist-org/alist/v3/internal/offline_download/pikpak"
_ "github.com/alist-org/alist/v3/internal/offline_download/qbit"
_ "github.com/alist-org/alist/v3/internal/offline_download/thunder"
_ "github.com/alist-org/alist/v3/internal/offline_download/transmission"
)
126 changes: 126 additions & 0 deletions internal/offline_download/thunder/thunder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package thunder

import (
"context"
"errors"
"fmt"
"strconv"

"github.com/alist-org/alist/v3/drivers/thunder"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/offline_download/tool"
"github.com/alist-org/alist/v3/internal/op"
)

type Thunder struct {
refreshTaskCache bool
}

func (t *Thunder) Name() string {
return "thunder"
}

func (t *Thunder) Items() []model.SettingItem {
return nil
}

func (t *Thunder) Run(task *tool.DownloadTask) error {
return errs.NotSupport
}

func (t *Thunder) Init() (string, error) {
t.refreshTaskCache = false
return "ok", nil
}

func (t *Thunder) IsReady() bool {
return true
}

func (t *Thunder) AddURL(args *tool.AddUrlArgs) (string, error) {
// 添加新任务刷新缓存
t.refreshTaskCache = true
// args.TempDir 已经被修改为了 DstDirPath
storage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)
if err != nil {
return "", err
}
thunderDriver, ok := storage.(*thunder.Thunder)
if !ok {
return "", fmt.Errorf("unsupported storage driver for offline download, only Thunder is supported")
}

ctx := context.Background()
parentDir, err := op.GetUnwrap(ctx, storage, actualPath)
if err != nil {
return "", err
}

task, err := thunderDriver.OfflineDownload(ctx, args.Url, parentDir, "")
if err != nil {
return "", fmt.Errorf("failed to add offline download task: %w", err)
}

return task.ID, nil
}

func (t *Thunder) Remove(task *tool.DownloadTask) error {
storage, _, err := op.GetStorageAndActualPath(task.DstDirPath)
if err != nil {
return err
}
thunderDriver, ok := storage.(*thunder.Thunder)
if !ok {
return fmt.Errorf("unsupported storage driver for offline download, only Thunder is supported")
}
ctx := context.Background()
err = thunderDriver.DeleteOfflineTasks(ctx, []string{task.GID}, false)
if err != nil {
return err
}
return nil
}

func (t *Thunder) Status(task *tool.DownloadTask) (*tool.Status, error) {
storage, _, err := op.GetStorageAndActualPath(task.DstDirPath)
if err != nil {
return nil, err
}
thunderDriver, ok := storage.(*thunder.Thunder)
if !ok {
return nil, fmt.Errorf("unsupported storage driver for offline download, only Thunder is supported")
}
tasks, err := t.GetTasks(thunderDriver)
if err != nil {
return nil, err
}
s := &tool.Status{
Progress: 0,
NewGID: "",
Completed: false,
Status: "the task has been deleted",
Err: nil,
}
for _, t := range tasks {
if t.ID == task.GID {
s.Progress = float64(t.Progress)
s.Status = t.Message
s.Completed = (t.Phase == "PHASE_TYPE_COMPLETE")
s.TotalBytes, err = strconv.ParseInt(t.FileSize, 10, 64)
if err != nil {
s.TotalBytes = 0
}
if t.Phase == "PHASE_TYPE_ERROR" {
s.Err = errors.New(t.Message)
}
return s, nil
}
}
s.Err = fmt.Errorf("the task has been deleted")
return s, nil
}

func init() {
tool.Tools.Add(&Thunder{})
}
42 changes: 42 additions & 0 deletions internal/offline_download/thunder/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package thunder

import (
"context"
"time"

"github.com/Xhofe/go-cache"
"github.com/alist-org/alist/v3/drivers/thunder"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/singleflight"
)

var taskCache = cache.NewMemCache(cache.WithShards[[]thunder.OfflineTask](16))
var taskG singleflight.Group[[]thunder.OfflineTask]

func (t *Thunder) GetTasks(thunderDriver *thunder.Thunder) ([]thunder.OfflineTask, error) {
key := op.Key(thunderDriver, "/drive/v1/task")
if !t.refreshTaskCache {
if tasks, ok := taskCache.Get(key); ok {
return tasks, nil
}
}
t.refreshTaskCache = false
tasks, err, _ := taskG.Do(key, func() ([]thunder.OfflineTask, error) {
ctx := context.Background()
tasks, err := thunderDriver.OfflineList(ctx, "")
if err != nil {
return nil, err
}
// 添加缓存 10s
if len(tasks) > 0 {
taskCache.Set(key, tasks, cache.WithEx[[]thunder.OfflineTask](time.Second*10))
} else {
taskCache.Del(key)
}
return tasks, nil
})
if err != nil {
return nil, err
}
return tasks, nil
}
4 changes: 4 additions & 0 deletions internal/offline_download/tool/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ func AddURL(ctx context.Context, args *AddURLArgs) (task.TaskExtensionInfo, erro
tempDir = args.DstDirPath
// 防止将下载好的文件删除
deletePolicy = DeleteNever
case "thunder":
tempDir = args.DstDirPath
// 防止将下载好的文件删除
deletePolicy = DeleteNever
}

taskCreator, _ := ctx.Value("user").(*model.User) // taskCreator is nil when convert failed
Expand Down
6 changes: 6 additions & 0 deletions internal/offline_download/tool/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ outer:
if t.tool.Name() == "pikpak" {
return nil
}
if t.tool.Name() == "thunder" {
return nil
}
if t.tool.Name() == "115 Cloud" {
// hack for 115
<-time.After(time.Second * 1)
Expand Down Expand Up @@ -161,6 +164,9 @@ func (t *DownloadTask) Complete() error {
if t.tool.Name() == "pikpak" {
return nil
}
if t.tool.Name() == "thunder" {
return nil
}
if t.tool.Name() == "115 Cloud" {
return nil
}
Expand Down

0 comments on commit 42243b1

Please sign in to comment.