新增插件ash

This commit is contained in:
www.xueximeng.com
2025-11-01 16:28:27 +08:00
parent a78ab2038d
commit 8dfffece47
5 changed files with 476 additions and 1 deletions

View File

@@ -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>

View 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"
}

View File

@@ -84,6 +84,7 @@ import (
_ "pansou/plugin/quark4k"
_ "pansou/plugin/quarksoo"
_ "pansou/plugin/sousou"
_ "pansou/plugin/ash"
)
// 全局缓存写入管理器

309
plugin/ash/ash.go Normal file
View 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)
}

View 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. 只返回包含有效网盘链接的结果