mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-07-19 01:48:42 +08:00
feat(thunder&thunder_browser): fix deviceId generation & support offline download and update login interface (#290)
* fix(thunder): fix deviceID generation * feat(thunder_browser): support offline download and update login interface * feat(thunder_browser): add fluent_play method for offline download
This commit is contained in:
@ -58,7 +58,7 @@ func (x *Thunder) Init(ctx context.Context) (err error) {
|
||||
},
|
||||
DeviceID: func() string {
|
||||
if len(x.DeviceID) != 32 {
|
||||
return utils.GetMD5EncodeStr(x.DeviceID)
|
||||
return utils.GetMD5EncodeStr(x.Username + x.Password)
|
||||
}
|
||||
return x.DeviceID
|
||||
}(),
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/drivers/base"
|
||||
"github.com/OpenListTeam/OpenList/internal/driver"
|
||||
@ -65,6 +66,7 @@ func (x *ThunderBrowser) Init(ctx context.Context) (err error) {
|
||||
UserAgent: BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), PackageName, SdkVersion, ClientVersion, PackageName),
|
||||
DownloadUserAgent: DownloadUserAgent,
|
||||
UseVideoUrl: x.UseVideoUrl,
|
||||
UseFluentPlay: x.UseFluentPlay,
|
||||
RemoveWay: x.Addition.RemoveWay,
|
||||
refreshCTokenCk: func(token string) {
|
||||
x.CaptchaToken = token
|
||||
@ -81,6 +83,8 @@ func (x *ThunderBrowser) Init(ctx context.Context) (err error) {
|
||||
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
// 清空 信任密钥
|
||||
x.Addition.CreditKey = ""
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
return err
|
||||
@ -93,10 +97,20 @@ func (x *ThunderBrowser) Init(ctx context.Context) (err error) {
|
||||
if ctoekn != "" {
|
||||
x.SetCaptchaToken(ctoekn)
|
||||
}
|
||||
if x.DeviceID == "" {
|
||||
x.SetDeviceID(utils.GetMD5EncodeStr(x.Username + x.Password))
|
||||
|
||||
if x.Addition.CreditKey != "" {
|
||||
x.SetCreditKey(x.Addition.CreditKey)
|
||||
}
|
||||
|
||||
if x.Addition.DeviceID != "" {
|
||||
x.Common.DeviceID = x.Addition.DeviceID
|
||||
} else {
|
||||
x.Addition.DeviceID = x.Common.DeviceID
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
|
||||
x.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.XunLeiBrowserCommon.UseFluentPlay = x.UseFluentPlay
|
||||
x.Addition.RootFolderID = x.RootFolderID
|
||||
// 防止重复登录
|
||||
identity := x.GetIdentity()
|
||||
@ -107,6 +121,8 @@ func (x *ThunderBrowser) Init(ctx context.Context) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 清空 信任密钥
|
||||
x.Addition.CreditKey = ""
|
||||
x.SetTokenResp(token)
|
||||
}
|
||||
|
||||
@ -188,6 +204,7 @@ func (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) {
|
||||
return DownloadUserAgent
|
||||
}(),
|
||||
UseVideoUrl: x.UseVideoUrl,
|
||||
UseFluentPlay: x.UseFluentPlay,
|
||||
RemoveWay: x.ExpertAddition.RemoveWay,
|
||||
refreshCTokenCk: func(token string) {
|
||||
x.CaptchaToken = token
|
||||
@ -200,7 +217,13 @@ func (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) {
|
||||
x.SetCaptchaToken(x.ExpertAddition.CaptchaToken)
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
if x.Common.DeviceID != "" {
|
||||
if x.ExpertAddition.CreditKey != "" {
|
||||
x.SetCreditKey(x.ExpertAddition.CreditKey)
|
||||
}
|
||||
|
||||
if x.ExpertAddition.DeviceID != "" {
|
||||
x.Common.DeviceID = x.ExpertAddition.DeviceID
|
||||
} else {
|
||||
x.ExpertAddition.DeviceID = x.Common.DeviceID
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
@ -213,6 +236,7 @@ func (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) {
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
x.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.XunLeiBrowserCommon.UseFluentPlay = x.UseFluentPlay
|
||||
x.ExpertAddition.RootFolderID = x.RootFolderID
|
||||
// 签名方法
|
||||
if x.SignType == "captcha_sign" {
|
||||
@ -253,6 +277,8 @@ func (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 清空 信任密钥
|
||||
x.ExpertAddition.CreditKey = ""
|
||||
x.SetTokenResp(token)
|
||||
x.SetRefreshTokenFunc(func() error {
|
||||
token, err := x.XunLeiBrowserCommon.RefreshToken(x.TokenResp.RefreshToken)
|
||||
@ -261,6 +287,8 @@ func (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) {
|
||||
if err != nil {
|
||||
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
||||
}
|
||||
// 清空 信任密钥
|
||||
x.ExpertAddition.CreditKey = ""
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
op.MustSaveDriverStorage(x)
|
||||
@ -286,6 +314,7 @@ func (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) {
|
||||
x.XunLeiBrowserCommon.UserAgent = x.UserAgent
|
||||
x.XunLeiBrowserCommon.DownloadUserAgent = x.DownloadUserAgent
|
||||
x.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.XunLeiBrowserCommon.UseFluentPlay = x.UseFluentPlay
|
||||
x.ExpertAddition.RootFolderID = x.RootFolderID
|
||||
}
|
||||
|
||||
@ -306,6 +335,7 @@ func (x *ThunderBrowserExpert) SetTokenResp(token *TokenResp) {
|
||||
type XunLeiBrowserCommon struct {
|
||||
*Common
|
||||
*TokenResp // 登录信息
|
||||
*CoreLoginResp // core登录信息
|
||||
|
||||
refreshTokenFunc func() error
|
||||
}
|
||||
@ -523,7 +553,8 @@ func (xc *XunLeiBrowserCommon) getFiles(ctx context.Context, dir model.Obj, path
|
||||
folderSpace = dirF.GetSpace()
|
||||
default:
|
||||
// 处理 根目录的情况
|
||||
folderSpace = ThunderBrowserDriveSpace
|
||||
//folderSpace = ThunderBrowserDriveSpace
|
||||
folderSpace = ThunderDriveSpace // 迅雷浏览器已经合并到迅雷云盘,因此变更根目录
|
||||
}
|
||||
params := map[string]string{
|
||||
"parent_id": dir.GetID(),
|
||||
@ -569,6 +600,11 @@ func (xc *XunLeiBrowserCommon) SetTokenResp(tr *TokenResp) {
|
||||
xc.TokenResp = tr
|
||||
}
|
||||
|
||||
// SetCoreTokenResp 设置CoreToken
|
||||
func (xc *XunLeiBrowserCommon) SetCoreTokenResp(tr *CoreLoginResp) {
|
||||
xc.CoreLoginResp = tr
|
||||
}
|
||||
|
||||
// SetSpaceTokenResp 设置Token
|
||||
func (xc *XunLeiBrowserCommon) SetSpaceTokenResp(spaceToken string) {
|
||||
xc.TokenResp.Token = spaceToken
|
||||
@ -614,14 +650,24 @@ func (xc *XunLeiBrowserCommon) Request(url string, method string, callback base.
|
||||
}
|
||||
if errResp.ErrorMsg == "captcha_invalid" {
|
||||
// 验证码token过期
|
||||
if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.UserID); err != nil {
|
||||
if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.TokenResp.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
|
||||
return nil, errors.New(errResp.ErrorMsg)
|
||||
default:
|
||||
// 处理未捕获到的验证码错误
|
||||
if errResp.ErrorMsg == "captcha_invalid" {
|
||||
// 验证码token过期
|
||||
if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.TokenResp.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return xc.Request(url, method, callback, resp)
|
||||
}
|
||||
|
||||
@ -667,20 +713,25 @@ func (xc *XunLeiBrowserCommon) GetSafeAccessToken(safePassword string) (string,
|
||||
|
||||
// Login 登录
|
||||
func (xc *XunLeiBrowserCommon) Login(username, password string) (*TokenResp, error) {
|
||||
url := XLUSER_API_URL + "/auth/signin"
|
||||
err := xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username)
|
||||
//v3 login拿到 sessionID
|
||||
sessionID, err := xc.CoreLogin(username, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//v1 login拿到令牌
|
||||
url := XLUSER_API_URL + "/auth/signin/token"
|
||||
if err = xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp TokenResp
|
||||
_, err = xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetPathParam("client_id", xc.ClientID)
|
||||
req.SetBody(&SignInRequest{
|
||||
CaptchaToken: xc.GetCaptchaToken(),
|
||||
ClientID: xc.ClientID,
|
||||
ClientSecret: xc.ClientSecret,
|
||||
Username: username,
|
||||
Password: password,
|
||||
Provider: SignProvider,
|
||||
SigninToken: sessionID,
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
@ -696,3 +747,157 @@ func (xc *XunLeiBrowserCommon) IsLogin() bool {
|
||||
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// OfflineDownload 离线下载文件
|
||||
func (xc *XunLeiBrowserCommon) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {
|
||||
var resp OfflineDownloadResp
|
||||
|
||||
body := base.Json{}
|
||||
|
||||
from := "cloudadd/"
|
||||
|
||||
if xc.UseFluentPlay {
|
||||
body = base.Json{
|
||||
"kind": FILE,
|
||||
"name": fileName,
|
||||
// 流畅播接口 强制将文件放在 "SPACE_FAVORITE" 文件夹
|
||||
//"parent_id": parentDir.GetID(),
|
||||
"upload_type": UPLOAD_TYPE_URL,
|
||||
"url": base.Json{
|
||||
"url": fileUrl,
|
||||
//"files": []string{"0"}, // 0 表示只下载第一个文件
|
||||
},
|
||||
"params": base.Json{
|
||||
"cookie": "null",
|
||||
"web_title": "",
|
||||
"lastSession": "",
|
||||
"flags": "9",
|
||||
"scene": "smart_spot_panel",
|
||||
"referer": "https://x.xunlei.com",
|
||||
"dedup_index": "0",
|
||||
},
|
||||
"need_dedup": true,
|
||||
"folder_type": "FAVORITE",
|
||||
"space": ThunderBrowserDriveFluentPlayFolderType,
|
||||
}
|
||||
|
||||
from = "FLUENT_PLAY/sniff_ball/fluent_play/SPACE_FAVORITE"
|
||||
} else {
|
||||
body = base.Json{
|
||||
"kind": FILE,
|
||||
"name": fileName,
|
||||
"parent_id": parentDir.GetID(),
|
||||
"upload_type": UPLOAD_TYPE_URL,
|
||||
"url": base.Json{
|
||||
"url": fileUrl,
|
||||
},
|
||||
}
|
||||
|
||||
if files, ok := parentDir.(*Files); ok {
|
||||
body["space"] = files.GetSpace()
|
||||
} else {
|
||||
// 如果不是 Files 类型,则默认使用 ThunderDriveSpace
|
||||
body["space"] = ThunderDriveSpace
|
||||
}
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetQueryParam("_from", from)
|
||||
r.SetBody(&body)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resp.Task, err
|
||||
}
|
||||
|
||||
// OfflineList 获取离线下载任务列表
|
||||
func (xc *XunLeiBrowserCommon) OfflineList(ctx context.Context, nextPageToken string) ([]OfflineTask, error) {
|
||||
res := make([]OfflineTask, 0)
|
||||
|
||||
var resp OfflineListResp
|
||||
_, err := xc.Request(TASK_API_URL, http.MethodGet, func(req *resty.Request) {
|
||||
req.SetContext(ctx).
|
||||
SetQueryParams(map[string]string{
|
||||
"type": "offline",
|
||||
"limit": "10000",
|
||||
"page_token": nextPageToken,
|
||||
"space": "default/*",
|
||||
})
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get offline list: %w", err)
|
||||
}
|
||||
res = append(res, resp.Tasks...)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) DeleteOfflineTasks(ctx context.Context, taskIDs []string) error {
|
||||
queryParams := map[string]string{
|
||||
"task_ids": strings.Join(taskIDs, ","),
|
||||
"_t": fmt.Sprintf("%d", time.Now().UnixMilli()),
|
||||
}
|
||||
if xc.UseFluentPlay {
|
||||
queryParams["space"] = ThunderBrowserDriveFluentPlayFolderType
|
||||
}
|
||||
|
||||
_, err := xc.Request(TASK_API_URL, http.MethodDelete, func(req *resty.Request) {
|
||||
req.SetContext(ctx).
|
||||
SetQueryParams(queryParams)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete tasks %v: %w", taskIDs, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) CoreLogin(username string, password string) (sessionID string, err error) {
|
||||
url := XLUSER_API_BASE_URL + "/xluser.core.login/v3/login"
|
||||
var resp CoreLoginResp
|
||||
res, err := xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetHeader("User-Agent", "android-ok-http-client/xl-acc-sdk/version-5.0.9.509300")
|
||||
req.SetBody(&CoreLoginRequest{
|
||||
ProtocolVersion: "301",
|
||||
SequenceNo: "1000010",
|
||||
PlatformVersion: "10",
|
||||
IsCompressed: "0",
|
||||
Appid: APPID,
|
||||
ClientVersion: xc.Common.ClientVersion,
|
||||
PeerID: "00000000000000000000000000000000",
|
||||
AppName: "ANDROID-com.xunlei.browser",
|
||||
SdkVersion: "509300",
|
||||
Devicesign: generateDeviceSign(xc.DeviceID, xc.PackageName),
|
||||
NetWorkType: "WIFI",
|
||||
ProviderName: "NONE",
|
||||
DeviceModel: "M2004J7AC",
|
||||
DeviceName: "Xiaomi_M2004j7ac",
|
||||
OSVersion: "12",
|
||||
Creditkey: xc.GetCreditKey(),
|
||||
Hl: "zh-CN",
|
||||
UserName: username,
|
||||
PassWord: password,
|
||||
VerifyKey: "",
|
||||
VerifyCode: "",
|
||||
IsMd5Pwd: "0",
|
||||
})
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = utils.Json.Unmarshal(res, &resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
xc.SetCoreTokenResp(&resp)
|
||||
|
||||
sessionID = resp.SessionID
|
||||
|
||||
return sessionID, nil
|
||||
}
|
||||
|
@ -25,19 +25,21 @@ type ExpertAddition struct {
|
||||
SafePassword string `json:"safe_password" required:"true" help:"super safe password"` // 超级保险箱密码
|
||||
|
||||
// 签名方法1
|
||||
Algorithms string `json:"algorithms" required:"true" help:"sign type is algorithms,this is required" default:"uWRwO7gPfdPB/0NfPtfQO+71,F93x+qPluYy6jdgNpq+lwdH1ap6WOM+nfz8/V,0HbpxvpXFsBK5CoTKam,dQhzbhzFRcawnsZqRETT9AuPAJ+wTQso82mRv,SAH98AmLZLRa6DB2u68sGhyiDh15guJpXhBzI,unqfo7Z64Rie9RNHMOB,7yxUdFADp3DOBvXdz0DPuKNVT35wqa5z0DEyEvf,RBG,ThTWPG5eC0UBqlbQ+04nZAptqGCdpv9o55A"`
|
||||
Algorithms string `json:"algorithms" required:"true" help:"sign type is algorithms,this is required" default:"Cw4kArmKJ/aOiFTxnQ0ES+D4mbbrIUsFn,HIGg0Qfbpm5ThZ/RJfjoao4YwgT9/M,u/PUD,OlAm8tPkOF1qO5bXxRN2iFttuDldrg,FFIiM6sFhWhU7tIMVUKOF7CUv/KzgwwV8FE,yN,4m5mglrIHksI6wYdq,LXEfS7,T+p+C+F2yjgsUtiXWU/cMNYEtJI4pq7GofW,14BrGIEMXkbvFvZ49nDUfVCRcHYFOJ1BP1Y,kWIH3Row,RAmRTKNCjucPWC"`
|
||||
// 签名方法2
|
||||
CaptchaSign string `json:"captcha_sign" required:"true" help:"sign type is captcha_sign,this is required"`
|
||||
Timestamp string `json:"timestamp" required:"true" help:"sign type is captcha_sign,this is required"`
|
||||
|
||||
// 验证码
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
// 信任密钥
|
||||
CreditKey string `json:"credit_key" help:"credit key,used for login"`
|
||||
|
||||
// 必要且影响登录,由签名决定
|
||||
DeviceID string `json:"device_id" required:"false" default:""`
|
||||
ClientID string `json:"client_id" required:"true" default:"ZUBzD9J_XPXfn7f7"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"yESVmHecEe6F0aou69vl-g"`
|
||||
ClientVersion string `json:"client_version" required:"true" default:"1.10.0.2633"`
|
||||
ClientVersion string `json:"client_version" required:"true" default:"1.40.0.7208"`
|
||||
PackageName string `json:"package_name" required:"true" default:"com.xunlei.browser"`
|
||||
|
||||
// 不影响登录,影响下载速度
|
||||
@ -46,6 +48,8 @@ type ExpertAddition struct {
|
||||
|
||||
// 优先使用视频链接代替下载链接
|
||||
UseVideoUrl bool `json:"use_video_url"`
|
||||
// 离线下载是否使用 流畅播(Fluent Play)接口
|
||||
UseFluentPlay bool `json:"use_fluent_play" default:"false" help:"use fluent play for offline download,only magnet links supported"`
|
||||
// 移除方式
|
||||
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
|
||||
}
|
||||
@ -79,7 +83,11 @@ type Addition struct {
|
||||
Password string `json:"password" required:"true"`
|
||||
SafePassword string `json:"safe_password" required:"true"` // 超级保险箱密码
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
CreditKey string `json:"credit_key" help:"credit key,used for login"` // 信任密钥
|
||||
DeviceID string `json:"device_id" default:""` // 登录设备ID
|
||||
UseVideoUrl bool `json:"use_video_url" default:"false"`
|
||||
// 离线下载是否使用 流畅播(Fluent Play)接口
|
||||
UseFluentPlay bool `json:"use_fluent_play" default:"false" help:"use fluent play for offline download,only magnet links supported"`
|
||||
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,10 @@ type ErrResp struct {
|
||||
}
|
||||
|
||||
func (e *ErrResp) IsError() bool {
|
||||
if e.ErrorMsg == "success" {
|
||||
return false
|
||||
}
|
||||
|
||||
return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != ""
|
||||
}
|
||||
|
||||
@ -68,13 +72,78 @@ func (t *TokenResp) GetSpaceToken() string {
|
||||
}
|
||||
|
||||
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"`
|
||||
Provider string `json:"provider"`
|
||||
SigninToken string `json:"signin_token"`
|
||||
}
|
||||
type CoreLoginRequest struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
SequenceNo string `json:"sequenceNo"`
|
||||
PlatformVersion string `json:"platformVersion"`
|
||||
IsCompressed string `json:"isCompressed"`
|
||||
Appid string `json:"appid"`
|
||||
ClientVersion string `json:"clientVersion"`
|
||||
PeerID string `json:"peerID"`
|
||||
AppName string `json:"appName"`
|
||||
SdkVersion string `json:"sdkVersion"`
|
||||
Devicesign string `json:"devicesign"`
|
||||
NetWorkType string `json:"netWorkType"`
|
||||
ProviderName string `json:"providerName"`
|
||||
DeviceModel string `json:"deviceModel"`
|
||||
DeviceName string `json:"deviceName"`
|
||||
OSVersion string `json:"OSVersion"`
|
||||
Creditkey string `json:"creditkey"`
|
||||
Hl string `json:"hl"`
|
||||
UserName string `json:"userName"`
|
||||
PassWord string `json:"passWord"`
|
||||
VerifyKey string `json:"verifyKey"`
|
||||
VerifyCode string `json:"verifyCode"`
|
||||
IsMd5Pwd string `json:"isMd5Pwd"`
|
||||
}
|
||||
|
||||
type CoreLoginResp struct {
|
||||
Account string `json:"account"`
|
||||
Creditkey string `json:"creditkey"`
|
||||
/* Error string `json:"error"`
|
||||
ErrorCode string `json:"errorCode"`
|
||||
ErrorDescription string `json:"error_description"`*/
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
IsCompressed string `json:"isCompressed"`
|
||||
IsSetPassWord string `json:"isSetPassWord"`
|
||||
KeepAliveMinPeriod string `json:"keepAliveMinPeriod"`
|
||||
KeepAlivePeriod string `json:"keepAlivePeriod"`
|
||||
LoginKey string `json:"loginKey"`
|
||||
NickName string `json:"nickName"`
|
||||
PlatformVersion string `json:"platformVersion"`
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
SecureKey string `json:"secureKey"`
|
||||
SequenceNo string `json:"sequenceNo"`
|
||||
SessionID string `json:"sessionID"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
UserID string `json:"userID"`
|
||||
UserName string `json:"userName"`
|
||||
UserNewNo string `json:"userNewNo"`
|
||||
Version string `json:"version"`
|
||||
/* VipList []struct {
|
||||
ExpireDate string `json:"expireDate"`
|
||||
IsAutoDeduct string `json:"isAutoDeduct"`
|
||||
IsVip string `json:"isVip"`
|
||||
IsYear string `json:"isYear"`
|
||||
PayID string `json:"payId"`
|
||||
PayName string `json:"payName"`
|
||||
Register string `json:"register"`
|
||||
Vasid string `json:"vasid"`
|
||||
VasType string `json:"vasType"`
|
||||
VipDayGrow string `json:"vipDayGrow"`
|
||||
VipGrow string `json:"vipGrow"`
|
||||
VipLevel string `json:"vipLevel"`
|
||||
Icon struct {
|
||||
General string `json:"general"`
|
||||
Small string `json:"small"`
|
||||
} `json:"icon"`
|
||||
} `json:"vipList"`*/
|
||||
}
|
||||
|
||||
/*
|
||||
@ -234,3 +303,76 @@ type UploadTaskResponse struct {
|
||||
|
||||
File Files `json:"file"`
|
||||
}
|
||||
|
||||
// OfflineDownloadResp 离线下载响应
|
||||
type OfflineDownloadResp struct {
|
||||
File *string `json:"file"`
|
||||
Task OfflineTask `json:"task"`
|
||||
UploadType string `json:"upload_type"`
|
||||
URL struct {
|
||||
Kind string `json:"kind"`
|
||||
} `json:"url"`
|
||||
}
|
||||
|
||||
// OfflineListResp 离线下载列表响应
|
||||
type OfflineListResp struct {
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
Tasks []OfflineTask `json:"tasks"`
|
||||
}
|
||||
|
||||
// OfflineTask 离线下载任务响应
|
||||
type OfflineTask struct {
|
||||
Callback string `json:"callback"`
|
||||
CreatedTime string `json:"created_time"`
|
||||
FileID string `json:"file_id"`
|
||||
FileName string `json:"file_name"`
|
||||
FileSize string `json:"file_size"`
|
||||
IconLink string `json:"icon_link"`
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Message string `json:"message"`
|
||||
Name string `json:"name"`
|
||||
Params Params `json:"params"`
|
||||
Phase string `json:"phase"` // PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING
|
||||
Progress int64 `json:"progress"`
|
||||
Space string `json:"space"`
|
||||
StatusSize int64 `json:"status_size"`
|
||||
Statuses []string `json:"statuses"`
|
||||
ThirdTaskID string `json:"third_task_id"`
|
||||
Type string `json:"type"`
|
||||
UpdatedTime string `json:"updated_time"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type Params struct {
|
||||
FolderType string `json:"folder_type"`
|
||||
PredictSpeed string `json:"predict_speed"`
|
||||
PredictType string `json:"predict_type"`
|
||||
}
|
||||
|
||||
// LoginReviewResp 登录验证响应
|
||||
type LoginReviewResp struct {
|
||||
Creditkey string `json:"creditkey"`
|
||||
Error string `json:"error"`
|
||||
ErrorCode string `json:"errorCode"`
|
||||
ErrorDesc string `json:"errorDesc"`
|
||||
ErrorDescURL string `json:"errorDescUrl"`
|
||||
ErrorIsRetry int `json:"errorIsRetry"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
IsCompressed string `json:"isCompressed"`
|
||||
PlatformVersion string `json:"platformVersion"`
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
Reviewurl string `json:"reviewurl"`
|
||||
SequenceNo string `json:"sequenceNo"`
|
||||
UserID string `json:"userID"`
|
||||
VerifyType string `json:"verifyType"`
|
||||
}
|
||||
|
||||
// ReviewData 验证数据
|
||||
type ReviewData struct {
|
||||
Creditkey string `json:"creditkey"`
|
||||
Reviewurl string `json:"reviewurl"`
|
||||
Deviceid string `json:"deviceid"`
|
||||
Devicesign string `json:"devicesign"`
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -19,28 +20,33 @@ import (
|
||||
const (
|
||||
API_URL = "https://x-api-pan.xunlei.com/drive/v1"
|
||||
FILE_API_URL = API_URL + "/files"
|
||||
XLUSER_API_URL = "https://xluser-ssl.xunlei.com/v1"
|
||||
TASK_API_URL = API_URL + "/tasks"
|
||||
XLUSER_API_BASE_URL = "https://xluser-ssl.xunlei.com"
|
||||
XLUSER_API_URL = XLUSER_API_BASE_URL + "/v1"
|
||||
)
|
||||
|
||||
var Algorithms = []string{
|
||||
"uWRwO7gPfdPB/0NfPtfQO+71",
|
||||
"F93x+qPluYy6jdgNpq+lwdH1ap6WOM+nfz8/V",
|
||||
"0HbpxvpXFsBK5CoTKam",
|
||||
"dQhzbhzFRcawnsZqRETT9AuPAJ+wTQso82mRv",
|
||||
"SAH98AmLZLRa6DB2u68sGhyiDh15guJpXhBzI",
|
||||
"unqfo7Z64Rie9RNHMOB",
|
||||
"7yxUdFADp3DOBvXdz0DPuKNVT35wqa5z0DEyEvf",
|
||||
"RBG",
|
||||
"ThTWPG5eC0UBqlbQ+04nZAptqGCdpv9o55A",
|
||||
"Cw4kArmKJ/aOiFTxnQ0ES+D4mbbrIUsFn",
|
||||
"HIGg0Qfbpm5ThZ/RJfjoao4YwgT9/M",
|
||||
"u/PUD",
|
||||
"OlAm8tPkOF1qO5bXxRN2iFttuDldrg",
|
||||
"FFIiM6sFhWhU7tIMVUKOF7CUv/KzgwwV8FE",
|
||||
"yN",
|
||||
"4m5mglrIHksI6wYdq",
|
||||
"LXEfS7",
|
||||
"T+p+C+F2yjgsUtiXWU/cMNYEtJI4pq7GofW",
|
||||
"14BrGIEMXkbvFvZ49nDUfVCRcHYFOJ1BP1Y",
|
||||
"kWIH3Row",
|
||||
"RAmRTKNCjucPWC",
|
||||
}
|
||||
|
||||
const (
|
||||
ClientID = "ZUBzD9J_XPXfn7f7"
|
||||
ClientSecret = "yESVmHecEe6F0aou69vl-g"
|
||||
ClientVersion = "1.10.0.2633"
|
||||
ClientVersion = "1.40.0.7208"
|
||||
PackageName = "com.xunlei.browser"
|
||||
DownloadUserAgent = "AndroidDownloadManager/13 (Linux; U; Android 13; M2004J7AC Build/SP1A.210812.016)"
|
||||
SdkVersion = "233100"
|
||||
SdkVersion = "509300"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -63,6 +69,13 @@ const (
|
||||
ThunderBrowserDriveSafeSpace = "SPACE_BROWSER_SAFE"
|
||||
ThunderDriveFolderType = "DEFAULT_ROOT"
|
||||
ThunderBrowserDriveSafeFolderType = "BROWSER_SAFE"
|
||||
ThunderBrowserDriveFluentPlayFolderType = "SPACE_FAVORITE" // 流畅播文件夹标识
|
||||
)
|
||||
|
||||
const (
|
||||
SignProvider = "access_end_point_token"
|
||||
APPID = "22062"
|
||||
APPKey = "a5d7416858147a4ab99573872ffccef8"
|
||||
)
|
||||
|
||||
func GetAction(method string, url string) string {
|
||||
@ -75,6 +88,8 @@ type Common struct {
|
||||
|
||||
captchaToken string
|
||||
|
||||
creditKey string
|
||||
|
||||
// 签名相关,二选一
|
||||
Algorithms []string
|
||||
Timestamp, CaptchaSign string
|
||||
@ -88,6 +103,7 @@ type Common struct {
|
||||
UserAgent string
|
||||
DownloadUserAgent string
|
||||
UseVideoUrl bool
|
||||
UseFluentPlay bool
|
||||
RemoveWay string
|
||||
|
||||
// 验证码token刷新成功回调
|
||||
@ -105,6 +121,13 @@ func (c *Common) GetCaptchaToken() string {
|
||||
return c.captchaToken
|
||||
}
|
||||
|
||||
func (c *Common) SetCreditKey(creditKey string) {
|
||||
c.creditKey = creditKey
|
||||
}
|
||||
func (c *Common) GetCreditKey() string {
|
||||
return c.creditKey
|
||||
}
|
||||
|
||||
// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)
|
||||
func (c *Common) RefreshCaptchaTokenAtLogin(action, userID string) error {
|
||||
metas := map[string]string{
|
||||
@ -206,12 +229,53 @@ func (c *Common) Request(url, method string, callback base.ReqCallback, resp int
|
||||
var erron ErrResp
|
||||
utils.Json.Unmarshal(res.Body(), &erron)
|
||||
if erron.IsError() {
|
||||
// review_panel 表示需要短信验证码进行验证
|
||||
if erron.ErrorMsg == "review_panel" {
|
||||
return nil, c.getReviewData(res)
|
||||
}
|
||||
|
||||
return nil, &erron
|
||||
}
|
||||
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
// 获取验证所需内容
|
||||
func (c *Common) getReviewData(res *resty.Response) error {
|
||||
var reviewResp LoginReviewResp
|
||||
var reviewData ReviewData
|
||||
|
||||
if err := utils.Json.Unmarshal(res.Body(), &reviewResp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deviceSign := generateDeviceSign(c.DeviceID, c.PackageName)
|
||||
|
||||
reviewData = ReviewData{
|
||||
Creditkey: reviewResp.Creditkey,
|
||||
Reviewurl: reviewResp.Reviewurl + "&deviceid=" + deviceSign,
|
||||
Deviceid: deviceSign,
|
||||
Devicesign: deviceSign,
|
||||
}
|
||||
|
||||
// 将reviewData转为JSON字符串
|
||||
reviewDataJSON, _ := json.MarshalIndent(reviewData, "", " ")
|
||||
//reviewDataJSON, _ := json.Marshal(reviewData)
|
||||
|
||||
return fmt.Errorf(`
|
||||
<div style="font-family: Arial, sans-serif; padding: 15px; border-radius: 5px; border: 1px solid #e0e0e0;>
|
||||
<h3 style="color: #d9534f; margin-top: 0;">
|
||||
<span style="font-size: 16px;">🔒 本次登录需要验证</span><br>
|
||||
<span style="font-size: 14px; font-weight: normal; color: #666;">This login requires verification</span>
|
||||
</h3>
|
||||
<p style="font-size: 14px; margin-bottom: 15px;">下面是验证所需要的数据,具体使用方法请参照对应的驱动文档<br>
|
||||
<span style="color: #666; font-size: 13px;">Below are the relevant verification data. For specific usage methods, please refer to the corresponding driver documentation.</span></p>
|
||||
<div style="border: 1px solid #ddd; border-radius: 4px; padding: 10px; overflow-x: auto; font-family: 'Courier New', monospace; font-size: 13px;">
|
||||
<pre style="margin: 0; white-space: pre-wrap;"><code>%s</code></pre>
|
||||
</div>
|
||||
</div>`, string(reviewDataJSON))
|
||||
}
|
||||
|
||||
// 计算文件Gcid
|
||||
func getGcid(r io.Reader, size int64) (string, error) {
|
||||
calcBlockSize := func(j int64) int64 {
|
||||
@ -274,7 +338,7 @@ func EncryptPassword(password string) string {
|
||||
|
||||
func generateDeviceSign(deviceID, packageName string) string {
|
||||
|
||||
signatureBase := fmt.Sprintf("%s%s%s%s", deviceID, packageName, "22062", "a5d7416858147a4ab99573872ffccef8")
|
||||
signatureBase := fmt.Sprintf("%s%s%s%s", deviceID, packageName, APPID, APPKey)
|
||||
|
||||
sha1Hash := sha1.New()
|
||||
sha1Hash.Write([]byte(signatureBase))
|
||||
@ -299,7 +363,7 @@ func BuildCustomUserAgent(deviceID, appName, sdkVersion, clientVersion, packageN
|
||||
|
||||
sb.WriteString(fmt.Sprintf("ANDROID-%s/%s ", appName, clientVersion))
|
||||
sb.WriteString("networkType/WIFI ")
|
||||
sb.WriteString(fmt.Sprintf("appid/%s ", "22062"))
|
||||
sb.WriteString(fmt.Sprintf("appid/%s ", APPID))
|
||||
sb.WriteString(fmt.Sprintf("deviceName/Xiaomi_M2004j7ac "))
|
||||
sb.WriteString(fmt.Sprintf("deviceModel/M2004J7AC "))
|
||||
sb.WriteString(fmt.Sprintf("OSVersion/13 "))
|
||||
|
@ -69,6 +69,9 @@ const (
|
||||
// thunder
|
||||
ThunderTempDir = "thunder_temp_dir"
|
||||
|
||||
// thunder_browser
|
||||
ThunderBrowserTempDir = "thunder_browser_temp_dir"
|
||||
|
||||
// single
|
||||
Token = "token"
|
||||
IndexProgress = "index_progress"
|
||||
|
@ -7,5 +7,6 @@ import (
|
||||
_ "github.com/OpenListTeam/OpenList/internal/offline_download/pikpak"
|
||||
_ "github.com/OpenListTeam/OpenList/internal/offline_download/qbit"
|
||||
_ "github.com/OpenListTeam/OpenList/internal/offline_download/thunder"
|
||||
_ "github.com/OpenListTeam/OpenList/internal/offline_download/thunder_browser"
|
||||
_ "github.com/OpenListTeam/OpenList/internal/offline_download/transmission"
|
||||
)
|
||||
|
171
internal/offline_download/thunder_browser/thunder_browser.go
Normal file
171
internal/offline_download/thunder_browser/thunder_browser.go
Normal file
@ -0,0 +1,171 @@
|
||||
package thunder_browser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/OpenListTeam/OpenList/drivers/thunder_browser"
|
||||
"github.com/OpenListTeam/OpenList/internal/conf"
|
||||
"github.com/OpenListTeam/OpenList/internal/setting"
|
||||
"strconv"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/internal/errs"
|
||||
"github.com/OpenListTeam/OpenList/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/internal/offline_download/tool"
|
||||
"github.com/OpenListTeam/OpenList/internal/op"
|
||||
)
|
||||
|
||||
type ThunderBrowser struct {
|
||||
refreshTaskCache bool
|
||||
}
|
||||
|
||||
func (t *ThunderBrowser) Name() string {
|
||||
return "ThunderBrowser"
|
||||
}
|
||||
|
||||
func (t *ThunderBrowser) Items() []model.SettingItem {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ThunderBrowser) Run(task *tool.DownloadTask) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (t *ThunderBrowser) Init() (string, error) {
|
||||
t.refreshTaskCache = false
|
||||
return "ok", nil
|
||||
}
|
||||
|
||||
func (t *ThunderBrowser) IsReady() bool {
|
||||
tempDir := setting.GetStr(conf.ThunderBrowserTempDir)
|
||||
if tempDir == "" {
|
||||
return false
|
||||
}
|
||||
storage, _, err := op.GetStorageAndActualPath(tempDir)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch storage.(type) {
|
||||
case *thunder_browser.ThunderBrowser, *thunder_browser.ThunderBrowserExpert:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ThunderBrowser) AddURL(args *tool.AddUrlArgs) (string, error) {
|
||||
// 添加新任务刷新缓存
|
||||
t.refreshTaskCache = true
|
||||
storage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if err := op.MakeDir(ctx, storage, actualPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
parentDir, err := op.GetUnwrap(ctx, storage, actualPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var task *thunder_browser.OfflineTask
|
||||
switch v := storage.(type) {
|
||||
case *thunder_browser.ThunderBrowser:
|
||||
task, err = v.OfflineDownload(ctx, args.Url, parentDir, "")
|
||||
case *thunder_browser.ThunderBrowserExpert:
|
||||
task, err = v.OfflineDownload(ctx, args.Url, parentDir, "")
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported storage driver for offline download, only ThunderBrowser is supported")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to add offline download task: %w", err)
|
||||
}
|
||||
|
||||
if task == nil {
|
||||
return "", fmt.Errorf("failed to add offline download task: task is nil")
|
||||
}
|
||||
|
||||
return task.ID, nil
|
||||
}
|
||||
|
||||
func (t *ThunderBrowser) Remove(task *tool.DownloadTask) error {
|
||||
storage, _, err := op.GetStorageAndActualPath(task.TempDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
switch v := storage.(type) {
|
||||
case *thunder_browser.ThunderBrowser:
|
||||
err = v.DeleteOfflineTasks(ctx, []string{task.GID})
|
||||
case *thunder_browser.ThunderBrowserExpert:
|
||||
err = v.DeleteOfflineTasks(ctx, []string{task.GID})
|
||||
default:
|
||||
return fmt.Errorf("unsupported storage driver for offline download, only ThunderBrowser is supported")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ThunderBrowser) Status(task *tool.DownloadTask) (*tool.Status, error) {
|
||||
storage, _, err := op.GetStorageAndActualPath(task.TempDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tasks []thunder_browser.OfflineTask
|
||||
|
||||
switch v := storage.(type) {
|
||||
case *thunder_browser.ThunderBrowser:
|
||||
tasks, err = t.GetTasks(v)
|
||||
case *thunder_browser.ThunderBrowserExpert:
|
||||
tasks, err = t.GetTasksExpert(v)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported storage driver for offline download, only ThunderBrowser is supported")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &tool.Status{
|
||||
Progress: 0,
|
||||
NewGID: "",
|
||||
Completed: false,
|
||||
Status: "the task has been deleted",
|
||||
Err: nil,
|
||||
}
|
||||
|
||||
for _, t := range tasks {
|
||||
if t.ID == task.GID {
|
||||
s.Progress = float64(t.Progress)
|
||||
s.Status = t.Message
|
||||
s.Completed = t.Phase == "PHASE_TYPE_COMPLETE"
|
||||
s.TotalBytes, err = strconv.ParseInt(t.FileSize, 10, 64)
|
||||
if err != nil {
|
||||
s.TotalBytes = 0
|
||||
}
|
||||
if t.Phase == "PHASE_TYPE_ERROR" {
|
||||
s.Err = errors.New(t.Message)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
|
||||
s.Err = fmt.Errorf("the task has been deleted")
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
tool.Tools.Add(&ThunderBrowser{})
|
||||
}
|
70
internal/offline_download/thunder_browser/util.go
Normal file
70
internal/offline_download/thunder_browser/util.go
Normal file
@ -0,0 +1,70 @@
|
||||
package thunder_browser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/drivers/thunder_browser"
|
||||
"github.com/OpenListTeam/OpenList/internal/op"
|
||||
"github.com/OpenListTeam/OpenList/pkg/singleflight"
|
||||
"github.com/Xhofe/go-cache"
|
||||
)
|
||||
|
||||
var taskCache = cache.NewMemCache(cache.WithShards[[]thunder_browser.OfflineTask](16))
|
||||
var taskG singleflight.Group[[]thunder_browser.OfflineTask]
|
||||
|
||||
func (t *ThunderBrowser) GetTasks(thunderDriver *thunder_browser.ThunderBrowser) ([]thunder_browser.OfflineTask, error) {
|
||||
key := op.Key(thunderDriver, "/drive/v1/task")
|
||||
if !t.refreshTaskCache {
|
||||
if tasks, ok := taskCache.Get(key); ok {
|
||||
return tasks, nil
|
||||
}
|
||||
}
|
||||
t.refreshTaskCache = false
|
||||
tasks, err, _ := taskG.Do(key, func() ([]thunder_browser.OfflineTask, error) {
|
||||
ctx := context.Background()
|
||||
tasks, err := thunderDriver.OfflineList(ctx, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 添加缓存 10s
|
||||
if len(tasks) > 0 {
|
||||
taskCache.Set(key, tasks, cache.WithEx[[]thunder_browser.OfflineTask](time.Second*10))
|
||||
} else {
|
||||
taskCache.Del(key)
|
||||
}
|
||||
return tasks, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func (t *ThunderBrowser) GetTasksExpert(thunderDriver *thunder_browser.ThunderBrowserExpert) ([]thunder_browser.OfflineTask, error) {
|
||||
key := op.Key(thunderDriver, "/drive/v1/task")
|
||||
if !t.refreshTaskCache {
|
||||
if tasks, ok := taskCache.Get(key); ok {
|
||||
return tasks, nil
|
||||
}
|
||||
}
|
||||
t.refreshTaskCache = false
|
||||
tasks, err, _ := taskG.Do(key, func() ([]thunder_browser.OfflineTask, error) {
|
||||
ctx := context.Background()
|
||||
tasks, err := thunderDriver.OfflineList(ctx, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 添加缓存 10s
|
||||
if len(tasks) > 0 {
|
||||
taskCache.Set(key, tasks, cache.WithEx[[]thunder_browser.OfflineTask](time.Second*10))
|
||||
} else {
|
||||
taskCache.Del(key)
|
||||
}
|
||||
return tasks, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
@ -2,6 +2,7 @@ package tool
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"net/url"
|
||||
stdpath "path"
|
||||
"path/filepath"
|
||||
@ -9,6 +10,7 @@ import (
|
||||
_115 "github.com/OpenListTeam/OpenList/drivers/115"
|
||||
"github.com/OpenListTeam/OpenList/drivers/pikpak"
|
||||
"github.com/OpenListTeam/OpenList/drivers/thunder"
|
||||
"github.com/OpenListTeam/OpenList/drivers/thunder_browser"
|
||||
"github.com/OpenListTeam/OpenList/internal/conf"
|
||||
"github.com/OpenListTeam/OpenList/internal/errs"
|
||||
"github.com/OpenListTeam/OpenList/internal/fs"
|
||||
@ -103,6 +105,13 @@ func AddURL(ctx context.Context, args *AddURLArgs) (task.TaskExtensionInfo, erro
|
||||
} else {
|
||||
tempDir = filepath.Join(setting.GetStr(conf.ThunderTempDir), uid)
|
||||
}
|
||||
case "ThunderBrowser":
|
||||
switch storage.(type) {
|
||||
case *thunder_browser.ThunderBrowser, *thunder_browser.ThunderBrowserExpert:
|
||||
tempDir = args.DstDirPath
|
||||
default:
|
||||
tempDir = filepath.Join(setting.GetStr(conf.ThunderBrowserTempDir), uid)
|
||||
}
|
||||
}
|
||||
|
||||
taskCreator, _ := ctx.Value("user").(*model.User) // taskCreator is nil when convert failed
|
||||
|
@ -87,6 +87,9 @@ outer:
|
||||
if t.tool.Name() == "Thunder" {
|
||||
return nil
|
||||
}
|
||||
if t.tool.Name() == "ThunderBrowser" {
|
||||
return nil
|
||||
}
|
||||
if t.tool.Name() == "115 Cloud" {
|
||||
// hack for 115
|
||||
<-time.After(time.Second * 1)
|
||||
@ -159,7 +162,7 @@ func (t *DownloadTask) Update() (bool, error) {
|
||||
|
||||
func (t *DownloadTask) Transfer() error {
|
||||
toolName := t.tool.Name()
|
||||
if toolName == "115 Cloud" || toolName == "PikPak" || toolName == "Thunder" {
|
||||
if toolName == "115 Cloud" || toolName == "PikPak" || toolName == "Thunder" || toolName == "ThunderBrowser" {
|
||||
// 如果不是直接下载到目标路径,则进行转存
|
||||
if t.TempDir != t.DstDirPath {
|
||||
return transferObj(t.Ctx(), t.TempDir, t.DstDirPath, t.DeletePolicy)
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
_115 "github.com/OpenListTeam/OpenList/drivers/115"
|
||||
"github.com/OpenListTeam/OpenList/drivers/pikpak"
|
||||
"github.com/OpenListTeam/OpenList/drivers/thunder"
|
||||
"github.com/OpenListTeam/OpenList/drivers/thunder_browser"
|
||||
"github.com/OpenListTeam/OpenList/internal/conf"
|
||||
"github.com/OpenListTeam/OpenList/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/internal/offline_download/tool"
|
||||
@ -239,6 +240,51 @@ func SetThunder(c *gin.Context) {
|
||||
common.SuccessResp(c, "ok")
|
||||
}
|
||||
|
||||
type SetThunderBrowserReq struct {
|
||||
TempDir string `json:"temp_dir" form:"temp_dir"`
|
||||
}
|
||||
|
||||
func SetThunderBrowser(c *gin.Context) {
|
||||
var req SetThunderBrowserReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if req.TempDir != "" {
|
||||
storage, _, err := op.GetStorageAndActualPath(req.TempDir)
|
||||
if err != nil {
|
||||
common.ErrorStrResp(c, "storage does not exists", 400)
|
||||
return
|
||||
}
|
||||
if storage.Config().CheckStatus && storage.GetStorage().Status != op.WORK {
|
||||
common.ErrorStrResp(c, "storage not init: "+storage.GetStorage().Status, 400)
|
||||
return
|
||||
}
|
||||
switch storage.(type) {
|
||||
case *thunder_browser.ThunderBrowser, *thunder_browser.ThunderBrowserExpert:
|
||||
default:
|
||||
common.ErrorStrResp(c, "unsupported storage driver for offline download, only ThunderBrowser is supported", 400)
|
||||
}
|
||||
}
|
||||
items := []model.SettingItem{
|
||||
{Key: conf.ThunderBrowserTempDir, Value: req.TempDir, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},
|
||||
}
|
||||
if err := op.SaveSettingItems(items); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
_tool, err := tool.Tools.Get("ThunderBrowser")
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
if _, err := _tool.Init(); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, "ok")
|
||||
}
|
||||
|
||||
func OfflineDownloadTools(c *gin.Context) {
|
||||
tools := tool.Tools.Names()
|
||||
common.SuccessResp(c, tools)
|
||||
|
@ -147,6 +147,7 @@ func admin(g *gin.RouterGroup) {
|
||||
setting.POST("/set_115", handles.Set115)
|
||||
setting.POST("/set_pikpak", handles.SetPikPak)
|
||||
setting.POST("/set_thunder", handles.SetThunder)
|
||||
setting.POST("/set_thunder_browser", handles.SetThunderBrowser)
|
||||
|
||||
// retain /admin/task API to ensure compatibility with legacy automation scripts
|
||||
_task(g.Group("/task"))
|
||||
|
Reference in New Issue
Block a user