Files
urldb/common/xunlei_pan.go

558 lines
16 KiB
Go
Raw Normal View History

2025-08-17 23:22:57 +08:00
// 1. 修正接口 Host增加配置项
// 2. POST/GET 区分xunleix 的 /drive/v1/share/list 是 GET不是 POST
// 3. 参数传递方式严格区分 query/body
// 4. header 应支持 AuthorizationBearer ...、x-device-id、x-client-id、x-captcha-token 等
// 5. 结构体返回字段需和 xunleix 100%一致(如 data 字段是 map 还是 list注意 code 字段为 int 还是 string
// 6. 错误处理,返回体未必有 code/msg需先判断 HTTP 状态码再判断 body
// 7. 建议增加日志和更清晰的错误提示
package pan
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
2025-08-18 02:30:15 +08:00
"log"
2025-08-17 23:22:57 +08:00
"net/http"
"net/url"
2025-08-18 02:30:15 +08:00
"strings"
2025-08-17 23:22:57 +08:00
"sync"
"time"
)
type XunleiPanService struct {
*BasePanService
configMutex sync.RWMutex
}
var (
xunleiInstance *XunleiPanService
xunleiOnce sync.Once
)
// 配置化 API Host
2025-08-20 00:13:31 +08:00
func (x *XunleiPanService) apiHost(apiType string) string {
if apiType == "user" {
return "https://xluser-ssl.xunlei.com"
}
2025-08-17 23:22:57 +08:00
return "https://api-pan.xunlei.com"
}
// 工具:自动补全必要 header
func (x *XunleiPanService) setCommonHeader(req *http.Request) {
for k, v := range x.headers {
req.Header.Set(k, v)
}
}
func NewXunleiPanService(config *PanConfig) *XunleiPanService {
xunleiOnce.Do(func() {
xunleiInstance = &XunleiPanService{
BasePanService: NewBasePanService(config),
}
xunleiInstance.SetHeaders(map[string]string{
"Content-Type": "application/json",
2025-08-20 00:13:31 +08:00
// "access-control-allow-credentials": "true",
// "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
// "access-control-allow-origin": "https://pan.xunlei.com",
// "access-control-max-age": "86400",
// "access-control-expose-headers": "Content-Type, Content-Length, Content-Encoding, X-Request-Id, X-Response-Time",
// "access-control-allow-headers": "Authorization, Content-Type,Accept, X-Project-Id, X-Device-Id, X-Request-Id, X-Captcha-Token, X-Client-Id, x-sdk-version, x-client-version, x-device-name, x-device-model, x-captcha-token, x-net-work-type, x-os-version, x-protocol-version, x-platform-version, x-provider-name, x-client-channel-id, x-appname, x-appid, x-device-sign, x-auto-login, x-peer-id, x-action",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
// "X-Client-Id": "Xqp0kJBXWhwaTpB6",
// "X-Device-Id": "c24ecadc44c643637d127fb847dbe36d",
// "X-Device-Sign": "wdi10.c24ecadc44c643637d127fb847dbe36d74ee67f56a443148fc801eb27bb3e058",
// "x-sdk-version": "5.2.4",
// "x-protocol-version": "301",
"Authorization": config.Cookie,
2025-08-17 23:22:57 +08:00
})
})
xunleiInstance.UpdateConfig(config)
return xunleiInstance
}
2025-08-18 02:30:15 +08:00
// GetXunleiInstance 获取迅雷网盘服务单例实例
func GetXunleiInstance() *XunleiPanService {
return NewXunleiPanService(nil)
}
2025-08-17 23:22:57 +08:00
func (x *XunleiPanService) UpdateConfig(config *PanConfig) {
if config == nil {
return
}
x.configMutex.Lock()
defer x.configMutex.Unlock()
x.config = config
if config.Cookie != "" {
x.SetHeader("Cookie", config.Cookie)
}
}
2025-08-18 02:30:15 +08:00
// GetServiceType 获取服务类型
func (x *XunleiPanService) GetServiceType() ServiceType {
return Xunlei
}
// Transfer 转存分享链接 - 实现 PanService 接口
func (x *XunleiPanService) Transfer(shareID string) (*TransferResult, error) {
// 读取配置(线程安全)
x.configMutex.RLock()
config := x.config
x.configMutex.RUnlock()
log.Printf("开始处理迅雷分享: %s", shareID)
// 检查是否为检验模式
if config.IsType == 1 {
// 检验模式:直接获取分享信息
shareInfo, err := x.getShareInfo(shareID)
if err != nil {
return ErrorResult(fmt.Sprintf("获取分享信息失败: %v", err)), nil
}
return SuccessResult("检验成功", map[string]interface{}{
"title": shareInfo.Title,
"shareUrl": config.URL,
}), nil
}
// 转存模式:实现完整的转存流程
// 1. 获取分享详情
shareDetail, err := x.GetShareFolder(shareID, "", "")
if err != nil {
return ErrorResult(fmt.Sprintf("获取分享详情失败: %v", err)), nil
}
// 2. 提取文件ID列表
fileIDs := make([]string, 0)
for _, file := range shareDetail.Data.Files {
fileIDs = append(fileIDs, file.FileID)
}
if len(fileIDs) == 0 {
return ErrorResult("分享中没有可转存的文件"), nil
}
// 3. 转存文件
restoreResult, err := x.Restore(shareID, "", fileIDs)
if err != nil {
return ErrorResult(fmt.Sprintf("转存失败: %v", err)), nil
}
// 4. 等待转存完成
taskID := restoreResult.Data.TaskID
_, err = x.waitForTask(taskID)
if err != nil {
return ErrorResult(fmt.Sprintf("等待转存完成失败: %v", err)), nil
}
// 5. 创建新的分享
shareResult, err := x.FileBatchShare(fileIDs, false, 0) // 永久分享
if err != nil {
return ErrorResult(fmt.Sprintf("创建分享失败: %v", err)), nil
}
// 6. 返回结果
return SuccessResult("转存成功", map[string]interface{}{
"shareUrl": shareResult.Data.ShareURL,
"title": fmt.Sprintf("迅雷分享_%s", shareID),
"fid": strings.Join(fileIDs, ","),
}), nil
}
// waitForTask 等待任务完成
func (x *XunleiPanService) waitForTask(taskID string) (*XLTaskResult, error) {
maxRetries := 50
retryDelay := 2 * time.Second
for retryIndex := 0; retryIndex < maxRetries; retryIndex++ {
result, err := x.getTaskStatus(taskID, retryIndex)
if err != nil {
return nil, err
}
if result.Status == 2 { // 任务完成
return result, nil
}
time.Sleep(retryDelay)
}
return nil, fmt.Errorf("任务超时")
}
// getTaskStatus 获取任务状态
func (x *XunleiPanService) getTaskStatus(taskID string, retryIndex int) (*XLTaskResult, error) {
2025-08-20 00:13:31 +08:00
apiURL := x.apiHost("") + "/drive/v1/task"
2025-08-18 02:30:15 +08:00
params := url.Values{}
params.Set("task_id", taskID)
params.Set("retry_index", fmt.Sprintf("%d", retryIndex))
apiURL = apiURL + "?" + params.Encode()
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
return nil, err
}
x.setCommonHeader(req)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
result, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(result))
}
var data XLTaskResult
if err := json.Unmarshal(result, &data); err != nil {
return nil, err
}
return &data, nil
}
// getShareInfo 获取分享信息(用于检验模式)
func (x *XunleiPanService) getShareInfo(shareID string) (*XLShareInfo, error) {
// 使用现有的 GetShareFolder 方法获取分享信息
shareDetail, err := x.GetShareFolder(shareID, "", "")
if err != nil {
return nil, err
}
// 构造分享信息
shareInfo := &XLShareInfo{
ShareID: shareID,
Title: fmt.Sprintf("迅雷分享_%s", shareID),
Files: make([]XLFileInfo, 0),
}
// 处理文件信息
for _, file := range shareDetail.Data.Files {
shareInfo.Files = append(shareInfo.Files, XLFileInfo{
FileID: file.FileID,
Name: file.Name,
})
}
return shareInfo, nil
}
// GetFiles 获取文件列表 - 实现 PanService 接口
func (x *XunleiPanService) GetFiles(pdirFid string) (*TransferResult, error) {
log.Printf("开始获取迅雷网盘文件列表目录ID: %s", pdirFid)
// 使用现有的 GetShareList 方法获取文件列表
shareList, err := x.GetShareList("")
if err != nil {
return ErrorResult(fmt.Sprintf("获取文件列表失败: %v", err)), nil
}
// 转换为通用格式
fileList := make([]interface{}, 0)
for _, share := range shareList.Data.List {
fileList = append(fileList, map[string]interface{}{
"share_id": share.ShareID,
"title": share.Title,
})
}
return SuccessResult("获取成功", fileList), nil
}
// DeleteFiles 删除文件 - 实现 PanService 接口
func (x *XunleiPanService) DeleteFiles(fileList []string) (*TransferResult, error) {
log.Printf("开始删除迅雷网盘文件,文件数量: %d", len(fileList))
// 使用现有的 ShareBatchDelete 方法删除分享
result, err := x.ShareBatchDelete(fileList)
if err != nil {
return ErrorResult(fmt.Sprintf("删除文件失败: %v", err)), nil
}
if result.Code != 0 {
return ErrorResult(fmt.Sprintf("删除文件失败: %s", result.Msg)), nil
}
return SuccessResult("删除成功", nil), nil
}
// GetUserInfo 获取用户信息 - 实现 PanService 接口
func (x *XunleiPanService) GetUserInfo(cookie string) (*UserInfo, error) {
log.Printf("开始获取迅雷网盘用户信息")
// 临时设置cookie
2025-08-20 00:13:31 +08:00
x.SetHeader("Authorization", cookie)
2025-08-18 02:30:15 +08:00
// 获取用户信息
2025-08-20 00:13:31 +08:00
apiURL := x.apiHost("user") + "/v1/user/me"
2025-08-18 02:30:15 +08:00
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
return nil, fmt.Errorf("创建请求失败: %v", err)
}
x.setCommonHeader(req)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("获取用户信息失败: %v", err)
}
defer resp.Body.Close()
result, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(result))
}
var response struct {
2025-08-20 00:13:31 +08:00
Username string `json:"name"`
2025-08-18 02:30:15 +08:00
}
if err := json.Unmarshal(result, &response); err != nil {
return nil, fmt.Errorf("解析用户信息失败: %v", err)
}
2025-08-20 00:13:31 +08:00
aboutURL := x.apiHost("") + "/drive/v1/about"
req, err = http.NewRequest("GET", aboutURL, nil)
if err != nil {
return nil, fmt.Errorf("创建请求失败: %v", err)
}
resp, err = client.Do(req)
if err != nil {
return nil, fmt.Errorf("获取用户信息失败: %v", err)
}
defer resp.Body.Close()
result, _ = ioutil.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(result))
2025-08-18 02:30:15 +08:00
}
return &UserInfo{
2025-08-20 00:13:31 +08:00
Username: response.Username,
2025-08-18 02:30:15 +08:00
ServiceType: "xunlei",
}, nil
}
2025-08-17 23:22:57 +08:00
// GetShareList 严格对齐 GET + queryxunleix实现
func (x *XunleiPanService) GetShareList(pageToken string) (*XLShareListResp, error) {
2025-08-20 00:13:31 +08:00
api := x.apiHost("") + "/drive/v1/share/list"
2025-08-17 23:22:57 +08:00
params := url.Values{}
params.Set("limit", "100")
params.Set("thumbnail_size", "SIZE_SMALL")
if pageToken != "" {
params.Set("page_token", pageToken)
}
2025-08-18 02:30:15 +08:00
apiURL := api + "?" + params.Encode()
2025-08-17 23:22:57 +08:00
2025-08-18 02:30:15 +08:00
req, err := http.NewRequest("GET", apiURL, nil)
2025-08-17 23:22:57 +08:00
if err != nil {
return nil, err
}
x.setCommonHeader(req)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
result, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(result))
}
var data XLShareListResp
if err := json.Unmarshal(result, &data); err != nil {
return nil, err
}
return &data, nil
}
// FileBatchShare 创建分享POST, body
func (x *XunleiPanService) FileBatchShare(ids []string, needPassword bool, expirationDays int) (*XLBatchShareResp, error) {
2025-08-20 00:13:31 +08:00
apiURL := x.apiHost("") + "/drive/v1/share/batch"
2025-08-17 23:22:57 +08:00
body := map[string]interface{}{
"file_ids": ids,
"need_password": needPassword,
"expiration_days": expirationDays,
}
bs, _ := json.Marshal(body)
2025-08-18 02:30:15 +08:00
req, err := http.NewRequest("POST", apiURL, bytes.NewReader(bs))
2025-08-17 23:22:57 +08:00
if err != nil {
return nil, err
}
x.setCommonHeader(req)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
result, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(result))
}
var data XLBatchShareResp
if err := json.Unmarshal(result, &data); err != nil {
return nil, err
}
return &data, nil
}
// ShareBatchDelete 取消分享POST, body
func (x *XunleiPanService) ShareBatchDelete(ids []string) (*XLCommonResp, error) {
2025-08-20 00:13:31 +08:00
apiURL := x.apiHost("") + "/drive/v1/share/batch/delete"
2025-08-17 23:22:57 +08:00
body := map[string]interface{}{
"share_ids": ids,
}
bs, _ := json.Marshal(body)
2025-08-18 02:30:15 +08:00
req, err := http.NewRequest("POST", apiURL, bytes.NewReader(bs))
2025-08-17 23:22:57 +08:00
if err != nil {
return nil, err
}
x.setCommonHeader(req)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
result, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(result))
}
var data XLCommonResp
if err := json.Unmarshal(result, &data); err != nil {
return nil, err
}
return &data, nil
}
// GetShareFolder 获取分享内容POST, body
func (x *XunleiPanService) GetShareFolder(shareID, passCodeToken, parentID string) (*XLShareFolderResp, error) {
2025-08-20 00:13:31 +08:00
apiURL := x.apiHost("") + "/drive/v1/share/detail"
2025-08-17 23:22:57 +08:00
body := map[string]interface{}{
"share_id": shareID,
"pass_code_token": passCodeToken,
"parent_id": parentID,
"limit": 100,
"thumbnail_size": "SIZE_LARGE",
"order": "6",
}
bs, _ := json.Marshal(body)
2025-08-18 02:30:15 +08:00
req, err := http.NewRequest("POST", apiURL, bytes.NewReader(bs))
2025-08-17 23:22:57 +08:00
if err != nil {
return nil, err
}
x.setCommonHeader(req)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
result, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(result))
}
var data XLShareFolderResp
if err := json.Unmarshal(result, &data); err != nil {
return nil, err
}
return &data, nil
}
// Restore 转存POST, body
func (x *XunleiPanService) Restore(shareID, passCodeToken string, fileIDs []string) (*XLRestoreResp, error) {
2025-08-20 00:13:31 +08:00
apiURL := x.apiHost("") + "/drive/v1/share/restore"
2025-08-17 23:22:57 +08:00
body := map[string]interface{}{
"share_id": shareID,
"pass_code_token": passCodeToken,
"file_ids": fileIDs,
"folder_type": "NORMAL",
"specify_parent_id": true,
"parent_id": "",
}
bs, _ := json.Marshal(body)
2025-08-18 02:30:15 +08:00
req, err := http.NewRequest("POST", apiURL, bytes.NewReader(bs))
2025-08-17 23:22:57 +08:00
if err != nil {
return nil, err
}
x.setCommonHeader(req)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
result, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(result))
}
var data XLRestoreResp
if err := json.Unmarshal(result, &data); err != nil {
return nil, err
}
return &data, nil
}
// 结构体完全对齐 xunleix
type XLShareListResp struct {
Data struct {
List []struct {
ShareID string `json:"share_id"`
Title string `json:"title"`
} `json:"list"`
} `json:"data"`
Code int `json:"code"`
Msg string `json:"msg"`
}
type XLBatchShareResp struct {
Data struct {
ShareURL string `json:"share_url"`
} `json:"data"`
Code int `json:"code"`
Msg string `json:"msg"`
}
type XLCommonResp struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
type XLShareFolderResp struct {
Data struct {
Files []struct {
FileID string `json:"file_id"`
Name string `json:"name"`
} `json:"files"`
} `json:"data"`
Code int `json:"code"`
Msg string `json:"msg"`
}
type XLRestoreResp struct {
Data struct {
TaskID string `json:"task_id"`
} `json:"data"`
Code int `json:"code"`
Msg string `json:"msg"`
}
2025-08-18 02:30:15 +08:00
// 新增辅助结构体
type XLShareInfo struct {
ShareID string `json:"share_id"`
Title string `json:"title"`
Files []XLFileInfo `json:"files"`
}
type XLFileInfo struct {
FileID string `json:"file_id"`
Name string `json:"name"`
}
type XLTaskResult struct {
Status int `json:"status"`
TaskID string `json:"task_id"`
Data struct {
ShareID string `json:"share_id"`
} `json:"data"`
}