From a4288461c65512eaa9b1b7d853db89672f06279b Mon Sep 17 00:00:00 2001
From: MianRou <2945065490@qq.com>
Date: Sat, 24 May 2025 19:36:36 +0800
Subject: [PATCH 1/3] fix(thunder): fix deviceID generation
---
drivers/thunder/driver.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/drivers/thunder/driver.go b/drivers/thunder/driver.go
index 1d2f2a81f65..03f5ea62f19 100644
--- a/drivers/thunder/driver.go
+++ b/drivers/thunder/driver.go
@@ -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
}(),
@@ -156,7 +156,7 @@ func (x *ThunderExpert) 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
}(),
@@ -637,7 +637,7 @@ func (xc *XunLeiCommon) CoreLogin(username string, password string) (sessionID s
PlatformVersion: "10",
IsCompressed: "0",
Appid: APPID,
- ClientVersion: "8.31.0.9726",
+ ClientVersion: xc.Common.ClientVersion,
PeerID: "00000000000000000000000000000000",
AppName: "ANDROID-com.xunlei.downloadprovider",
SdkVersion: "512000",
From 054a26300b73cef1073a52e0ff3adc475925de81 Mon Sep 17 00:00:00 2001
From: MianRou <2945065490@qq.com>
Date: Sat, 24 May 2025 19:39:52 +0800
Subject: [PATCH 2/3] feat(thunder_browser): support offline download and
update login interface
---
drivers/thunder_browser/driver.go | 164 +++++++++++++++--
drivers/thunder_browser/meta.go | 8 +-
drivers/thunder_browser/types.go | 150 ++++++++++++++-
drivers/thunder_browser/util.go | 94 ++++++++--
internal/conf/const.go | 3 +
internal/offline_download/all.go | 1 +
.../thunder_browser/thunder_browser.go | 171 ++++++++++++++++++
.../offline_download/thunder_browser/util.go | 70 +++++++
internal/offline_download/tool/add.go | 8 +
internal/offline_download/tool/download.go | 5 +-
server/handles/offline_download.go | 46 +++++
server/router.go | 1 +
12 files changed, 687 insertions(+), 34 deletions(-)
create mode 100644 internal/offline_download/thunder_browser/thunder_browser.go
create mode 100644 internal/offline_download/thunder_browser/util.go
diff --git a/drivers/thunder_browser/driver.go b/drivers/thunder_browser/driver.go
index 0b38d07714f..4c5112f806f 100644
--- a/drivers/thunder_browser/driver.go
+++ b/drivers/thunder_browser/driver.go
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
+ "strconv"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
@@ -81,6 +82,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,9 +96,18 @@ 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.Addition.RootFolderID = x.RootFolderID
// 防止重复登录
@@ -107,6 +119,8 @@ func (x *ThunderBrowser) Init(ctx context.Context) (err error) {
if err != nil {
return err
}
+ // 清空 信任密钥
+ x.Addition.CreditKey = ""
x.SetTokenResp(token)
}
@@ -200,7 +214,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)
}
@@ -253,6 +273,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 +283,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)
@@ -305,7 +329,8 @@ func (x *ThunderBrowserExpert) SetTokenResp(token *TokenResp) {
type XunLeiBrowserCommon struct {
*Common
- *TokenResp // 登录信息
+ *TokenResp // 登录信息
+ *CoreLoginResp // core登录信息
refreshTokenFunc func() error
}
@@ -523,7 +548,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 +595,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,7 +645,7 @@ 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
}
}
@@ -667,20 +698,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 +732,109 @@ func (xc *XunLeiBrowserCommon) IsLogin() bool {
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
return err == nil
}
+
+// 离线下载文件
+func (xc *XunLeiBrowserCommon) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {
+ var resp OfflineDownloadResp
+ _, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
+ r.SetContext(ctx)
+ r.SetBody(&base.Json{
+ "kind": FILE,
+ "name": fileName,
+ "parent_id": parentDir.GetID(),
+ "upload_type": UPLOAD_TYPE_URL,
+ "url": base.Json{
+ "url": fileUrl,
+ },
+ "space": parentDir.(*Files).GetSpace(),
+ })
+ }, &resp)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &resp.Task, err
+}
+
+/*
+获取离线下载任务列表
+*/
+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,
+ })
+ }, &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, deleteFiles bool) error {
+ _, err := xc.Request(TASK_API_URL, http.MethodDelete, func(req *resty.Request) {
+ req.SetContext(ctx).
+ SetQueryParams(map[string]string{
+ "task_ids": strings.Join(taskIDs, ","),
+ "delete_files": strconv.FormatBool(deleteFiles),
+ })
+ }, 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
+}
diff --git a/drivers/thunder_browser/meta.go b/drivers/thunder_browser/meta.go
index f535ea6cd47..5809836e000 100644
--- a/drivers/thunder_browser/meta.go
+++ b/drivers/thunder_browser/meta.go
@@ -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"`
// 不影响登录,影响下载速度
@@ -79,6 +81,8 @@ 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"`
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
}
diff --git a/drivers/thunder_browser/types.go b/drivers/thunder_browser/types.go
index b3e21d2bc08..d7d9c8439f6 100644
--- a/drivers/thunder_browser/types.go
+++ b/drivers/thunder_browser/types.go
@@ -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"`
}
+
+// 添加离线下载响应
+type OfflineDownloadResp struct {
+ File *string `json:"file"`
+ Task OfflineTask `json:"task"`
+ UploadType string `json:"upload_type"`
+ URL struct {
+ Kind string `json:"kind"`
+ } `json:"url"`
+}
+
+// 离线下载列表
+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"`
+}
diff --git a/drivers/thunder_browser/util.go b/drivers/thunder_browser/util.go
index befd1a904c8..3b196810567 100644
--- a/drivers/thunder_browser/util.go
+++ b/drivers/thunder_browser/util.go
@@ -4,6 +4,7 @@ import (
"crypto/md5"
"crypto/sha1"
"encoding/hex"
+ "encoding/json"
"fmt"
"io"
"net/http"
@@ -17,30 +18,35 @@ 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"
+ API_URL = "https://x-api-pan.xunlei.com/drive/v1"
+ FILE_API_URL = API_URL + "/files"
+ 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 (
@@ -65,6 +71,12 @@ const (
ThunderBrowserDriveSafeFolderType = "BROWSER_SAFE"
)
+const (
+ SignProvider = "access_end_point_token"
+ APPID = "22062"
+ APPKey = "a5d7416858147a4ab99573872ffccef8"
+)
+
func GetAction(method string, url string) string {
urlpath := regexp.MustCompile(`://[^/]+((/[^/\s?#]+)*)`).FindStringSubmatch(url)[1]
return method + ":" + urlpath
@@ -75,6 +87,8 @@ type Common struct {
captchaToken string
+ creditKey string
+
// 签名相关,二选一
Algorithms []string
Timestamp, CaptchaSign string
@@ -105,6 +119,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 +227,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(`
+
+
🔒 本次登录需要验证
+
This login requires verification
+
+
下面是验证所需要的数据,具体使用方法请参照对应的驱动文档
+ Below are the relevant verification data. For specific usage methods, please refer to the corresponding driver documentation.
+
+
`, string(reviewDataJSON))
+}
+
// 计算文件Gcid
func getGcid(r io.Reader, size int64) (string, error) {
calcBlockSize := func(j int64) int64 {
@@ -274,7 +336,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 +361,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 "))
diff --git a/internal/conf/const.go b/internal/conf/const.go
index 5cb8d850bf0..eb804a66b86 100644
--- a/internal/conf/const.go
+++ b/internal/conf/const.go
@@ -69,6 +69,9 @@ const (
// thunder
ThunderTempDir = "thunder_temp_dir"
+ // thunder_browser
+ ThunderBrowserTempDir = "thunder_browser_temp_dir"
+
// single
Token = "token"
IndexProgress = "index_progress"
diff --git a/internal/offline_download/all.go b/internal/offline_download/all.go
index 3d0c7c73a0b..cc7247df1c6 100644
--- a/internal/offline_download/all.go
+++ b/internal/offline_download/all.go
@@ -7,5 +7,6 @@ import (
_ "github.com/alist-org/alist/v3/internal/offline_download/pikpak"
_ "github.com/alist-org/alist/v3/internal/offline_download/qbit"
_ "github.com/alist-org/alist/v3/internal/offline_download/thunder"
+ _ "github.com/alist-org/alist/v3/internal/offline_download/thunder_browser"
_ "github.com/alist-org/alist/v3/internal/offline_download/transmission"
)
diff --git a/internal/offline_download/thunder_browser/thunder_browser.go b/internal/offline_download/thunder_browser/thunder_browser.go
new file mode 100644
index 00000000000..974fd1c95b4
--- /dev/null
+++ b/internal/offline_download/thunder_browser/thunder_browser.go
@@ -0,0 +1,171 @@
+package thunder_browser
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/alist-org/alist/v3/drivers/thunder_browser"
+ "github.com/alist-org/alist/v3/internal/conf"
+ "github.com/alist-org/alist/v3/internal/setting"
+ "strconv"
+
+ "github.com/alist-org/alist/v3/internal/errs"
+ "github.com/alist-org/alist/v3/internal/model"
+ "github.com/alist-org/alist/v3/internal/offline_download/tool"
+ "github.com/alist-org/alist/v3/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}, false)
+ case *thunder_browser.ThunderBrowserExpert:
+ err = v.DeleteOfflineTasks(ctx, []string{task.GID}, false)
+ 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{})
+}
diff --git a/internal/offline_download/thunder_browser/util.go b/internal/offline_download/thunder_browser/util.go
new file mode 100644
index 00000000000..48a212b3c7b
--- /dev/null
+++ b/internal/offline_download/thunder_browser/util.go
@@ -0,0 +1,70 @@
+package thunder_browser
+
+import (
+ "context"
+ "time"
+
+ "github.com/Xhofe/go-cache"
+ "github.com/alist-org/alist/v3/drivers/thunder_browser"
+ "github.com/alist-org/alist/v3/internal/op"
+ "github.com/alist-org/alist/v3/pkg/singleflight"
+)
+
+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
+}
diff --git a/internal/offline_download/tool/add.go b/internal/offline_download/tool/add.go
index d64e43e8615..74c974cb119 100644
--- a/internal/offline_download/tool/add.go
+++ b/internal/offline_download/tool/add.go
@@ -9,6 +9,7 @@ import (
_115 "github.com/alist-org/alist/v3/drivers/115"
"github.com/alist-org/alist/v3/drivers/pikpak"
"github.com/alist-org/alist/v3/drivers/thunder"
+ "github.com/alist-org/alist/v3/drivers/thunder_browser"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/fs"
@@ -103,6 +104,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
diff --git a/internal/offline_download/tool/download.go b/internal/offline_download/tool/download.go
index 42b2dbfb2cb..921482aa1db 100644
--- a/internal/offline_download/tool/download.go
+++ b/internal/offline_download/tool/download.go
@@ -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)
diff --git a/server/handles/offline_download.go b/server/handles/offline_download.go
index 24ff7a05369..9435a63095c 100644
--- a/server/handles/offline_download.go
+++ b/server/handles/offline_download.go
@@ -4,6 +4,7 @@ import (
_115 "github.com/alist-org/alist/v3/drivers/115"
"github.com/alist-org/alist/v3/drivers/pikpak"
"github.com/alist-org/alist/v3/drivers/thunder"
+ "github.com/alist-org/alist/v3/drivers/thunder_browser"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/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)
diff --git a/server/router.go b/server/router.go
index 09a0bb44faf..9005a28f033 100644
--- a/server/router.go
+++ b/server/router.go
@@ -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"))
From 1a250af747ab33e677c5e0bd79c881bfef080b98 Mon Sep 17 00:00:00 2001
From: MianRou <2945065490@qq.com>
Date: Mon, 26 May 2025 13:07:30 +0800
Subject: [PATCH 3/3] feat(thunder_browser): add fluent_play method for offline
download
---
drivers/thunder_browser/driver.go | 99 +++++++++++++++----
drivers/thunder_browser/meta.go | 6 +-
drivers/thunder_browser/types.go | 6 +-
drivers/thunder_browser/util.go | 14 +--
.../thunder_browser/thunder_browser.go | 4 +-
5 files changed, 99 insertions(+), 30 deletions(-)
diff --git a/drivers/thunder_browser/driver.go b/drivers/thunder_browser/driver.go
index 4c5112f806f..42b7d09ccfb 100644
--- a/drivers/thunder_browser/driver.go
+++ b/drivers/thunder_browser/driver.go
@@ -6,8 +6,8 @@ import (
"fmt"
"io"
"net/http"
- "strconv"
"strings"
+ "time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
@@ -66,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
@@ -109,6 +110,7 @@ func (x *ThunderBrowser) Init(ctx context.Context) (err error) {
}
x.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl
+ x.XunLeiBrowserCommon.UseFluentPlay = x.UseFluentPlay
x.Addition.RootFolderID = x.RootFolderID
// 防止重复登录
identity := x.GetIdentity()
@@ -201,8 +203,9 @@ func (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) {
}
return DownloadUserAgent
}(),
- UseVideoUrl: x.UseVideoUrl,
- RemoveWay: x.ExpertAddition.RemoveWay,
+ UseVideoUrl: x.UseVideoUrl,
+ UseFluentPlay: x.UseFluentPlay,
+ RemoveWay: x.ExpertAddition.RemoveWay,
refreshCTokenCk: func(token string) {
x.CaptchaToken = token
op.MustSaveDriverStorage(x)
@@ -233,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" {
@@ -310,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
}
@@ -649,10 +654,20 @@ func (xc *XunLeiBrowserCommon) Request(url string, method string, callback base.
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)
}
@@ -733,12 +748,42 @@ func (xc *XunLeiBrowserCommon) IsLogin() bool {
return err == nil
}
-// 离线下载文件
+// OfflineDownload 离线下载文件
func (xc *XunLeiBrowserCommon) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {
var resp OfflineDownloadResp
- _, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
- r.SetContext(ctx)
- r.SetBody(&base.Json{
+
+ 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(),
@@ -746,8 +791,20 @@ func (xc *XunLeiBrowserCommon) OfflineDownload(ctx context.Context, fileUrl stri
"url": base.Json{
"url": fileUrl,
},
- "space": parentDir.(*Files).GetSpace(),
- })
+ }
+
+ 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 {
@@ -757,9 +814,7 @@ func (xc *XunLeiBrowserCommon) OfflineDownload(ctx context.Context, fileUrl stri
return &resp.Task, err
}
-/*
-获取离线下载任务列表
-*/
+// OfflineList 获取离线下载任务列表
func (xc *XunLeiBrowserCommon) OfflineList(ctx context.Context, nextPageToken string) ([]OfflineTask, error) {
res := make([]OfflineTask, 0)
@@ -770,6 +825,7 @@ func (xc *XunLeiBrowserCommon) OfflineList(ctx context.Context, nextPageToken st
"type": "offline",
"limit": "10000",
"page_token": nextPageToken,
+ "space": "default/*",
})
}, &resp)
@@ -777,20 +833,27 @@ func (xc *XunLeiBrowserCommon) OfflineList(ctx context.Context, nextPageToken st
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, deleteFiles bool) error {
+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(map[string]string{
- "task_ids": strings.Join(taskIDs, ","),
- "delete_files": strconv.FormatBool(deleteFiles),
- })
+ SetQueryParams(queryParams)
}, nil)
if err != nil {
return fmt.Errorf("failed to delete tasks %v: %w", taskIDs, err)
}
+
return nil
}
diff --git a/drivers/thunder_browser/meta.go b/drivers/thunder_browser/meta.go
index 5809836e000..8e50e250f08 100644
--- a/drivers/thunder_browser/meta.go
+++ b/drivers/thunder_browser/meta.go
@@ -48,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"`
}
@@ -84,7 +86,9 @@ type Addition struct {
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"`
- RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
+ // 离线下载是否使用 流畅播(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"`
}
// GetIdentity 登录特征,用于判断是否重新登录
diff --git a/drivers/thunder_browser/types.go b/drivers/thunder_browser/types.go
index d7d9c8439f6..2d230e9ede5 100644
--- a/drivers/thunder_browser/types.go
+++ b/drivers/thunder_browser/types.go
@@ -304,7 +304,7 @@ type UploadTaskResponse struct {
File Files `json:"file"`
}
-// 添加离线下载响应
+// OfflineDownloadResp 离线下载响应
type OfflineDownloadResp struct {
File *string `json:"file"`
Task OfflineTask `json:"task"`
@@ -314,14 +314,14 @@ type OfflineDownloadResp struct {
} `json:"url"`
}
-// 离线下载列表
+// OfflineListResp 离线下载列表响应
type OfflineListResp struct {
ExpiresIn int64 `json:"expires_in"`
NextPageToken string `json:"next_page_token"`
Tasks []OfflineTask `json:"tasks"`
}
-// offlineTask
+// OfflineTask 离线下载任务响应
type OfflineTask struct {
Callback string `json:"callback"`
CreatedTime string `json:"created_time"`
diff --git a/drivers/thunder_browser/util.go b/drivers/thunder_browser/util.go
index 3b196810567..6be031cf037 100644
--- a/drivers/thunder_browser/util.go
+++ b/drivers/thunder_browser/util.go
@@ -63,12 +63,13 @@ const (
)
const (
- ThunderDriveSpace = ""
- ThunderDriveSafeSpace = "SPACE_SAFE"
- ThunderBrowserDriveSpace = "SPACE_BROWSER"
- ThunderBrowserDriveSafeSpace = "SPACE_BROWSER_SAFE"
- ThunderDriveFolderType = "DEFAULT_ROOT"
- ThunderBrowserDriveSafeFolderType = "BROWSER_SAFE"
+ ThunderDriveSpace = ""
+ ThunderDriveSafeSpace = "SPACE_SAFE"
+ ThunderBrowserDriveSpace = "SPACE_BROWSER"
+ ThunderBrowserDriveSafeSpace = "SPACE_BROWSER_SAFE"
+ ThunderDriveFolderType = "DEFAULT_ROOT"
+ ThunderBrowserDriveSafeFolderType = "BROWSER_SAFE"
+ ThunderBrowserDriveFluentPlayFolderType = "SPACE_FAVORITE" // 流畅播文件夹标识
)
const (
@@ -102,6 +103,7 @@ type Common struct {
UserAgent string
DownloadUserAgent string
UseVideoUrl bool
+ UseFluentPlay bool
RemoveWay string
// 验证码token刷新成功回调
diff --git a/internal/offline_download/thunder_browser/thunder_browser.go b/internal/offline_download/thunder_browser/thunder_browser.go
index 974fd1c95b4..6917a5a6f99 100644
--- a/internal/offline_download/thunder_browser/thunder_browser.go
+++ b/internal/offline_download/thunder_browser/thunder_browser.go
@@ -104,9 +104,9 @@ func (t *ThunderBrowser) Remove(task *tool.DownloadTask) error {
switch v := storage.(type) {
case *thunder_browser.ThunderBrowser:
- err = v.DeleteOfflineTasks(ctx, []string{task.GID}, false)
+ err = v.DeleteOfflineTasks(ctx, []string{task.GID})
case *thunder_browser.ThunderBrowserExpert:
- err = v.DeleteOfflineTasks(ctx, []string{task.GID}, false)
+ err = v.DeleteOfflineTasks(ctx, []string{task.GID})
default:
return fmt.Errorf("unsupported storage driver for offline download, only ThunderBrowser is supported")
}