diff --git a/drivers/pikpak/driver.go b/drivers/pikpak/driver.go index 2dab2a9b066..3ecc31d6bff 100644 --- a/drivers/pikpak/driver.go +++ b/drivers/pikpak/driver.go @@ -2,8 +2,10 @@ package pikpak import ( "context" + "encoding/json" "fmt" "net/http" + "strconv" "strings" "github.com/alist-org/alist/v3/drivers/base" @@ -207,4 +209,92 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr return err } +// 离线下载文件 +func (d *PikPak) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) { + requestBody := base.Json{ + "kind": "drive#file", + "name": fileName, + "upload_type": "UPLOAD_TYPE_URL", + "url": base.Json{ + "url": fileUrl, + }, + "parent_id": parentDir.GetID(), + "folder_type": "", + } + + var resp OfflineDownloadResp + _, err := d.request("https://api-drive.mypikpak.com/drive/v1/files", http.MethodPost, func(req *resty.Request) { + req.SetBody(requestBody) + }, &resp) + + if err != nil { + return nil, err + } + + return &resp.Task, err +} + +/* +获取离线下载任务列表 +phase 可能的取值: +PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING +*/ +func (d *PikPak) OfflineList(ctx context.Context, nextPageToken string, phase []string) ([]OfflineTask, error) { + res := make([]OfflineTask, 0) + url := "https://api-drive.mypikpak.com/drive/v1/tasks" + + if len(phase) == 0 { + phase = []string{"PHASE_TYPE_RUNNING", "PHASE_TYPE_ERROR", "PHASE_TYPE_COMPLETE", "PHASE_TYPE_PENDING"} + } + params := map[string]string{ + "type": "offline", + "thumbnail_size": "SIZE_SMALL", + "limit": "10000", + "page_token": nextPageToken, + "with": "reference_resource", + } + + // 处理 phase 参数 + if len(phase) > 0 { + filters := base.Json{ + "phase": map[string]string{ + "in": strings.Join(phase, ","), + }, + } + filtersJSON, err := json.Marshal(filters) + if err != nil { + return nil, fmt.Errorf("failed to marshal filters: %w", err) + } + params["filters"] = string(filtersJSON) + } + + var resp OfflineListResp + _, err := d.request(url, http.MethodGet, func(req *resty.Request) { + req.SetContext(ctx). + SetQueryParams(params) + }, &resp) + + if err != nil { + return nil, fmt.Errorf("failed to get offline list: %w", err) + } + res = append(res, resp.Tasks...) + return res, nil +} + +func (d *PikPak) DeleteOfflineTasks(ctx context.Context, taskIDs []string, deleteFiles bool) error { + url := "https://api-drive.mypikpak.com/drive/v1/tasks" + params := map[string]string{ + "task_ids": strings.Join(taskIDs, ","), + "delete_files": strconv.FormatBool(deleteFiles), + } + _, err := d.request(url, http.MethodDelete, func(req *resty.Request) { + req.SetContext(ctx). + SetQueryParams(params) + }, nil) + if err != nil { + return fmt.Errorf("failed to delete tasks %v: %w", taskIDs, err) + } + return nil +} + var _ driver.Driver = (*PikPak)(nil) diff --git a/drivers/pikpak/types.go b/drivers/pikpak/types.go index 489a1efe713..a9928d00ec2 100644 --- a/drivers/pikpak/types.go +++ b/drivers/pikpak/types.go @@ -99,3 +99,72 @@ type UploadTaskData struct { File File `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"` + ReferenceResource ReferenceResource `json:"reference_resource"` + 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 { + Age string `json:"age"` + MIMEType *string `json:"mime_type,omitempty"` + PredictType string `json:"predict_type"` + URL string `json:"url"` +} + +type ReferenceResource struct { + Type string `json:"@type"` + Audit interface{} `json:"audit"` + Hash string `json:"hash"` + IconLink string `json:"icon_link"` + ID string `json:"id"` + Kind string `json:"kind"` + Medias []Media `json:"medias"` + MIMEType string `json:"mime_type"` + Name string `json:"name"` + Params map[string]interface{} `json:"params"` + ParentID string `json:"parent_id"` + Phase string `json:"phase"` + Size string `json:"size"` + Space string `json:"space"` + Starred bool `json:"starred"` + Tags []string `json:"tags"` + ThumbnailLink string `json:"thumbnail_link"` +} diff --git a/internal/offline_download/all.go b/internal/offline_download/all.go index 2229a855468..67869dee8d2 100644 --- a/internal/offline_download/all.go +++ b/internal/offline_download/all.go @@ -3,5 +3,6 @@ package offline_download import ( _ "github.com/alist-org/alist/v3/internal/offline_download/aria2" _ "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" ) diff --git a/internal/offline_download/pikpak/pikpak.go b/internal/offline_download/pikpak/pikpak.go new file mode 100644 index 00000000000..618b1442b8a --- /dev/null +++ b/internal/offline_download/pikpak/pikpak.go @@ -0,0 +1,120 @@ +package pikpak + +import ( + "context" + "fmt" + + "github.com/alist-org/alist/v3/drivers/pikpak" + "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 PikPak struct { + refreshTaskCache bool +} + +func (p *PikPak) Name() string { + return "pikpak" +} + +func (p *PikPak) Items() []model.SettingItem { + return nil +} + +func (p *PikPak) Run(task *tool.DownloadTask) error { + return errs.NotSupport +} + +func (p *PikPak) Init() (string, error) { + p.refreshTaskCache = false + return "ok", nil +} + +func (p *PikPak) IsReady() bool { + return true +} + +func (p *PikPak) AddURL(args *tool.AddUrlArgs) (string, error) { + // 添加新任务刷新缓存 + p.refreshTaskCache = true + // args.TempDir 已经被修改为了 DstDirPath + storage, actualPath, err := op.GetStorageAndActualPath(args.TempDir) + if err != nil { + return "", err + } + pikpakDriver, ok := storage.(*pikpak.PikPak) + if !ok { + return "", fmt.Errorf("unsupported storage driver for offline download, only Pikpak is supported") + } + + ctx := context.Background() + parentDir, err := op.GetUnwrap(ctx, storage, actualPath) + if err != nil { + return "", err + } + + t, err := pikpakDriver.OfflineDownload(ctx, args.Url, parentDir, "") + if err != nil { + return "", fmt.Errorf("failed to add offline download task: %w", err) + } + + return t.ID, nil +} + +func (p *PikPak) Remove(task *tool.DownloadTask) error { + storage, _, err := op.GetStorageAndActualPath(task.DstDirPath) + if err != nil { + return err + } + pikpakDriver, ok := storage.(*pikpak.PikPak) + if !ok { + return fmt.Errorf("unsupported storage driver for offline download, only Pikpak is supported") + } + ctx := context.Background() + err = pikpakDriver.DeleteOfflineTasks(ctx, []string{task.GID}, false) + if err != nil { + return err + } + return nil +} + +func (p *PikPak) Status(task *tool.DownloadTask) (*tool.Status, error) { + storage, _, err := op.GetStorageAndActualPath(task.DstDirPath) + if err != nil { + return nil, err + } + pikpakDriver, ok := storage.(*pikpak.PikPak) + if !ok { + return nil, fmt.Errorf("unsupported storage driver for offline download, only Pikpak is supported") + } + tasks, err := p.GetTasks(pikpakDriver) + 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") + if t.Phase == "PHASE_TYPE_ERROR" { + s.Err = fmt.Errorf(t.Message) + } + return s, nil + } + } + s.Err = fmt.Errorf("the task has been deleted") + return s, nil +} + +func init() { + tool.Tools.Add(&PikPak{}) +} diff --git a/internal/offline_download/pikpak/util.go b/internal/offline_download/pikpak/util.go new file mode 100644 index 00000000000..f7bf9282621 --- /dev/null +++ b/internal/offline_download/pikpak/util.go @@ -0,0 +1,43 @@ +package pikpak + +import ( + "context" + "time" + + "github.com/Xhofe/go-cache" + "github.com/alist-org/alist/v3/drivers/pikpak" + "github.com/alist-org/alist/v3/internal/op" + "github.com/alist-org/alist/v3/pkg/singleflight" +) + +var taskCache = cache.NewMemCache(cache.WithShards[[]pikpak.OfflineTask](16)) +var taskG singleflight.Group[[]pikpak.OfflineTask] + +func (p *PikPak) GetTasks(pikpakDriver *pikpak.PikPak) ([]pikpak.OfflineTask, error) { + key := op.Key(pikpakDriver, "/drive/v1/task") + if !p.refreshTaskCache { + if tasks, ok := taskCache.Get(key); ok { + return tasks, nil + } + } + p.refreshTaskCache = false + tasks, err, _ := taskG.Do(key, func() ([]pikpak.OfflineTask, error) { + ctx := context.Background() + phase := []string{"PHASE_TYPE_RUNNING", "PHASE_TYPE_ERROR", "PHASE_TYPE_PENDING", "PHASE_TYPE_COMPLETE"} + tasks, err := pikpakDriver.OfflineList(ctx, "", phase) + if err != nil { + return nil, err + } + // 添加缓存 10s + if len(tasks) > 0 { + taskCache.Set(key, tasks, cache.WithEx[[]pikpak.OfflineTask](time.Second*10)) + } else { + taskCache.Del(key) + } + return tasks, nil + }) + if err != nil { + return nil, err + } + return tasks, nil +} diff --git a/internal/offline_download/tool/add.go b/internal/offline_download/tool/add.go index 3da05c8df68..e9bcdc50e52 100644 --- a/internal/offline_download/tool/add.go +++ b/internal/offline_download/tool/add.go @@ -2,13 +2,14 @@ package tool import ( "context" + "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/google/uuid" "github.com/pkg/errors" "github.com/xhofe/tache" - "path/filepath" ) type DeletePolicy string @@ -64,11 +65,17 @@ func AddURL(ctx context.Context, args *AddURLArgs) (tache.TaskWithInfo, error) { uid := uuid.NewString() tempDir := filepath.Join(conf.Conf.TempDir, args.Tool, uid) + deletePolicy := args.DeletePolicy + if args.Tool == "pikpak" { + tempDir = args.DstDirPath + // 防止将下载好的文件删除 + deletePolicy = DeleteNever + } t := &DownloadTask{ Url: args.URL, DstDirPath: args.DstDirPath, TempDir: tempDir, - DeletePolicy: args.DeletePolicy, + DeletePolicy: deletePolicy, tool: tool, } DownloadTaskManager.Add(t) diff --git a/internal/offline_download/tool/download.go b/internal/offline_download/tool/download.go index f0a5d5d4376..79a29ef0c9a 100644 --- a/internal/offline_download/tool/download.go +++ b/internal/offline_download/tool/download.go @@ -71,6 +71,9 @@ outer: if err != nil { return err } + if t.tool.Name() == "pikpak" { + return nil + } t.Status = "offline download completed, maybe transferring" // hack for qBittorrent if t.tool.Name() == "qBittorrent" { @@ -123,6 +126,9 @@ func (t *DownloadTask) Complete() error { files []File err error ) + if t.tool.Name() == "pikpak" { + return nil + } if getFileser, ok := t.tool.(GetFileser); ok { files = getFileser.GetFiles(t) } else { @@ -132,7 +138,7 @@ func (t *DownloadTask) Complete() error { } } // upload files - for i, _ := range files { + for i := range files { file := files[i] TransferTaskManager.Add(&TransferTask{ file: file,