Skip to content

Commit 42243b1

Browse files
Lanfeixhofe
andauthored
feat(thunder): add offline download tool (#7673)
* 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>
1 parent 48916cd commit 42243b1

File tree

8 files changed

+288
-0
lines changed

8 files changed

+288
-0
lines changed

drivers/thunder/driver.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"strconv"
78
"strings"
89

910
"github.com/alist-org/alist/v3/drivers/base"
@@ -522,3 +523,63 @@ func (xc *XunLeiCommon) IsLogin() bool {
522523
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
523524
return err == nil
524525
}
526+
527+
// 离线下载文件
528+
func (xc *XunLeiCommon) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {
529+
var resp OfflineDownloadResp
530+
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
531+
r.SetContext(ctx)
532+
r.SetBody(&base.Json{
533+
"kind": FILE,
534+
"name": fileName,
535+
"parent_id": parentDir.GetID(),
536+
"upload_type": UPLOAD_TYPE_URL,
537+
"url": base.Json{
538+
"url": fileUrl,
539+
},
540+
})
541+
}, &resp)
542+
543+
if err != nil {
544+
return nil, err
545+
}
546+
547+
return &resp.Task, err
548+
}
549+
550+
/*
551+
获取离线下载任务列表
552+
*/
553+
func (xc *XunLeiCommon) OfflineList(ctx context.Context, nextPageToken string) ([]OfflineTask, error) {
554+
res := make([]OfflineTask, 0)
555+
556+
var resp OfflineListResp
557+
_, err := xc.Request(TASK_API_URL, http.MethodGet, func(req *resty.Request) {
558+
req.SetContext(ctx).
559+
SetQueryParams(map[string]string{
560+
"type": "offline",
561+
"limit": "10000",
562+
"page_token": nextPageToken,
563+
})
564+
}, &resp)
565+
566+
if err != nil {
567+
return nil, fmt.Errorf("failed to get offline list: %w", err)
568+
}
569+
res = append(res, resp.Tasks...)
570+
return res, nil
571+
}
572+
573+
func (xc *XunLeiCommon) DeleteOfflineTasks(ctx context.Context, taskIDs []string, deleteFiles bool) error {
574+
_, err := xc.Request(TASK_API_URL, http.MethodDelete, func(req *resty.Request) {
575+
req.SetContext(ctx).
576+
SetQueryParams(map[string]string{
577+
"task_ids": strings.Join(taskIDs, ","),
578+
"delete_files": strconv.FormatBool(deleteFiles),
579+
})
580+
}, nil)
581+
if err != nil {
582+
return fmt.Errorf("failed to delete tasks %v: %w", taskIDs, err)
583+
}
584+
return nil
585+
}

drivers/thunder/types.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,50 @@ type UploadTaskResponse struct {
204204

205205
File Files `json:"file"`
206206
}
207+
208+
// 添加离线下载响应
209+
type OfflineDownloadResp struct {
210+
File *string `json:"file"`
211+
Task OfflineTask `json:"task"`
212+
UploadType string `json:"upload_type"`
213+
URL struct {
214+
Kind string `json:"kind"`
215+
} `json:"url"`
216+
}
217+
218+
// 离线下载列表
219+
type OfflineListResp struct {
220+
ExpiresIn int64 `json:"expires_in"`
221+
NextPageToken string `json:"next_page_token"`
222+
Tasks []OfflineTask `json:"tasks"`
223+
}
224+
225+
// offlineTask
226+
type OfflineTask struct {
227+
Callback string `json:"callback"`
228+
CreatedTime string `json:"created_time"`
229+
FileID string `json:"file_id"`
230+
FileName string `json:"file_name"`
231+
FileSize string `json:"file_size"`
232+
IconLink string `json:"icon_link"`
233+
ID string `json:"id"`
234+
Kind string `json:"kind"`
235+
Message string `json:"message"`
236+
Name string `json:"name"`
237+
Params Params `json:"params"`
238+
Phase string `json:"phase"` // PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING
239+
Progress int64 `json:"progress"`
240+
Space string `json:"space"`
241+
StatusSize int64 `json:"status_size"`
242+
Statuses []string `json:"statuses"`
243+
ThirdTaskID string `json:"third_task_id"`
244+
Type string `json:"type"`
245+
UpdatedTime string `json:"updated_time"`
246+
UserID string `json:"user_id"`
247+
}
248+
249+
type Params struct {
250+
FolderType string `json:"folder_type"`
251+
PredictSpeed string `json:"predict_speed"`
252+
PredictType string `json:"predict_type"`
253+
}

drivers/thunder/util.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
const (
1818
API_URL = "https://api-pan.xunlei.com/drive/v1"
1919
FILE_API_URL = API_URL + "/files"
20+
TASK_API_URL = API_URL + "/tasks"
2021
XLUSER_API_URL = "https://xluser-ssl.xunlei.com/v1"
2122
)
2223

internal/offline_download/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ import (
66
_ "github.com/alist-org/alist/v3/internal/offline_download/http"
77
_ "github.com/alist-org/alist/v3/internal/offline_download/pikpak"
88
_ "github.com/alist-org/alist/v3/internal/offline_download/qbit"
9+
_ "github.com/alist-org/alist/v3/internal/offline_download/thunder"
910
_ "github.com/alist-org/alist/v3/internal/offline_download/transmission"
1011
)
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package thunder
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"strconv"
8+
9+
"github.com/alist-org/alist/v3/drivers/thunder"
10+
"github.com/alist-org/alist/v3/internal/errs"
11+
"github.com/alist-org/alist/v3/internal/model"
12+
"github.com/alist-org/alist/v3/internal/offline_download/tool"
13+
"github.com/alist-org/alist/v3/internal/op"
14+
)
15+
16+
type Thunder struct {
17+
refreshTaskCache bool
18+
}
19+
20+
func (t *Thunder) Name() string {
21+
return "thunder"
22+
}
23+
24+
func (t *Thunder) Items() []model.SettingItem {
25+
return nil
26+
}
27+
28+
func (t *Thunder) Run(task *tool.DownloadTask) error {
29+
return errs.NotSupport
30+
}
31+
32+
func (t *Thunder) Init() (string, error) {
33+
t.refreshTaskCache = false
34+
return "ok", nil
35+
}
36+
37+
func (t *Thunder) IsReady() bool {
38+
return true
39+
}
40+
41+
func (t *Thunder) AddURL(args *tool.AddUrlArgs) (string, error) {
42+
// 添加新任务刷新缓存
43+
t.refreshTaskCache = true
44+
// args.TempDir 已经被修改为了 DstDirPath
45+
storage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)
46+
if err != nil {
47+
return "", err
48+
}
49+
thunderDriver, ok := storage.(*thunder.Thunder)
50+
if !ok {
51+
return "", fmt.Errorf("unsupported storage driver for offline download, only Thunder is supported")
52+
}
53+
54+
ctx := context.Background()
55+
parentDir, err := op.GetUnwrap(ctx, storage, actualPath)
56+
if err != nil {
57+
return "", err
58+
}
59+
60+
task, err := thunderDriver.OfflineDownload(ctx, args.Url, parentDir, "")
61+
if err != nil {
62+
return "", fmt.Errorf("failed to add offline download task: %w", err)
63+
}
64+
65+
return task.ID, nil
66+
}
67+
68+
func (t *Thunder) Remove(task *tool.DownloadTask) error {
69+
storage, _, err := op.GetStorageAndActualPath(task.DstDirPath)
70+
if err != nil {
71+
return err
72+
}
73+
thunderDriver, ok := storage.(*thunder.Thunder)
74+
if !ok {
75+
return fmt.Errorf("unsupported storage driver for offline download, only Thunder is supported")
76+
}
77+
ctx := context.Background()
78+
err = thunderDriver.DeleteOfflineTasks(ctx, []string{task.GID}, false)
79+
if err != nil {
80+
return err
81+
}
82+
return nil
83+
}
84+
85+
func (t *Thunder) Status(task *tool.DownloadTask) (*tool.Status, error) {
86+
storage, _, err := op.GetStorageAndActualPath(task.DstDirPath)
87+
if err != nil {
88+
return nil, err
89+
}
90+
thunderDriver, ok := storage.(*thunder.Thunder)
91+
if !ok {
92+
return nil, fmt.Errorf("unsupported storage driver for offline download, only Thunder is supported")
93+
}
94+
tasks, err := t.GetTasks(thunderDriver)
95+
if err != nil {
96+
return nil, err
97+
}
98+
s := &tool.Status{
99+
Progress: 0,
100+
NewGID: "",
101+
Completed: false,
102+
Status: "the task has been deleted",
103+
Err: nil,
104+
}
105+
for _, t := range tasks {
106+
if t.ID == task.GID {
107+
s.Progress = float64(t.Progress)
108+
s.Status = t.Message
109+
s.Completed = (t.Phase == "PHASE_TYPE_COMPLETE")
110+
s.TotalBytes, err = strconv.ParseInt(t.FileSize, 10, 64)
111+
if err != nil {
112+
s.TotalBytes = 0
113+
}
114+
if t.Phase == "PHASE_TYPE_ERROR" {
115+
s.Err = errors.New(t.Message)
116+
}
117+
return s, nil
118+
}
119+
}
120+
s.Err = fmt.Errorf("the task has been deleted")
121+
return s, nil
122+
}
123+
124+
func init() {
125+
tool.Tools.Add(&Thunder{})
126+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package thunder
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/Xhofe/go-cache"
8+
"github.com/alist-org/alist/v3/drivers/thunder"
9+
"github.com/alist-org/alist/v3/internal/op"
10+
"github.com/alist-org/alist/v3/pkg/singleflight"
11+
)
12+
13+
var taskCache = cache.NewMemCache(cache.WithShards[[]thunder.OfflineTask](16))
14+
var taskG singleflight.Group[[]thunder.OfflineTask]
15+
16+
func (t *Thunder) GetTasks(thunderDriver *thunder.Thunder) ([]thunder.OfflineTask, error) {
17+
key := op.Key(thunderDriver, "/drive/v1/task")
18+
if !t.refreshTaskCache {
19+
if tasks, ok := taskCache.Get(key); ok {
20+
return tasks, nil
21+
}
22+
}
23+
t.refreshTaskCache = false
24+
tasks, err, _ := taskG.Do(key, func() ([]thunder.OfflineTask, error) {
25+
ctx := context.Background()
26+
tasks, err := thunderDriver.OfflineList(ctx, "")
27+
if err != nil {
28+
return nil, err
29+
}
30+
// 添加缓存 10s
31+
if len(tasks) > 0 {
32+
taskCache.Set(key, tasks, cache.WithEx[[]thunder.OfflineTask](time.Second*10))
33+
} else {
34+
taskCache.Del(key)
35+
}
36+
return tasks, nil
37+
})
38+
if err != nil {
39+
return nil, err
40+
}
41+
return tasks, nil
42+
}

internal/offline_download/tool/add.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ func AddURL(ctx context.Context, args *AddURLArgs) (task.TaskExtensionInfo, erro
7777
tempDir = args.DstDirPath
7878
// 防止将下载好的文件删除
7979
deletePolicy = DeleteNever
80+
case "thunder":
81+
tempDir = args.DstDirPath
82+
// 防止将下载好的文件删除
83+
deletePolicy = DeleteNever
8084
}
8185

8286
taskCreator, _ := ctx.Value("user").(*model.User) // taskCreator is nil when convert failed

internal/offline_download/tool/download.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ outer:
8383
if t.tool.Name() == "pikpak" {
8484
return nil
8585
}
86+
if t.tool.Name() == "thunder" {
87+
return nil
88+
}
8689
if t.tool.Name() == "115 Cloud" {
8790
// hack for 115
8891
<-time.After(time.Second * 1)
@@ -161,6 +164,9 @@ func (t *DownloadTask) Complete() error {
161164
if t.tool.Name() == "pikpak" {
162165
return nil
163166
}
167+
if t.tool.Name() == "thunder" {
168+
return nil
169+
}
164170
if t.tool.Name() == "115 Cloud" {
165171
return nil
166172
}

0 commit comments

Comments
 (0)