Repository: AlistGo/alist (language: go)

Issue:
Pikpak分享挂载播放失效, 需实现自动转存播放
### Please make sure of the following things

- [X] I have read the [documentation](https://alist.nn.ci).
- [X] I'm sure there are no duplicate issues or discussions.
- [X] I'm sure this feature is not implemented.
- [X] I'm sure it's a reasonable and popular requirement.

### Description of the feature / 需求描述

Pikpak官方最近已经不允许分享挂载播放, Pikpak分享挂载播放失效,  转存到网盘可以播放,  这个过程是否能自动化

### Suggested solution / 实现思路

当点击pikpak分享挂载的视频时, 将该视频转存到pikpak网盘, 并返回直链播放视频,
如果空间不够, 可以清除网盘里面的内容, 然后转存, 然后返回直链播放

### Additional context / 附件

_No response_


Discussion / hints:
Thanks for opening your first issue here! Be sure to follow the issue template!


Fix patch:
diff --git a/.github/stale.yml b/.github/stale.yml
index 33aa77dd1cd..944e11b4b08 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -15,5 +15,5 @@ markComment: >
   for your contributions.
 # Comment to post when closing a stale issue. Set to `false` to disable
 closeComment: >
-  This issue was closed due to inactive more than 52 days. You can reopen or 
+  This issue was closed due to inactive more than 52 days. You can reopen or
   recreate it if you think it should continue. Thank you for your contributions again.
\ No newline at end of file
diff --git a/drivers/aliyundrive_open/driver.go b/drivers/aliyundrive_open/driver.go
new file mode 100644
index 00000000000..dbcdbcaf379
--- /dev/null
+++ b/drivers/aliyundrive_open/driver.go
@@ -0,0 +1,234 @@
+package aliyundrive_open
+
+import (
+	"context"
+	"io"
+	"math"
+	"net/http"
+	"time"
+
+	"github.com/alist-org/alist/v3/drivers/base"
+	"github.com/alist-org/alist/v3/internal/driver"
+	"github.com/alist-org/alist/v3/internal/errs"
+	"github.com/alist-org/alist/v3/internal/model"
+	"github.com/alist-org/alist/v3/internal/op"
+	"github.com/alist-org/alist/v3/pkg/cron"
+	"github.com/alist-org/alist/v3/pkg/utils"
+	"github.com/go-resty/resty/v2"
+	log "github.com/sirupsen/logrus"
+)
+
+type AliyundriveOpen struct {
+	model.Storage
+	Addition
+	base string
+	cron *cron.Cron
+
+	AccessToken string
+	DriveId     string
+}
+
+func (d *AliyundriveOpen) Config() driver.Config {
+	return config
+}
+
+func (d *AliyundriveOpen) GetAddition() driver.Additional {
+	return &d.Addition
+}
+
+func (d *AliyundriveOpen) Init(ctx context.Context) error {
+	err := d.refreshToken()
+	if err != nil {
+		return err
+	}
+	res, err := d.request("/adrive/v1.0/user/getDriveInfo", http.MethodPost, nil)
+	if err != nil {
+		return err
+	}
+	d.DriveId = utils.Json.Get(res, "default_drive_id").ToString()
+	d.cron = cron.NewCron(time.Hour * 2)
+	d.cron.Do(func() {
+		err := d.refreshToken()
+		d.Status = err.Error()
+		op.MustSaveDriverStorage(d)
+		if err != nil {
+			log.Errorf("%+v", err)
+		}
+	})
+	return nil
+}
+
+func (d *AliyundriveOpen) Drop(ctx context.Context) error {
+	if d.cron != nil {
+		d.cron.Stop()
+	}
+	return nil
+}
+
+func (d *AliyundriveOpen) 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 *AliyundriveOpen) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
+	res, err := d.request("/adrive/v1.0/openFile/getDownloadUrl", http.MethodPost, func(req *resty.Request) {
+		req.SetBody(base.Json{
+			"drive_id": d.DriveId,
+			"file_id":  file.GetID(),
+		})
+	})
+	if err != nil {
+		return nil, err
+	}
+	url := utils.Json.Get(res, "url").ToString()
+	return &model.Link{
+		URL: url,
+	}, nil
+}
+
+func (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
+	_, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
+		req.SetBody(base.Json{
+			"drive_id":        d.DriveId,
+			"parent_file_id":  parentDir.GetID(),
+			"name":            dirName,
+			"type":            "folder",
+			"check_name_mode": "refuse",
+		})
+	})
+	return err
+}
+
+func (d *AliyundriveOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
+	_, err := d.request("/adrive/v1.0/openFile/move", http.MethodPost, func(req *resty.Request) {
+		req.SetBody(base.Json{
+			"drive_id":          d.DriveId,
+			"file_id":           srcObj.GetID(),
+			"to_parent_file_id": dstDir.GetID(),
+			"check_name_mode":   "refuse", // optional:ignore,auto_rename,refuse
+			//"new_name":          "newName", // The new name to use when a file of the same name exists
+		})
+	})
+	return err
+}
+
+func (d *AliyundriveOpen) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
+	_, err := d.request("/adrive/v1.0/openFile/update", http.MethodPost, func(req *resty.Request) {
+		req.SetBody(base.Json{
+			"drive_id": d.DriveId,
+			"file_id":  srcObj.GetID(),
+			"name":     newName,
+		})
+	})
+	return err
+}
+
+func (d *AliyundriveOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
+	_, err := d.request("/adrive/v1.0/openFile/copy", http.MethodPost, func(req *resty.Request) {
+		req.SetBody(base.Json{
+			"drive_id":          d.DriveId,
+			"file_id":           srcObj.GetID(),
+			"to_parent_file_id": dstDir.GetID(),
+			"auto_rename":       true,
+		})
+	})
+	return err
+}
+
+func (d *AliyundriveOpen) Remove(ctx context.Context, obj model.Obj) error {
+	_, err := d.request("/adrive/v1.0/openFile/recyclebin/trash", http.MethodPost, func(req *resty.Request) {
+		req.SetBody(base.Json{
+			"drive_id": d.DriveId,
+			"file_id":  obj.GetID(),
+		})
+	})
+	return err
+}
+
+func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
+	// rapid_upload is not currently supported
+	// 1. create
+	const DEFAULT int64 = 20971520
+	createData := base.Json{
+		"drive_id":        d.DriveId,
+		"parent_file_id":  dstDir.GetID(),
+		"name":            stream.GetName(),
+		"type":            "file",
+		"check_name_mode": "ignore",
+	}
+	count := 1
+	if stream.GetSize() > DEFAULT {
+		count = int(math.Ceil(float64(stream.GetSize()) / float64(DEFAULT)))
+		partInfoList := make([]base.Json, 0, count)
+		for i := 1; i <= count; i++ {
+			partInfoList = append(partInfoList, base.Json{"part_number": i})
+		}
+		createData["part_info_list"] = partInfoList
+	}
+	var createResp CreateResp
+	_, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
+		req.SetBody(createData).SetResult(&createResp)
+	})
+	if err != nil {
+		return err
+	}
+	// 2. upload
+	for i, partInfo := range createResp.PartInfoList {
+		if utils.IsCanceled(ctx) {
+			return ctx.Err()
+		}
+		req, err := http.NewRequest("PUT", partInfo.UploadUrl, io.LimitReader(stream, DEFAULT))
+		if err != nil {
+			return err
+		}
+		req = req.WithContext(ctx)
+		res, err := base.HttpClient.Do(req)
+		if err != nil {
+			return err
+		}
+		res.Body.Close()
+		if count > 0 {
+			up(i * 100 / count)
+		}
+	}
+	// 3. complete
+	_, err = d.request("/adrive/v1.0/openFile/complete", http.MethodPost, func(req *resty.Request) {
+		req.SetBody(base.Json{
+			"drive_id":  d.DriveId,
+			"file_id":   createResp.FileId,
+			"upload_id": createResp.UploadId,
+		})
+	})
+	return err
+}
+
+func (d *AliyundriveOpen) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
+	var resp base.Json
+	var uri string
+	data := base.Json{
+		"drive_id": d.DriveId,
+		"file_id":  args.Obj.GetID(),
+	}
+	switch args.Method {
+	case "video_preview":
+		uri = "/adrive/v1.0/openFile/getVideoPreviewPlayInfo"
+		data["category"] = "live_transcoding"
+		data["url_expire_sec"] = 14400
+	default:
+		return nil, errs.NotSupport
+	}
+	_, err := d.request(uri, http.MethodPost, func(req *resty.Request) {
+		req.SetBody(data).SetResult(&resp)
+	})
+	if err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+var _ driver.Driver = (*AliyundriveOpen)(nil)
diff --git a/drivers/aliyundrive_open/meta.go b/drivers/aliyundrive_open/meta.go
new file mode 100644
index 00000000000..6bf3b5cfd4e
--- /dev/null
+++ b/drivers/aliyundrive_open/meta.go
@@ -0,0 +1,35 @@
+package aliyundrive_open
+
+import (
+	"github.com/alist-org/alist/v3/internal/driver"
+	"github.com/alist-org/alist/v3/internal/op"
+)
+
+type Addition struct {
+	driver.RootID
+	RefreshToken   string `json:"refresh_token" required:"true"`
+	OrderBy        string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
+	OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
+	OauthTokenURL  string `json:"oauth_token_url" default:"https://api.nn.ci/alist/ali_open/token"`
+	ClientID       string `json:"client_id" required:"false"`
+	ClientSecret   string `json:"client_secret" required:"false"`
+}
+
+var config = driver.Config{
+	Name:        "AliyundriveOpen",
+	LocalSort:   false,
+	OnlyLocal:   false,
+	OnlyProxy:   false,
+	NoCache:     false,
+	NoUpload:    false,
+	NeedMs:      false,
+	DefaultRoot: "root",
+}
+
+func init() {
+	op.RegisterDriver(func() driver.Driver {
+		return &AliyundriveOpen{
+			base: "https://open.aliyundrive.com",
+		}
+	})
+}
diff --git a/drivers/aliyundrive_open/types.go b/drivers/aliyundrive_open/types.go
new file mode 100644
index 00000000000..3aceb16e808
--- /dev/null
+++ b/drivers/aliyundrive_open/types.go
@@ -0,0 +1,67 @@
+package aliyundrive_open
+
+import (
+	"time"
+
+	"github.com/alist-org/alist/v3/internal/model"
+)
+
+type ErrResp struct {
+	Code    string `json:"code"`
+	Message string `json:"message"`
+}
+
+type Files struct {
+	Items      []File `json:"items"`
+	NextMarker string `json:"next_marker"`
+}
+
+type File struct {
+	DriveId       string     `json:"drive_id"`
+	FileId        string     `json:"file_id"`
+	ParentFileId  string     `json:"parent_file_id"`
+	Name          string     `json:"name"`
+	Size          int64      `json:"size"`
+	FileExtension string     `json:"file_extension"`
+	ContentHash   string     `json:"content_hash"`
+	Category      string     `json:"category"`
+	Type          string     `json:"type"`
+	Thumbnail     string     `json:"thumbnail"`
+	Url           string     `json:"url"`
+	CreatedAt     *time.Time `json:"created_at"`
+	UpdatedAt     time.Time  `json:"updated_at"`
+}
+
+func fileToObj(f File) *model.ObjThumb {
+	return &model.ObjThumb{
+		Object: model.Object{
+			ID:       f.FileId,
+			Name:     f.Name,
+			Size:     f.Size,
+			Modified: f.UpdatedAt,
+			IsFolder: f.Type == "folder",
+		},
+		Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
+	}
+}
+
+type CreateResp struct {
+	//Type         string `json:"type"`
+	//ParentFileId string `json:"parent_file_id"`
+	//DriveId      string `json:"drive_id"`
+	FileId string `json:"file_id"`
+	//RevisionId   string `json:"revision_id"`
+	//EncryptMode  string `json:"encrypt_mode"`
+	//DomainId     string `json:"domain_id"`
+	//FileName     string `json:"file_name"`
+	UploadId string `json:"upload_id"`
+	//Location     string `json:"location"`
+	RapidUpload  bool `json:"rapid_upload"`
+	PartInfoList []struct {
+		Etag        interface{} `json:"etag"`
+		PartNumber  int         `json:"part_number"`
+		PartSize    interface{} `json:"part_size"`
+		UploadUrl   string      `json:"upload_url"`
+		ContentType string      `json:"content_type"`
+	} `json:"part_info_list"`
+}
diff --git a/drivers/aliyundrive_open/util.go b/drivers/aliyundrive_open/util.go
new file mode 100644
index 00000000000..6d62db02b9c
--- /dev/null
+++ b/drivers/aliyundrive_open/util.go
@@ -0,0 +1,100 @@
+package aliyundrive_open
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/alist-org/alist/v3/drivers/base"
+	"github.com/alist-org/alist/v3/internal/op"
+	"github.com/go-resty/resty/v2"
+)
+
+// do others that not defined in Driver interface
+
+func (d *AliyundriveOpen) refreshToken() error {
+	url := d.base + "/oauth/access_token"
+	if d.OauthTokenURL != "" && d.ClientID == "" {
+		url = d.OauthTokenURL
+	}
+	var resp base.TokenResp
+	var e ErrResp
+	_, err := base.RestyClient.R().
+		ForceContentType("application/json").
+		SetBody(base.Json{
+			"client_id":     d.ClientID,
+			"client_secret": d.ClientSecret,
+			"grant_type":    "refresh_token",
+			"refresh_token": d.RefreshToken,
+		}).
+		SetResult(&resp).
+		SetError(&e).
+		Post(url)
+	if err != nil {
+		return err
+	}
+	if e.Code != "" {
+		return fmt.Errorf("failed to refresh token: %s", e.Message)
+	}
+	if resp.RefreshToken == "" {
+		return errors.New("failed to refresh token: refresh token is empty")
+	}
+	d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken
+	op.MustSaveDriverStorage(d)
+	return nil
+}
+
+func (d *AliyundriveOpen) request(uri, method string, callback base.ReqCallback) ([]byte, error) {
+	req := base.RestyClient.R()
+	// TODO check whether access_token is expired
+	req.SetHeader("Authorization", "Bearer "+d.AccessToken)
+	if method == http.MethodPost {
+		req.SetHeader("Content-Type", "application/json")
+	}
+	if callback != nil {
+		callback(req)
+	}
+	var e ErrResp
+	req.SetError(&e)
+	res, err := req.Execute(method, d.base+uri)
+	if err != nil {
+		return nil, err
+	}
+	if e.Code != "" {
+		return nil, fmt.Errorf("%s:%s", e.Code, e.Message)
+	}
+	return res.Body(), nil
+}
+
+func (d *AliyundriveOpen) getFiles(fileId string) ([]File, error) {
+	marker := "first"
+	res := make([]File, 0)
+	for marker != "" {
+		if marker == "first" {
+			marker = ""
+		}
+		var resp Files
+		data := base.Json{
+			"drive_id":        d.DriveId,
+			"limit":           200,
+			"marker":          marker,
+			"order_by":        d.OrderBy,
+			"order_direction": d.OrderDirection,
+			"parent_file_id":  fileId,
+			//"category":              "",
+			//"type":                  "",
+			//"video_thumbnail_time":  120000,
+			//"video_thumbnail_width": 480,
+			//"image_thumbnail_width": 480,
+		}
+		_, err := d.request("/adrive/v1.0/openFile/list", http.MethodPost, func(req *resty.Request) {
+			req.SetBody(data).SetResult(&resp)
+		})
+		if err != nil {
+			return nil, err
+		}
+		marker = resp.NextMarker
+		res = append(res, resp.Items...)
+	}
+	return res, nil
+}
diff --git a/drivers/all.go b/drivers/all.go
index a141d4665fb..eeffd9561d1 100644
--- a/drivers/all.go
+++ b/drivers/all.go
@@ -9,6 +9,7 @@ import (
 	_ "github.com/alist-org/alist/v3/drivers/alist_v2"
 	_ "github.com/alist-org/alist/v3/drivers/alist_v3"
 	_ "github.com/alist-org/alist/v3/drivers/aliyundrive"
+	_ "github.com/alist-org/alist/v3/drivers/aliyundrive_open"
 	_ "github.com/alist-org/alist/v3/drivers/aliyundrive_share"
 	_ "github.com/alist-org/alist/v3/drivers/baidu_netdisk"
 	_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
diff --git a/server/router.go b/server/router.go
index 37dbd0e5ed9..39c2cfbd828 100644
--- a/server/router.go
+++ b/server/router.go
@@ -19,9 +19,9 @@ func Init(e *gin.Engine) {
 			c.Redirect(302, conf.URL.Path)
 		})
 	}
+	Cors(e)
 	g := e.Group(conf.URL.Path)
 	common.SecretKey = []byte(conf.Conf.JwtSecret)
-	Cors(g)
 	g.Use(middlewares.StoragesLoaded)
 	if conf.Conf.MaxConnections > 0 {
 		g.Use(middlewares.MaxAllowed(conf.Conf.MaxConnections))
@@ -133,7 +133,7 @@ func _fs(g *gin.RouterGroup) {
 	g.POST("/add_qbit", handles.AddQbittorrent)
 }
 
-func Cors(r *gin.RouterGroup) {
+func Cors(r *gin.Engine) {
 	config := cors.DefaultConfig()
 	config.AllowAllOrigins = true
 	config.AllowHeaders = []string{"*"}


Test patch: