mirror of
https://github.com/fish2018/pansou.git
synced 2025-11-25 03:14:59 +08:00
新增插件libvio
This commit is contained in:
59
main.go
59
main.go
@@ -24,35 +24,36 @@ import (
|
||||
"pansou/util/cache"
|
||||
|
||||
// 以下是插件的空导入,用于触发各插件的init函数,实现自动注册
|
||||
// 添加新插件时,只需在此处添加对应的导入语句即可
|
||||
_ "pansou/plugin/hdr4k"
|
||||
_ "pansou/plugin/hunhepan"
|
||||
_ "pansou/plugin/jikepan"
|
||||
_ "pansou/plugin/panwiki"
|
||||
_ "pansou/plugin/pansearch"
|
||||
_ "pansou/plugin/panta"
|
||||
_ "pansou/plugin/qupansou"
|
||||
_ "pansou/plugin/susu"
|
||||
_ "pansou/plugin/thepiratebay"
|
||||
_ "pansou/plugin/wanou"
|
||||
_ "pansou/plugin/xuexizhinan"
|
||||
_ "pansou/plugin/panyq"
|
||||
_ "pansou/plugin/zhizhen"
|
||||
_ "pansou/plugin/labi"
|
||||
_ "pansou/plugin/muou"
|
||||
_ "pansou/plugin/ouge"
|
||||
_ "pansou/plugin/shandian"
|
||||
_ "pansou/plugin/duoduo"
|
||||
_ "pansou/plugin/huban"
|
||||
_ "pansou/plugin/cyg"
|
||||
_ "pansou/plugin/erxiao"
|
||||
_ "pansou/plugin/miaoso"
|
||||
_ "pansou/plugin/fox4k"
|
||||
_ "pansou/plugin/pianku"
|
||||
_ "pansou/plugin/clmao"
|
||||
_ "pansou/plugin/wuji"
|
||||
_ "pansou/plugin/cldi"
|
||||
_ "pansou/plugin/xiaozhang"
|
||||
// // 添加新插件时,只需在此处添加对应的导入语句即可
|
||||
// _ "pansou/plugin/hdr4k"
|
||||
// _ "pansou/plugin/hunhepan"
|
||||
// _ "pansou/plugin/jikepan"
|
||||
// _ "pansou/plugin/panwiki"
|
||||
// _ "pansou/plugin/pansearch"
|
||||
// _ "pansou/plugin/panta"
|
||||
// _ "pansou/plugin/qupansou"
|
||||
// _ "pansou/plugin/susu"
|
||||
// _ "pansou/plugin/thepiratebay"
|
||||
// _ "pansou/plugin/wanou"
|
||||
// _ "pansou/plugin/xuexizhinan"
|
||||
// _ "pansou/plugin/panyq"
|
||||
// _ "pansou/plugin/zhizhen"
|
||||
// _ "pansou/plugin/labi"
|
||||
// _ "pansou/plugin/muou"
|
||||
// _ "pansou/plugin/ouge"
|
||||
// _ "pansou/plugin/shandian"
|
||||
// _ "pansou/plugin/duoduo"
|
||||
// _ "pansou/plugin/huban"
|
||||
// _ "pansou/plugin/cyg"
|
||||
// _ "pansou/plugin/erxiao"
|
||||
// _ "pansou/plugin/miaoso"
|
||||
// _ "pansou/plugin/fox4k"
|
||||
// _ "pansou/plugin/pianku"
|
||||
// _ "pansou/plugin/clmao"
|
||||
// _ "pansou/plugin/wuji"
|
||||
// _ "pansou/plugin/cldi"
|
||||
// _ "pansou/plugin/xiaozhang"
|
||||
_ "pansou/plugin/libvio"
|
||||
)
|
||||
|
||||
// 全局缓存写入管理器
|
||||
|
||||
190
plugin/libvio/html结构分析.md
Normal file
190
plugin/libvio/html结构分析.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# libvio插件HTML结构分析
|
||||
|
||||
## 网站信息
|
||||
- 网站名称:LIBVIO
|
||||
- 主域名:https://www.libvio.mov
|
||||
- 网站类型:影视资源在线播放/下载网站
|
||||
- 特点:提供网盘下载链接(夸克、UC等)
|
||||
|
||||
## 访问流程说明
|
||||
该网站需要通过三步来获取网盘链接:
|
||||
1. **搜索页面** → 获取详情页链接
|
||||
2. **详情页面** → 获取网盘下载页链接(注意选择"下载"而非"播放")
|
||||
3. **播放页面** → 从JavaScript对象中提取网盘URL
|
||||
|
||||
## 1. 搜索页面结构
|
||||
|
||||
### 搜索URL格式
|
||||
```
|
||||
https://www.libvio.mov/search/-------------.html?wd={关键词}&submit=
|
||||
```
|
||||
|
||||
### 搜索结果容器
|
||||
- 主容器:`<ul class="stui-vodlist clearfix">`
|
||||
- 单个结果项:`<li class="col-md-6 col-sm-4 col-xs-3">`
|
||||
|
||||
### 搜索结果项结构
|
||||
```html
|
||||
<li class="col-md-6 col-sm-4 col-xs-3">
|
||||
<div class="stui-vodlist__box">
|
||||
<a class="stui-vodlist__thumb lazyload" href="/detail/4095.html" title="瑞克和莫蒂 第五季"
|
||||
data-original="https://xxx.jpg">
|
||||
<span class="play hidden-xs"></span>
|
||||
<span class="pic-text text-right">10集全</span>
|
||||
<span class="pic-tag pic-tag-top">9.6</span>
|
||||
</a>
|
||||
<div class="stui-vodlist__detail">
|
||||
<h4 class="title text-overflow">
|
||||
<a href="/detail/4095.html" title="瑞克和莫蒂 第五季">瑞克和莫蒂 第五季</a>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
```
|
||||
|
||||
### 字段提取要点
|
||||
- **标题**:`.stui-vodlist__detail h4 a` 的文本内容或 `title` 属性
|
||||
- **详情页链接**:`.stui-vodlist__thumb` 或 `.stui-vodlist__detail h4 a` 的 `href` 属性
|
||||
- **封面图片**:`.stui-vodlist__thumb` 的 `data-original` 属性
|
||||
- **集数信息**:`.pic-text` 的文本内容
|
||||
- **评分**:`.pic-tag` 的文本内容
|
||||
|
||||
## 2. 详情页面结构
|
||||
|
||||
### 详情页URL格式
|
||||
```
|
||||
https://www.libvio.mov/detail/{id}.html
|
||||
```
|
||||
|
||||
### 下载链接容器
|
||||
详情页包含多个播放/下载源,我们需要查找带有"下载"关键字的源:
|
||||
|
||||
```html
|
||||
<div class="stui-vodlist__head">
|
||||
<div class="stui-pannel__head clearfix">
|
||||
<span class="more text-muted pull-right"></span>
|
||||
<h3 class="iconfont icon-iconfontplay2">视频下载(UC) </h3>
|
||||
</div>
|
||||
<ul class="stui-content__playlist clearfix">
|
||||
<li>
|
||||
<a href="/play/714892571-2-1.html">合集</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="stui-vodlist__head">
|
||||
<div class="stui-pannel__head clearfix">
|
||||
<span class="more text-muted pull-right"></span>
|
||||
<h3 class="iconfont icon-iconfontplay2">视频下载 (夸克) </h3>
|
||||
</div>
|
||||
<ul class="stui-content__playlist clearfix">
|
||||
<li>
|
||||
<a href="/play/714892571-1-1.html">合集</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 字段提取要点
|
||||
- **下载源标题**:`h3` 标签内容,需要包含"下载"关键字
|
||||
- **播放页链接**:`.stui-content__playlist li a` 的 `href` 属性
|
||||
- **网盘类型识别**:从标题中提取(如"UC"、"夸克"等)
|
||||
|
||||
### 筛选规则
|
||||
- 只提取标题包含"下载"的播放源
|
||||
- 忽略标题只有"播放"的源(这些通常是在线播放链接)
|
||||
|
||||
## 3. 播放页面结构(获取网盘链接)
|
||||
|
||||
### 播放页URL格式
|
||||
```
|
||||
https://www.libvio.mov/play/{id}-{sid}-{nid}.html
|
||||
```
|
||||
- `id`:影片ID
|
||||
- `sid`:播放源ID
|
||||
- `nid`:集数ID
|
||||
|
||||
### 网盘链接提取方式
|
||||
网盘链接存储在页面内的JavaScript对象中:
|
||||
|
||||
```javascript
|
||||
<script type="text/javascript">
|
||||
var player_aaaa = {
|
||||
"flag": "play",
|
||||
"encrypt": 3,
|
||||
"trysee": 10,
|
||||
"points": 0,
|
||||
"link": "/play/714892571-1-1.html",
|
||||
"link_next": "",
|
||||
"link_pre": "",
|
||||
"url": "https://drive.uc.cn/s/132a6339c94d4?public=1",
|
||||
"url_next": "",
|
||||
"from": "uc",
|
||||
"server": "no",
|
||||
"note": "",
|
||||
"id": "714892571",
|
||||
"sid": 2,
|
||||
"nid": 1
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 字段提取要点
|
||||
- **网盘链接**:`player_aaaa.url` 字段
|
||||
- **网盘类型**:`player_aaaa.from` 字段(如 "uc"、"quark" 等)
|
||||
- **集数信息**:`player_aaaa.nid` 字段
|
||||
|
||||
### 提取方法
|
||||
1. 使用正则表达式匹配 `var player_aaaa = {...}` 内容
|
||||
2. 解析JSON对象
|
||||
3. 提取 `url` 字段即为网盘链接
|
||||
|
||||
## 4. 支持的网盘类型
|
||||
|
||||
根据HTML结构分析,网站主要支持:
|
||||
- UC网盘(drive.uc.cn)
|
||||
- 夸克网盘(pan.quark.cn)
|
||||
|
||||
## 5. 特殊处理事项
|
||||
|
||||
### JavaScript对象解析
|
||||
- 需要处理转义字符(如 `\/` → `/`)
|
||||
- 注意JSON对象的格式可能不标准,需要容错处理
|
||||
|
||||
### 多集处理
|
||||
- 电视剧/动漫可能有多集,每集有独立的播放页链接
|
||||
- 需要处理"合集"类型的资源(通常包含整季资源)
|
||||
|
||||
### 错误处理
|
||||
- 需要处理播放页没有player_aaaa对象的情况
|
||||
- 需要处理URL字段为空的情况
|
||||
- 需要处理网站改版导致的结构变化
|
||||
|
||||
## 6. 实现建议
|
||||
|
||||
### 搜索流程
|
||||
1. 发送搜索请求,解析HTML获取搜索结果
|
||||
2. 提取每个结果的详情页链接
|
||||
|
||||
### 详情页处理
|
||||
1. 访问详情页,查找所有播放源
|
||||
2. 筛选出标题包含"下载"的源
|
||||
3. 提取对应的播放页链接
|
||||
|
||||
### 网盘链接提取
|
||||
1. 访问播放页
|
||||
2. 使用正则表达式提取player_aaaa对象
|
||||
3. 解析JSON获取网盘URL
|
||||
4. 根据from字段确定网盘类型
|
||||
|
||||
### 并发优化
|
||||
- 可以对多个详情页访问进行并发
|
||||
- 可以对多个播放页访问进行并发
|
||||
- 使用缓存避免重复请求
|
||||
|
||||
### 链接类型映射
|
||||
```
|
||||
from字段 → 网盘类型
|
||||
"uc" → "UC网盘"
|
||||
"quark" → "夸克网盘"
|
||||
```
|
||||
649
plugin/libvio/libvio.go
Normal file
649
plugin/libvio/libvio.go
Normal file
@@ -0,0 +1,649 @@
|
||||
package libvio
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"pansou/model"
|
||||
"pansou/plugin"
|
||||
"pansou/util/json"
|
||||
)
|
||||
|
||||
const (
|
||||
BaseURL = "https://www.libvio.mov"
|
||||
SearchPath = "/search/-------------.html"
|
||||
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"
|
||||
MaxConcurrency = 20 // 详情页最大并发数
|
||||
MaxPages = 1 // 最大搜索页数(暂时只搜索第一页)
|
||||
)
|
||||
|
||||
// LibvioPlugin LIBVIO插件
|
||||
type LibvioPlugin struct {
|
||||
*plugin.BaseAsyncPlugin
|
||||
debugMode bool
|
||||
detailCache sync.Map // 缓存详情页结果
|
||||
playCache sync.Map // 缓存播放页结果
|
||||
cacheTTL time.Duration
|
||||
}
|
||||
|
||||
// NewLibvioPlugin 创建新的LIBVIO插件实例
|
||||
func NewLibvioPlugin() *LibvioPlugin {
|
||||
// 检查调试模式
|
||||
debugMode := false // 开启调试模式
|
||||
|
||||
p := &LibvioPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPluginWithFilter("libvio", 4, true ), // 优先级4
|
||||
debugMode: debugMode,
|
||||
cacheTTL: 30 * time.Minute,
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Name 返回插件名称
|
||||
func (p *LibvioPlugin) Name() string {
|
||||
return "libvio"
|
||||
}
|
||||
|
||||
// DisplayName 返回插件显示名称
|
||||
func (p *LibvioPlugin) DisplayName() string {
|
||||
return "LIBVIO"
|
||||
}
|
||||
|
||||
// Description 返回插件描述
|
||||
func (p *LibvioPlugin) Description() string {
|
||||
return "LIBVIO - 影视资源网盘下载"
|
||||
}
|
||||
|
||||
// Search 执行搜索并返回结果(兼容性方法)
|
||||
func (p *LibvioPlugin) 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 *LibvioPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {
|
||||
return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)
|
||||
}
|
||||
|
||||
// setRequestHeaders 设置请求头
|
||||
func (p *LibvioPlugin) setRequestHeaders(req *http.Request, referer string) {
|
||||
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("Accept-Encoding", "gzip, deflate")
|
||||
req.Header.Set("Cache-Control", "no-cache")
|
||||
req.Header.Set("Pragma", "no-cache")
|
||||
if referer != "" {
|
||||
req.Header.Set("Referer", referer)
|
||||
}
|
||||
}
|
||||
|
||||
// doRequest 发送HTTP请求
|
||||
func (p *LibvioPlugin) doRequest(client *http.Client, url string, referer string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.setRequestHeaders(req, referer)
|
||||
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 发送请求: %s", url)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 请求失败: %v", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 响应状态: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// searchImpl 实际的搜索实现
|
||||
func (p *LibvioPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
|
||||
searchURL := fmt.Sprintf("%s%s?wd=%s&submit=", BaseURL, SearchPath, url.QueryEscape(keyword))
|
||||
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 开始搜索: %s", keyword)
|
||||
log.Printf("[Libvio] 搜索URL: %s", searchURL)
|
||||
}
|
||||
|
||||
// 发送搜索请求
|
||||
resp, err := p.doRequest(client, searchURL, BaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送搜索请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("搜索响应状态码异常: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 处理响应体(可能是gzip压缩的)
|
||||
reader, err := p.getResponseReader(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析HTML
|
||||
doc, err := goquery.NewDocumentFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析HTML失败: %w", err)
|
||||
}
|
||||
|
||||
// 提取搜索结果
|
||||
results := p.extractSearchResults(doc, keyword)
|
||||
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 找到 %d 个搜索结果", len(results))
|
||||
}
|
||||
|
||||
// 并发获取详情页的下载链接
|
||||
results = p.enrichWithDetailLinks(client, results, keyword)
|
||||
|
||||
if p.debugMode {
|
||||
// 统计链接数量
|
||||
totalLinks := 0
|
||||
for i, r := range results {
|
||||
log.Printf("[Libvio] 结果 %d: %s, 链接数: %d", i+1, r.Title, len(r.Links))
|
||||
totalLinks += len(r.Links)
|
||||
}
|
||||
log.Printf("[Libvio] 总计: %d 个结果,%d 个链接", len(results), totalLinks)
|
||||
}
|
||||
|
||||
// 过滤结果
|
||||
filteredResults := plugin.FilterResultsByKeyword(results, keyword)
|
||||
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 过滤后剩余 %d 个结果", len(filteredResults))
|
||||
}
|
||||
|
||||
return filteredResults, nil
|
||||
}
|
||||
|
||||
// getResponseReader 获取响应读取器(处理gzip压缩)
|
||||
func (p *LibvioPlugin) getResponseReader(resp *http.Response) (io.Reader, error) {
|
||||
var reader io.Reader = resp.Body
|
||||
|
||||
// 检查Content-Encoding
|
||||
contentEncoding := resp.Header.Get("Content-Encoding")
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] Content-Encoding: %s", contentEncoding)
|
||||
}
|
||||
|
||||
// 如果是gzip压缩,手动解压
|
||||
if contentEncoding == "gzip" {
|
||||
gzReader, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建gzip reader失败: %w", err)
|
||||
}
|
||||
// 注意:不要在这里关闭gzReader,它需要在外部使用
|
||||
reader = gzReader
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// extractSearchResults 从HTML中提取搜索结果
|
||||
func (p *LibvioPlugin) extractSearchResults(doc *goquery.Document, keyword string) []model.SearchResult {
|
||||
var results []model.SearchResult
|
||||
|
||||
// 选择所有搜索结果项
|
||||
doc.Find("ul.stui-vodlist li").Each(func(i int, s *goquery.Selection) {
|
||||
// 提取标题和详情页链接
|
||||
titleElem := s.Find(".stui-vodlist__detail h4 a")
|
||||
title := strings.TrimSpace(titleElem.Text())
|
||||
if title == "" {
|
||||
title, _ = titleElem.Attr("title")
|
||||
}
|
||||
|
||||
detailPath, _ := titleElem.Attr("href")
|
||||
if detailPath == "" {
|
||||
// 尝试从缩略图链接获取
|
||||
thumbLink := s.Find("a.stui-vodlist__thumb")
|
||||
detailPath, _ = thumbLink.Attr("href")
|
||||
}
|
||||
|
||||
if title == "" || detailPath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 构建完整的详情页URL
|
||||
detailURL := BaseURL + detailPath
|
||||
|
||||
// 提取其他信息
|
||||
episodeInfo := strings.TrimSpace(s.Find(".pic-text").Text())
|
||||
rating := strings.TrimSpace(s.Find(".pic-tag").Text())
|
||||
|
||||
// 从详情页路径提取ID(如:/detail/4095.html -> 4095)
|
||||
idMatch := regexp.MustCompile(`/detail/(\d+)\.html`).FindStringSubmatch(detailPath)
|
||||
resourceID := ""
|
||||
if len(idMatch) > 1 {
|
||||
resourceID = idMatch[1]
|
||||
} else {
|
||||
resourceID = fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 提取结果 %d: %s, URL: %s", i+1, title, detailURL)
|
||||
}
|
||||
|
||||
// 构建内容描述
|
||||
content := ""
|
||||
if episodeInfo != "" {
|
||||
content = episodeInfo
|
||||
}
|
||||
if rating != "" {
|
||||
if content != "" {
|
||||
content += " | "
|
||||
}
|
||||
content += "评分: " + rating
|
||||
}
|
||||
|
||||
result := model.SearchResult{
|
||||
Title: title,
|
||||
Content: content,
|
||||
Channel: "",
|
||||
MessageID: fmt.Sprintf("%s-%s", p.Name(), resourceID),
|
||||
UniqueID: fmt.Sprintf("%s-%s", p.Name(), resourceID),
|
||||
Datetime: time.Now(),
|
||||
Links: []model.Link{}, // 稍后填充
|
||||
}
|
||||
|
||||
// 将详情页URL存储在Tags中供后续使用
|
||||
result.Tags = []string{detailURL}
|
||||
|
||||
results = append(results, result)
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// enrichWithDetailLinks 并发获取详情页的下载链接
|
||||
func (p *LibvioPlugin) enrichWithDetailLinks(client *http.Client, results []model.SearchResult, keyword string) []model.SearchResult {
|
||||
if len(results) == 0 {
|
||||
return results
|
||||
}
|
||||
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 开始获取 %d 个详情页的下载链接", len(results))
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
semaphore := make(chan struct{}, MaxConcurrency)
|
||||
|
||||
for i := range results {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
// 添加小延迟避免请求过快
|
||||
time.Sleep(time.Duration(idx*50) * time.Millisecond)
|
||||
|
||||
// 从Tags中获取详情页URL
|
||||
if len(results[idx].Tags) > 0 {
|
||||
detailURL := results[idx].Tags[0]
|
||||
links := p.fetchDetailPageLinks(client, detailURL, keyword)
|
||||
|
||||
mu.Lock()
|
||||
results[idx].Links = links
|
||||
// 清空Tags,避免返回给用户
|
||||
results[idx].Tags = nil
|
||||
mu.Unlock()
|
||||
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 详情页 %d/%d 获取到 %d 个链接", idx+1, len(results), len(links))
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// fetchDetailPageLinks 获取详情页的下载链接
|
||||
func (p *LibvioPlugin) fetchDetailPageLinks(client *http.Client, detailURL string, keyword string) []model.Link {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 开始获取详情页: %s", detailURL)
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
if cached, ok := p.detailCache.Load(detailURL); ok {
|
||||
if links, ok := cached.([]model.Link); ok {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 使用缓存的详情页结果: %s, 链接数: %d", detailURL, len(links))
|
||||
}
|
||||
return links
|
||||
}
|
||||
}
|
||||
|
||||
// 访问详情页
|
||||
resp, err := p.doRequest(client, detailURL, BaseURL)
|
||||
if err != nil {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 获取详情页失败: %s, 错误: %v", detailURL, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 详情页响应状态码异常: %s, 状态码: %d", detailURL, resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理响应体
|
||||
reader, err := p.getResponseReader(resp)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解析HTML
|
||||
doc, err := goquery.NewDocumentFromReader(reader)
|
||||
if err != nil {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 解析详情页HTML失败: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 提取下载播放页链接(只提取包含"下载"的)
|
||||
playLinks := p.extractDownloadPlayLinks(doc)
|
||||
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 找到 %d 个下载播放页链接", len(playLinks))
|
||||
}
|
||||
|
||||
if len(playLinks) == 0 {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 未找到下载链接")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取网盘链接
|
||||
var links []model.Link
|
||||
for _, playLink := range playLinks {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 获取网盘链接: %s", playLink.URL)
|
||||
}
|
||||
panLink := p.fetchPanLink(client, playLink.URL, detailURL)
|
||||
if panLink != nil {
|
||||
links = append(links, *panLink)
|
||||
} else if p.debugMode {
|
||||
log.Printf("[Libvio] 未能获取网盘链接: %s", playLink.URL)
|
||||
}
|
||||
}
|
||||
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 详情页 %s 最终获取到 %d 个网盘链接", detailURL, len(links))
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
p.detailCache.Store(detailURL, links)
|
||||
|
||||
// 设置缓存过期
|
||||
go func() {
|
||||
time.Sleep(p.cacheTTL)
|
||||
p.detailCache.Delete(detailURL)
|
||||
}()
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
// PlayLinkInfo 播放链接信息
|
||||
type PlayLinkInfo struct {
|
||||
URL string
|
||||
PanType string // 网盘类型(从标题提取)
|
||||
}
|
||||
|
||||
// extractDownloadPlayLinks 提取下载播放页链接
|
||||
func (p *LibvioPlugin) extractDownloadPlayLinks(doc *goquery.Document) []PlayLinkInfo {
|
||||
var playLinks []PlayLinkInfo
|
||||
|
||||
// 查找所有播放源
|
||||
allHeads := doc.Find(".stui-vodlist__head")
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 找到 %d 个播放源头部", allHeads.Length())
|
||||
}
|
||||
|
||||
allHeads.Each(func(i int, s *goquery.Selection) {
|
||||
// 获取标题
|
||||
title := strings.TrimSpace(s.Find("h3").Text())
|
||||
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 播放源 %d 标题: %s", i+1, title)
|
||||
}
|
||||
|
||||
// 只处理包含"下载"的源
|
||||
if !strings.Contains(title, "下载") {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 跳过非下载源: %s", title)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 提取网盘类型
|
||||
panType := ""
|
||||
if strings.Contains(title, "夸克") || strings.Contains(title, "quark") {
|
||||
panType = "quark"
|
||||
} else if strings.Contains(title, "UC") || strings.Contains(title, "uc") {
|
||||
panType = "uc"
|
||||
} else if strings.Contains(title, "百度") || strings.Contains(title, "baidu") {
|
||||
panType = "baidu"
|
||||
}
|
||||
|
||||
// 提取播放页链接
|
||||
playlistLinks := s.Find(".stui-content__playlist li a")
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 播放列表中有 %d 个链接", playlistLinks.Length())
|
||||
}
|
||||
|
||||
// 通常只取第一个链接(合集)
|
||||
firstLink := playlistLinks.First()
|
||||
if firstLink.Length() > 0 {
|
||||
href, exists := firstLink.Attr("href")
|
||||
if exists && href != "" {
|
||||
// 构建完整URL
|
||||
playURL := BaseURL + href
|
||||
|
||||
playLinks = append(playLinks, PlayLinkInfo{
|
||||
URL: playURL,
|
||||
PanType: panType,
|
||||
})
|
||||
|
||||
if p.debugMode {
|
||||
linkText := strings.TrimSpace(firstLink.Text())
|
||||
log.Printf("[Libvio] 找到下载链接: %s (%s) [%s]", playURL, panType, linkText)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return playLinks
|
||||
}
|
||||
|
||||
// fetchPanLink 获取网盘链接
|
||||
func (p *LibvioPlugin) fetchPanLink(client *http.Client, playURL string, referer string) *model.Link {
|
||||
// 检查缓存
|
||||
if cached, ok := p.playCache.Load(playURL); ok {
|
||||
if link, ok := cached.(*model.Link); ok {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 使用缓存的播放页结果: %s", playURL)
|
||||
}
|
||||
return link
|
||||
}
|
||||
}
|
||||
|
||||
// 访问播放页
|
||||
resp, err := p.doRequest(client, playURL, referer)
|
||||
if err != nil {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 获取播放页失败: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 播放页响应状态码异常: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理响应体(可能是gzip压缩的)
|
||||
reader, err := p.getResponseReader(resp)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 提取player_aaaa对象
|
||||
playerDataRegex := regexp.MustCompile(`var\s+player_aaaa\s*=\s*({[^}]+})`)
|
||||
matches := playerDataRegex.FindStringSubmatch(string(body))
|
||||
|
||||
if len(matches) < 2 {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 未找到player_aaaa对象")
|
||||
// 输出部分body内容用于调试
|
||||
bodyStr := string(body)
|
||||
if len(bodyStr) > 500 {
|
||||
log.Printf("[Libvio] 页面内容前500字符: %s", bodyStr[:500])
|
||||
} else {
|
||||
log.Printf("[Libvio] 页面内容: %s", bodyStr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解析JSON
|
||||
playerJSON := matches[1]
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 找到player_aaaa: %s", playerJSON)
|
||||
}
|
||||
|
||||
// 处理转义字符
|
||||
playerJSON = strings.ReplaceAll(playerJSON, `\/`, `/`)
|
||||
|
||||
var playerData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(playerJSON), &playerData); err != nil {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 解析player_aaaa失败: %v, JSON: %s", err, playerJSON)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 提取URL
|
||||
panURL, ok := playerData["url"].(string)
|
||||
if !ok || panURL == "" {
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] player_aaaa中没有url字段")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 提取网盘类型
|
||||
from, _ := playerData["from"].(string)
|
||||
linkType := p.mapPanType(from, panURL)
|
||||
|
||||
link := &model.Link{
|
||||
URL: panURL,
|
||||
Type: linkType,
|
||||
}
|
||||
|
||||
if p.debugMode {
|
||||
log.Printf("[Libvio] 提取到网盘链接: %s (from=%s, type=%s)", panURL, from, linkType)
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
p.playCache.Store(playURL, link)
|
||||
|
||||
// 设置缓存过期
|
||||
go func() {
|
||||
time.Sleep(p.cacheTTL)
|
||||
p.playCache.Delete(playURL)
|
||||
}()
|
||||
|
||||
return link
|
||||
}
|
||||
|
||||
// mapPanType 映射网盘类型
|
||||
func (p *LibvioPlugin) mapPanType(from string, url string) string {
|
||||
// 首先根据from字段判断
|
||||
switch strings.ToLower(from) {
|
||||
case "uc":
|
||||
return "uc"
|
||||
case "quark":
|
||||
return "quark"
|
||||
case "baidu":
|
||||
return "baidu"
|
||||
case "aliyun", "alipan":
|
||||
return "aliyun"
|
||||
case "xunlei", "thunder":
|
||||
return "xunlei"
|
||||
case "115":
|
||||
return "115"
|
||||
case "123", "123pan":
|
||||
return "123"
|
||||
}
|
||||
|
||||
// 如果from字段不明确,根据URL判断
|
||||
url = strings.ToLower(url)
|
||||
if strings.Contains(url, "drive.uc.cn") {
|
||||
return "uc"
|
||||
} else if strings.Contains(url, "pan.quark.cn") {
|
||||
return "quark"
|
||||
} else if strings.Contains(url, "pan.baidu.com") {
|
||||
return "baidu"
|
||||
} else if strings.Contains(url, "alipan.com") || strings.Contains(url, "aliyundrive.com") {
|
||||
return "aliyun"
|
||||
} else if strings.Contains(url, "pan.xunlei.com") {
|
||||
return "xunlei"
|
||||
} else if strings.Contains(url, "115.com") {
|
||||
return "115"
|
||||
} else if strings.Contains(url, "123pan.com") || strings.Contains(url, "123684.com") {
|
||||
return "123"
|
||||
} else if strings.Contains(url, "cloud.189.cn") {
|
||||
return "tianyi"
|
||||
}
|
||||
|
||||
// 默认返回others
|
||||
return "others"
|
||||
}
|
||||
|
||||
func init() {
|
||||
plugin.RegisterGlobalPlugin(NewLibvioPlugin())
|
||||
}
|
||||
@@ -204,7 +204,7 @@ func (p *PanyqPlugin) doSearch(client *http.Client, keyword string, ext map[stri
|
||||
// 尝试获取或发现 Action ID
|
||||
actionIDs, err := p.getOrDiscoverActionIDs()
|
||||
if err != nil {
|
||||
fmt.Println("panyq: failed to get Action IDs:", err)
|
||||
// fmt.Println("panyq: failed to get Action IDs:", err)
|
||||
return nil, fmt.Errorf("获取Action ID失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ func (p *PanyqPlugin) doSearch(client *http.Client, keyword string, ext map[stri
|
||||
|
||||
pageHits, _, err := p.getSearchResults(credentials.Sign, pageNum, client)
|
||||
if err != nil {
|
||||
fmt.Printf("panyq: failed to get page %d: %v\n", pageNum, err)
|
||||
// fmt.Printf("panyq: failed to get page %d: %v\n", pageNum, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -428,7 +428,7 @@ func (p *PanyqPlugin) discoverActionIDs() (map[string]string, error) {
|
||||
}
|
||||
|
||||
if DebugLog {
|
||||
fmt.Printf("panyq: 找到 %d 个潜在的 Action ID\n", len(potentialIDs))
|
||||
// fmt.Printf("panyq: 找到 %d 个潜在的 Action ID\n", len(potentialIDs))
|
||||
if len(potentialIDs) > 0 {
|
||||
fmt.Printf("panyq: 样例ID: %s\n", potentialIDs[0])
|
||||
}
|
||||
|
||||
3
util/cache/global_buffer_manager.go
vendored
3
util/cache/global_buffer_manager.go
vendored
@@ -415,9 +415,6 @@ func (g *GlobalBufferManager) performCleanup() {
|
||||
// 计算内存使用量
|
||||
g.updateMemoryUsage()
|
||||
|
||||
if len(toDelete) > 0 {
|
||||
fmt.Printf("🧹 [全局缓冲区] 清理完成,删除%d个过期缓冲区\n", len(toDelete))
|
||||
}
|
||||
}
|
||||
|
||||
// updateMemoryUsage 更新内存使用量估算
|
||||
|
||||
Reference in New Issue
Block a user