diff --git a/drivers/all.go b/drivers/all.go index b976f92f2a6..1f015ef7d61 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -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" diff --git a/drivers/quark_uc_tv/driver.go b/drivers/quark_uc_tv/driver.go new file mode 100644 index 00000000000..ff7ccf20f7a --- /dev/null +++ b/drivers/quark_uc_tv/driver.go @@ -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 := `
+ + ` + 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) diff --git a/drivers/quark_uc_tv/meta.go b/drivers/quark_uc_tv/meta.go new file mode 100644 index 00000000000..cf7e478566e --- /dev/null +++ b/drivers/quark_uc_tv/meta.go @@ -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", + }, + } + }) +} diff --git a/drivers/quark_uc_tv/types.go b/drivers/quark_uc_tv/types.go new file mode 100644 index 00000000000..fb35b8b2d6d --- /dev/null +++ b/drivers/quark_uc_tv/types.go @@ -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"` +} diff --git a/drivers/quark_uc_tv/util.go b/drivers/quark_uc_tv/util.go new file mode 100644 index 00000000000..fefbb0361fb --- /dev/null +++ b/drivers/quark_uc_tv/util.go @@ -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 +}