Files
pansou/plugin/qupansou/qupansou.go

340 lines
8.3 KiB
Go
Raw Normal View History

2025-07-12 19:53:44 +08:00
package qupansou
import (
"bytes"
"fmt"
"io"
"net/http"
"strings"
2025-07-15 00:03:02 +08:00
"sync"
2025-07-12 19:53:44 +08:00
"time"
"pansou/model"
"pansou/plugin"
2025-07-15 00:03:02 +08:00
"pansou/util/json"
)
// 缓存相关变量
var (
// API响应缓存键为关键词值为缓存的响应
apiResponseCache = sync.Map{}
// 最后一次清理缓存的时间
lastCacheCleanTime = time.Now()
// 缓存有效期1小时
cacheTTL = 1 * time.Hour
2025-07-12 19:53:44 +08:00
)
// 在init函数中注册插件
func init() {
// 使用全局超时时间创建插件实例并注册
plugin.RegisterGlobalPlugin(NewQuPanSouPlugin())
2025-07-15 00:03:02 +08:00
// 启动缓存清理goroutine
go startCacheCleaner()
}
// startCacheCleaner 启动一个定期清理缓存的goroutine
func startCacheCleaner() {
// 每小时清理一次缓存
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
// 清空所有缓存
apiResponseCache = sync.Map{}
lastCacheCleanTime = time.Now()
}
2025-07-12 19:53:44 +08:00
}
const (
// API端点
ApiURL = "https://v.funletu.com/search"
// 默认超时时间
DefaultTimeout = 6 * time.Second
// 默认页大小
DefaultPageSize = 1000
)
// QuPanSouPlugin 趣盘搜插件
type QuPanSouPlugin struct {
client *http.Client
timeout time.Duration
}
// NewQuPanSouPlugin 创建新的趣盘搜插件
func NewQuPanSouPlugin() *QuPanSouPlugin {
timeout := DefaultTimeout
return &QuPanSouPlugin{
client: &http.Client{
Timeout: timeout,
},
timeout: timeout,
}
}
// Name 返回插件名称
func (p *QuPanSouPlugin) Name() string {
return "qupansou"
}
// Priority 返回插件优先级
func (p *QuPanSouPlugin) Priority() int {
return 2 // 较高优先级
}
// Search 执行搜索并返回结果
func (p *QuPanSouPlugin) Search(keyword string) ([]model.SearchResult, error) {
2025-07-15 00:03:02 +08:00
// 生成缓存键
cacheKey := keyword
// 检查缓存中是否已有结果
if cachedItems, ok := apiResponseCache.Load(cacheKey); ok {
// 检查缓存是否过期
cachedResult := cachedItems.(cachedResponse)
if time.Since(cachedResult.timestamp) < cacheTTL {
return cachedResult.results, nil
}
}
2025-07-12 19:53:44 +08:00
// 发送API请求
items, err := p.searchAPI(keyword)
if err != nil {
return nil, fmt.Errorf("qupansou API error: %w", err)
}
// 转换为标准格式
results := p.convertResults(items)
2025-07-15 00:03:02 +08:00
// 缓存结果
apiResponseCache.Store(cacheKey, cachedResponse{
results: results,
timestamp: time.Now(),
})
2025-07-12 19:53:44 +08:00
return results, nil
}
2025-07-15 00:03:02 +08:00
// 缓存响应结构
type cachedResponse struct {
results []model.SearchResult
timestamp time.Time
}
2025-07-12 19:53:44 +08:00
// searchAPI 向API发送请求
func (p *QuPanSouPlugin) searchAPI(keyword string) ([]QuPanSouItem, error) {
// 构建请求体
reqBody := map[string]interface{}{
"style": "get",
"datasrc": "search",
"query": map[string]interface{}{
"id": "",
"datetime": "",
"courseid": 1,
"categoryid": "",
"filetypeid": "",
"filetype": "",
"reportid": "",
"validid": "",
"searchtext": keyword,
},
"page": map[string]interface{}{
"pageSize": DefaultPageSize,
"pageIndex": 1,
},
"order": map[string]interface{}{
"prop": "sort",
"order": "desc",
},
"message": "请求资源列表数据",
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("marshal request failed: %w", err)
}
req, err := http.NewRequest("POST", ApiURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("create request failed: %w", err)
}
req.Header.Set("Content-Type", "application/json")
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("Referer", "https://pan.funletu.com/")
// 发送请求
resp, err := p.client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
// 读取响应体
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response body failed: %w", err)
}
// 解析响应
var apiResp QuPanSouResponse
if err := json.Unmarshal(respBody, &apiResp); err != nil {
return nil, fmt.Errorf("decode response failed: %w", err)
}
// 检查响应状态
if apiResp.Status != 200 {
return nil, fmt.Errorf("API returned error: %s", apiResp.Message)
}
return apiResp.Data, nil
}
// convertResults 将API响应转换为标准SearchResult格式
func (p *QuPanSouPlugin) convertResults(items []QuPanSouItem) []model.SearchResult {
results := make([]model.SearchResult, 0, len(items))
for _, item := range items {
// 跳过无效的URL
if item.URL == "" {
continue
}
// 创建链接
link := model.Link{
URL: item.URL,
Type: p.determineLinkType(item.URL),
Password: "", // 趣盘搜API不返回密码
}
// 创建唯一ID
uniqueID := fmt.Sprintf("qupansou-%d", item.ID)
// 解析时间
var datetime time.Time
if item.UpdateTime != "" {
// 尝试解析时间格式2025-07-05 00:31:38
parsedTime, err := time.Parse("2006-01-02 15:04:05", item.UpdateTime)
if err == nil {
datetime = parsedTime
}
}
// 如果时间解析失败,使用零值
if datetime.IsZero() {
datetime = time.Time{}
}
// 清理标题中的HTML标签
title := cleanHTML(item.Title)
// 创建搜索结果
result := model.SearchResult{
UniqueID: uniqueID,
Title: title,
Content: fmt.Sprintf("类别: %s, 文件类型: %s, 大小: %s", item.Category, item.FileType, item.Size),
Datetime: datetime,
Links: []model.Link{link},
}
results = append(results, result)
}
return results
}
// determineLinkType 根据URL确定链接类型
func (p *QuPanSouPlugin) determineLinkType(url string) string {
lowerURL := strings.ToLower(url)
2025-07-15 00:03:02 +08:00
if strings.Contains(lowerURL, "pan.baidu.com") {
2025-07-12 19:53:44 +08:00
return "baidu"
2025-07-15 00:03:02 +08:00
} else if strings.Contains(lowerURL, "aliyundrive.com") || strings.Contains(lowerURL, "alipan.com") {
2025-07-12 19:53:44 +08:00
return "aliyun"
2025-07-15 00:03:02 +08:00
} else if strings.Contains(lowerURL, "pan.quark.cn") {
return "quark"
} else if strings.Contains(lowerURL, "cloud.189.cn") {
2025-07-12 19:53:44 +08:00
return "tianyi"
2025-07-15 00:03:02 +08:00
} else if strings.Contains(lowerURL, "pan.xunlei.com") {
return "xunlei"
} else if strings.Contains(lowerURL, "caiyun.139.com") || strings.Contains(lowerURL, "www.caiyun.139.com") {
2025-07-12 19:53:44 +08:00
return "mobile"
2025-07-15 00:03:02 +08:00
} else if strings.Contains(lowerURL, "115.com") {
2025-07-12 19:53:44 +08:00
return "115"
2025-07-15 00:03:02 +08:00
} else if strings.Contains(lowerURL, "drive.uc.cn") {
return "uc"
} else if strings.Contains(lowerURL, "pan.123.com") || strings.Contains(lowerURL, "123pan.com") {
2025-07-12 19:53:44 +08:00
return "123"
2025-07-15 00:03:02 +08:00
} else if strings.Contains(lowerURL, "mypikpak.com") {
return "pikpak"
} else if strings.Contains(lowerURL, "lanzou") {
return "lanzou"
} else {
2025-07-12 19:53:44 +08:00
return "others"
}
}
// cleanHTML 清理HTML标签
func cleanHTML(html string) string {
2025-07-15 00:03:02 +08:00
// 一次性替换所有常见HTML标签
2025-07-12 19:53:44 +08:00
replacements := map[string]string{
"<em>": "",
"</em>": "",
"<b>": "",
"</b>": "",
"<strong>": "",
"</strong>": "",
2025-07-15 00:03:02 +08:00
"<i>": "",
"</i>": "",
2025-07-12 19:53:44 +08:00
}
result := html
for tag, replacement := range replacements {
result = strings.Replace(result, tag, replacement, -1)
}
2025-07-15 00:03:02 +08:00
// 移除多余的空格
2025-07-12 19:53:44 +08:00
return strings.TrimSpace(result)
}
// QuPanSouResponse API响应结构
type QuPanSouResponse struct {
Text string `json:"text"`
Data []QuPanSouItem `json:"data"`
Total int `json:"total"`
Status int `json:"status"`
Message string `json:"message"`
}
// QuPanSouItem API响应中的单个结果项
type QuPanSouItem struct {
ID int `json:"id"`
Title string `json:"title"`
Filename string `json:"filename"`
URL string `json:"url"`
Link string `json:"link"`
SearchText string `json:"searchtext"`
ExtCode string `json:"extcode"`
UnzipCode string `json:"unzipcode"`
Size string `json:"size"`
CategoryID int `json:"categoryid"`
Category string `json:"category"`
CourseID int `json:"courseid"`
Course string `json:"course"`
FileTypeID int `json:"filetypeid"`
FileType string `json:"filetype"`
UpdateTime string `json:"updatetime"`
CreateTime string `json:"createtime"`
Views int `json:"views"`
ViewsHistory int `json:"viewshistory"`
Diff int `json:"diff"`
Violate int `json:"violate"`
State int `json:"state"`
Sort int `json:"sort"`
Top int `json:"top"`
Valid int `json:"valid"`
}