mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-06 21:36:37 +08:00
添加strm生成
添加strm
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
// 115 开放平台只读 API:列目录、详情、下载直链、授权(二维码/换 token/刷新)。
|
||||
package cloud115
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ─── 文件模型 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
type FileType string
|
||||
|
||||
const (
|
||||
TypeFile FileType = "1"
|
||||
TypeDir FileType = "0"
|
||||
)
|
||||
|
||||
// RemoteFile 是 115 文件列表中的一个条目。
|
||||
type RemoteFile struct {
|
||||
FileId string `json:"fid"` // 文件 ID
|
||||
Pid string `json:"pid"` // 父文件夹 ID
|
||||
Category FileType `json:"fc"` // 0 文件夹 1 文件
|
||||
FileName string `json:"fn"` // 文件名
|
||||
PickCode string `json:"pc"` // 提取码
|
||||
Utime int64 `json:"upt"` // 修改时间
|
||||
Ptime int64 `json:"uppt"` // 上传时间
|
||||
Sha1 string `json:"sha1"` // SHA1
|
||||
FileSize int64 `json:"fs"` // 大小
|
||||
Fta string `json:"fta"` // 0/2 未上传完成,1 已完成
|
||||
}
|
||||
|
||||
func (f RemoteFile) ModifiedAt() int64 {
|
||||
if f.Utime > 0 {
|
||||
return f.Utime
|
||||
}
|
||||
return f.Ptime
|
||||
}
|
||||
|
||||
type FileListResp struct {
|
||||
RespBase
|
||||
Path []struct {
|
||||
Name string `json:"name"`
|
||||
FileId string `json:"cid"`
|
||||
} `json:"path"`
|
||||
PathStr string `json:"path_str"`
|
||||
}
|
||||
|
||||
// GetFsList 列目录(cid=0 为根目录)。
|
||||
func (c *OpenClient) GetFsList(ctx context.Context, cid string, offset, limit int) ([]RemoteFile, string, error) {
|
||||
if cid == "" {
|
||||
cid = "0"
|
||||
}
|
||||
params := map[string]string{"cid": cid}
|
||||
if limit > 0 {
|
||||
params["limit"] = fmt.Sprint(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
params["offset"] = fmt.Sprint(offset)
|
||||
}
|
||||
params["cur"] = "1"
|
||||
params["show_dir"] = "1"
|
||||
resp, err := c.doAuthJSON(ctx, "GET", ProAPIBase+"/open/ufile/files", params, 2)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
// state=false(token 过期、限流、业务失败等)绝不能当作空目录返回:
|
||||
// 同步流程会据此认为远端已清空并清理本地 .strm/元数据文件。
|
||||
if !resp.State {
|
||||
return nil, "", NewOpenAPIResponseError(resp.Code, resp.Errno, resp.Message, resp.Error, "115 接口调用失败")
|
||||
}
|
||||
var list FileListResp
|
||||
list.RespBase = *resp
|
||||
if len(resp.Raw) > 0 {
|
||||
_ = json.Unmarshal(resp.Raw, &list)
|
||||
}
|
||||
files, err := openList[RemoteFile](resp.Data)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("115: 解析文件列表失败:%w", err)
|
||||
}
|
||||
pathStr := make([]string, 0, len(list.Path))
|
||||
for _, item := range list.Path {
|
||||
if item.FileId == "0" {
|
||||
continue
|
||||
}
|
||||
pathStr = append(pathStr, item.Name)
|
||||
}
|
||||
return files, strings.Join(pathStr, "/"), nil
|
||||
}
|
||||
|
||||
// GetFsDetailByCid 查询文件(夹)详情。
|
||||
func (c *OpenClient) GetFsDetailByCid(ctx context.Context, fileId string) (*RemoteFileDetail, error) {
|
||||
params := map[string]string{"file_id": fileId}
|
||||
resp, err := c.doAuthJSON(ctx, "GET", ProAPIBase+"/open/folder/get_info", params, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return openFirstList[RemoteFileDetail](resp.Data)
|
||||
}
|
||||
|
||||
// RemoteFileDetail 是文件详情。
|
||||
type RemoteFileDetail struct {
|
||||
FileId string `json:"file_id"`
|
||||
FileName string `json:"file_name"`
|
||||
PickCode string `json:"pick_code"`
|
||||
Sha1 string `json:"sha1"`
|
||||
Category FileType `json:"file_category"`
|
||||
SizeByte int64 `json:"size_byte"`
|
||||
Paths []struct {
|
||||
FileId string `json:"file_id"`
|
||||
Name string `json:"file_name"`
|
||||
} `json:"paths"`
|
||||
}
|
||||
|
||||
// ─── 下载直链 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
type downloadURLData struct {
|
||||
FileName string `json:"file_name"`
|
||||
PickCode string `json:"pick_code"`
|
||||
Sha1 string `json:"sha1"`
|
||||
URL struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"url"`
|
||||
}
|
||||
|
||||
// GetDownloadURL 获取下载直链(pickcode)。
|
||||
func (c *OpenClient) GetDownloadURL(ctx context.Context, pickCode string) (string, error) {
|
||||
params := map[string]string{"pick_code": pickCode}
|
||||
resp, err := c.doAuthJSON(ctx, "POST", ProAPIBase+"/open/ufile/downurl", params, 1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var data map[string]downloadURLData
|
||||
if err := json.Unmarshal(resp.Data, &data); err != nil {
|
||||
return "", fmt.Errorf("115: 解析下载地址失败:%w", err)
|
||||
}
|
||||
first := firstOrEmpty(data)
|
||||
if first.URL.URL == "" {
|
||||
return "", fmt.Errorf("115: 下载地址为空(文件可能未上传完成或已被删除)")
|
||||
}
|
||||
return first.URL.URL, nil
|
||||
}
|
||||
|
||||
// ─── 授权(设备码扫码) ──────────────────────────────────────────────────────
|
||||
|
||||
// QrCodeScanStatus 扫码状态。
|
||||
type QrCodeScanStatus int
|
||||
|
||||
const (
|
||||
QrCodeScanStatusExpired QrCodeScanStatus = 5
|
||||
QrCodeScanStatusNotScanned QrCodeScanStatus = 2
|
||||
QrCodeScanStatusScanned QrCodeScanStatus = 3
|
||||
QrCodeScanStatusConfirmed QrCodeScanStatus = 4
|
||||
)
|
||||
|
||||
func (s QrCodeScanStatus) String() string {
|
||||
switch s {
|
||||
case QrCodeScanStatusNotScanned:
|
||||
return "waiting"
|
||||
case QrCodeScanStatusScanned:
|
||||
return "scanned"
|
||||
case QrCodeScanStatusConfirmed:
|
||||
return "confirmed"
|
||||
default:
|
||||
return "expired"
|
||||
}
|
||||
}
|
||||
|
||||
func (s QrCodeScanStatus) Tip() string {
|
||||
switch s {
|
||||
case QrCodeScanStatusNotScanned:
|
||||
return "等待扫码"
|
||||
case QrCodeScanStatusScanned:
|
||||
return "已扫码,请在 115 客户端确认"
|
||||
case QrCodeScanStatusConfirmed:
|
||||
return "授权成功"
|
||||
default:
|
||||
return "二维码已过期"
|
||||
}
|
||||
}
|
||||
|
||||
// QrCodeData 是设备码二维码数据。
|
||||
type QrCodeData struct {
|
||||
Uid string `json:"uid"`
|
||||
Time int64 `json:"time"`
|
||||
Sign string `json:"sign"`
|
||||
Qrcode string `json:"qrcode"` // 二维码图片 URL
|
||||
}
|
||||
|
||||
// QrCodeDataReturn 含 PKCE code_verifier。
|
||||
type QrCodeDataReturn struct {
|
||||
QrCodeData
|
||||
CodeVerifier string `json:"code_verifier"`
|
||||
}
|
||||
|
||||
// TokenData 是 115 访问令牌。
|
||||
type TokenData struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// GetQrCode 获取设备码登录二维码。
|
||||
func (c *OpenClient) GetQrCode() (*QrCodeDataReturn, error) {
|
||||
codeVerifier := RandomString(64)
|
||||
params := map[string]string{
|
||||
"client_id": c.AppID,
|
||||
"code_challenge": genCodeChallenge(codeVerifier),
|
||||
"code_challenge_method": "sha256",
|
||||
}
|
||||
resp, err := c.doJSON(context.Background(), "POST", PassportAPIBase+"/open/authDeviceCode", params, false, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code, err := openFirstList[QrCodeData](resp.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &QrCodeDataReturn{QrCodeData: *code, CodeVerifier: codeVerifier}, nil
|
||||
}
|
||||
|
||||
// QrCodeScanStatus 查询扫码状态。
|
||||
func (c *OpenClient) QrCodeScanStatus(codeData *QrCodeData) (QrCodeScanStatus, error) {
|
||||
if codeData == nil {
|
||||
return QrCodeScanStatusExpired, fmt.Errorf("空二维码数据")
|
||||
}
|
||||
params := map[string]string{
|
||||
"uid": codeData.Uid,
|
||||
"time": fmt.Sprint(codeData.Time),
|
||||
"sign": codeData.Sign,
|
||||
}
|
||||
resp, err := c.doJSON(context.Background(), "GET", QRCodeAPIBase+"/get/status/", params, false, 1)
|
||||
if err != nil {
|
||||
return QrCodeScanStatusExpired, err
|
||||
}
|
||||
status, err := openFirstList[struct {
|
||||
Status int `json:"status"` // 0 未扫码 1 已扫码 2 已确认
|
||||
}](resp.Data)
|
||||
if err != nil {
|
||||
return QrCodeScanStatusExpired, err
|
||||
}
|
||||
switch status.Status {
|
||||
case 1:
|
||||
return QrCodeScanStatusScanned, nil
|
||||
case 2:
|
||||
return QrCodeScanStatusConfirmed, nil
|
||||
case 0:
|
||||
return QrCodeScanStatusNotScanned, nil
|
||||
default:
|
||||
return QrCodeScanStatusExpired, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetToken 用设备码换访问令牌。
|
||||
func (c *OpenClient) GetToken(qrCode *QrCodeDataReturn) (*TokenData, error) {
|
||||
if qrCode == nil || qrCode.Uid == "" {
|
||||
return nil, fmt.Errorf("空二维码数据")
|
||||
}
|
||||
params := map[string]string{
|
||||
"uid": qrCode.Uid,
|
||||
"code_verifier": qrCode.CodeVerifier,
|
||||
}
|
||||
resp, err := c.doJSON(context.Background(), "POST", PassportAPIBase+"/open/deviceCodeToToken", params, false, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := openFirstList[TokenData](resp.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.SetAuthToken(token.AccessToken, token.RefreshToken)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// RefreshToken 刷新访问令牌。
|
||||
func (c *OpenClient) RefreshToken(refreshToken string) (*TokenData, error) {
|
||||
if refreshToken == "" {
|
||||
refreshToken = c.RefreshTokenStr
|
||||
}
|
||||
if refreshToken == "" {
|
||||
return nil, fmt.Errorf("没有可用的 refresh_token")
|
||||
}
|
||||
params := map[string]string{"refresh_token": refreshToken}
|
||||
resp, err := c.doJSON(context.Background(), "POST", PassportAPIBase+"/open/refreshToken", params, false, 0)
|
||||
if err != nil && resp == nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.State {
|
||||
apiErr := NewOpenAPIResponseError(resp.Code, resp.Errno, resp.Message, resp.Error, "115 开放平台刷新访问凭证失败")
|
||||
if IsRefreshTokenDead(apiErr) {
|
||||
c.SetAuthToken("", "")
|
||||
}
|
||||
return nil, apiErr
|
||||
}
|
||||
token, err := openFirstList[TokenData](resp.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.SetAuthToken(token.AccessToken, token.RefreshToken)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// ─── 用户信息 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// UserInfo 是 115 用户信息。
|
||||
type UserInfo struct {
|
||||
UserId json.Number `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
}
|
||||
|
||||
// FetchUserInfo 获取用户信息。
|
||||
func (c *OpenClient) FetchUserInfo(ctx context.Context) (*UserInfo, error) {
|
||||
resp, err := c.doAuthJSON(ctx, "GET", ProAPIBase+"/open/user/info", nil, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var info UserInfo
|
||||
if err := json.Unmarshal(resp.Data, &info); err != nil {
|
||||
return nil, fmt.Errorf("115: 解析用户信息失败:%w", err)
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
Reference in New Issue
Block a user