Skip to content

Commit fc26c3d

Browse files
authored
feat: add GuangYaPan offline download (#9505)
1 parent 0fa863e commit fc26c3d

10 files changed

Lines changed: 523 additions & 13 deletions

File tree

drivers/guangyapan/offline.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package guangyapan
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/url"
8+
stdpath "path"
9+
"strings"
10+
11+
"github.com/alist-org/alist/v3/internal/model"
12+
)
13+
14+
func (d *GuangYaPan) ResolveOfflineResource(ctx context.Context, fileURL string) (*OfflineResolveData, error) {
15+
if err := d.ensureAccessToken(ctx); err != nil {
16+
return nil, err
17+
}
18+
fileURL = strings.TrimSpace(fileURL)
19+
if fileURL == "" {
20+
return nil, errors.New("offline url is empty")
21+
}
22+
23+
var resp offlineResolveResp
24+
if err := d.postAPI(ctx, "/cloudcollection/v1/resolve_res", map[string]any{
25+
"url": fileURL,
26+
}, &resp); err != nil {
27+
return nil, err
28+
}
29+
if !isSuccessMsg(resp.Msg) {
30+
return nil, fmt.Errorf("resolve offline resource failed: %s", strings.TrimSpace(resp.Msg))
31+
}
32+
return &resp.Data, nil
33+
}
34+
35+
func (d *GuangYaPan) OfflineDownload(ctx context.Context, fileURL string, parentDir model.Obj, fileName string) (*OfflineTask, error) {
36+
resolved, err := d.ResolveOfflineResource(ctx, fileURL)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
parentID := parentDir.GetID()
42+
if parentID == d.RootFolderID {
43+
parentID = ""
44+
}
45+
46+
taskURL := strings.TrimSpace(resolved.URL)
47+
if taskURL == "" {
48+
taskURL = strings.TrimSpace(fileURL)
49+
}
50+
name := strings.TrimSpace(fileName)
51+
if name == "" {
52+
name = resolved.defaultName(taskURL)
53+
}
54+
55+
body := map[string]any{
56+
"url": taskURL,
57+
"parentId": parentID,
58+
"newName": name,
59+
}
60+
if indexes := resolved.fileIndexes(); len(indexes) > 0 {
61+
body["fileIndexes"] = indexes
62+
}
63+
64+
var resp offlineCreateResp
65+
if err := d.postAPI(ctx, "/cloudcollection/v1/create_task", body, &resp); err != nil {
66+
return nil, err
67+
}
68+
if !isSuccessMsg(resp.Msg) {
69+
return nil, fmt.Errorf("create offline task failed: %s", strings.TrimSpace(resp.Msg))
70+
}
71+
taskID := strings.TrimSpace(resp.Data.TaskID)
72+
if taskID == "" {
73+
return nil, errors.New("create offline task failed: empty task id")
74+
}
75+
return &OfflineTask{
76+
TaskID: taskID,
77+
FileName: name,
78+
Res: taskURL,
79+
}, nil
80+
}
81+
82+
func (d *GuangYaPan) OfflineList(ctx context.Context, taskIDs []string, statuses []int, cursor string, pageSize int) ([]OfflineTask, error) {
83+
if err := d.ensureAccessToken(ctx); err != nil {
84+
return nil, err
85+
}
86+
body := map[string]any{}
87+
if len(taskIDs) > 0 {
88+
body["taskIds"] = taskIDs
89+
}
90+
if len(statuses) > 0 {
91+
body["status"] = statuses
92+
}
93+
if cursor = strings.TrimSpace(cursor); cursor != "" {
94+
body["cursor"] = cursor
95+
}
96+
if pageSize > 0 {
97+
body["pageSize"] = pageSize
98+
}
99+
100+
var resp offlineListResp
101+
if err := d.postAPI(ctx, "/cloudcollection/v1/list_task", body, &resp); err != nil {
102+
return nil, err
103+
}
104+
if !isSuccessMsg(resp.Msg) {
105+
return nil, fmt.Errorf("list offline tasks failed: %s", strings.TrimSpace(resp.Msg))
106+
}
107+
return resp.Data.List, nil
108+
}
109+
110+
func (d *GuangYaPan) DeleteOfflineTasks(ctx context.Context, taskIDs []string, deleteFiles bool) error {
111+
if err := d.ensureAccessToken(ctx); err != nil {
112+
return err
113+
}
114+
if len(taskIDs) == 0 {
115+
return nil
116+
}
117+
118+
var resp offlineDeleteResp
119+
if err := d.postAPI(ctx, "/cloudcollection/v2/delete_task", map[string]any{
120+
"taskIds": taskIDs,
121+
}, &resp); err != nil {
122+
return err
123+
}
124+
if !isSuccessMsg(resp.Msg) {
125+
return fmt.Errorf("delete offline tasks failed: %s", strings.TrimSpace(resp.Msg))
126+
}
127+
return nil
128+
}
129+
130+
func (d OfflineResolveData) defaultName(fileURL string) string {
131+
if d.BTResInfo != nil && strings.TrimSpace(d.BTResInfo.FileName) != "" {
132+
return strings.TrimSpace(d.BTResInfo.FileName)
133+
}
134+
u, err := url.Parse(fileURL)
135+
if err == nil {
136+
name := strings.TrimSpace(stdpath.Base(u.Path))
137+
if name != "" && name != "." && name != "/" {
138+
if decoded, err := url.PathUnescape(name); err == nil {
139+
name = decoded
140+
}
141+
return name
142+
}
143+
}
144+
return "offline_download"
145+
}
146+
147+
func (d OfflineResolveData) fileIndexes() []int {
148+
if d.BTResInfo == nil || len(d.BTResInfo.Subfiles) == 0 {
149+
return nil
150+
}
151+
indexes := make([]int, 0, len(d.BTResInfo.Subfiles))
152+
for i, file := range d.BTResInfo.Subfiles {
153+
if file.FileIndex != nil {
154+
indexes = append(indexes, *file.FileIndex)
155+
continue
156+
}
157+
indexes = append(indexes, i)
158+
}
159+
return indexes
160+
}
161+
162+
func isSuccessMsg(msg string) bool {
163+
msg = strings.TrimSpace(msg)
164+
return msg == "" || strings.EqualFold(msg, "success")
165+
}

drivers/guangyapan/types.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,79 @@ type taskInfoResp struct {
133133
} `json:"data"`
134134
}
135135

136+
type offlineResolveResp struct {
137+
Code int `json:"code"`
138+
Msg string `json:"msg"`
139+
Data OfflineResolveData `json:"data"`
140+
}
141+
142+
type OfflineResolveData struct {
143+
ResType int `json:"resType"`
144+
BTResInfo *OfflineBTResInfo `json:"btResInfo"`
145+
URL string `json:"url"`
146+
}
147+
148+
type OfflineBTResInfo struct {
149+
InfoHash string `json:"infoHash"`
150+
FileName string `json:"fileName"`
151+
FileSize int64 `json:"fileSize"`
152+
SubfilesNum int `json:"subfilesNum"`
153+
Subfiles []OfflineSubfile `json:"subfiles"`
154+
CreateTime int64 `json:"createTime"`
155+
ExcludeIndices []int `json:"excludeIndices"`
156+
}
157+
158+
type OfflineSubfile struct {
159+
FileName string `json:"fileName"`
160+
FileIndex *int `json:"fileIndex"`
161+
FileSize int64 `json:"fileSize"`
162+
}
163+
164+
type offlineCreateResp struct {
165+
Code int `json:"code"`
166+
Msg string `json:"msg"`
167+
Data struct {
168+
TaskID string `json:"taskId"`
169+
URL string `json:"url"`
170+
} `json:"data"`
171+
}
172+
173+
type offlineDeleteResp struct {
174+
Code int `json:"code"`
175+
Msg string `json:"msg"`
176+
Data struct {
177+
TaskIDs []string `json:"taskIds"`
178+
} `json:"data"`
179+
}
180+
181+
type offlineListResp struct {
182+
Code int `json:"code"`
183+
Msg string `json:"msg"`
184+
Data struct {
185+
StatusCounts []struct {
186+
Status int `json:"status"`
187+
Count int `json:"count"`
188+
} `json:"statusCounts"`
189+
Cursor string `json:"cursor"`
190+
List []OfflineTask `json:"list"`
191+
Total int `json:"total"`
192+
} `json:"data"`
193+
}
194+
195+
type OfflineTask struct {
196+
TaskID string `json:"taskId"`
197+
FileName string `json:"fileName"`
198+
TotalSize int64 `json:"totalSize"`
199+
Status int `json:"status"`
200+
CreateTime int64 `json:"createTime"`
201+
Res string `json:"res"`
202+
ResType int `json:"resType"`
203+
Progress int `json:"progress"`
204+
FileID string `json:"fileId"`
205+
IsDir bool `json:"isDir"`
206+
Exist bool `json:"exist"`
207+
}
208+
136209
func unixOrZero(v int64) time.Time {
137210
if v <= 0 {
138211
return time.Time{}

internal/conf/const.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ const (
7676
// thunder
7777
ThunderTempDir = "thunder_temp_dir"
7878

79+
// guangyapan
80+
GuangYaPanTempDir = "guangyapan_temp_dir"
81+
7982
// single
8083
Token = "token"
8184
IndexProgress = "index_progress"
@@ -126,19 +129,19 @@ const (
126129
FTPTLSPublicCertPath = "ftp_tls_public_cert_path"
127130

128131
// frp
129-
FRPEnabled = "frp_enabled"
130-
FRPServerAddr = "frp_server_addr"
131-
FRPServerPort = "frp_server_port"
132-
FRPAuthToken = "frp_auth_token"
133-
FRPProxyName = "frp_proxy_name"
134-
FRPProxyType = "frp_proxy_type"
135-
FRPCustomDomain = "frp_custom_domain"
136-
FRPSubdomain = "frp_subdomain"
137-
FRPRemotePort = "frp_remote_port"
138-
FRPLocalPort = "frp_local_port"
139-
FRPTLSEnable = "frp_tls_enable"
132+
FRPEnabled = "frp_enabled"
133+
FRPServerAddr = "frp_server_addr"
134+
FRPServerPort = "frp_server_port"
135+
FRPAuthToken = "frp_auth_token"
136+
FRPProxyName = "frp_proxy_name"
137+
FRPProxyType = "frp_proxy_type"
138+
FRPCustomDomain = "frp_custom_domain"
139+
FRPSubdomain = "frp_subdomain"
140+
FRPRemotePort = "frp_remote_port"
141+
FRPLocalPort = "frp_local_port"
142+
FRPTLSEnable = "frp_tls_enable"
140143
FRPSTCPSecretKey = "frp_stcp_secret_key"
141-
FRPStatus = "frp_status"
144+
FRPStatus = "frp_status"
142145

143146
// traffic
144147
TaskOfflineDownloadThreadsNum = "offline_download_task_threads_num"

internal/offline_download/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package offline_download
33
import (
44
_ "github.com/alist-org/alist/v3/internal/offline_download/115"
55
_ "github.com/alist-org/alist/v3/internal/offline_download/aria2"
6+
_ "github.com/alist-org/alist/v3/internal/offline_download/guangyapan"
67
_ "github.com/alist-org/alist/v3/internal/offline_download/http"
78
_ "github.com/alist-org/alist/v3/internal/offline_download/pikpak"
89
_ "github.com/alist-org/alist/v3/internal/offline_download/qbit"

0 commit comments

Comments
 (0)