diff --git a/drivers/all.go b/drivers/all.go index 306ac79a..6d5b56e1 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -21,6 +21,7 @@ import ( _ "github.com/alist-org/alist/v3/drivers/mega" _ "github.com/alist-org/alist/v3/drivers/onedrive" _ "github.com/alist-org/alist/v3/drivers/pikpak" + _ "github.com/alist-org/alist/v3/drivers/pikpak_share" _ "github.com/alist-org/alist/v3/drivers/quark" _ "github.com/alist-org/alist/v3/drivers/s3" _ "github.com/alist-org/alist/v3/drivers/sftp" diff --git a/drivers/pikpak_share/driver.go b/drivers/pikpak_share/driver.go new file mode 100644 index 00000000..8c625c1c --- /dev/null +++ b/drivers/pikpak_share/driver.go @@ -0,0 +1,112 @@ +package pikpak_share + +import ( + "context" + "net/http" + + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/internal/errs" + "github.com/alist-org/alist/v3/pkg/utils" + "github.com/go-resty/resty/v2" + log "github.com/sirupsen/logrus" +) + +type PikPakShare struct { + model.Storage + Addition + RefreshToken string + AccessToken string + PassCodeToken string +} + +func (d *PikPakShare) Config() driver.Config { + return config +} + +func (d *PikPakShare) GetAddition() driver.Additional { + return &d.Addition +} + +func (d *PikPakShare) Init(ctx context.Context) error { + err := d.login() + if err != nil { + return err + } + if d.SharePwd != "" { + err = d.getSharePassToken() + if err != nil { + return err + } + } + return nil +} + +func (d *PikPakShare) Drop(ctx context.Context) error { + return nil +} + +func (d *PikPakShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + files, err := d.getFiles(dir.GetID()) + if err != nil { + return nil, err + } + return utils.SliceConvert(files, func(src File) (model.Obj, error) { + return fileToObj(src), nil + }) +} + +func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + var resp ShareResp + query := map[string]string{ + "share_id": d.ShareId, + "file_id": file.GetID(), + "pass_code_token": d.PassCodeToken, + } + _, err := d.request("https://api-drive.mypikpak.com/drive/v1/share/file_info", http.MethodGet, func(req *resty.Request) { + req.SetQueryParams(query) + }, &resp) + if err != nil { + return nil, err + } + link := model.Link{ + URL: resp.FileInfo.WebContentLink, + } + if len(resp.FileInfo.Medias) > 0 && resp.FileInfo.Medias[0].Link.Url != "" { + log.Debugln("use media link") + link.URL = resp.FileInfo.Medias[0].Link.Url + } + return &link, nil +} + +func (d *PikPakShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + // TODO create folder + return errs.NotSupport +} + +func (d *PikPakShare) Move(ctx context.Context, srcObj, dstDir model.Obj) error { + // TODO move obj + return errs.NotSupport +} + +func (d *PikPakShare) Rename(ctx context.Context, srcObj model.Obj, newName string) error { + // TODO rename obj + return errs.NotSupport +} + +func (d *PikPakShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + // TODO copy obj + return errs.NotSupport +} + +func (d *PikPakShare) Remove(ctx context.Context, obj model.Obj) error { + // TODO remove obj + return errs.NotSupport +} + +func (d *PikPakShare) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { + // TODO upload file + return errs.NotSupport +} + +var _ driver.Driver = (*PikPakShare)(nil) diff --git a/drivers/pikpak_share/meta.go b/drivers/pikpak_share/meta.go new file mode 100644 index 00000000..bf77e22b --- /dev/null +++ b/drivers/pikpak_share/meta.go @@ -0,0 +1,27 @@ +package pikpak_share + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + driver.RootID + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` + ShareId string `json:"share_id" required:"true"` + SharePwd string `json:"share_pwd"` +} + +var config = driver.Config{ + Name: "PikPakShare", + LocalSort: true, + NoUpload: true, + DefaultRoot: "", +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &PikPakShare{} + }) +} diff --git a/drivers/pikpak_share/types.go b/drivers/pikpak_share/types.go new file mode 100644 index 00000000..144a05a8 --- /dev/null +++ b/drivers/pikpak_share/types.go @@ -0,0 +1,80 @@ +package pikpak_share + +import ( + "strconv" + "time" + + "github.com/alist-org/alist/v3/internal/model" +) + +type RespErr struct { + ErrorCode int `json:"error_code"` + Error string `json:"error"` +} + +type ShareResp struct { + ShareStatus string `json:"share_status"` + ShareStatusText string `json:"share_status_text"` + FileInfo File `json:"file_info"` + Files []File `json:"files"` + NextPageToken string `json:"next_page_token"` + PassCodeToken string `json:"pass_code_token"` +} + +type File struct { + Id string `json:"id"` + ShareId string `json:"share_id"` + Kind string `json:"kind"` + Name string `json:"name"` + ModifiedTime time.Time `json:"modified_time"` + Size string `json:"size"` + ThumbnailLink string `json:"thumbnail_link"` + WebContentLink string `json:"web_content_link"` + Medias []Media `json:"medias"` +} + +func fileToObj(f File) *model.ObjThumb { + size, _ := strconv.ParseInt(f.Size, 10, 64) + return &model.ObjThumb{ + Object: model.Object{ + ID: f.Id, + Name: f.Name, + Size: size, + Modified: f.ModifiedTime, + IsFolder: f.Kind == "drive#folder", + }, + Thumbnail: model.Thumbnail{ + Thumbnail: f.ThumbnailLink, + }, + } +} + +type Media struct { + MediaId string `json:"media_id"` + MediaName string `json:"media_name"` + Video struct { + Height int `json:"height"` + Width int `json:"width"` + Duration int `json:"duration"` + BitRate int `json:"bit_rate"` + FrameRate int `json:"frame_rate"` + VideoCodec string `json:"video_codec"` + AudioCodec string `json:"audio_codec"` + VideoType string `json:"video_type"` + } `json:"video"` + Link struct { + Url string `json:"url"` + Token string `json:"token"` + Expire time.Time `json:"expire"` + } `json:"link"` + NeedMoreQuota bool `json:"need_more_quota"` + VipTypes []interface{} `json:"vip_types"` + RedirectLink string `json:"redirect_link"` + IconLink string `json:"icon_link"` + IsDefault bool `json:"is_default"` + Priority int `json:"priority"` + IsOrigin bool `json:"is_origin"` + ResolutionName string `json:"resolution_name"` + IsVisible bool `json:"is_visible"` + Category string `json:"category"` +} diff --git a/drivers/pikpak_share/util.go b/drivers/pikpak_share/util.go new file mode 100644 index 00000000..e62f1784 --- /dev/null +++ b/drivers/pikpak_share/util.go @@ -0,0 +1,154 @@ +package pikpak_share + +import ( + "errors" + "net/http" + + "github.com/alist-org/alist/v3/drivers/base" + "github.com/alist-org/alist/v3/internal/op" + "github.com/go-resty/resty/v2" + jsoniter "github.com/json-iterator/go" +) + +// do others that not defined in Driver interface + +func (d *PikPakShare) login() error { + url := "https://user.mypikpak.com/v1/auth/signin" + var e RespErr + res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{ + "captcha_token": "", + "client_id": "YNxT9w7GMdWvEOKa", + "client_secret": "dbw2OtmVEeuUvIptb1Coyg", + "username": d.Username, + "password": d.Password, + }).Post(url) + if err != nil { + return err + } + if e.ErrorCode != 0 { + return errors.New(e.Error) + } + data := res.Body() + d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString() + d.AccessToken = jsoniter.Get(data, "access_token").ToString() + return nil +} + +func (d *PikPakShare) refreshToken() error { + url := "https://user.mypikpak.com/v1/auth/token" + var e RespErr + res, err := base.RestyClient.R().SetError(&e). + SetHeader("user-agent", "").SetBody(base.Json{ + "client_id": "YNxT9w7GMdWvEOKa", + "client_secret": "dbw2OtmVEeuUvIptb1Coyg", + "grant_type": "refresh_token", + "refresh_token": d.RefreshToken, + }).Post(url) + if err != nil { + d.Status = err.Error() + op.MustSaveDriverStorage(d) + return err + } + if e.ErrorCode != 0 { + if e.ErrorCode == 4126 { + // refresh_token invalid, re-login + return d.login() + } + d.Status = e.Error + op.MustSaveDriverStorage(d) + return errors.New(e.Error) + } + data := res.Body() + d.Status = "work" + d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString() + d.AccessToken = jsoniter.Get(data, "access_token").ToString() + op.MustSaveDriverStorage(d) + return nil +} + +func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { + req := base.RestyClient.R() + req.SetHeader("Authorization", "Bearer "+d.AccessToken) + if callback != nil { + callback(req) + } + if resp != nil { + req.SetResult(resp) + } + var e RespErr + req.SetError(&e) + res, err := req.Execute(method, url) + if err != nil { + return nil, err + } + if e.ErrorCode != 0 { + if e.ErrorCode == 16 { + // login / refresh token + err = d.refreshToken() + if err != nil { + return nil, err + } + return d.request(url, method, callback, resp) + } + return nil, errors.New(e.Error) + } + return res.Body(), nil +} + +func (d *PikPakShare) getSharePassToken() error { + query := map[string]string{ + "share_id": d.ShareId, + "pass_code": d.SharePwd, + "thumbnail_size": "SIZE_LARGE", + "limit": "100", + } + var resp ShareResp + _, err := d.request("https://api-drive.mypikpak.com/drive/v1/share", http.MethodGet, func(req *resty.Request) { + req.SetQueryParams(query) + }, &resp) + if err != nil { + return err + } + d.PassCodeToken = resp.PassCodeToken + return nil +} + +func (d *PikPakShare) getFiles(id string) ([]File, error) { + res := make([]File, 0) + pageToken := "first" + for pageToken != "" { + if pageToken == "first" { + pageToken = "" + } + query := map[string]string{ + "parent_id": id, + "share_id": d.ShareId, + "thumbnail_size": "SIZE_LARGE", + "with_audit": "true", + "limit": "100", + "filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`, + "page_token": pageToken, + "pass_code_token": d.PassCodeToken, + } + var resp ShareResp + _, err := d.request("https://api-drive.mypikpak.com/drive/v1/share/detail", http.MethodGet, func(req *resty.Request) { + req.SetQueryParams(query) + }, &resp) + if err != nil { + return nil, err + } + if resp.ShareStatus != "OK" { + if resp.ShareStatus == "PASS_CODE_EMPTY" || resp.ShareStatus == "PASS_CODE_ERROR" { + err = d.getSharePassToken() + if err != nil { + return nil, err + } + return d.getFiles(id) + } + return nil, errors.New(resp.ShareStatusText) + } + pageToken = resp.NextPageToken + res = append(res, resp.Files...) + } + return res, nil +}