mirror of
https://github.com/fish2018/pansou.git
synced 2025-11-25 11:29:30 +08:00
新增插件xys
This commit is contained in:
1
main.go
1
main.go
@@ -56,6 +56,7 @@ import (
|
|||||||
_ "pansou/plugin/libvio"
|
_ "pansou/plugin/libvio"
|
||||||
_ "pansou/plugin/leijing"
|
_ "pansou/plugin/leijing"
|
||||||
_ "pansou/plugin/xb6v"
|
_ "pansou/plugin/xb6v"
|
||||||
|
_ "pansou/plugin/xys"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 全局缓存写入管理器
|
// 全局缓存写入管理器
|
||||||
|
|||||||
114
plugin/xys/html结构分析.md
Normal file
114
plugin/xys/html结构分析.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# XYS(小云搜索)插件HTML结构分析
|
||||||
|
|
||||||
|
## API流程概述
|
||||||
|
|
||||||
|
### 第一步:获取Token
|
||||||
|
- **请求URL**: `https://www.yunso.net/index/user/s?wd={keyword}&mode=undefined&stype=undefined`
|
||||||
|
- **方法**: GET
|
||||||
|
- **Headers**:
|
||||||
|
- `Referer: https://www.yunso.net/`
|
||||||
|
- `User-Agent: Mozilla/5.0...`
|
||||||
|
- **Token提取**: 从返回HTML中匹配 `const DToken = "42b63a003f80bd5ff0a731fcd2a49fd40aefb5e96a46d546abbf92094da54763";`
|
||||||
|
|
||||||
|
### 第二步:执行搜索
|
||||||
|
- **请求URL**: `https://www.yunso.net/api/validate/searchX2`
|
||||||
|
- **方法**: POST
|
||||||
|
- **URL参数**:
|
||||||
|
- `DToken2={token}`
|
||||||
|
- `requestID=undefined`
|
||||||
|
- `mode=90002`
|
||||||
|
- `stype=undefined`
|
||||||
|
- `scope_content=0`
|
||||||
|
- `wd={keyword}` (URL编码)
|
||||||
|
- `uk=`
|
||||||
|
- `page=1`
|
||||||
|
- `limit=20`
|
||||||
|
- `screen_filetype=`
|
||||||
|
- **Headers**:
|
||||||
|
- `Referer: https://www.yunso.net/`
|
||||||
|
- `Content-Type: application/x-www-form-urlencoded`
|
||||||
|
|
||||||
|
## 搜索结果结构
|
||||||
|
|
||||||
|
### JSON响应格式
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "",
|
||||||
|
"time": "1755998625",
|
||||||
|
"data": "HTML内容"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML结构 (在data字段中)
|
||||||
|
|
||||||
|
#### 搜索结果项
|
||||||
|
```html
|
||||||
|
<div class="layui-card" style="..." id="{qid}-{timestamp}-{hash}" data-qid="{qid}">
|
||||||
|
<div class="layui-card-header" style="...">
|
||||||
|
<div style="...">
|
||||||
|
序号、 <span class="layui-badge">24小时内</span>
|
||||||
|
<img src="/assets/xyso/icon/filetype_folder.png" style="...">
|
||||||
|
<a onclick="open_sid(this)" id="{qid}-{timestamp}-{hash}"
|
||||||
|
url="{base64_url}" href="{real_url}" pa="{password}" target="_blank">
|
||||||
|
标题内容
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="responsive-container">
|
||||||
|
<div><i class="layui-icon layui-icon-time"></i> 2025-08-24 22:56:32</div>
|
||||||
|
<div>按钮组</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-card-body">
|
||||||
|
<p>
|
||||||
|
<span>所有文件共计: 合计 :N/A</span>
|
||||||
|
<img src="/assets/xyso/{type}.png" alt="{platform}">
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据提取要点
|
||||||
|
|
||||||
|
### 1. 标题提取
|
||||||
|
- **选择器**: `.layui-card-header a[onclick="open_sid(this)"]`
|
||||||
|
- **内容**: 链接文本内容,可能包含 `@` 等特殊符号需要清理
|
||||||
|
|
||||||
|
### 2. 链接提取
|
||||||
|
- **属性**: `href` - 真实链接URL
|
||||||
|
- **属性**: `url` - Base64编码的URL (备用)
|
||||||
|
- **属性**: `pa` - 提取码/密码
|
||||||
|
|
||||||
|
### 3. 时间提取
|
||||||
|
- **选择器**: `.layui-icon-time` 的父元素或下一个兄弟元素
|
||||||
|
- **格式**: `2025-08-24 22:56:32`
|
||||||
|
|
||||||
|
### 4. 网盘类型提取
|
||||||
|
- **选择器**: `.layui-card-body img[alt]`
|
||||||
|
- **类型映射**:
|
||||||
|
- `夸克` → quark
|
||||||
|
- `百度` → baidu
|
||||||
|
- `阿里` → aliyun
|
||||||
|
- 等等
|
||||||
|
|
||||||
|
### 5. 结果统计
|
||||||
|
- **总数**: 从顶部 `找到相关结果约 <strong>5919</strong> 个` 提取
|
||||||
|
|
||||||
|
## 特殊处理
|
||||||
|
|
||||||
|
### 1. 标题清理
|
||||||
|
- 移除 `@` 符号: `凡@人@修@仙@传` → `凡人修仙传`
|
||||||
|
- 移除HTML标签: `<font color='red'>凡人修仙传</font>` → `凡人修仙传`
|
||||||
|
|
||||||
|
### 2. 链接处理
|
||||||
|
- 优先使用 `href` 属性
|
||||||
|
- 如果没有则解码 `url` 属性 (Base64)
|
||||||
|
- 提取密码从 `pa` 属性
|
||||||
|
|
||||||
|
### 3. 时间解析
|
||||||
|
- 格式: `2025-08-24 22:56:32`
|
||||||
|
- 转换为标准时间格式
|
||||||
|
|
||||||
|
### 4. 网盘识别
|
||||||
|
- 根据图片alt属性确定网盘类型
|
||||||
|
- 根据URL域名辅助识别
|
||||||
475
plugin/xys/xys.go
Normal file
475
plugin/xys/xys.go
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
package xys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"pansou/model"
|
||||||
|
"pansou/plugin"
|
||||||
|
"pansou/util/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PluginName = "xys"
|
||||||
|
DisplayName = "小云搜索"
|
||||||
|
Description = "小云搜索 - 阿里云盘、夸克网盘、百度网盘等多网盘搜索引擎"
|
||||||
|
BaseURL = "https://www.yunso.net"
|
||||||
|
TokenPath = "/index/user/s"
|
||||||
|
SearchPath = "/api/validate/searchX2"
|
||||||
|
UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||||
|
MaxResults = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
// XysPlugin 小云搜索插件
|
||||||
|
type XysPlugin struct {
|
||||||
|
*plugin.BaseAsyncPlugin
|
||||||
|
debugMode bool
|
||||||
|
tokenCache sync.Map // 缓存token,避免频繁获取
|
||||||
|
cacheTTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenCache token缓存结构
|
||||||
|
type TokenCache struct {
|
||||||
|
Token string
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchResponse API响应结构
|
||||||
|
type SearchResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// init 注册插件
|
||||||
|
func init() {
|
||||||
|
plugin.RegisterGlobalPlugin(NewXysPlugin())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewXysPlugin 创建新的小云搜索插件实例
|
||||||
|
func NewXysPlugin() *XysPlugin {
|
||||||
|
// 检查调试模式
|
||||||
|
debugMode := false // 生产环境关闭调试
|
||||||
|
|
||||||
|
p := &XysPlugin{
|
||||||
|
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin(PluginName, 3), // 标准网盘插件,启用Service层过滤
|
||||||
|
debugMode: debugMode,
|
||||||
|
cacheTTL: 30 * time.Minute, // token缓存30分钟
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name 插件名称
|
||||||
|
func (p *XysPlugin) Name() string {
|
||||||
|
return PluginName
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayName 插件显示名称
|
||||||
|
func (p *XysPlugin) DisplayName() string {
|
||||||
|
return DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description 插件描述
|
||||||
|
func (p *XysPlugin) Description() string {
|
||||||
|
return Description
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search 搜索接口
|
||||||
|
func (p *XysPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
|
||||||
|
return p.searchImpl(&http.Client{Timeout: 30 * time.Second}, keyword, ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchImpl 搜索实现
|
||||||
|
func (p *XysPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
|
||||||
|
if p.debugMode {
|
||||||
|
log.Printf("[XYS] 开始搜索: %s", keyword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一步:获取token
|
||||||
|
token, err := p.getToken(client, keyword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取token失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.debugMode {
|
||||||
|
log.Printf("[XYS] 获取到token: %s", token[:10]+"...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二步:执行搜索
|
||||||
|
results, err := p.executeSearch(client, token, keyword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("执行搜索失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.debugMode {
|
||||||
|
log.Printf("[XYS] 搜索完成,获取到 %d 个结果", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getToken 获取搜索token
|
||||||
|
func (p *XysPlugin) getToken(client *http.Client, keyword string) (string, error) {
|
||||||
|
// 检查缓存
|
||||||
|
cacheKey := "token"
|
||||||
|
if cached, found := p.tokenCache.Load(cacheKey); found {
|
||||||
|
if tokenCache, ok := cached.(TokenCache); ok {
|
||||||
|
// 检查是否过期
|
||||||
|
if time.Since(tokenCache.Timestamp) < p.cacheTTL {
|
||||||
|
if p.debugMode {
|
||||||
|
log.Printf("[XYS] 使用缓存的token")
|
||||||
|
}
|
||||||
|
return tokenCache.Token, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求URL
|
||||||
|
tokenURL := fmt.Sprintf("%s%s?wd=%s&mode=undefined&stype=undefined",
|
||||||
|
BaseURL, TokenPath, url.QueryEscape(keyword))
|
||||||
|
|
||||||
|
// 创建带超时的上下文
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", tokenURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("[%s] 创建token请求失败: %w", p.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置完整的请求头
|
||||||
|
req.Header.Set("User-Agent", UserAgent)
|
||||||
|
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", BaseURL+"/")
|
||||||
|
|
||||||
|
resp, err := p.doRequestWithRetry(req, client)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("[%s] token请求失败: %w", p.Name(), err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return "", fmt.Errorf("[%s] token请求HTTP状态错误: %d", p.Name(), resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析HTML提取token
|
||||||
|
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("[%s] 解析token页面HTML失败: %w", p.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找script标签中的DToken定义
|
||||||
|
var token string
|
||||||
|
doc.Find("script").Each(func(i int, s *goquery.Selection) {
|
||||||
|
scriptContent := s.Text()
|
||||||
|
if strings.Contains(scriptContent, "DToken") {
|
||||||
|
// 使用正则表达式提取token
|
||||||
|
re := regexp.MustCompile(`const\s+DToken\s*=\s*"([^"]+)"`)
|
||||||
|
matches := re.FindStringSubmatch(scriptContent)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
token = matches[1]
|
||||||
|
if p.debugMode {
|
||||||
|
log.Printf("[XYS] 从script中提取到token: %s", token[:10]+"...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
return "", fmt.Errorf("未找到DToken")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存token
|
||||||
|
p.tokenCache.Store(cacheKey, TokenCache{
|
||||||
|
Token: token,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// doRequestWithRetry 带重试机制的HTTP请求
|
||||||
|
func (p *XysPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*http.Response, error) {
|
||||||
|
maxRetries := 3
|
||||||
|
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("[%s] 重试 %d 次后仍然失败: %w", p.Name(), maxRetries, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeSearch 执行搜索请求
|
||||||
|
func (p *XysPlugin) executeSearch(client *http.Client, token, keyword string) ([]model.SearchResult, error) {
|
||||||
|
// 构建搜索URL
|
||||||
|
searchURL := fmt.Sprintf("%s%s?DToken2=%s&requestID=undefined&mode=90002&stype=undefined&scope_content=0&wd=%s&uk=&page=1&limit=20&screen_filetype=",
|
||||||
|
BaseURL, SearchPath, token, url.QueryEscape(keyword))
|
||||||
|
|
||||||
|
// 创建带超时的上下文
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", searchURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("[%s] 创建搜索请求失败: %w", p.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置完整的请求头
|
||||||
|
req.Header.Set("User-Agent", UserAgent)
|
||||||
|
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||||
|
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||||
|
req.Header.Set("Connection", "keep-alive")
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("Referer", BaseURL+"/")
|
||||||
|
req.Header.Set("Origin", BaseURL)
|
||||||
|
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
||||||
|
|
||||||
|
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] 搜索请求HTTP状态错误: %d", p.Name(), resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取响应体
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("[%s] 读取响应体失败: %w", p.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析JSON响应
|
||||||
|
var searchResp SearchResponse
|
||||||
|
if err := json.Unmarshal(respBody, &searchResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("[%s] JSON解析失败: %w", p.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if searchResp.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("[%s] 搜索API返回错误: %s", p.Name(), searchResp.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.debugMode {
|
||||||
|
log.Printf("[XYS] 搜索API响应成功,data长度: %d", len(searchResp.Data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析HTML内容
|
||||||
|
return p.parseSearchResults(searchResp.Data, keyword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSearchResults 解析搜索结果HTML
|
||||||
|
func (p *XysPlugin) parseSearchResults(htmlData, keyword string) ([]model.SearchResult, error) {
|
||||||
|
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("[%s] 解析搜索结果HTML失败: %w", p.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []model.SearchResult
|
||||||
|
|
||||||
|
// 查找搜索结果项
|
||||||
|
doc.Find(".layui-card[data-qid]").Each(func(i int, s *goquery.Selection) {
|
||||||
|
if len(results) >= MaxResults {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := p.parseResultItem(s, i+1)
|
||||||
|
if result != nil {
|
||||||
|
results = append(results, *result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if p.debugMode {
|
||||||
|
log.Printf("[XYS] 解析到 %d 个原始结果", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关键词过滤(标准网盘插件需要过滤)
|
||||||
|
filteredResults := plugin.FilterResultsByKeyword(results, keyword)
|
||||||
|
|
||||||
|
if p.debugMode {
|
||||||
|
log.Printf("[XYS] 关键词过滤后剩余 %d 个结果", len(filteredResults))
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredResults, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseResultItem 解析单个搜索结果项
|
||||||
|
func (p *XysPlugin) parseResultItem(s *goquery.Selection, index int) *model.SearchResult {
|
||||||
|
// 提取QID
|
||||||
|
qid, _ := s.Attr("data-qid")
|
||||||
|
if qid == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取标题和链接
|
||||||
|
linkEl := s.Find(`a[onclick="open_sid(this)"]`)
|
||||||
|
if linkEl.Length() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取标题
|
||||||
|
title := p.cleanTitle(linkEl.Text())
|
||||||
|
if title == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取链接URL
|
||||||
|
href, _ := linkEl.Attr("href")
|
||||||
|
if href == "" {
|
||||||
|
// 尝试从url属性解码
|
||||||
|
urlAttr, _ := linkEl.Attr("url")
|
||||||
|
if urlAttr != "" {
|
||||||
|
if decoded, err := base64.StdEncoding.DecodeString(urlAttr); err == nil {
|
||||||
|
href = string(decoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if href == "" {
|
||||||
|
if p.debugMode {
|
||||||
|
log.Printf("[XYS] 跳过无链接的结果: %s", title)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取密码
|
||||||
|
password, _ := linkEl.Attr("pa")
|
||||||
|
|
||||||
|
// 提取时间
|
||||||
|
timeStr := strings.TrimSpace(s.Find(".layui-icon-time").Parent().Text())
|
||||||
|
publishTime := p.parseTime(timeStr)
|
||||||
|
|
||||||
|
// 提取网盘类型
|
||||||
|
platform := p.extractPlatform(s, href)
|
||||||
|
|
||||||
|
// 构建链接对象
|
||||||
|
link := model.Link{
|
||||||
|
Type: platform,
|
||||||
|
URL: href,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建结果对象
|
||||||
|
result := model.SearchResult{
|
||||||
|
Title: title,
|
||||||
|
Content: fmt.Sprintf("来源:%s", platform),
|
||||||
|
Channel: "", // 插件搜索结果必须为空字符串(按开发指南要求)
|
||||||
|
MessageID: fmt.Sprintf("%s-%s-%d", p.Name(), qid, index),
|
||||||
|
UniqueID: fmt.Sprintf("%s-%s-%d", p.Name(), qid, index),
|
||||||
|
Datetime: publishTime,
|
||||||
|
Links: []model.Link{link},
|
||||||
|
Tags: []string{platform},
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.debugMode {
|
||||||
|
log.Printf("[XYS] 解析结果: %s (%s)", title, platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanTitle 清理标题
|
||||||
|
func (p *XysPlugin) cleanTitle(title string) string {
|
||||||
|
if title == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除HTML标签
|
||||||
|
re := regexp.MustCompile(`<[^>]*>`)
|
||||||
|
cleaned := re.ReplaceAllString(title, "")
|
||||||
|
|
||||||
|
// 移除@符号
|
||||||
|
cleaned = strings.ReplaceAll(cleaned, "@", "")
|
||||||
|
|
||||||
|
// 清理多余的空格
|
||||||
|
cleaned = strings.TrimSpace(cleaned)
|
||||||
|
re = regexp.MustCompile(`\s+`)
|
||||||
|
cleaned = re.ReplaceAllString(cleaned, " ")
|
||||||
|
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTime 解析时间字符串
|
||||||
|
func (p *XysPlugin) parseTime(timeStr string) time.Time {
|
||||||
|
// 清理时间字符串,移除图标等
|
||||||
|
timeStr = strings.TrimSpace(timeStr)
|
||||||
|
|
||||||
|
// 查找时间格式 YYYY-MM-DD HH:MM:SS
|
||||||
|
re := regexp.MustCompile(`(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})`)
|
||||||
|
matches := re.FindStringSubmatch(timeStr)
|
||||||
|
|
||||||
|
if len(matches) > 1 {
|
||||||
|
if t, err := time.Parse("2006-01-02 15:04:05", matches[1]); err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果解析失败,返回当前时间
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractPlatform 提取网盘平台类型(按开发指南标准实现)
|
||||||
|
func (p *XysPlugin) extractPlatform(s *goquery.Selection, href string) string {
|
||||||
|
return determineCloudType(href)
|
||||||
|
}
|
||||||
|
|
||||||
|
// determineCloudType 根据URL自动识别网盘类型(按开发指南完整列表)
|
||||||
|
func determineCloudType(url string) string {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(url, "pan.quark.cn"):
|
||||||
|
return "quark"
|
||||||
|
case strings.Contains(url, "drive.uc.cn"):
|
||||||
|
return "uc"
|
||||||
|
case strings.Contains(url, "pan.baidu.com"):
|
||||||
|
return "baidu"
|
||||||
|
case strings.Contains(url, "aliyundrive.com") || strings.Contains(url, "alipan.com"):
|
||||||
|
return "aliyun"
|
||||||
|
case strings.Contains(url, "pan.xunlei.com"):
|
||||||
|
return "xunlei"
|
||||||
|
case strings.Contains(url, "cloud.189.cn"):
|
||||||
|
return "tianyi"
|
||||||
|
case strings.Contains(url, "caiyun.139.com"):
|
||||||
|
return "mobile"
|
||||||
|
case strings.Contains(url, "magnet:"):
|
||||||
|
return "magnet"
|
||||||
|
case strings.Contains(url, "ed2k://"):
|
||||||
|
return "ed2k"
|
||||||
|
default:
|
||||||
|
return "others"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user