mirror of https://github.com/Xhofe/alist
feat: add xunlei driver
parent
40882443c2
commit
35d177b67b
|
@ -20,6 +20,7 @@ import (
|
||||||
_ "github.com/alist-org/alist/v3/drivers/uss"
|
_ "github.com/alist-org/alist/v3/drivers/uss"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/virtual"
|
_ "github.com/alist-org/alist/v3/drivers/virtual"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/webdav"
|
_ "github.com/alist-org/alist/v3/drivers/webdav"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/xunlei"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/yandex_disk"
|
_ "github.com/alist-org/alist/v3/drivers/yandex_disk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,495 @@
|
||||||
|
package xunlei
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"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/utils"
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type XunLei struct {
|
||||||
|
*XunLeiCommon
|
||||||
|
model.Storage
|
||||||
|
Addition
|
||||||
|
|
||||||
|
identity string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *XunLei) Config() driver.Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *XunLei) GetAddition() driver.Additional {
|
||||||
|
return x.Addition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *XunLei) Init(ctx context.Context, storage model.Storage) (err error) {
|
||||||
|
x.Storage = storage
|
||||||
|
if err = utils.Json.UnmarshalFromString(x.Storage.Addition, &x.Addition); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化所需参数
|
||||||
|
if x.XunLeiCommon == nil {
|
||||||
|
x.XunLeiCommon = &XunLeiCommon{
|
||||||
|
Common: &Common{
|
||||||
|
client: base.NewRestyClient(),
|
||||||
|
Algorithms: []string{
|
||||||
|
"HPxr4BVygTQVtQkIMwQH33ywbgYG5l4JoR",
|
||||||
|
"GzhNkZ8pOBsCY+7",
|
||||||
|
"v+l0ImTpG7c7/",
|
||||||
|
"e5ztohgVXNP",
|
||||||
|
"t",
|
||||||
|
"EbXUWyVVqQbQX39Mbjn2geok3/0WEkAVxeqhtx857++kjJiRheP8l77gO",
|
||||||
|
"o7dvYgbRMOpHXxCs",
|
||||||
|
"6MW8TD8DphmakaxCqVrfv7NReRRN7ck3KLnXBculD58MvxjFRqT+",
|
||||||
|
"kmo0HxCKVfmxoZswLB4bVA/dwqbVAYghSb",
|
||||||
|
"j",
|
||||||
|
"4scKJNdd7F27Hv7tbt",
|
||||||
|
},
|
||||||
|
DeviceID: "9aa5c268e7bcfc197a9ad88e2fb330e5",
|
||||||
|
ClientID: "Xp6vsxz_7IYVw2BB",
|
||||||
|
ClientSecret: "Xp6vsy4tN9toTVdMSpomVdXpRmES",
|
||||||
|
ClientVersion: "7.51.0.8196",
|
||||||
|
PackageName: "com.xunlei.downloadprovider",
|
||||||
|
UserAgent: "ANDROID-com.xunlei.downloadprovider/7.51.0.8196 netWorkType/5G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)",
|
||||||
|
DownUserAgent: "Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)",
|
||||||
|
},
|
||||||
|
refreshTokenFunc: func() error {
|
||||||
|
// 通过RefreshToken刷新
|
||||||
|
token, err := x.RefreshToken(x.TokenResp.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
// 重新登录
|
||||||
|
token, err = x.Login(x.Username, x.Password)
|
||||||
|
if err != nil {
|
||||||
|
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
||||||
|
op.MustSaveDriverStorage(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x.SetTokenResp(token)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义验证码token
|
||||||
|
ctoekn := strings.TrimSpace(x.CaptchaToken)
|
||||||
|
if ctoekn != "" {
|
||||||
|
x.SetCaptchaToken(ctoekn)
|
||||||
|
x.CaptchaToken = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止重复登录
|
||||||
|
identity := x.GetIdentity()
|
||||||
|
if x.identity != identity || !x.IsLogin() {
|
||||||
|
x.identity = identity
|
||||||
|
// 登录
|
||||||
|
token, err := x.Login(x.Username, x.Password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
x.SetTokenResp(token)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *XunLei) Drop(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type XunLeiExpert struct {
|
||||||
|
*XunLeiCommon
|
||||||
|
model.Storage
|
||||||
|
ExpertAddition
|
||||||
|
|
||||||
|
identity string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *XunLeiExpert) Config() driver.Config {
|
||||||
|
return configExpert
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *XunLeiExpert) GetAddition() driver.Additional {
|
||||||
|
return x.ExpertAddition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *XunLeiExpert) Init(ctx context.Context, storage model.Storage) (err error) {
|
||||||
|
x.Storage = storage
|
||||||
|
if err = utils.Json.UnmarshalFromString(x.Storage.Addition, &x.ExpertAddition); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止重复登录
|
||||||
|
identity := x.GetIdentity()
|
||||||
|
if identity != x.identity || !x.IsLogin() {
|
||||||
|
x.identity = identity
|
||||||
|
x.XunLeiCommon = &XunLeiCommon{
|
||||||
|
Common: &Common{
|
||||||
|
client: base.NewRestyClient(),
|
||||||
|
|
||||||
|
DeviceID: x.DeviceID,
|
||||||
|
ClientID: x.ClientID,
|
||||||
|
ClientSecret: x.ClientSecret,
|
||||||
|
ClientVersion: x.ClientVersion,
|
||||||
|
PackageName: x.PackageName,
|
||||||
|
UserAgent: x.UserAgent,
|
||||||
|
DownUserAgent: x.DownUserAgent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if x.CaptchaToken != "" {
|
||||||
|
x.SetCaptchaToken(x.CaptchaToken)
|
||||||
|
x.CaptchaToken = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 签名方法
|
||||||
|
if x.SignType == "capcha_sign" {
|
||||||
|
x.Common.Timestamp = x.Timestamp
|
||||||
|
x.Common.CaptchaSign = x.CaptchaSign
|
||||||
|
} else {
|
||||||
|
x.Common.Algorithms = strings.Split(x.Algorithms, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录方式
|
||||||
|
if x.LoginType == "refresh_token" {
|
||||||
|
// 通过RefreshToken登录
|
||||||
|
token, err := x.XunLeiCommon.RefreshToken(x.ExpertAddition.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
x.SetTokenResp(token)
|
||||||
|
|
||||||
|
// 刷新token方法
|
||||||
|
x.SetRefreshTokenFunc(func() error {
|
||||||
|
token, err := x.XunLeiCommon.RefreshToken(x.TokenResp.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
||||||
|
}
|
||||||
|
x.SetTokenResp(token)
|
||||||
|
op.MustSaveDriverStorage(x)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 通过用户密码登录
|
||||||
|
token, err := x.Login(x.Username, x.Password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
x.SetTokenResp(token)
|
||||||
|
x.SetRefreshTokenFunc(func() error {
|
||||||
|
token, err := x.XunLeiCommon.RefreshToken(x.TokenResp.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
token, err = x.Login(x.Username, x.Password)
|
||||||
|
if err != nil {
|
||||||
|
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x.SetTokenResp(token)
|
||||||
|
op.MustSaveDriverStorage(x)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 仅修改验证码token
|
||||||
|
if x.CaptchaToken != "" {
|
||||||
|
x.SetCaptchaToken(x.CaptchaToken)
|
||||||
|
x.CaptchaToken = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *XunLeiExpert) Drop(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *XunLeiExpert) SetTokenResp(token *TokenResp) {
|
||||||
|
x.XunLeiCommon.SetTokenResp(token)
|
||||||
|
if token != nil {
|
||||||
|
x.ExpertAddition.RefreshToken = token.RefreshToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type XunLeiCommon struct {
|
||||||
|
*Common
|
||||||
|
*TokenResp // 登录信息
|
||||||
|
|
||||||
|
refreshTokenFunc func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xc *XunLeiCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
return xc.getFiles(ctx, dir.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xc *XunLeiCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
var lFile Files
|
||||||
|
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodGet, func(r *resty.Request) {
|
||||||
|
r.SetContext(ctx)
|
||||||
|
r.SetPathParam("fileID", file.GetID())
|
||||||
|
//r.SetQueryParam("space", "")
|
||||||
|
}, &lFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
link := &model.Link{
|
||||||
|
URL: lFile.WebContentLink,
|
||||||
|
Header: http.Header{
|
||||||
|
"User-Agent": {xc.DownUserAgent},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
strs := regexp.MustCompile(`e=([0-9]*)`).FindStringSubmatch(lFile.WebContentLink)
|
||||||
|
if len(strs) == 2 {
|
||||||
|
timestamp, err := strconv.ParseInt(strs[1], 10, 64)
|
||||||
|
if err == nil {
|
||||||
|
expired := time.Duration(timestamp-time.Now().Unix()) * time.Second
|
||||||
|
link.Expiration = &expired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return link, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xc *XunLeiCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
|
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
||||||
|
r.SetContext(ctx)
|
||||||
|
r.SetBody(&base.Json{
|
||||||
|
"kind": FOLDER,
|
||||||
|
"name": dirName,
|
||||||
|
"parent_id": parentDir.GetID(),
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xc *XunLeiCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
_, err := xc.Request(FILE_API_URL+":batchMove", http.MethodPost, func(r *resty.Request) {
|
||||||
|
r.SetContext(ctx)
|
||||||
|
r.SetBody(&base.Json{
|
||||||
|
"to": base.Json{"parent_id": dstDir.GetID()},
|
||||||
|
"ids": []string{srcObj.GetID()},
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xc *XunLeiCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||||
|
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodPatch, func(r *resty.Request) {
|
||||||
|
r.SetContext(ctx)
|
||||||
|
r.SetPathParam("fileID", srcObj.GetID())
|
||||||
|
r.SetBody(&base.Json{"name": newName})
|
||||||
|
}, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xc *XunLeiCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
_, err := xc.Request(FILE_API_URL+":batchCopy", http.MethodPost, func(r *resty.Request) {
|
||||||
|
r.SetContext(ctx)
|
||||||
|
r.SetBody(&base.Json{
|
||||||
|
"to": base.Json{"parent_id": dstDir.GetID()},
|
||||||
|
"ids": []string{srcObj.GetID()},
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xc *XunLeiCommon) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
_, err := xc.Request(FILE_API_URL+"/{fileID}/trash", http.MethodPatch, func(r *resty.Request) {
|
||||||
|
r.SetContext(ctx)
|
||||||
|
r.SetPathParam("fileID", obj.GetID())
|
||||||
|
r.SetBody("{}")
|
||||||
|
}, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xc *XunLeiCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
|
var resp UploadTaskResponse
|
||||||
|
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
||||||
|
r.SetContext(ctx)
|
||||||
|
r.SetBody(&base.Json{
|
||||||
|
"kind": FILE,
|
||||||
|
"parent_id": dstDir.GetID(),
|
||||||
|
"name": stream.GetName(),
|
||||||
|
"size": stream.GetSize(),
|
||||||
|
"hash": "1CF254FBC456E1B012CD45C546636AA62CF8350E",
|
||||||
|
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||||
|
})
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
param := resp.Resumable.Params
|
||||||
|
if resp.UploadType == UPLOAD_TYPE_RESUMABLE {
|
||||||
|
param.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+".")
|
||||||
|
s, err := session.NewSession(&aws.Config{
|
||||||
|
Credentials: credentials.NewStaticCredentials(param.AccessKeyID, param.AccessKeySecret, param.SecurityToken),
|
||||||
|
Region: aws.String("xunlei"),
|
||||||
|
Endpoint: aws.String(param.Endpoint),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = s3manager.NewUploader(s).UploadWithContext(ctx, &s3manager.UploadInput{
|
||||||
|
Bucket: aws.String(param.Bucket),
|
||||||
|
Key: aws.String(param.Key),
|
||||||
|
Expires: aws.Time(param.Expiration),
|
||||||
|
Body: stream,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xc *XunLeiCommon) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||||
|
return nil, errs.NotSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xc *XunLeiCommon) getFiles(ctx context.Context, folderId string) ([]model.Obj, error) {
|
||||||
|
files := make([]model.Obj, 0)
|
||||||
|
var pageToken string
|
||||||
|
for {
|
||||||
|
var fileList FileList
|
||||||
|
_, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {
|
||||||
|
r.SetContext(ctx)
|
||||||
|
r.SetQueryParams(map[string]string{
|
||||||
|
"space": "",
|
||||||
|
"__type": "drive",
|
||||||
|
"refresh": "true",
|
||||||
|
"__sync": "true",
|
||||||
|
"parent_id": folderId,
|
||||||
|
"page_token": pageToken,
|
||||||
|
"with_audit": "true",
|
||||||
|
"limit": "100",
|
||||||
|
"filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
|
||||||
|
})
|
||||||
|
}, &fileList)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(fileList.Files); i++ {
|
||||||
|
files = append(files, &fileList.Files[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileList.NextPageToken == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pageToken = fileList.NextPageToken
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置刷新Token的方法
|
||||||
|
func (xc *XunLeiCommon) SetRefreshTokenFunc(fn func() error) {
|
||||||
|
xc.refreshTokenFunc = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置Token
|
||||||
|
func (xc *XunLeiCommon) SetTokenResp(tr *TokenResp) {
|
||||||
|
xc.TokenResp = tr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 携带Authorization和CaptchaToken的请求
|
||||||
|
func (xc *XunLeiCommon) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
|
data, err := xc.Common.Request(url, method, func(req *resty.Request) {
|
||||||
|
req.SetHeaders(map[string]string{
|
||||||
|
"Authorization": xc.Token(),
|
||||||
|
"X-Captcha-Token": xc.GetCaptchaToken(),
|
||||||
|
})
|
||||||
|
if callback != nil {
|
||||||
|
callback(req)
|
||||||
|
}
|
||||||
|
}, resp)
|
||||||
|
|
||||||
|
errResp, ok := err.(*ErrResp)
|
||||||
|
if !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch errResp.ErrorCode {
|
||||||
|
case 0:
|
||||||
|
return data, nil
|
||||||
|
case 4122, 4121, 10, 16:
|
||||||
|
if xc.refreshTokenFunc != nil {
|
||||||
|
if err = xc.refreshTokenFunc(); err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
case 9: // 验证码token过期
|
||||||
|
if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.UserID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return xc.Request(url, method, callback, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新Token
|
||||||
|
func (xc *XunLeiCommon) RefreshToken(refreshToken string) (*TokenResp, error) {
|
||||||
|
var resp TokenResp
|
||||||
|
_, err := xc.Common.Request(XLUSER_API_URL+"/auth/token", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetBody(&base.Json{
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": refreshToken,
|
||||||
|
"client_id": xc.ClientID,
|
||||||
|
"client_secret": xc.ClientSecret,
|
||||||
|
})
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.RefreshToken == "" {
|
||||||
|
return nil, errs.EmptyToken
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
func (xc *XunLeiCommon) Login(username, password string) (*TokenResp, error) {
|
||||||
|
url := XLUSER_API_URL + "/auth/signin"
|
||||||
|
err := xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp TokenResp
|
||||||
|
_, err = xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetBody(&SignInRequest{
|
||||||
|
CaptchaToken: xc.GetCaptchaToken(),
|
||||||
|
ClientID: xc.ClientID,
|
||||||
|
ClientSecret: xc.ClientSecret,
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
})
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xc *XunLeiCommon) IsLogin() bool {
|
||||||
|
if xc.TokenResp == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
|
||||||
|
return err == nil
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package xunlei
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 高级设置
|
||||||
|
type ExpertAddition struct {
|
||||||
|
driver.RootID
|
||||||
|
|
||||||
|
LoginType string `json:"login_type" type:"select" options:"user,refresh_token" default:"user"`
|
||||||
|
SignType string `json:"sign_type" type:"select" options:"algorithms,capcha_sign" default:"algorithms"`
|
||||||
|
|
||||||
|
// 登录方式1
|
||||||
|
Username string `json:"username" required:"true" help:"login type is user,this is required"`
|
||||||
|
Password string `json:"password" required:"true" help:"login type is user,this is required"`
|
||||||
|
// 登录方式2
|
||||||
|
RefreshToken string `json:"refresh_token" required:"true" help:"login type is refresh_token,this is required"`
|
||||||
|
|
||||||
|
// 签名方法1
|
||||||
|
Algorithms string `json:"algorithms" required:"true" help:"sign type is algorithms,this is required" default:"HPxr4BVygTQVtQkIMwQH33ywbgYG5l4JoR,GzhNkZ8pOBsCY+7,v+l0ImTpG7c7/,e5ztohgVXNP,t,EbXUWyVVqQbQX39Mbjn2geok3/0WEkAVxeqhtx857++kjJiRheP8l77gO,o7dvYgbRMOpHXxCs,6MW8TD8DphmakaxCqVrfv7NReRRN7ck3KLnXBculD58MvxjFRqT+,kmo0HxCKVfmxoZswLB4bVA/dwqbVAYghSb,j,4scKJNdd7F27Hv7tbt"`
|
||||||
|
// 签名方法2
|
||||||
|
CaptchaSign string `json:"captcha_sign" required:"true" help:"sign type is capcha_sign,this is required"`
|
||||||
|
Timestamp string `json:"timestamp" required:"true" help:"sign type is capcha_sign,this is required"`
|
||||||
|
|
||||||
|
// 验证码
|
||||||
|
CaptchaToken string `json:"captcha_token"`
|
||||||
|
|
||||||
|
// 必要且影响登录,由签名决定
|
||||||
|
DeviceID string `json:"device_id" required:"true" default:"9aa5c268e7bcfc197a9ad88e2fb330e5"`
|
||||||
|
ClientID string `json:"client_id" required:"true" default:"Xp6vsxz_7IYVw2BB"`
|
||||||
|
ClientSecret string `json:"client_secret" required:"true" default:"Xp6vsy4tN9toTVdMSpomVdXpRmES"`
|
||||||
|
ClientVersion string `json:"client_version" required:"true" default:"7.51.0.8196"`
|
||||||
|
PackageName string `json:"package_name" required:"true" default:"com.xunlei.downloadprovider"`
|
||||||
|
|
||||||
|
//不影响登录,影响下载速度
|
||||||
|
UserAgent string `json:"user_agent" required:"true" default:"ANDROID-com.xunlei.downloadprovider/7.51.0.8196 netWorkType/4G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_14_186-perf-gdcf98eab238b) (JAVA 0)"`
|
||||||
|
DownUserAgent string `json:"down_user_agent" required:"true" default:"Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录特征,用于判断是否重新登录
|
||||||
|
func (i *ExpertAddition) GetIdentity() string {
|
||||||
|
hash := md5.New()
|
||||||
|
if i.LoginType == "refresh_token" {
|
||||||
|
hash.Write([]byte(i.RefreshToken))
|
||||||
|
} else {
|
||||||
|
hash.Write([]byte(i.Username + i.Password))
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.SignType == "capcha_sign" {
|
||||||
|
hash.Write([]byte(i.CaptchaSign + i.Timestamp))
|
||||||
|
} else {
|
||||||
|
hash.Write([]byte(i.Algorithms))
|
||||||
|
}
|
||||||
|
|
||||||
|
hash.Write([]byte(i.DeviceID))
|
||||||
|
hash.Write([]byte(i.ClientID))
|
||||||
|
hash.Write([]byte(i.ClientSecret))
|
||||||
|
hash.Write([]byte(i.ClientVersion))
|
||||||
|
hash.Write([]byte(i.PackageName))
|
||||||
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Addition struct {
|
||||||
|
driver.RootID
|
||||||
|
Username string `json:"username" required:"true"`
|
||||||
|
Password string `json:"password" required:"true"`
|
||||||
|
CaptchaToken string `json:"captcha_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录特征,用于判断是否重新登录
|
||||||
|
func (i *Addition) GetIdentity() string {
|
||||||
|
return utils.GetMD5Encode(i.Username + i.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = driver.Config{
|
||||||
|
Name: "XunLei",
|
||||||
|
LocalSort: true,
|
||||||
|
OnlyProxy: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var configExpert = driver.Config{
|
||||||
|
Name: "XunLeiExpert",
|
||||||
|
LocalSort: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterDriver(config, func() driver.Driver {
|
||||||
|
return &XunLei{}
|
||||||
|
})
|
||||||
|
op.RegisterDriver(configExpert, func() driver.Driver {
|
||||||
|
return &XunLeiExpert{}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,188 @@
|
||||||
|
package xunlei
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrResp struct {
|
||||||
|
ErrorCode int64 `json:"error_code"`
|
||||||
|
ErrorMsg string `json:"error"`
|
||||||
|
ErrorDescription string `json:"error_description"`
|
||||||
|
// ErrorDetails interface{} `json:"error_details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrResp) IsError() bool {
|
||||||
|
return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrResp) Error() string {
|
||||||
|
return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ErrorDescription: %s ", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 验证码Token
|
||||||
|
**/
|
||||||
|
type CaptchaTokenRequest struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
CaptchaToken string `json:"captcha_token"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
|
Meta map[string]string `json:"meta"`
|
||||||
|
RedirectUri string `json:"redirect_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaptchaTokenResponse struct {
|
||||||
|
CaptchaToken string `json:"captcha_token"`
|
||||||
|
ExpiresIn int64 `json:"expires_in"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 登录
|
||||||
|
**/
|
||||||
|
type TokenResp struct {
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
ExpiresIn int64 `json:"expires_in"`
|
||||||
|
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TokenResp) Token() string {
|
||||||
|
return fmt.Sprint(t.TokenType, " ", t.AccessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignInRequest struct {
|
||||||
|
CaptchaToken string `json:"captcha_token"`
|
||||||
|
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 文件
|
||||||
|
**/
|
||||||
|
type FileList struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
NextPageToken string `json:"next_page_token"`
|
||||||
|
Files []Files `json:"files"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
VersionOutdated bool `json:"version_outdated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Files struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
ParentID string `json:"parent_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
//UserID string `json:"user_id"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
//Revision string `json:"revision"`
|
||||||
|
//FileExtension string `json:"file_extension"`
|
||||||
|
//MimeType string `json:"mime_type"`
|
||||||
|
//Starred bool `json:"starred"`
|
||||||
|
WebContentLink string `json:"web_content_link"`
|
||||||
|
CreatedTime time.Time `json:"created_time"`
|
||||||
|
ModifiedTime time.Time `json:"modified_time"`
|
||||||
|
IconLink string `json:"icon_link"`
|
||||||
|
ThumbnailLink string `json:"thumbnail_link"`
|
||||||
|
//Md5Checksum string `json:"md5_checksum"`
|
||||||
|
//Hash string `json:"hash"`
|
||||||
|
//Links struct{} `json:"links"`
|
||||||
|
Phase string `json:"phase"`
|
||||||
|
Audit struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
} `json:"audit"`
|
||||||
|
/* Medias []struct {
|
||||||
|
Category string `json:"category"`
|
||||||
|
IconLink string `json:"icon_link"`
|
||||||
|
IsDefault bool `json:"is_default"`
|
||||||
|
IsOrigin bool `json:"is_origin"`
|
||||||
|
IsVisible bool `json:"is_visible"`
|
||||||
|
//Link interface{} `json:"link"`
|
||||||
|
MediaID string `json:"media_id"`
|
||||||
|
MediaName string `json:"media_name"`
|
||||||
|
NeedMoreQuota bool `json:"need_more_quota"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
RedirectLink string `json:"redirect_link"`
|
||||||
|
ResolutionName string `json:"resolution_name"`
|
||||||
|
Video struct {
|
||||||
|
AudioCodec string `json:"audio_codec"`
|
||||||
|
BitRate int `json:"bit_rate"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
FrameRate int `json:"frame_rate"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
VideoCodec string `json:"video_codec"`
|
||||||
|
VideoType string `json:"video_type"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
} `json:"video"`
|
||||||
|
VipTypes []string `json:"vip_types"`
|
||||||
|
} `json:"medias"` */
|
||||||
|
Trashed bool `json:"trashed"`
|
||||||
|
DeleteTime string `json:"delete_time"`
|
||||||
|
OriginalURL string `json:"original_url"`
|
||||||
|
//Params struct{} `json:"params"`
|
||||||
|
//OriginalFileIndex int `json:"original_file_index"`
|
||||||
|
//Space string `json:"space"`
|
||||||
|
//Apps []interface{} `json:"apps"`
|
||||||
|
//Writable bool `json:"writable"`
|
||||||
|
//FolderType string `json:"folder_type"`
|
||||||
|
//Collection interface{} `json:"collection"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Files) GetSize() int64 { size, _ := strconv.ParseInt(c.Size, 10, 64); return size }
|
||||||
|
func (c *Files) GetName() string { return c.Name }
|
||||||
|
func (c *Files) ModTime() time.Time { return c.ModifiedTime }
|
||||||
|
func (c *Files) IsDir() bool { return c.Kind == FOLDER }
|
||||||
|
func (c *Files) GetID() string { return c.ID }
|
||||||
|
func (c *Files) GetPath() string { return "" }
|
||||||
|
func (c *Files) Thumb() string { return c.ThumbnailLink }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 上传
|
||||||
|
**/
|
||||||
|
type UploadTaskResponse struct {
|
||||||
|
UploadType string `json:"upload_type"`
|
||||||
|
|
||||||
|
/*//UPLOAD_TYPE_FORM
|
||||||
|
Form struct {
|
||||||
|
//Headers struct{} `json:"headers"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
MultiParts struct {
|
||||||
|
OSSAccessKeyID string `json:"OSSAccessKeyId"`
|
||||||
|
Signature string `json:"Signature"`
|
||||||
|
Callback string `json:"callback"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Policy string `json:"policy"`
|
||||||
|
XUserData string `json:"x:user_data"`
|
||||||
|
} `json:"multi_parts"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"form"`*/
|
||||||
|
|
||||||
|
//UPLOAD_TYPE_RESUMABLE
|
||||||
|
Resumable struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Params struct {
|
||||||
|
AccessKeyID string `json:"access_key_id"`
|
||||||
|
AccessKeySecret string `json:"access_key_secret"`
|
||||||
|
Bucket string `json:"bucket"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
Expiration time.Time `json:"expiration"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
SecurityToken string `json:"security_token"`
|
||||||
|
} `json:"params"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
} `json:"resumable"`
|
||||||
|
|
||||||
|
File Files `json:"file"`
|
||||||
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
package xunlei
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
API_URL = "https://api-pan.xunlei.com/drive/v1"
|
||||||
|
FILE_API_URL = API_URL + "/files"
|
||||||
|
XLUSER_API_URL = "https://xluser-ssl.xunlei.com/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FOLDER = "drive#folder"
|
||||||
|
FILE = "drive#file"
|
||||||
|
RESUMABLE = "drive#resumable"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
UPLOAD_TYPE_UNKNOWN = "UPLOAD_TYPE_UNKNOWN"
|
||||||
|
//UPLOAD_TYPE_FORM = "UPLOAD_TYPE_FORM"
|
||||||
|
UPLOAD_TYPE_RESUMABLE = "UPLOAD_TYPE_RESUMABLE"
|
||||||
|
UPLOAD_TYPE_URL = "UPLOAD_TYPE_URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAction(method string, url string) string {
|
||||||
|
urlpath := regexp.MustCompile(`://[^/]+((/[^/\s?#]+)*)`).FindStringSubmatch(url)[1]
|
||||||
|
return method + ":" + urlpath
|
||||||
|
}
|
||||||
|
|
||||||
|
type Common struct {
|
||||||
|
client *resty.Client
|
||||||
|
|
||||||
|
captchaToken string
|
||||||
|
|
||||||
|
// 签名相关,二选一
|
||||||
|
Algorithms []string
|
||||||
|
Timestamp, CaptchaSign string
|
||||||
|
|
||||||
|
// 必要值,签名相关
|
||||||
|
DeviceID string
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
ClientVersion string
|
||||||
|
PackageName string
|
||||||
|
UserAgent string
|
||||||
|
DownUserAgent string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Common) SetCaptchaToken(captchaToken string) {
|
||||||
|
c.captchaToken = captchaToken
|
||||||
|
}
|
||||||
|
func (c *Common) GetCaptchaToken() string {
|
||||||
|
return c.captchaToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新验证码token(登录后)
|
||||||
|
func (c *Common) RefreshCaptchaTokenAtLogin(action, userID string) error {
|
||||||
|
metas := map[string]string{
|
||||||
|
"client_version": c.ClientVersion,
|
||||||
|
"package_name": c.PackageName,
|
||||||
|
"user_id": userID,
|
||||||
|
}
|
||||||
|
metas["timestamp"], metas["captcha_sign"] = c.GetCaptchaSign()
|
||||||
|
return c.refreshCaptchaToken(action, metas)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新验证码token(登录时)
|
||||||
|
func (c *Common) RefreshCaptchaTokenInLogin(action, username string) error {
|
||||||
|
metas := make(map[string]string)
|
||||||
|
if ok, _ := regexp.MatchString(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`, username); ok {
|
||||||
|
metas["email"] = username
|
||||||
|
} else if len(username) >= 11 && len(username) <= 18 {
|
||||||
|
metas["phone_number"] = username
|
||||||
|
} else {
|
||||||
|
metas["username"] = username
|
||||||
|
}
|
||||||
|
return c.refreshCaptchaToken(action, metas)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取验证码签名
|
||||||
|
func (c *Common) GetCaptchaSign() (timestamp, sign string) {
|
||||||
|
if len(c.Algorithms) == 0 {
|
||||||
|
return c.Timestamp, c.CaptchaSign
|
||||||
|
}
|
||||||
|
timestamp = fmt.Sprint(time.Now().UnixMilli())
|
||||||
|
str := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp)
|
||||||
|
for _, algorithm := range c.Algorithms {
|
||||||
|
str = utils.GetMD5Encode(str + algorithm)
|
||||||
|
}
|
||||||
|
sign = "1." + str
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新验证码token
|
||||||
|
func (c *Common) refreshCaptchaToken(action string, metas map[string]string) error {
|
||||||
|
param := CaptchaTokenRequest{
|
||||||
|
Action: action,
|
||||||
|
CaptchaToken: c.captchaToken,
|
||||||
|
ClientID: c.ClientID,
|
||||||
|
DeviceID: c.DeviceID,
|
||||||
|
Meta: metas,
|
||||||
|
RedirectUri: "xlaccsdk01://xunlei.com/callback?state=harbor",
|
||||||
|
}
|
||||||
|
var e ErrResp
|
||||||
|
var resp CaptchaTokenResponse
|
||||||
|
_, err := c.Request(XLUSER_API_URL+"/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetError(&e).SetBody(param)
|
||||||
|
}, &resp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.IsError() {
|
||||||
|
return &e
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Url != "" {
|
||||||
|
return fmt.Errorf("need verify:%s", resp.Url)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.CaptchaToken == "" {
|
||||||
|
return fmt.Errorf("empty captchaToken")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetCaptchaToken(resp.CaptchaToken)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有基础信息的请求
|
||||||
|
func (c *Common) Request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
|
req := c.client.R().SetHeaders(map[string]string{
|
||||||
|
"user-agent": c.UserAgent,
|
||||||
|
"accept": "application/json;charset=UTF-8",
|
||||||
|
"x-device-id": c.DeviceID,
|
||||||
|
"x-client-id": c.ClientID,
|
||||||
|
"x-client-version": c.ClientVersion,
|
||||||
|
})
|
||||||
|
|
||||||
|
if callback != nil {
|
||||||
|
callback(req)
|
||||||
|
}
|
||||||
|
if resp != nil {
|
||||||
|
req.SetResult(resp)
|
||||||
|
}
|
||||||
|
res, err := req.Execute(method, url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var erron ErrResp
|
||||||
|
utils.Json.Unmarshal(res.Body(), &erron)
|
||||||
|
if erron.IsError() {
|
||||||
|
return nil, &erron
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Body(), nil
|
||||||
|
}
|
Loading…
Reference in New Issue