diff --git a/main.go b/main.go index 9801d32..ee5ffaf 100644 --- a/main.go +++ b/main.go @@ -93,6 +93,7 @@ import ( _ "pansou/plugin/ypfxw" _ "pansou/plugin/mikuclub" _ "pansou/plugin/daishudj" + _ "pansou/plugin/dyyj" ) // 全局缓存写入管理器 diff --git a/plugin/dyyj/dyyj.go b/plugin/dyyj/dyyj.go new file mode 100644 index 0000000..456f089 --- /dev/null +++ b/plugin/dyyj/dyyj.go @@ -0,0 +1,1176 @@ +package dyyj + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "regexp" + "strings" + "sync" + "time" + + "github.com/PuerkitoBio/goquery" + "pansou/model" + "pansou/plugin" +) + +const ( + PluginName = "dyyj" + DisplayName = "电影云集" + Description = "电影云集 - 影视资源网盘链接搜索" + BaseURL = "https://bbs.dyyjmax.org" + SearchPath = "/?q=%s" + 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 = 100 + MaxConcurrency = 100 + RequestTimeout = 30 * time.Second + + // HTTP连接池配置(性能优化) + MaxIdleConns = 100 // 最大空闲连接数 + MaxIdleConnsPerHost = 100 // 每个主机的最大空闲连接数 + MaxConnsPerHost = 100 // 每个主机的最大连接数 + IdleConnTimeout = 90 * time.Second // 空闲连接超时 + TLSHandshakeTimeout = 10 * time.Second // TLS握手超时 + ExpectContinueTimeout = 1 * time.Second // Expect: 100-continue超时 +) + +// 预编译的正则表达式(性能优化:避免重复编译) +var ( + // 提取文章ID的正则 + postIDRegex = regexp.MustCompile(`/d/(\d+)`) + + // 提取noscript标签的正则 + noscriptRegex = regexp.MustCompile(``) + + // 提取li标签内链接的正则 + liLinkRegex = regexp.MustCompile(`
夸克[^<]*
\s*]*href\s*=\s*["']([^"']+)["'][^>]*>`), "quark"},
+ {"百度网盘", regexp.MustCompile(` 百度[^<]* ]*href\s*=\s*["']([^"']+)["'][^>]*>`), "baidu"},
+ {"阿里云盘", regexp.MustCompile(` 阿里[^<]* ]*href\s*=\s*["']([^"']+)["'][^>]*>`), "aliyun"},
+ {"天翼云盘", regexp.MustCompile(` 天翼[^<]* ]*href\s*=\s*["']([^"']+)["'][^>]*>`), "tianyi"},
+ {"迅雷网盘", regexp.MustCompile(` 迅雷[^<]* ]*href\s*=\s*["']([^"']+)["'][^>]*>`), "xunlei"},
+ {"通用网盘", regexp.MustCompile(`]*href\s*=\s*["'](https?://[^"']*(?:pan|drive|cloud)[^"']*)["'][^>]*>`), "others"},
+ }
+)
+
+// DyyjPlugin 电影云集插件
+type DyyjPlugin struct {
+ *plugin.BaseAsyncPlugin
+ debugMode bool
+ detailCache sync.Map // 缓存详情页结果
+ cacheTTL time.Duration
+ optimizedClient *http.Client // 优化的HTTP客户端(连接池)
+}
+
+// init 注册插件
+func init() {
+ plugin.RegisterGlobalPlugin(NewDyyjPlugin())
+}
+
+// NewDyyjPlugin 创建新的电影云集插件实例
+func NewDyyjPlugin() *DyyjPlugin {
+ debugMode := false // 生产环境关闭调试
+
+ p := &DyyjPlugin{
+ BaseAsyncPlugin: plugin.NewBaseAsyncPlugin(PluginName, 2), // 质量良好,优先级2
+ debugMode: debugMode,
+ cacheTTL: 30 * time.Minute, // 详情页缓存30分钟
+ optimizedClient: createOptimizedHTTPClient(), // 创建优化的HTTP客户端
+ }
+
+ return p
+}
+
+// createOptimizedHTTPClient 创建优化的HTTP客户端(连接池配置)
+func createOptimizedHTTPClient() *http.Client {
+ transport := &http.Transport{
+ MaxIdleConns: MaxIdleConns,
+ MaxIdleConnsPerHost: MaxIdleConnsPerHost,
+ MaxConnsPerHost: MaxConnsPerHost,
+ IdleConnTimeout: IdleConnTimeout,
+ TLSHandshakeTimeout: TLSHandshakeTimeout,
+ ExpectContinueTimeout: ExpectContinueTimeout,
+ ForceAttemptHTTP2: true, // 启用HTTP/2支持
+ DisableKeepAlives: false, // 启用Keep-Alive连接复用
+ }
+
+ return &http.Client{
+ Transport: transport,
+ Timeout: RequestTimeout,
+ }
+}
+
+// Name 插件名称
+func (p *DyyjPlugin) Name() string {
+ return PluginName
+}
+
+// DisplayName 插件显示名称
+func (p *DyyjPlugin) DisplayName() string {
+ return DisplayName
+}
+
+// Description 插件描述
+func (p *DyyjPlugin) Description() string {
+ return Description
+}
+
+// Search 搜索接口
+func (p *DyyjPlugin) 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 *DyyjPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {
+ return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)
+}
+
+// searchImpl 搜索实现
+func (p *DyyjPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
+ if p.debugMode {
+ log.Printf("[DYYJ] 开始搜索: %s", keyword)
+ }
+
+ // 第一步:执行搜索获取结果列表
+ // 使用优化的客户端(连接池)而不是传入的client
+ searchResults, err := p.executeSearch(p.optimizedClient, keyword)
+ if err != nil {
+ if p.debugMode {
+ log.Printf("[DYYJ] 执行搜索失败: %v", err)
+ }
+ return nil, fmt.Errorf("[%s] 执行搜索失败: %w", p.Name(), err)
+ }
+
+ if p.debugMode {
+ log.Printf("[DYYJ] 搜索获取到 %d 个结果", len(searchResults))
+ }
+
+ // 第二步:先对标题进行关键词过滤,只处理包含关键词的结果(避免不必要的详情页请求)
+ titleFilteredResults := p.filterByTitleKeyword(searchResults, keyword)
+ if p.debugMode {
+ log.Printf("[DYYJ] 标题关键词过滤后剩余 %d 个结果(将只对这些结果获取详情页)", len(titleFilteredResults))
+ }
+
+ // 第三步:并发获取详情页链接(只对标题包含关键词的结果)
+ // 使用优化的客户端(连接池)而不是传入的client
+ finalResults := p.fetchDetailLinks(p.optimizedClient, titleFilteredResults, keyword)
+
+ if p.debugMode {
+ log.Printf("[DYYJ] 最终获取到 %d 个有效结果", len(finalResults))
+ }
+
+ // 第四步:最终关键词过滤(对标题和内容都进行过滤,标准网盘插件需要过滤)
+ filteredResults := plugin.FilterResultsByKeyword(finalResults, keyword)
+
+ if p.debugMode {
+ log.Printf("[DYYJ] 最终关键词过滤后剩余 %d 个结果", len(filteredResults))
+ }
+
+ return filteredResults, nil
+}
+
+// executeSearch 执行搜索请求
+func (p *DyyjPlugin) executeSearch(client *http.Client, keyword string) ([]model.SearchResult, error) {
+ // 构建搜索URL
+ searchURL := fmt.Sprintf("%s%s", BaseURL, fmt.Sprintf(SearchPath, url.QueryEscape(keyword)))
+
+ if p.debugMode {
+ log.Printf("[DYYJ] 搜索URL: %s", searchURL)
+ }
+
+ // 创建带超时的上下文
+ ctx, cancel := context.WithTimeout(context.Background(), RequestTimeout)
+ defer cancel()
+
+ req, err := http.NewRequestWithContext(ctx, "GET", searchURL, nil)
+ if err != nil {
+ if p.debugMode {
+ log.Printf("[DYYJ] 创建搜索请求失败: %v", err)
+ }
+ return nil, fmt.Errorf("[%s] 创建搜索请求失败: %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 {
+ if p.debugMode {
+ log.Printf("[DYYJ] 搜索请求失败: %v", err)
+ }
+ return nil, fmt.Errorf("[%s] 搜索请求失败: %w", p.Name(), err)
+ }
+ defer resp.Body.Close()
+
+ if p.debugMode {
+ log.Printf("[DYYJ] 搜索请求响应状态码: %d", resp.StatusCode)
+ }
+
+ if resp.StatusCode != 200 {
+ if p.debugMode {
+ log.Printf("[DYYJ] 搜索请求HTTP状态错误: %d", resp.StatusCode)
+ }
+ return nil, fmt.Errorf("[%s] 搜索请求HTTP状态错误: %d", p.Name(), resp.StatusCode)
+ }
+
+ // 读取响应体用于调试
+ bodyBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ if p.debugMode {
+ log.Printf("[DYYJ] 读取响应体失败: %v", err)
+ }
+ return nil, fmt.Errorf("[%s] 读取响应体失败: %w", p.Name(), err)
+ }
+
+ bodyString := string(bodyBytes)
+ if p.debugMode {
+ log.Printf("[DYYJ] 响应体大小: %d 字节", len(bodyString))
+
+ // 保存完整HTML到文件用于分析
+ filename := fmt.Sprintf("./dyyj_search_%s_%d.html", url.QueryEscape(keyword), time.Now().Unix())
+ if err := os.WriteFile(filename, bodyBytes, 0644); err == nil {
+ log.Printf("[DYYJ] 完整HTML已保存到: %s", filename)
+ } else {
+ log.Printf("[DYYJ] 保存HTML文件失败: %v", err)
+ }
+
+ // 输出HTML的前2000个字符用于调试
+ previewLen := 2000
+ if len(bodyString) < previewLen {
+ previewLen = len(bodyString)
+ }
+ log.Printf("[DYYJ] HTML内容预览(前%d字符):\n%s", previewLen, bodyString[:previewLen])
+
+ // 检查关键元素是否存在
+ hasNoscript := strings.Contains(bodyString, "