mirror of https://github.com/Xhofe/alist
feat: add support for quark tv driver and uc tv driver (#6959)
parent
979d0cfeee
commit
62ed169a39
|
@ -41,6 +41,7 @@ import (
|
|||
_ "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_uc"
|
||||
_ "github.com/alist-org/alist/v3/drivers/quark_uc_tv"
|
||||
_ "github.com/alist-org/alist/v3/drivers/quqi"
|
||||
_ "github.com/alist-org/alist/v3/drivers/s3"
|
||||
_ "github.com/alist-org/alist/v3/drivers/seafile"
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
package quark_uc_tv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
type QuarkUCTV struct {
|
||||
*QuarkUCTVCommon
|
||||
model.Storage
|
||||
Addition
|
||||
config driver.Config
|
||||
conf Conf
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Config() driver.Config {
|
||||
return d.config
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Init(ctx context.Context) error {
|
||||
|
||||
if d.Addition.DeviceID == "" {
|
||||
d.Addition.DeviceID = utils.GetMD5EncodeStr(time.Now().String())
|
||||
}
|
||||
op.MustSaveDriverStorage(d)
|
||||
|
||||
if d.QuarkUCTVCommon == nil {
|
||||
d.QuarkUCTVCommon = &QuarkUCTVCommon{
|
||||
AccessToken: "",
|
||||
}
|
||||
}
|
||||
ctx1, cancelFunc := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancelFunc()
|
||||
if d.Addition.RefreshToken == "" {
|
||||
if d.Addition.QueryToken == "" {
|
||||
qrData, err := d.getLoginCode(ctx1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 展示二维码
|
||||
qrTemplate := `<body>
|
||||
<img src="data:image/jpeg;base64,%s"/>
|
||||
</body>`
|
||||
qrPage := fmt.Sprintf(qrTemplate, qrData)
|
||||
return fmt.Errorf("need verify: \n%s", qrPage)
|
||||
} else {
|
||||
// 通过query token获取code -> refresh token
|
||||
code, err := d.getCode(ctx1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 通过code获取refresh token
|
||||
err = d.getRefreshTokenByTV(ctx1, code, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// 通过refresh token获取access token
|
||||
if d.QuarkUCTVCommon.AccessToken == "" {
|
||||
err := d.getRefreshTokenByTV(ctx1, d.Addition.RefreshToken, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 access token 是否有效
|
||||
_, err := d.isLogin(ctx1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
files := make([]model.Obj, 0)
|
||||
pageIndex := int64(0)
|
||||
pageSize := int64(100)
|
||||
for {
|
||||
var filesData FilesData
|
||||
_, err := d.request(ctx, "/file", "GET", func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"method": "list",
|
||||
"parent_fid": dir.GetID(),
|
||||
"order_by": "3",
|
||||
"desc": "1",
|
||||
"category": "",
|
||||
"source": "",
|
||||
"ex_source": "",
|
||||
"list_all": "0",
|
||||
"page_size": strconv.FormatInt(pageSize, 10),
|
||||
"page_index": strconv.FormatInt(pageIndex, 10),
|
||||
})
|
||||
}, &filesData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range filesData.Data.Files {
|
||||
files = append(files, &filesData.Data.Files[i])
|
||||
}
|
||||
if pageIndex*pageSize >= filesData.Data.TotalCount {
|
||||
break
|
||||
} else {
|
||||
pageIndex++
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
files := &model.Link{}
|
||||
var fileLink FileLink
|
||||
_, err := d.request(ctx, "/file", "GET", func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"method": "download",
|
||||
"group_by": "source",
|
||||
"fid": file.GetID(),
|
||||
"resolution": "low,normal,high,super,2k,4k",
|
||||
"support": "dolby_vision",
|
||||
})
|
||||
}, &fileLink)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files.URL = fileLink.Data.DownloadURL
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Remove(ctx context.Context, obj model.Obj) error {
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
type QuarkUCTVCommon struct {
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*QuarkUCTV)(nil)
|
|
@ -0,0 +1,67 @@
|
|||
package quark_uc_tv
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
// Usually one of two
|
||||
driver.RootID
|
||||
// define other
|
||||
RefreshToken string `json:"refresh_token" required:"false" default:""`
|
||||
// 必要且影响登录,由签名决定
|
||||
DeviceID string `json:"device_id" required:"false" default:""`
|
||||
// 登陆所用的数据 无需手动填写
|
||||
QueryToken string `json:"query_token" required:"false" default:"" help:"don't edit'"`
|
||||
}
|
||||
|
||||
type Conf struct {
|
||||
api string
|
||||
clientID string
|
||||
signKey string
|
||||
appVer string
|
||||
channel string
|
||||
codeApi string
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &QuarkUCTV{
|
||||
config: driver.Config{
|
||||
Name: "QuarkTV",
|
||||
OnlyLocal: false,
|
||||
DefaultRoot: "0",
|
||||
NoOverwriteUpload: true,
|
||||
NoUpload: true,
|
||||
},
|
||||
conf: Conf{
|
||||
api: "https://open-api-drive.quark.cn",
|
||||
clientID: "d3194e61504e493eb6222857bccfed94",
|
||||
signKey: "kw2dvtd7p4t3pjl2d9ed9yc8yej8kw2d",
|
||||
appVer: "1.5.6",
|
||||
channel: "CP",
|
||||
codeApi: "http://api.extscreen.com/quarkdrive",
|
||||
},
|
||||
}
|
||||
})
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &QuarkUCTV{
|
||||
config: driver.Config{
|
||||
Name: "UCTV",
|
||||
OnlyLocal: false,
|
||||
DefaultRoot: "0",
|
||||
NoOverwriteUpload: true,
|
||||
NoUpload: true,
|
||||
},
|
||||
conf: Conf{
|
||||
api: "https://open-api-drive.uc.cn",
|
||||
clientID: "5acf882d27b74502b7040b0c65519aa7",
|
||||
signKey: "l3srvtd7p42l0d0x1u8d7yc8ye9kki4d",
|
||||
appVer: "1.6.5",
|
||||
channel: "UCTVOFFICIALWEB",
|
||||
codeApi: "http://api.extscreen.com/ucdrive",
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package quark_uc_tv
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Resp struct {
|
||||
CommonRsp
|
||||
Errno int `json:"errno"`
|
||||
ErrorInfo string `json:"error_info"`
|
||||
}
|
||||
|
||||
type CommonRsp struct {
|
||||
Status int `json:"status"`
|
||||
ReqID string `json:"req_id"`
|
||||
}
|
||||
|
||||
type RefreshTokenAuthResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Status int `json:"status"`
|
||||
Errno int `json:"errno"`
|
||||
ErrorInfo string `json:"error_info"`
|
||||
ReqID string `json:"req_id"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
} `json:"data"`
|
||||
}
|
||||
type Files struct {
|
||||
Fid string `json:"fid"`
|
||||
ParentFid string `json:"parent_fid"`
|
||||
Category int `json:"category"`
|
||||
Filename string `json:"filename"`
|
||||
Size int64 `json:"size"`
|
||||
FileType string `json:"file_type"`
|
||||
SubItems int `json:"sub_items,omitempty"`
|
||||
Isdir int `json:"isdir"`
|
||||
Duration int `json:"duration"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
IsBackup int `json:"is_backup"`
|
||||
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
||||
}
|
||||
|
||||
func (f *Files) GetSize() int64 {
|
||||
return f.Size
|
||||
}
|
||||
|
||||
func (f *Files) GetName() string {
|
||||
return f.Filename
|
||||
}
|
||||
|
||||
func (f *Files) ModTime() time.Time {
|
||||
//return time.Unix(f.UpdatedAt, 0)
|
||||
return time.Unix(0, f.UpdatedAt*int64(time.Millisecond))
|
||||
}
|
||||
|
||||
func (f *Files) CreateTime() time.Time {
|
||||
//return time.Unix(f.CreatedAt, 0)
|
||||
return time.Unix(0, f.CreatedAt*int64(time.Millisecond))
|
||||
}
|
||||
|
||||
func (f *Files) IsDir() bool {
|
||||
return f.Isdir == 1
|
||||
}
|
||||
|
||||
func (f *Files) GetHash() utils.HashInfo {
|
||||
return utils.HashInfo{}
|
||||
}
|
||||
|
||||
func (f *Files) GetID() string {
|
||||
return f.Fid
|
||||
}
|
||||
|
||||
func (f *Files) GetPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
var _ model.Obj = (*Files)(nil)
|
||||
|
||||
type FilesData struct {
|
||||
CommonRsp
|
||||
Data struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
Files []Files `json:"files"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type FileLink struct {
|
||||
CommonRsp
|
||||
Data struct {
|
||||
Fid string `json:"fid"`
|
||||
FileName string `json:"file_name"`
|
||||
Size int64 `json:"size"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
} `json:"data"`
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
package quark_uc_tv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
UserAgent = "Mozilla/5.0 (Linux; U; Android 13; zh-cn; M2004J7AC Build/UKQ1.231108.001) AppleWebKit/533.1 (KHTML, like Gecko) Mobile Safari/533.1"
|
||||
DeviceBrand = "Xiaomi"
|
||||
Platform = "tv"
|
||||
DeviceName = "M2004J7AC"
|
||||
DeviceModel = "M2004J7AC"
|
||||
BuildDevice = "M2004J7AC"
|
||||
BuildProduct = "M2004J7AC"
|
||||
DeviceGpu = "Adreno (TM) 550"
|
||||
ActivityRect = "{}"
|
||||
)
|
||||
|
||||
func (d *QuarkUCTV) request(ctx context.Context, pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
u := d.conf.api + pathname
|
||||
tm, token, reqID := d.generateReqSign(method, pathname, d.conf.signKey)
|
||||
req := base.RestyClient.R()
|
||||
req.SetContext(ctx)
|
||||
req.SetHeaders(map[string]string{
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"User-Agent": UserAgent,
|
||||
"x-pan-tm": tm,
|
||||
"x-pan-token": token,
|
||||
"x-pan-client-id": d.conf.clientID,
|
||||
})
|
||||
req.SetQueryParams(map[string]string{
|
||||
"req_id": reqID,
|
||||
"access_token": d.QuarkUCTVCommon.AccessToken,
|
||||
"app_ver": d.conf.appVer,
|
||||
"device_id": d.Addition.DeviceID,
|
||||
"device_brand": DeviceBrand,
|
||||
"platform": Platform,
|
||||
"device_name": DeviceName,
|
||||
"device_model": DeviceModel,
|
||||
"build_device": BuildDevice,
|
||||
"build_product": BuildProduct,
|
||||
"device_gpu": DeviceGpu,
|
||||
"activity_rect": ActivityRect,
|
||||
"channel": d.conf.channel,
|
||||
})
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var e Resp
|
||||
req.SetError(&e)
|
||||
res, err := req.Execute(method, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 判断 是否需要 刷新 access_token
|
||||
if e.Status == -1 && e.Errno == 10001 {
|
||||
// token 过期
|
||||
err = d.getRefreshTokenByTV(ctx, d.Addition.RefreshToken, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx1, cancelFunc := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancelFunc()
|
||||
return d.request(ctx1, pathname, method, callback, resp)
|
||||
}
|
||||
|
||||
if e.Status >= 400 || e.Errno != 0 {
|
||||
return nil, errors.New(e.ErrorInfo)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) getLoginCode(ctx context.Context) (string, error) {
|
||||
// 获取登录二维码
|
||||
pathname := "/oauth/authorize"
|
||||
var resp struct {
|
||||
CommonRsp
|
||||
QrData string `json:"qr_data"`
|
||||
QueryToken string `json:"query_token"`
|
||||
}
|
||||
_, err := d.request(ctx, pathname, "GET", func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"auth_type": "code",
|
||||
"client_id": d.conf.clientID,
|
||||
"scope": "netdisk",
|
||||
"qrcode": "1",
|
||||
"qr_width": "460",
|
||||
"qr_height": "460",
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 保存query_token 用于后续登录
|
||||
if resp.QueryToken != "" {
|
||||
d.Addition.QueryToken = resp.QueryToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
}
|
||||
return resp.QrData, nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) getCode(ctx context.Context) (string, error) {
|
||||
// 通过query token获取code
|
||||
pathname := "/oauth/code"
|
||||
var resp struct {
|
||||
CommonRsp
|
||||
Code string `json:"code"`
|
||||
}
|
||||
_, err := d.request(ctx, pathname, "GET", func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"client_id": d.conf.clientID,
|
||||
"scope": "netdisk",
|
||||
"query_token": d.Addition.QueryToken,
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Code, nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) getRefreshTokenByTV(ctx context.Context, code string, isRefresh bool) error {
|
||||
pathname := "/token"
|
||||
_, _, reqID := d.generateReqSign("POST", pathname, d.conf.signKey)
|
||||
u := d.conf.codeApi + pathname
|
||||
var resp RefreshTokenAuthResp
|
||||
body := map[string]string{
|
||||
"req_id": reqID,
|
||||
"app_ver": d.conf.appVer,
|
||||
"device_id": d.Addition.DeviceID,
|
||||
"device_brand": DeviceBrand,
|
||||
"platform": Platform,
|
||||
"device_name": DeviceName,
|
||||
"device_model": DeviceModel,
|
||||
"build_device": BuildDevice,
|
||||
"build_product": BuildProduct,
|
||||
"device_gpu": DeviceGpu,
|
||||
"activity_rect": ActivityRect,
|
||||
"channel": d.conf.channel,
|
||||
}
|
||||
if isRefresh {
|
||||
body["refresh_token"] = code
|
||||
} else {
|
||||
body["code"] = code
|
||||
}
|
||||
|
||||
_, err := base.RestyClient.R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(body).
|
||||
SetResult(&resp).
|
||||
SetContext(ctx).
|
||||
Post(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
return errors.New(resp.Message)
|
||||
}
|
||||
if resp.Data.RefreshToken != "" {
|
||||
d.Addition.RefreshToken = resp.Data.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
d.QuarkUCTVCommon.AccessToken = resp.Data.AccessToken
|
||||
} else {
|
||||
return errors.New("refresh token is empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) isLogin(ctx context.Context) (bool, error) {
|
||||
_, err := d.request(ctx, "/user", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"method": "user_info",
|
||||
})
|
||||
}, nil)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) generateReqSign(method string, pathname string, key string) (string, string, string) {
|
||||
//timestamp 13位时间戳
|
||||
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
|
||||
deviceID := d.Addition.DeviceID
|
||||
if deviceID == "" {
|
||||
deviceID = utils.GetMD5EncodeStr(timestamp)
|
||||
d.Addition.DeviceID = deviceID
|
||||
op.MustSaveDriverStorage(d)
|
||||
}
|
||||
// 生成req_id
|
||||
reqID := md5.Sum([]byte(deviceID + timestamp))
|
||||
reqIDHex := hex.EncodeToString(reqID[:])
|
||||
|
||||
// 生成x_pan_token
|
||||
tokenData := method + "&" + pathname + "&" + timestamp + "&" + key
|
||||
xPanToken := sha256.Sum256([]byte(tokenData))
|
||||
xPanTokenHex := hex.EncodeToString(xPanToken[:])
|
||||
|
||||
return timestamp, xPanTokenHex, reqIDHex
|
||||
}
|
Loading…
Reference in New Issue