Files
pansou/plugin/u3c3/u3c3.go

422 lines
10 KiB
Go
Raw Normal View History

2025-08-25 18:56:33 +08:00
package u3c3
import (
"fmt"
"io"
"log"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"pansou/model"
"pansou/plugin"
)
const (
BaseURL = "https://u3c3u3c3.u3c3u3c3u3c3.com"
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
MaxRetries = 3
RetryDelay = 2 * time.Second
)
// U3c3Plugin U3C3插件
type U3c3Plugin struct {
*plugin.BaseAsyncPlugin
debugMode bool
search2 string // 缓存的search2参数
lastSync time.Time
}
func init() {
p := &U3c3Plugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPluginWithFilter("u3c3", 5, true),
debugMode: false,
}
plugin.RegisterGlobalPlugin(p)
}
// Search 搜索接口实现
func (p *U3c3Plugin) 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 搜索并返回详细结果
func (p *U3c3Plugin) SearchWithResult(keyword string, ext map[string]interface{}) (*model.PluginSearchResult, error) {
if p.debugMode {
log.Printf("[U3C3] 开始搜索: %s", keyword)
}
// 第一步获取search2参数
search2, err := p.getSearch2Parameter()
if err != nil {
if p.debugMode {
log.Printf("[U3C3] 获取search2参数失败: %v", err)
}
return nil, fmt.Errorf("获取search2参数失败: %v", err)
}
// 第二步:执行搜索
results, err := p.doSearch(keyword, search2)
if err != nil {
if p.debugMode {
log.Printf("[U3C3] 搜索失败: %v", err)
}
return nil, err
}
if p.debugMode {
log.Printf("[U3C3] 搜索完成,获得 %d 个结果", len(results))
}
// 应用关键词过滤
filteredResults := plugin.FilterResultsByKeyword(results, keyword)
return &model.PluginSearchResult{
Results: filteredResults,
IsFinal: true,
Timestamp: time.Now(),
Source: p.Name(),
Message: fmt.Sprintf("找到 %d 个结果", len(filteredResults)),
}, nil
}
// getSearch2Parameter 获取search2参数
func (p *U3c3Plugin) getSearch2Parameter() (string, error) {
// 如果缓存有效1小时内直接返回
if p.search2 != "" && time.Since(p.lastSync) < time.Hour {
return p.search2, nil
}
if p.debugMode {
log.Printf("[U3C3] 正在获取search2参数...")
}
client := &http.Client{
Timeout: 30 * time.Second,
}
req, err := http.NewRequest("GET", BaseURL, nil)
if err != nil {
return "", 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")
var resp *http.Response
var lastErr error
// 重试机制
for i := 0; i < MaxRetries; i++ {
resp, lastErr = client.Do(req)
if lastErr == nil && resp.StatusCode == 200 {
break
}
if resp != nil {
resp.Body.Close()
}
if i < MaxRetries-1 {
time.Sleep(RetryDelay)
}
}
if lastErr != nil {
return "", lastErr
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("HTTP状态码错误: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
// 从JavaScript中提取search2参数
search2 := p.extractSearch2FromHTML(string(body))
if search2 == "" {
return "", fmt.Errorf("无法从首页提取search2参数")
}
// 缓存参数
p.search2 = search2
p.lastSync = time.Now()
if p.debugMode {
log.Printf("[U3C3] 获取到search2参数: %s", search2)
}
return search2, nil
}
// extractSearch2FromHTML 从HTML中提取search2参数
func (p *U3c3Plugin) extractSearch2FromHTML(html string) string {
// 按行处理,排除注释行
lines := strings.Split(html, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
// 跳过注释行
if strings.HasPrefix(line, "//") {
continue
}
// 查找包含nmefafej的行
if strings.Contains(line, "nmefafej") && strings.Contains(line, `"`) {
// 使用正则提取引号内的值
re := regexp.MustCompile(`var\s+nmefafej\s*=\s*"([^"]+)"`)
matches := re.FindStringSubmatch(line)
if len(matches) > 1 && len(matches[1]) > 5 {
if p.debugMode {
log.Printf("[U3C3] 提取到search2参数: %s (来自行: %s)", matches[1], line)
}
return matches[1]
}
// 备用方案:直接提取引号内容
start := strings.Index(line, `"`)
if start != -1 {
end := strings.Index(line[start+1:], `"`)
if end != -1 && end > 5 {
candidate := line[start+1 : start+1+end]
if len(candidate) > 5 {
if p.debugMode {
log.Printf("[U3C3] 备用方案提取search2: %s (来自行: %s)", candidate, line)
}
return candidate
}
}
}
}
}
if p.debugMode {
log.Printf("[U3C3] 未能找到search2参数")
}
return ""
}
// doSearch 执行搜索
func (p *U3c3Plugin) doSearch(keyword, search2 string) ([]model.SearchResult, error) {
// 构建搜索URL
encodedKeyword := url.QueryEscape(keyword)
searchURL := fmt.Sprintf("%s/?search2=%s&search=%s", BaseURL, search2, encodedKeyword)
if p.debugMode {
log.Printf("[U3C3] 搜索URL: %s", searchURL)
}
client := &http.Client{
Timeout: 30 * time.Second,
}
req, err := http.NewRequest("GET", searchURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", UserAgent)
req.Header.Set("Referer", BaseURL+"/")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
var resp *http.Response
var lastErr error
// 重试机制
for i := 0; i < MaxRetries; i++ {
resp, lastErr = client.Do(req)
if lastErr == nil && resp.StatusCode == 200 {
break
}
if resp != nil {
resp.Body.Close()
}
if i < MaxRetries-1 {
time.Sleep(RetryDelay)
}
}
if lastErr != nil {
return nil, lastErr
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("搜索请求失败,状态码: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return p.parseSearchResults(string(body))
}
// parseSearchResults 解析搜索结果
func (p *U3c3Plugin) parseSearchResults(html string) ([]model.SearchResult, error) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
return nil, err
}
var results []model.SearchResult
// 查找搜索结果表格行
doc.Find("tbody tr.default").Each(func(i int, s *goquery.Selection) {
// 跳过广告行(通常包含置顶标识)
titleCell := s.Find("td:nth-child(2)")
titleText := titleCell.Text()
if strings.Contains(titleText, "[置顶]") {
return // 跳过置顶广告
}
// 提取标题和详情链接
titleLink := titleCell.Find("a")
title := strings.TrimSpace(titleLink.Text())
if title == "" {
return // 跳过空标题
}
// 清理标题中的HTML标签和特殊字符
title = p.cleanTitle(title)
// 提取详情页链接(可选,用于后续扩展)
detailURL, _ := titleLink.Attr("href")
if detailURL != "" && !strings.HasPrefix(detailURL, "http") {
detailURL = BaseURL + detailURL
}
// 提取链接信息
linkCell := s.Find("td:nth-child(3)")
var links []model.Link
// 磁力链接
linkCell.Find("a[href^='magnet:']").Each(func(j int, link *goquery.Selection) {
href, exists := link.Attr("href")
if exists && href != "" {
links = append(links, model.Link{
URL: href,
Type: "magnet",
})
}
})
// 种子文件链接
linkCell.Find("a[href$='.torrent']").Each(func(j int, link *goquery.Selection) {
href, exists := link.Attr("href")
if exists && href != "" {
if !strings.HasPrefix(href, "http") {
href = BaseURL + href
}
links = append(links, model.Link{
URL: href,
Type: "torrent",
})
}
})
// 提取文件大小
sizeText := strings.TrimSpace(s.Find("td:nth-child(4)").Text())
// 提取上传时间
dateText := strings.TrimSpace(s.Find("td:nth-child(5)").Text())
// 提取分类
categoryText := s.Find("td:nth-child(1) a").AttrOr("title", "")
// 构建内容信息
var contentParts []string
if categoryText != "" {
contentParts = append(contentParts, "分类: "+categoryText)
}
if sizeText != "" {
contentParts = append(contentParts, "大小: "+sizeText)
}
if dateText != "" {
contentParts = append(contentParts, "时间: "+dateText)
}
content := strings.Join(contentParts, " | ")
// 生成唯一ID
uniqueID := p.generateUniqueID(title, sizeText)
result := model.SearchResult{
Title: title,
Content: content,
Channel: "", // 插件搜索结果必须为空
Tags: []string{"种子", "磁力链接"},
Datetime: p.parseDateTime(dateText),
Links: links,
UniqueID: uniqueID,
}
results = append(results, result)
})
if p.debugMode {
log.Printf("[U3C3] 解析到 %d 个搜索结果", len(results))
}
return results, nil
}
// cleanTitle 清理标题文本
func (p *U3c3Plugin) cleanTitle(title string) string {
// 移除HTML标签
title = regexp.MustCompile(`<[^>]*>`).ReplaceAllString(title, "")
// 移除多余的空白字符
title = regexp.MustCompile(`\s+`).ReplaceAllString(title, " ")
// 移除前后空白
title = strings.TrimSpace(title)
return title
}
// parseDateTime 解析日期时间
func (p *U3c3Plugin) parseDateTime(dateStr string) time.Time {
if dateStr == "" {
return time.Time{}
}
// 尝试解析常见的日期格式
formats := []string{
"2006-01-02 15:04:05",
"2006-01-02",
"01-02 15:04",
}
for _, format := range formats {
if t, err := time.Parse(format, dateStr); err == nil {
return t
}
}
// 如果解析失败,返回零值
return time.Time{}
}
// generateUniqueID 生成唯一ID
func (p *U3c3Plugin) generateUniqueID(title, size string) string {
// 使用插件名、标题和大小生成唯一ID
source := fmt.Sprintf("%s-%s-%s", p.Name(), title, size)
// 简单的哈希处理(实际项目中可使用更复杂的哈希算法)
hash := 0
for _, char := range source {
hash = hash*31 + int(char)
}
if hash < 0 {
hash = -hash
}
return fmt.Sprintf("u3c3-%d", hash)
}