mirror of
https://github.com/fish2018/pansou.git
synced 2025-11-25 03:14:59 +08:00
新增插件ash
This commit is contained in:
@@ -41,7 +41,7 @@ susu,thepiratebay,wanou,xuexizhinan,panyq,zhizhen,labi,muou,ouge,shandian,
|
||||
duoduo,huban,cyg,erxiao,miaoso,fox4k,pianku,clmao,wuji,cldi,xiaozhang,
|
||||
libvio,leijing,xb6v,xys,ddys,hdmoli,yuhuage,u3c3,javdb,clxiong,jutoushe,
|
||||
sdso,xiaoji,xdyh,haisou,bixin,djgou,nyaa,xinjuc,aikanzy,qupanshe,xdpan,
|
||||
discourse,yunsou,ahhhhfs,nsgame,gying
|
||||
discourse,yunsou,ahhhhfs,nsgame,gying,quark4k,quarksoo,sousou,ash
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
|
||||
12
cache/gying_users/d6f03d1c9e4101f0637a7a6b14366970704c7dcef558a51b5fe6bf310fadaf82.json
vendored
Normal file
12
cache/gying_users/d6f03d1c9e4101f0637a7a6b14366970704c7dcef558a51b5fe6bf310fadaf82.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"hash": "d6f03d1c9e4101f0637a7a6b14366970704c7dcef558a51b5fe6bf310fadaf82",
|
||||
"username": "",
|
||||
"username_masked": "",
|
||||
"encrypted_password": "",
|
||||
"cookie": "",
|
||||
"status": "pending",
|
||||
"created_at": "2025-11-01T14:43:04.081663+08:00",
|
||||
"login_at": "0001-01-01T00:00:00Z",
|
||||
"expire_at": "0001-01-01T00:00:00Z",
|
||||
"last_access_at": "2025-11-01T14:43:04.081663+08:00"
|
||||
}
|
||||
1
main.go
1
main.go
@@ -84,6 +84,7 @@ import (
|
||||
_ "pansou/plugin/quark4k"
|
||||
_ "pansou/plugin/quarksoo"
|
||||
_ "pansou/plugin/sousou"
|
||||
_ "pansou/plugin/ash"
|
||||
)
|
||||
|
||||
// 全局缓存写入管理器
|
||||
|
||||
309
plugin/ash/ash.go
Normal file
309
plugin/ash/ash.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package ash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"pansou/model"
|
||||
"pansou/plugin"
|
||||
"pansou/util/json"
|
||||
)
|
||||
|
||||
type AshPlugin struct {
|
||||
*plugin.BaseAsyncPlugin
|
||||
}
|
||||
|
||||
const (
|
||||
// 错误的夸克域名
|
||||
wrongQuarkDomain = "pan.qualk.cn"
|
||||
// 正确的夸克域名
|
||||
correctQuarkDomain = "pan.quark.cn"
|
||||
)
|
||||
|
||||
var (
|
||||
// 提取JSON数据的正则表达式(预编译)
|
||||
jsonDataRegex = regexp.MustCompile(`var jsonData = '(\[.*?\])';`)
|
||||
|
||||
// 控制字符清理正则(预编译)
|
||||
controlCharRegex = regexp.MustCompile(`[\x00-\x1F\x7F]`)
|
||||
)
|
||||
|
||||
// AshResult 表示ASH搜索结果的数据结构
|
||||
type AshResult struct {
|
||||
ID int `json:"id"`
|
||||
SourceCategoryID int `json:"source_category_id"`
|
||||
Title string `json:"title"`
|
||||
IsType int `json:"is_type"`
|
||||
Code interface{} `json:"code"` // 可能是null或string
|
||||
URL string `json:"url"`
|
||||
IsTime int `json:"is_time"`
|
||||
Name string `json:"name"`
|
||||
Times string `json:"times"`
|
||||
Category interface{} `json:"category"` // 可能是null或string
|
||||
}
|
||||
|
||||
func init() {
|
||||
p := &AshPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("ash", 2), // 优先级2,质量良好的影视资源
|
||||
}
|
||||
plugin.RegisterGlobalPlugin(p)
|
||||
}
|
||||
|
||||
// Search 执行搜索并返回结果
|
||||
func (p *AshPlugin) 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 *AshPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {
|
||||
return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)
|
||||
}
|
||||
|
||||
// searchImpl 实际的搜索实现(优化版本)
|
||||
func (p *AshPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
|
||||
// 构建搜索URL
|
||||
searchURL := fmt.Sprintf("https://so.allsharehub.com/s/%s.html", url.QueryEscape(keyword))
|
||||
|
||||
// 创建带超时的上下文(减少超时时间,提高响应速度)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", searchURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%s] 创建请求失败: %w", p.Name(), err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
p.setRequestHeaders(req)
|
||||
|
||||
// 发送请求(优化重试)
|
||||
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] 请求返回状态码: %d", p.Name(), resp.StatusCode)
|
||||
}
|
||||
|
||||
// 读取响应(使用有限制的读取,避免读取过大内容)
|
||||
// ASH页面通常不会太大,限制在2MB以内
|
||||
limitReader := io.LimitReader(resp.Body, 2*1024*1024)
|
||||
body, err := io.ReadAll(limitReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%s] 读取响应失败: %w", p.Name(), err)
|
||||
}
|
||||
|
||||
// 从HTML中提取JSON数据(直接传递字节,避免字符串转换)
|
||||
results, err := p.extractResultsFromBytes(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%s] 提取搜索结果失败: %w", p.Name(), err)
|
||||
}
|
||||
|
||||
// 关键词过滤
|
||||
filtered := plugin.FilterResultsByKeyword(results, keyword)
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// extractResultsFromBytes 从字节数组中提取搜索结果(优化版本,避免字符串转换)
|
||||
func (p *AshPlugin) extractResultsFromBytes(data []byte) ([]model.SearchResult, error) {
|
||||
// 直接在字节数组中查找JSON数据(避免转换为字符串)
|
||||
html := string(data) // 只转换一次
|
||||
|
||||
// 查找JSON数据
|
||||
matches := jsonDataRegex.FindStringSubmatch(html)
|
||||
if len(matches) < 2 {
|
||||
return []model.SearchResult{}, nil // 没有找到数据,返回空结果
|
||||
}
|
||||
|
||||
// 提取JSON字符串
|
||||
jsonStr := matches[1]
|
||||
|
||||
// 清理JSON字符串(批量操作,减少内存分配)
|
||||
if strings.Contains(jsonStr, "\\/") {
|
||||
jsonStr = strings.ReplaceAll(jsonStr, "\\/", "/")
|
||||
}
|
||||
jsonStr = controlCharRegex.ReplaceAllString(jsonStr, "")
|
||||
|
||||
// 解析JSON - 使用高性能的sonic库
|
||||
var ashResults []AshResult
|
||||
if err := json.Unmarshal([]byte(jsonStr), &ashResults); err != nil {
|
||||
return nil, fmt.Errorf("JSON解析失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果没有结果,直接返回
|
||||
if len(ashResults) == 0 {
|
||||
return []model.SearchResult{}, nil
|
||||
}
|
||||
|
||||
// 预分配切片容量,避免动态扩容
|
||||
results := make([]model.SearchResult, 0, len(ashResults))
|
||||
|
||||
// 批量处理所有结果
|
||||
for i := range ashResults {
|
||||
item := &ashResults[i]
|
||||
|
||||
// 提前检查URL是否有效,避免无效处理
|
||||
if item.URL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理网盘链接
|
||||
panURL := p.fixPanURL(item.URL)
|
||||
if panURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 确定网盘类型(内联优化)
|
||||
var panType string
|
||||
switch item.IsType {
|
||||
case 0:
|
||||
panType = "quark"
|
||||
case 2:
|
||||
panType = "baidu"
|
||||
case 3:
|
||||
panType = "uc"
|
||||
case 4:
|
||||
panType = "xunlei"
|
||||
default:
|
||||
panType = "quark"
|
||||
}
|
||||
|
||||
// 处理提取码
|
||||
var password string
|
||||
if item.Code != nil {
|
||||
if codeStr, ok := item.Code.(string); ok && codeStr != "" {
|
||||
password = codeStr
|
||||
}
|
||||
}
|
||||
|
||||
// 解析时间
|
||||
var datetime time.Time
|
||||
if item.Times != "" {
|
||||
if parsedTime, err := time.Parse("2006-01-02", item.Times); err == nil {
|
||||
datetime = parsedTime
|
||||
} else {
|
||||
datetime = time.Now()
|
||||
}
|
||||
} else {
|
||||
datetime = time.Now()
|
||||
}
|
||||
|
||||
// 获取标签
|
||||
var tags []string
|
||||
if item.SourceCategoryID > 0 && item.SourceCategoryID <= 6 {
|
||||
categoryNames := [...]string{"短剧", "电影", "电视剧", "动漫", "综艺", "充电视频"}
|
||||
tags = []string{categoryNames[item.SourceCategoryID-1]}
|
||||
}
|
||||
|
||||
// 构建搜索结果
|
||||
results = append(results, model.SearchResult{
|
||||
UniqueID: fmt.Sprintf("%s-%d", p.Name(), item.ID),
|
||||
Title: item.Title,
|
||||
Content: item.Name,
|
||||
Datetime: datetime,
|
||||
Channel: "",
|
||||
Links: []model.Link{{
|
||||
Type: panType,
|
||||
URL: panURL,
|
||||
Password: password,
|
||||
}},
|
||||
Tags: tags,
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// fixPanURL 修复网盘链接 - 关键功能!(优化版本)
|
||||
func (p *AshPlugin) fixPanURL(url string) string {
|
||||
// 快速检查是否为有效的HTTP/HTTPS链接
|
||||
if len(url) < 8 { // 最短的URL: http://a
|
||||
return ""
|
||||
}
|
||||
|
||||
// 验证链接协议(使用更快的检查方式)
|
||||
if url[0] != 'h' || (url[4] != ':' && url[5] != ':') {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 只在包含错误域名时才进行替换,避免不必要的字符串操作
|
||||
if strings.Contains(url, wrongQuarkDomain) {
|
||||
return strings.Replace(url, wrongQuarkDomain, correctQuarkDomain, 1)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
// setRequestHeaders 设置请求头
|
||||
func (p *AshPlugin) setRequestHeaders(req *http.Request) {
|
||||
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("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", "https://so.allsharehub.com/")
|
||||
}
|
||||
|
||||
// doRequestWithRetry 带重试机制的HTTP请求(优化版本)
|
||||
func (p *AshPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*http.Response, error) {
|
||||
maxRetries := 2 // 减少重试次数,提高响应速度
|
||||
var lastErr error
|
||||
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
if i > 0 {
|
||||
// 更短的退避时间
|
||||
backoff := time.Duration(100<<uint(i-1)) * time.Millisecond
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
|
||||
// 克隆请求(只在重试时克隆)
|
||||
var reqToUse *http.Request
|
||||
if i == 0 {
|
||||
reqToUse = req
|
||||
} else {
|
||||
reqToUse = req.Clone(req.Context())
|
||||
}
|
||||
|
||||
resp, err := client.Do(reqToUse)
|
||||
|
||||
// 成功返回
|
||||
if err == nil && resp.StatusCode == 200 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 清理响应
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
|
||||
// 如果是上下文取消或超时,不再重试
|
||||
if req.Context().Err() != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return nil, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("重试 %d 次后仍然失败", maxRetries)
|
||||
}
|
||||
|
||||
153
plugin/ash/html结构分析.md
Normal file
153
plugin/ash/html结构分析.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# ASH搜剧助手 HTML结构分析
|
||||
|
||||
## 网站信息
|
||||
- **网站名称**: ASH搜剧助手
|
||||
- **域名**: so.allsharehub.com
|
||||
- **类型**: 影视资源搜索引擎
|
||||
- **特点**: 专门搜索影视剧资源,主要提供夸克网盘链接
|
||||
- **搜索模式**: 本地搜索(从网站数据库查询,不使用全网搜)
|
||||
|
||||
## 搜索页面结构
|
||||
|
||||
### 1. 搜索URL模式
|
||||
```
|
||||
https://so.allsharehub.com/s/[关键词].html
|
||||
|
||||
示例:
|
||||
https://so.allsharehub.com/s/%E4%BB%99%E9%80%86.html
|
||||
|
||||
参数说明:
|
||||
- 关键词: URL编码的搜索关键词
|
||||
- 支持分页: /s/[关键词]-[页码].html
|
||||
- 支持分类: /s/[关键词]-[页码]-[分类ID].html
|
||||
```
|
||||
|
||||
### 2. 数据提取方式
|
||||
|
||||
#### JavaScript数据源(唯一方式)
|
||||
搜索结果嵌入在页面JavaScript变量中(本地搜索数据):
|
||||
```javascript
|
||||
var jsonData = '[{"id":987,"source_category_id":0,"title":"仙逆剧场版神临之战4K完整版","is_type":0,"code":null,"url":"https://pan.qualk.cn/s/095628b04e6c","is_time":0,"name":"仙逆剧场版神临之战4K完整版","times":"2025-08-31","category":null}]';
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- 只使用本地搜索数据(currentSource === 0)
|
||||
- 不需要处理全网搜的SSE流式数据(currentSource === 1)
|
||||
|
||||
### 3. 数据字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 | 示例 |
|
||||
|------|------|------|------|
|
||||
| `id` | number | 资源ID | 987 |
|
||||
| `source_category_id` | number | 分类ID | 0 |
|
||||
| `title` | string | 资源标题 | "仙逆剧场版神临之战4K完整版" |
|
||||
| `is_type` | number | 网盘类型 (0=夸克) | 0 |
|
||||
| `code` | string/null | 提取码 | null 或 "1234" |
|
||||
| `url` | string | 网盘链接 | "https://pan.qualk.cn/s/095628b04e6c" |
|
||||
| `is_time` | number | 时间标记 | 0 |
|
||||
| `name` | string | 资源名称 | "仙逆剧场版神临之战4K完整版" |
|
||||
| `times` | string | 发布时间 | "2025-08-31" |
|
||||
| `category` | string/null | 分类 | null |
|
||||
|
||||
### 4. HTML结构(备用方式)
|
||||
|
||||
#### 搜索结果容器
|
||||
- **父容器**: `.listBox .left .box .list`
|
||||
- **结果项**: `.item` (每个搜索结果)
|
||||
|
||||
#### 单个搜索结果结构
|
||||
```html
|
||||
<div class="item">
|
||||
<!-- 标题 -->
|
||||
<a href="javascript:;" onclick="linkBtn(this)" data-index="0" class="title">
|
||||
仙逆剧场版神临之战4K完整版
|
||||
</a>
|
||||
|
||||
<!-- 发布时间 -->
|
||||
<div class="type time">2025-08-31</div>
|
||||
|
||||
<!-- 来源 -->
|
||||
<div class="type">
|
||||
<span>来源:夸克网盘</span>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="btns">
|
||||
<div class="btn" @click.stop="copyText(...)">
|
||||
<i class="iconfont icon-fenxiang1"></i>复制分享
|
||||
</div>
|
||||
<a href="/d/987.html" class="btn">
|
||||
<i class="iconfont icon-fangwen"></i>查看详情
|
||||
</a>
|
||||
<a href="javascript:;" onclick="linkBtn(this)" data-index="0" class="btn">
|
||||
立即访问
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 重要实现要点
|
||||
|
||||
### 1. 网盘链接转换 ⭐ 非常重要
|
||||
页面返回的链接使用错误的域名,必须进行转换:
|
||||
```
|
||||
原始链接: https://pan.qualk.cn/s/095628b04e6c
|
||||
正确链接: https://pan.quark.cn/s/095628b04e6c
|
||||
|
||||
转换规则: 将 "pan.qualk.cn" 替换为 "pan.quark.cn"
|
||||
```
|
||||
|
||||
### 2. 数据提取正则表达式
|
||||
```go
|
||||
// 提取JSON数据
|
||||
jsonDataRegex := regexp.MustCompile(`var jsonData = '(\[.*?\])';`)
|
||||
|
||||
// 清理JSON中的控制字符
|
||||
jsonData = strings.ReplaceAll(jsonData, "\\/", "/")
|
||||
jsonData = regexp.MustCompile(`[\x00-\x1F\x7F]`).ReplaceAllString(jsonData, "")
|
||||
```
|
||||
|
||||
### 3. 网盘类型映射
|
||||
```go
|
||||
is_type 值映射:
|
||||
0 -> "quark" (夸克网盘)
|
||||
2 -> "baidu" (百度网盘)
|
||||
3 -> "uc" (UC网盘)
|
||||
4 -> "xunlei" (迅雷网盘)
|
||||
```
|
||||
|
||||
### 4. 时间格式
|
||||
- 格式: `YYYY-MM-DD`
|
||||
- 需要转换为标准时间格式: `time.Parse("2006-01-02", timeStr)`
|
||||
|
||||
### 5. 分类信息
|
||||
页面支持按分类筛选:
|
||||
- 0: 全部
|
||||
- 1: 短剧
|
||||
- 2: 电影
|
||||
- 3: 电视剧
|
||||
- 4: 动漫
|
||||
- 5: 综艺
|
||||
- 6: 充电视频
|
||||
|
||||
## CSS选择器总结
|
||||
|
||||
| 数据项 | CSS选择器 | 提取方式 |
|
||||
|--------|-----------|----------|
|
||||
| 搜索结果列表 | `.listBox .left .box .list .item` | 遍历所有结果项 |
|
||||
| 标题 | `.item .title` | 文本内容 |
|
||||
| 发布时间 | `.item .type.time` | 文本内容 |
|
||||
| 来源类型 | `.item .type span` | 文本内容 |
|
||||
| 详情页链接 | `.item a[href^="/d/"]` | href 属性 |
|
||||
|
||||
## 优先级建议
|
||||
- **优先级**: 2-3 (质量良好的影视资源搜索)
|
||||
- **跳过Service层过滤**: false (标准中文资源,保持过滤)
|
||||
- **缓存TTL**: 2小时
|
||||
|
||||
## 搜索策略
|
||||
1. 优先使用JavaScript变量提取数据(更快、更准确)
|
||||
2. 如果JavaScript解析失败,回退到HTML解析
|
||||
3. 必须对所有链接进行域名转换(pan.qualk.cn -> pan.quark.cn)
|
||||
4. 只返回包含有效网盘链接的结果
|
||||
|
||||
Reference in New Issue
Block a user