mirror of
https://github.com/fish2018/pansou.git
synced 2025-11-25 03:14:59 +08:00
新增插件yunsou
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.DS_Store
|
||||
|
||||
341
plugin/yunsou/html结构分析.md
Normal file
341
plugin/yunsou/html结构分析.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# 云搜影视 (yunsou) 网站搜索结果HTML结构分析
|
||||
|
||||
## 网站信息
|
||||
|
||||
- **网站名称**: 云搜影视
|
||||
- **域名**: `yunsou.xyz`
|
||||
- **搜索URL格式**: `https://yunsou.xyz/s/{关键词}.html`
|
||||
- **主要特点**: 提供网盘资源搜索,支持夸克、百度、UC、迅雷、阿里云盘等多种网盘
|
||||
|
||||
## 搜索源类型
|
||||
|
||||
云搜影视提供两种搜索源:
|
||||
1. **本地搜** (currentSource=0): 结果直接内嵌在HTML中
|
||||
2. **全网搜** (currentSource=1): 通过SSE流式接口获取
|
||||
|
||||
本插件实现**本地搜**功能,因为:
|
||||
- 结果更稳定可靠
|
||||
- 响应速度更快
|
||||
- 数据格式规范
|
||||
|
||||
## HTML结构
|
||||
|
||||
### 搜索结果页面结构
|
||||
|
||||
搜索结果直接内嵌在HTML的JavaScript代码中,以JSON格式存储:
|
||||
|
||||
```html
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
function linkBtn(element) {
|
||||
const index = element.getAttribute('data-index');
|
||||
var jsonData = '[{"id":51199,"source_category_id":3,"title":"凡人修仙传 真人版 [2025][奇幻 古装 大陆][杨洋 金晨]","is_type":4,"code":null,"url":"https://pan.xunlei.com/s/VOW9WQT6nyFBDjHwYjjGj13YA1?pwd=v2m9","is_time":0,"name":"凡人修仙传 真人版 [2025][奇幻 古装 大陆][杨洋 金晨]","times":"2025-07-27","category":{"source_category_id":3,"name":"电视剧"}},...]';
|
||||
// ...
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### JSON数据结构
|
||||
|
||||
每个搜索结果项的JSON结构如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 51199,
|
||||
"source_category_id": 3,
|
||||
"title": "凡人修仙传 真人版 [2025][奇幻 古装 大陆][杨洋 金晨]",
|
||||
"is_type": 4,
|
||||
"code": null,
|
||||
"url": "https://pan.xunlei.com/s/VOW9WQT6nyFBDjHwYjjGj13YA1?pwd=v2m9",
|
||||
"is_time": 0,
|
||||
"name": "凡人修仙传 真人版 [2025][奇幻 古装 大陆][杨洋 金晨]",
|
||||
"times": "2025-07-27",
|
||||
"category": {
|
||||
"source_category_id": 3,
|
||||
"name": "电视剧"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### JSON字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 | 示例 |
|
||||
|------|------|------|------|
|
||||
| `id` | number | 资源唯一ID | 51199 |
|
||||
| `title` | string | 资源标题 | "凡人修仙传 真人版 [2025][奇幻 古装 大陆][杨洋 金晨]" |
|
||||
| `is_type` | number | 网盘类型标识 | 0=夸克, 1=阿里, 2=百度, 3=UC, 4=迅雷 |
|
||||
| `code` | string\|null | 提取码(可能为null) | "v2m9" 或 null |
|
||||
| `url` | string | 网盘链接 | "https://pan.xunlei.com/s/..." |
|
||||
| `times` | string | 发布时间 | "2025-07-27" |
|
||||
| `category.name` | string | 资源分类 | "电视剧", "动漫", "电影"等 |
|
||||
|
||||
#### 网盘类型映射 (is_type)
|
||||
|
||||
```
|
||||
0 -> 夸克网盘 (quark)
|
||||
1 -> 阿里云盘 (aliyun)
|
||||
2 -> 百度网盘 (baidu)
|
||||
3 -> UC网盘 (uc)
|
||||
4 -> 迅雷网盘 (xunlei)
|
||||
```
|
||||
|
||||
### HTML展示结构
|
||||
|
||||
虽然数据在JS中,HTML中也有对应的展示结构:
|
||||
|
||||
```html
|
||||
<div class="list">
|
||||
<div class="item">
|
||||
<a href="javascript:;" onclick="linkBtn(this)" data-index="0" class="title">
|
||||
凡人修仙传 真人版 [2025][奇幻 古装 大陆][杨洋 金晨]
|
||||
</a>
|
||||
<div class="type cate">分类:电视剧</div>
|
||||
<div class="type time">2025-07-27</div>
|
||||
<div class="type">
|
||||
<span>来源:迅雷网盘</span>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<div class="btn">复制分享</div>
|
||||
<a href="/d/51199.html" class="btn">查看详情</a>
|
||||
<a href="javascript:;" onclick="linkBtn(this)" data-index="0" class="btn">立即访问</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 提取逻辑
|
||||
|
||||
### 1. 搜索结果提取流程
|
||||
|
||||
```
|
||||
1. 发送GET请求到搜索URL
|
||||
├─ URL: https://yunsou.xyz/s/{URL编码的关键词}.html
|
||||
└─ 设置完整的浏览器请求头
|
||||
|
||||
2. 解析HTML响应
|
||||
├─ 查找包含 "var jsonData = " 的script标签
|
||||
└─ 提取JSON字符串
|
||||
|
||||
3. 清理并解析JSON
|
||||
├─ 移除控制字符和转义
|
||||
├─ 解析为结构化数据
|
||||
└─ 处理异常数据
|
||||
|
||||
4. 转换为SearchResult格式
|
||||
├─ 生成UniqueID: "yunsou-{id}"
|
||||
├─ 设置标题、内容、时间
|
||||
├─ 根据is_type确定网盘类型
|
||||
├─ 构建Link对象(包含URL和提取码)
|
||||
└─ 添加分类标签
|
||||
|
||||
5. 关键词过滤
|
||||
└─ 使用FilterResultsByKeyword过滤结果
|
||||
```
|
||||
|
||||
### 2. JSON提取正则表达式
|
||||
|
||||
```go
|
||||
// 提取JSON数据的正则表达式
|
||||
var jsonData = '[...]';
|
||||
```
|
||||
|
||||
匹配模式:
|
||||
- 查找 `var jsonData = '` 开头
|
||||
- 提取单引号内的完整JSON字符串
|
||||
- 处理转义字符和特殊字符
|
||||
|
||||
### 3. 网盘链接处理
|
||||
|
||||
#### 提取码处理
|
||||
|
||||
提取码可能存在于两个位置:
|
||||
1. **JSON中的code字段**: 单独的提取码字段
|
||||
2. **URL中的pwd参数**: `?pwd=xxxx` 格式
|
||||
|
||||
处理逻辑:
|
||||
```go
|
||||
password := ""
|
||||
if code != nil && *code != "" {
|
||||
password = *code
|
||||
} else if strings.Contains(url, "?pwd=") {
|
||||
// 从URL中提取pwd参数
|
||||
password = extractPwdFromURL(url)
|
||||
}
|
||||
```
|
||||
|
||||
#### 网盘类型转换
|
||||
|
||||
```go
|
||||
func convertNetDiskType(isType int) string {
|
||||
switch isType {
|
||||
case 0:
|
||||
return "quark" // 夸克网盘
|
||||
case 1:
|
||||
return "aliyun" // 阿里云盘
|
||||
case 2:
|
||||
return "baidu" // 百度网盘
|
||||
case 3:
|
||||
return "uc" // UC网盘
|
||||
case 4:
|
||||
return "xunlei" // 迅雷网盘
|
||||
default:
|
||||
return "others"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 时间格式转换
|
||||
|
||||
时间格式为 "2025-07-27",需要解析为 time.Time:
|
||||
|
||||
```go
|
||||
// 解析时间字符串
|
||||
const timeLayout = "2006-01-02"
|
||||
parsedTime, err := time.Parse(timeLayout, times)
|
||||
if err != nil {
|
||||
parsedTime = time.Now() // 解析失败使用当前时间
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 内容构建
|
||||
|
||||
内容字段组合多个信息:
|
||||
|
||||
```go
|
||||
contentParts := []string{}
|
||||
if category != "" {
|
||||
contentParts = append(contentParts, "【"+category+"】")
|
||||
}
|
||||
// 可以添加更多描述信息
|
||||
content := strings.Join(contentParts, " ")
|
||||
```
|
||||
|
||||
## 实现要点
|
||||
|
||||
### 1. HTTP请求
|
||||
|
||||
必须设置完整的浏览器请求头:
|
||||
|
||||
```go
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
req.Header.Set("Referer", "https://yunsou.xyz/")
|
||||
```
|
||||
|
||||
### 2. JSON提取
|
||||
|
||||
需要处理的特殊情况:
|
||||
- 单引号包裹的JSON字符串
|
||||
- 转义的双引号 `\"`
|
||||
- 可能存在的控制字符(`\x00-\x1F`, `\x7F`)
|
||||
- Unicode转义序列
|
||||
|
||||
清理代码:
|
||||
```go
|
||||
// 移除控制字符
|
||||
jsonStr = regexp.MustCompile(`[\x00-\x1F\x7F]`).ReplaceAllString(jsonStr, "")
|
||||
|
||||
// 处理转义
|
||||
jsonStr = strings.ReplaceAll(jsonStr, `\"`, `"`)
|
||||
jsonStr = strings.ReplaceAll(jsonStr, `\/`, `/`)
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
需要处理的错误情况:
|
||||
- 网络请求失败
|
||||
- HTTP状态码非200
|
||||
- 未找到JSON数据
|
||||
- JSON解析失败
|
||||
- 空结果集
|
||||
|
||||
### 4. 数据验证
|
||||
|
||||
在添加到结果前验证:
|
||||
- UniqueID 不为空
|
||||
- Title 不为空
|
||||
- URL 是有效的网盘链接
|
||||
- Links 数组不为空(系统会自动过滤无链接结果)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **URL编码**: 关键词必须进行URL编码
|
||||
2. **中文支持**: 确保正确处理UTF-8编码
|
||||
3. **提取码位置**: 优先使用code字段,其次从URL提取
|
||||
4. **时间解析**: 处理时间解析失败的情况
|
||||
5. **空值处理**: code字段可能为null,需要类型断言
|
||||
6. **链接验证**: 确保网盘链接格式正确
|
||||
7. **插件规范**:
|
||||
- Channel字段必须为空字符串
|
||||
- Links不能为空(会被系统过滤)
|
||||
- 使用FilterResultsByKeyword进行关键词过滤
|
||||
|
||||
## 示例代码片段
|
||||
|
||||
### JSON数据提取
|
||||
|
||||
```go
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 提取JSON数据
|
||||
func extractJSONData(htmlContent string) (string, error) {
|
||||
// 查找 var jsonData = '...'
|
||||
pattern := regexp.MustCompile(`var jsonData = '(.+?)';`)
|
||||
matches := pattern.FindStringSubmatch(htmlContent)
|
||||
|
||||
if len(matches) < 2 {
|
||||
return "", fmt.Errorf("未找到JSON数据")
|
||||
}
|
||||
|
||||
jsonStr := matches[1]
|
||||
|
||||
// 清理控制字符
|
||||
jsonStr = regexp.MustCompile(`[\x00-\x1F\x7F]`).ReplaceAllString(jsonStr, "")
|
||||
|
||||
// 处理转义字符
|
||||
jsonStr = strings.ReplaceAll(jsonStr, `\\/`, `/`)
|
||||
jsonStr = strings.ReplaceAll(jsonStr, `\\"`, `"`)
|
||||
|
||||
return jsonStr, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 网盘链接构建
|
||||
|
||||
```go
|
||||
// 构建Link对象
|
||||
func buildLink(item YunsouItem) model.Link {
|
||||
link := model.Link{
|
||||
Type: convertNetDiskType(item.IsType),
|
||||
URL: item.URL,
|
||||
}
|
||||
|
||||
// 处理提取码
|
||||
if item.Code != nil && *item.Code != "" {
|
||||
link.Password = *item.Code
|
||||
} else if strings.Contains(item.URL, "?pwd=") {
|
||||
link.Password = extractPwdFromURL(item.URL)
|
||||
}
|
||||
|
||||
return link
|
||||
}
|
||||
```
|
||||
|
||||
## 优先级建议
|
||||
|
||||
根据云搜影视的特点,建议设置优先级为 **2**:
|
||||
- ✅ 数据源质量良好,资源较新
|
||||
- ✅ 支持多种网盘类型
|
||||
- ✅ 响应速度较快
|
||||
- ✅ 数据格式规范,易于解析
|
||||
- ⚠️ 作为聚合搜索站点,可能有少量失效链接
|
||||
|
||||
## 相关链接
|
||||
|
||||
- 搜索示例: `https://yunsou.xyz/s/凡人修仙传.html`
|
||||
- 详情页示例: `https://yunsou.xyz/d/51199.html` (可选,插件不需要访问)
|
||||
|
||||
313
plugin/yunsou/yunsou.go
Normal file
313
plugin/yunsou/yunsou.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package yunsou
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"pansou/model"
|
||||
"pansou/plugin"
|
||||
"pansou/util/json"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 预编译的正则表达式
|
||||
var (
|
||||
// 提取JSON数据的正则表达式
|
||||
jsonDataRegex = regexp.MustCompile(`var jsonData = '(.+?)';`)
|
||||
|
||||
// 提取pwd参数的正则表达式
|
||||
pwdParamRegex = regexp.MustCompile(`[?&]pwd=([0-9a-zA-Z]+)`)
|
||||
|
||||
// 控制字符清理正则
|
||||
controlCharsRegex = regexp.MustCompile(`[\x00-\x1F\x7F]`)
|
||||
)
|
||||
|
||||
// 常量定义
|
||||
const (
|
||||
// 插件名称
|
||||
pluginName = "yunsou"
|
||||
|
||||
// 搜索URL模板
|
||||
searchURLTemplate = "https://yunsou.xyz/s/%s.html"
|
||||
|
||||
// 默认优先级
|
||||
defaultPriority = 2
|
||||
|
||||
// 默认超时时间
|
||||
defaultTimeout = 30 * time.Second
|
||||
|
||||
// 最大重试次数
|
||||
maxRetries = 3
|
||||
|
||||
// 时间格式
|
||||
timeLayout = "2006-01-02"
|
||||
)
|
||||
|
||||
// YunsouAsyncPlugin 是云搜影视网站的异步搜索插件实现
|
||||
type YunsouAsyncPlugin struct {
|
||||
*plugin.BaseAsyncPlugin
|
||||
optimizedClient *http.Client
|
||||
}
|
||||
|
||||
// YunsouCategory 分类信息
|
||||
type YunsouCategory struct {
|
||||
SourceCategoryID int `json:"source_category_id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// YunsouItem 单个搜索结果项
|
||||
type YunsouItem struct {
|
||||
ID int `json:"id"`
|
||||
SourceCategoryID int `json:"source_category_id"`
|
||||
Title string `json:"title"`
|
||||
IsType int `json:"is_type"` // 0=夸克, 1=阿里, 2=百度, 3=UC, 4=迅雷
|
||||
Code *string `json:"code"` // 提取码,可能为null
|
||||
URL string `json:"url"`
|
||||
IsTime int `json:"is_time"`
|
||||
Name string `json:"name"`
|
||||
Times string `json:"times"` // 发布时间 "2025-07-27"
|
||||
Category YunsouCategory `json:"category"`
|
||||
}
|
||||
|
||||
// 确保YunsouAsyncPlugin实现了AsyncSearchPlugin接口
|
||||
var _ plugin.AsyncSearchPlugin = (*YunsouAsyncPlugin)(nil)
|
||||
|
||||
// init 在包初始化时注册插件
|
||||
func init() {
|
||||
plugin.RegisterGlobalPlugin(NewYunsouAsyncPlugin())
|
||||
}
|
||||
|
||||
// createOptimizedHTTPClient 创建优化的HTTP客户端
|
||||
func createOptimizedHTTPClient() *http.Client {
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 20,
|
||||
MaxConnsPerHost: 50,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: defaultTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// NewYunsouAsyncPlugin 创建一个新的云搜影视异步插件实例
|
||||
func NewYunsouAsyncPlugin() *YunsouAsyncPlugin {
|
||||
return &YunsouAsyncPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin(pluginName, defaultPriority),
|
||||
optimizedClient: createOptimizedHTTPClient(),
|
||||
}
|
||||
}
|
||||
|
||||
// Search 执行搜索并返回结果(兼容性方法)
|
||||
func (p *YunsouAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
|
||||
result, err := p.SearchWithResult(keyword, ext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Results, nil
|
||||
}
|
||||
|
||||
// SearchWithResult 执行搜索并返回包含IsFinal标记的结果
|
||||
func (p *YunsouAsyncPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {
|
||||
return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)
|
||||
}
|
||||
|
||||
// searchImpl 实现具体的搜索逻辑
|
||||
func (p *YunsouAsyncPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
|
||||
// 1. 构建搜索URL
|
||||
searchURL := fmt.Sprintf(searchURLTemplate, url.QueryEscape(keyword))
|
||||
|
||||
// 2. 创建带超时的上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// 3. 创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", searchURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%s] 创建请求失败: %w", p.Name(), err)
|
||||
}
|
||||
|
||||
// 4. 设置完整的请求头(避免反爬虫)
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
req.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||
req.Header.Set("Cache-Control", "max-age=0")
|
||||
req.Header.Set("Referer", "https://yunsou.xyz/")
|
||||
|
||||
// 5. 发送请求(带重试机制)
|
||||
resp, err := p.doRequestWithRetry(req, client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%s] 搜索请求失败: %w", p.Name(), err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("[%s] 搜索请求返回状态码: %d", p.Name(), resp.StatusCode)
|
||||
}
|
||||
|
||||
// 6. 读取响应内容
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%s] 读取响应失败: %w", p.Name(), err)
|
||||
}
|
||||
|
||||
htmlContent := string(bodyBytes)
|
||||
|
||||
// 7. 提取JSON数据
|
||||
jsonStr, err := p.extractJSONData(htmlContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%s] 提取JSON数据失败: %w", p.Name(), err)
|
||||
}
|
||||
|
||||
// 8. 解析JSON数据
|
||||
var items []YunsouItem
|
||||
if err := json.Unmarshal([]byte(jsonStr), &items); err != nil {
|
||||
return nil, fmt.Errorf("[%s] 解析JSON失败: %w", p.Name(), err)
|
||||
}
|
||||
|
||||
// 9. 转换为标准格式
|
||||
results := make([]model.SearchResult, 0, len(items))
|
||||
for _, item := range items {
|
||||
result := p.convertToSearchResult(item)
|
||||
if result.UniqueID != "" && len(result.Links) > 0 {
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
|
||||
// 10. 关键词过滤
|
||||
return plugin.FilterResultsByKeyword(results, keyword), nil
|
||||
}
|
||||
|
||||
// extractJSONData 从HTML中提取JSON数据
|
||||
func (p *YunsouAsyncPlugin) extractJSONData(htmlContent string) (string, error) {
|
||||
// 查找 var jsonData = '...'
|
||||
matches := jsonDataRegex.FindStringSubmatch(htmlContent)
|
||||
if len(matches) < 2 {
|
||||
return "", fmt.Errorf("未找到JSON数据")
|
||||
}
|
||||
|
||||
jsonStr := matches[1]
|
||||
|
||||
// 清理控制字符
|
||||
jsonStr = controlCharsRegex.ReplaceAllString(jsonStr, "")
|
||||
|
||||
// 处理转义字符
|
||||
jsonStr = strings.ReplaceAll(jsonStr, `\/`, `/`)
|
||||
|
||||
return jsonStr, nil
|
||||
}
|
||||
|
||||
// convertToSearchResult 将YunsouItem转换为SearchResult
|
||||
func (p *YunsouAsyncPlugin) convertToSearchResult(item YunsouItem) model.SearchResult {
|
||||
result := model.SearchResult{
|
||||
UniqueID: fmt.Sprintf("%s-%d", p.Name(), item.ID),
|
||||
Title: item.Title,
|
||||
Channel: "", // 插件搜索结果必须为空字符串
|
||||
}
|
||||
|
||||
// 解析时间
|
||||
if item.Times != "" {
|
||||
if parsedTime, err := time.Parse(timeLayout, item.Times); err == nil {
|
||||
result.Datetime = parsedTime
|
||||
} else {
|
||||
result.Datetime = time.Now()
|
||||
}
|
||||
} else {
|
||||
result.Datetime = time.Now()
|
||||
}
|
||||
|
||||
// 构建内容描述
|
||||
var contentParts []string
|
||||
if item.Category.Name != "" {
|
||||
contentParts = append(contentParts, "【"+item.Category.Name+"】")
|
||||
}
|
||||
result.Content = strings.Join(contentParts, " ")
|
||||
|
||||
// 添加分类标签
|
||||
if item.Category.Name != "" {
|
||||
result.Tags = []string{item.Category.Name}
|
||||
}
|
||||
|
||||
// 构建网盘链接
|
||||
if item.URL != "" {
|
||||
link := model.Link{
|
||||
Type: p.convertNetDiskType(item.IsType),
|
||||
URL: item.URL,
|
||||
}
|
||||
|
||||
// 处理提取码
|
||||
if item.Code != nil && *item.Code != "" {
|
||||
link.Password = *item.Code
|
||||
} else if strings.Contains(item.URL, "?pwd=") {
|
||||
link.Password = p.extractPwdFromURL(item.URL)
|
||||
}
|
||||
|
||||
result.Links = []model.Link{link}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// convertNetDiskType 将is_type转换为网盘类型标识
|
||||
func (p *YunsouAsyncPlugin) convertNetDiskType(isType int) string {
|
||||
switch isType {
|
||||
case 0:
|
||||
return "quark" // 夸克网盘
|
||||
case 1:
|
||||
return "aliyun" // 阿里云盘
|
||||
case 2:
|
||||
return "baidu" // 百度网盘
|
||||
case 3:
|
||||
return "uc" // UC网盘
|
||||
case 4:
|
||||
return "xunlei" // 迅雷网盘
|
||||
default:
|
||||
return "others"
|
||||
}
|
||||
}
|
||||
|
||||
// extractPwdFromURL 从URL中提取pwd参数
|
||||
func (p *YunsouAsyncPlugin) extractPwdFromURL(urlStr string) string {
|
||||
matches := pwdParamRegex.FindStringSubmatch(urlStr)
|
||||
if len(matches) >= 2 {
|
||||
return matches[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// doRequestWithRetry 带重试机制的HTTP请求
|
||||
func (p *YunsouAsyncPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*http.Response, error) {
|
||||
var lastErr error
|
||||
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
if i > 0 {
|
||||
// 指数退避重试
|
||||
backoff := time.Duration(1<<uint(i-1)) * 200 * time.Millisecond
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
|
||||
// 克隆请求避免并发问题
|
||||
reqClone := req.Clone(req.Context())
|
||||
|
||||
resp, err := client.Do(reqClone)
|
||||
if err == nil && resp.StatusCode == 200 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user