mirror of
https://github.com/fish2018/pansou.git
synced 2025-11-25 03:14:59 +08:00
新增插件feikuai
This commit is contained in:
384
docs/插件开发指南.md
384
docs/插件开发指南.md
@@ -309,7 +309,111 @@ func (p *MyPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 链接转换
|
### 3. 链接转换与 work_title 字段
|
||||||
|
|
||||||
|
#### Link 结构定义
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Link struct {
|
||||||
|
Type string `json:"type"` // 网盘类型
|
||||||
|
URL string `json:"url"` // 链接地址
|
||||||
|
Password string `json:"password"` // 提取码/密码
|
||||||
|
Datetime time.Time `json:"datetime,omitempty"` // 链接更新时间(可选)
|
||||||
|
WorkTitle string `json:"work_title,omitempty"` // 作品标题(重要:用于区分同一消息中多个作品的链接)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### work_title 字段详解
|
||||||
|
|
||||||
|
**字段作用**:
|
||||||
|
- 用于区分**同一条消息/结果中包含的多个不同作品**的链接
|
||||||
|
- 特别适用于论坛帖子、TG频道消息等一次性发布多部影视资源的场景
|
||||||
|
|
||||||
|
**使用场景示例**:
|
||||||
|
|
||||||
|
```
|
||||||
|
📺 TG频道消息示例:
|
||||||
|
【今日更新】多部热门剧集
|
||||||
|
1. 凡人修仙传 第30集
|
||||||
|
夸克:https://pan.quark.cn/s/abc123
|
||||||
|
2. 唐朝诡事录 第20集
|
||||||
|
夸克:https://pan.quark.cn/s/def456
|
||||||
|
3. 庆余年2 全集
|
||||||
|
百度:https://pan.baidu.com/s/xyz789?pwd=abcd
|
||||||
|
```
|
||||||
|
|
||||||
|
**不使用 work_title 的问题**:
|
||||||
|
- 所有链接的标题都是 "【今日更新】多部热门剧集"
|
||||||
|
- 用户无法区分哪个链接对应哪部剧集
|
||||||
|
|
||||||
|
**使用 work_title 后的效果**:
|
||||||
|
```go
|
||||||
|
links := []model.Link{
|
||||||
|
{
|
||||||
|
Type: "quark",
|
||||||
|
URL: "https://pan.quark.cn/s/abc123",
|
||||||
|
WorkTitle: "凡人修仙传 第30集", // 独立作品标题
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "quark",
|
||||||
|
URL: "https://pan.quark.cn/s/def456",
|
||||||
|
WorkTitle: "唐朝诡事录 第20集", // 独立作品标题
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "baidu",
|
||||||
|
URL: "https://pan.baidu.com/s/xyz789?pwd=abcd",
|
||||||
|
Password: "abcd",
|
||||||
|
WorkTitle: "庆余年2 全集", // 独立作品标题
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**PanSou系统的智能处理**:
|
||||||
|
|
||||||
|
PanSou 会根据消息中的链接数量自动决定是否提取 work_title:
|
||||||
|
|
||||||
|
1. **链接数量 ≤ 4**:所有链接使用相同的 work_title(即消息标题)
|
||||||
|
```go
|
||||||
|
// 示例:一条消息只包含同一部剧的不同网盘链接
|
||||||
|
// 消息标题:"凡人修仙传 第30集"
|
||||||
|
// 链接1(夸克)、链接2(百度) → work_title 都是 "凡人修仙传 第30集"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **链接数量 > 4**:系统智能识别每个链接对应的作品标题
|
||||||
|
```go
|
||||||
|
// 示例:一条消息包含5个不同作品的链接
|
||||||
|
// 系统会分析消息文本,为每个链接提取独立的 work_title
|
||||||
|
```
|
||||||
|
|
||||||
|
**插件实现 work_title 的两种方式**:
|
||||||
|
|
||||||
|
**方式1: 依赖系统自动提取**(适用于TG频道、论坛等)
|
||||||
|
```go
|
||||||
|
// 直接返回链接,系统会自动调用 extractWorkTitlesForLinks 进行处理
|
||||||
|
links := []model.Link{
|
||||||
|
{Type: "quark", URL: "https://pan.quark.cn/s/abc123"},
|
||||||
|
{Type: "baidu", URL: "https://pan.baidu.com/s/xyz789"},
|
||||||
|
}
|
||||||
|
// PanSou会根据消息文本自动为每个链接提取work_title
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式2: 插件手动设置**(适用于API插件、磁力搜索等)
|
||||||
|
```go
|
||||||
|
// 插件直接设置 work_title(如feikuai、thepiratebay等)
|
||||||
|
links := []model.Link{
|
||||||
|
{
|
||||||
|
Type: "magnet",
|
||||||
|
URL: magnetURL,
|
||||||
|
WorkTitle: buildWorkTitle(keyword, fileName), // 插件自己构建
|
||||||
|
Datetime: publishedTime,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**插件开发建议**:
|
||||||
|
- **网盘API插件**: 如果API直接返回单一作品,可以不设置 work_title(留空)
|
||||||
|
- **磁力搜索插件**: 建议设置 work_title,特别是文件名不含中文时需要拼接关键词
|
||||||
|
- **爬虫插件**: 如果能从页面提取每个链接的独立标题,建议设置 work_title
|
||||||
|
|
||||||
#### 支持的网盘类型
|
#### 支持的网盘类型
|
||||||
|
|
||||||
@@ -400,7 +504,283 @@ func convertAPILinks(apiLinks []APILink) []model.Link {
|
|||||||
|
|
||||||
## 高级特性
|
## 高级特性
|
||||||
|
|
||||||
### 1. Service层过滤控制详解
|
### 1. 插件Web路由注册(自定义HTTP接口)
|
||||||
|
|
||||||
|
#### 概述
|
||||||
|
|
||||||
|
PanSou 支持插件注册自定义的 HTTP 路由,用于实现插件专属的管理页面、配置接口或其他Web功能。
|
||||||
|
|
||||||
|
**典型应用场景**:
|
||||||
|
- 插件配置管理界面(如 QQPD 的用户登录和频道管理)
|
||||||
|
- 插件数据查询接口
|
||||||
|
- 插件状态监控页面
|
||||||
|
- OAuth回调接口
|
||||||
|
|
||||||
|
#### 接口定义
|
||||||
|
|
||||||
|
```go
|
||||||
|
// PluginWithWebHandler 支持Web路由的插件接口
|
||||||
|
// 插件可以选择实现此接口来注册自定义的HTTP路由
|
||||||
|
type PluginWithWebHandler interface {
|
||||||
|
AsyncSearchPlugin // 继承搜索插件接口
|
||||||
|
|
||||||
|
// RegisterWebRoutes 注册Web路由
|
||||||
|
// router: gin的路由组,插件可以在此注册自己的路由
|
||||||
|
RegisterWebRoutes(router *gin.RouterGroup)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 实现步骤
|
||||||
|
|
||||||
|
**步骤1: 插件结构实现接口**
|
||||||
|
|
||||||
|
```go
|
||||||
|
package myplugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"pansou/plugin"
|
||||||
|
"pansou/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MyPlugin struct {
|
||||||
|
*plugin.BaseAsyncPlugin
|
||||||
|
// ... 其他字段
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保插件实现了 PluginWithWebHandler 接口
|
||||||
|
var _ plugin.PluginWithWebHandler = (*MyPlugin)(nil)
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤2: 实现 RegisterWebRoutes 方法**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// RegisterWebRoutes 注册Web路由
|
||||||
|
func (p *MyPlugin) RegisterWebRoutes(router *gin.RouterGroup) {
|
||||||
|
// 创建插件专属的路由组
|
||||||
|
myGroup := router.Group("/myplugin")
|
||||||
|
|
||||||
|
// 注册GET路由
|
||||||
|
myGroup.GET("/status", p.handleGetStatus)
|
||||||
|
|
||||||
|
// 注册POST路由
|
||||||
|
myGroup.POST("/config", p.handleUpdateConfig)
|
||||||
|
|
||||||
|
// 支持动态路径参数
|
||||||
|
myGroup.GET("/:id", p.handleGetByID)
|
||||||
|
myGroup.POST("/:id/action", p.handleAction)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤3: 实现路由处理函数**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// handleGetStatus 获取插件状态
|
||||||
|
func (p *MyPlugin) handleGetStatus(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "ok",
|
||||||
|
"plugin": p.Name(),
|
||||||
|
"version": "1.0.0",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdateConfig 更新插件配置
|
||||||
|
func (p *MyPlugin) handleUpdateConfig(c *gin.Context) {
|
||||||
|
var config map[string]interface{}
|
||||||
|
|
||||||
|
if err := c.BindJSON(&config); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": "Invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理配置更新逻辑
|
||||||
|
// ...
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "配置已更新",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetByID 根据ID获取数据
|
||||||
|
func (p *MyPlugin) handleGetByID(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
// 根据ID查询数据
|
||||||
|
// ...
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"id": id,
|
||||||
|
"data": "...",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 实际案例: QQPD 插件
|
||||||
|
|
||||||
|
QQPD 插件实现了完整的用户管理和频道配置功能:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// RegisterWebRoutes 注册Web路由
|
||||||
|
func (p *QQPDPlugin) RegisterWebRoutes(router *gin.RouterGroup) {
|
||||||
|
qqpd := router.Group("/qqpd")
|
||||||
|
|
||||||
|
// GET /:param - 显示管理页面(HTML)
|
||||||
|
qqpd.GET("/:param", p.handleManagePage)
|
||||||
|
|
||||||
|
// POST /:param - 处理管理操作(JSON API)
|
||||||
|
qqpd.POST("/:param", p.handleManagePagePOST)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleManagePage 渲染管理页面
|
||||||
|
func (p *QQPDPlugin) handleManagePage(c *gin.Context) {
|
||||||
|
param := c.Param("param")
|
||||||
|
|
||||||
|
// 生成用户专属的管理页面
|
||||||
|
html := strings.ReplaceAll(HTMLTemplate, "HASH_PLACEHOLDER", param)
|
||||||
|
|
||||||
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
c.String(200, html)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleManagePagePOST 处理管理操作
|
||||||
|
func (p *QQPDPlugin) handleManagePagePOST(c *gin.Context) {
|
||||||
|
param := c.Param("param")
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Channels []string `json:"channels,omitempty"`
|
||||||
|
Keyword string `json:"keyword,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
respondError(c, "无效的请求格式")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据不同的 action 执行不同的操作
|
||||||
|
switch req.Action {
|
||||||
|
case "get_status":
|
||||||
|
p.handleGetStatus(c, param)
|
||||||
|
case "set_channels":
|
||||||
|
p.handleSetChannels(c, param, req.Channels)
|
||||||
|
case "test_search":
|
||||||
|
p.handleTestSearch(c, param, req.Keyword)
|
||||||
|
case "logout":
|
||||||
|
p.handleLogout(c, param)
|
||||||
|
default:
|
||||||
|
respondError(c, "未知的操作")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 实际案例: Gying 插件
|
||||||
|
|
||||||
|
```go
|
||||||
|
// RegisterWebRoutes 注册Web路由
|
||||||
|
func (p *GyingPlugin) RegisterWebRoutes(router *gin.RouterGroup) {
|
||||||
|
gying := router.Group("/gying")
|
||||||
|
gying.GET("/:param", p.handleManagePage)
|
||||||
|
gying.POST("/:param", p.handleManagePagePOST)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 路由访问示例
|
||||||
|
|
||||||
|
插件注册的路由可以通过以下方式访问:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# QQPD 插件管理页面
|
||||||
|
GET http://localhost:8888/qqpd/user123
|
||||||
|
|
||||||
|
# QQPD 插件配置接口
|
||||||
|
POST http://localhost:8888/qqpd/user123
|
||||||
|
Content-Type: application/json
|
||||||
|
{
|
||||||
|
"action": "set_channels",
|
||||||
|
"channels": ["pd97631607", "kuake12345"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 自定义插件接口
|
||||||
|
GET http://localhost:8888/myplugin/status
|
||||||
|
POST http://localhost:8888/myplugin/config
|
||||||
|
GET http://localhost:8888/myplugin/resource123
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 系统集成
|
||||||
|
|
||||||
|
PanSou 在启动时会自动扫描并注册所有实现了 `PluginWithWebHandler` 接口的插件路由:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// api/router.go 中的自动注册逻辑
|
||||||
|
func SetupRouter(searchService *service.SearchService) *gin.Engine {
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// ... 其他路由配置 ...
|
||||||
|
|
||||||
|
// 注册插件的Web路由(如果插件实现了PluginWithWebHandler接口)
|
||||||
|
allPlugins := plugin.GetRegisteredPlugins()
|
||||||
|
for _, p := range allPlugins {
|
||||||
|
if webPlugin, ok := p.(plugin.PluginWithWebHandler); ok {
|
||||||
|
webPlugin.RegisterWebRoutes(r.Group(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 开发建议
|
||||||
|
|
||||||
|
1. **路由命名规范**: 使用插件名作为路由前缀,避免与其他插件冲突
|
||||||
|
```go
|
||||||
|
// ✅ 推荐
|
||||||
|
router.Group("/myplugin")
|
||||||
|
|
||||||
|
// ❌ 避免
|
||||||
|
router.Group("/config") // 可能与其他插件冲突
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **安全考虑**:
|
||||||
|
- 对敏感操作进行身份验证
|
||||||
|
- 验证用户输入,防止注入攻击
|
||||||
|
- 使用哈希或加密保护敏感参数
|
||||||
|
|
||||||
|
3. **错误处理**: 统一错误响应格式
|
||||||
|
```go
|
||||||
|
func respondError(c *gin.Context, message string) {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func respondSuccess(c *gin.Context, message string, data interface{}) {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": message,
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **HTML模板**: 可以内嵌HTML模板提供管理界面
|
||||||
|
```go
|
||||||
|
const HTMLTemplate = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>插件管理</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ .PluginName }} 管理界面</h1>
|
||||||
|
<!-- ... -->
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **可选实现**: Web路由是**可选功能**,只有需要自定义HTTP接口的插件才需要实现
|
||||||
|
|
||||||
|
### 2. Service层过滤控制详解
|
||||||
|
|
||||||
#### 构造函数选择
|
#### 构造函数选择
|
||||||
|
|
||||||
|
|||||||
1
main.go
1
main.go
@@ -87,6 +87,7 @@ import (
|
|||||||
_ "pansou/plugin/ash"
|
_ "pansou/plugin/ash"
|
||||||
_ "pansou/plugin/qqpd"
|
_ "pansou/plugin/qqpd"
|
||||||
_ "pansou/plugin/weibo"
|
_ "pansou/plugin/weibo"
|
||||||
|
_ "pansou/plugin/feikuai"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 全局缓存写入管理器
|
// 全局缓存写入管理器
|
||||||
|
|||||||
422
plugin/feikuai/feikuai.go
Normal file
422
plugin/feikuai/feikuai.go
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
package feikuai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"pansou/model"
|
||||||
|
"pansou/plugin"
|
||||||
|
"pansou/util/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// API URL格式
|
||||||
|
SearchAPIURL = "https://feikuai.tv/t_search/bm_search.php?kw=%s"
|
||||||
|
|
||||||
|
// 默认超时时间
|
||||||
|
DefaultTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
// HTTP连接池配置
|
||||||
|
MaxIdleConns = 100
|
||||||
|
MaxIdleConnsPerHost = 30
|
||||||
|
MaxConnsPerHost = 50
|
||||||
|
IdleConnTimeout = 90 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// 预编译正则表达式
|
||||||
|
var (
|
||||||
|
// 文件扩展名正则
|
||||||
|
fileExtRegex = regexp.MustCompile(`\.(mkv|mp4|avi|rmvb|wmv|flv|mov|ts|m2ts|iso)$`)
|
||||||
|
|
||||||
|
// 文件大小信息正则
|
||||||
|
fileSizeRegex = regexp.MustCompile(`\s*·\s*[\d.]+\s*[KMGT]B\s*$`)
|
||||||
|
|
||||||
|
// 日期时间提取正则
|
||||||
|
dateTimeRegex = regexp.MustCompile(`@[^-]+-(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// FeikuaiPlugin Feikuai磁力搜索插件
|
||||||
|
type FeikuaiPlugin struct {
|
||||||
|
*plugin.BaseAsyncPlugin
|
||||||
|
optimizedClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// createOptimizedHTTPClient 创建优化的HTTP客户端
|
||||||
|
func createOptimizedHTTPClient() *http.Client {
|
||||||
|
transport := &http.Transport{
|
||||||
|
MaxIdleConns: MaxIdleConns,
|
||||||
|
MaxIdleConnsPerHost: MaxIdleConnsPerHost,
|
||||||
|
MaxConnsPerHost: MaxConnsPerHost,
|
||||||
|
IdleConnTimeout: IdleConnTimeout,
|
||||||
|
DisableKeepAlives: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
Timeout: DefaultTimeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFeikuaiPlugin 创建新的Feikuai插件
|
||||||
|
func NewFeikuaiPlugin() *FeikuaiPlugin {
|
||||||
|
return &FeikuaiPlugin{
|
||||||
|
BaseAsyncPlugin: plugin.NewBaseAsyncPluginWithFilter("feikuai", 3, true), // 跳过Service层过滤
|
||||||
|
optimizedClient: createOptimizedHTTPClient(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
plugin.RegisterGlobalPlugin(NewFeikuaiPlugin())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search 同步搜索接口
|
||||||
|
func (p *FeikuaiPlugin) 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 *FeikuaiPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {
|
||||||
|
return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchImpl 搜索实现
|
||||||
|
func (p *FeikuaiPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
|
||||||
|
// 使用优化的客户端
|
||||||
|
if p.optimizedClient != nil {
|
||||||
|
client = p.optimizedClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建API搜索URL
|
||||||
|
searchURL := fmt.Sprintf(SearchAPIURL, url.QueryEscape(keyword))
|
||||||
|
|
||||||
|
// 创建带超时的上下文
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 创建请求
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", searchURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("[%s] 创建请求失败: %w", p.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
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", "application/json, text/plain, */*")
|
||||||
|
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||||
|
req.Header.Set("Connection", "keep-alive")
|
||||||
|
req.Header.Set("Referer", "https://feikuai.tv/")
|
||||||
|
|
||||||
|
// 发送请求(带重试)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取并解析JSON响应
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("[%s] 读取响应失败: %w", p.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResp FeikuaiAPIResponse
|
||||||
|
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("[%s] JSON解析失败: %w", p.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查API响应状态
|
||||||
|
if apiResp.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("[%s] API返回错误: %s (code: %d)", p.Name(), apiResp.Msg, apiResp.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析搜索结果
|
||||||
|
var results []model.SearchResult
|
||||||
|
for _, item := range apiResp.Items {
|
||||||
|
// 每个item可能包含多个种子
|
||||||
|
for _, torrent := range item.Torrents {
|
||||||
|
result := p.parseTorrent(keyword, item, torrent)
|
||||||
|
if result.Title != "" && len(result.Links) > 0 {
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用关键词过滤结果
|
||||||
|
return plugin.FilterResultsByKeyword(results, keyword), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeikuaiAPIResponse API响应结构
|
||||||
|
type FeikuaiAPIResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Keyword string `json:"keyword"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Items []FeikuaiAPIItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeikuaiAPIItem API数据项
|
||||||
|
type FeikuaiAPIItem struct {
|
||||||
|
ContentID *string `json:"content_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Year *int `json:"year"`
|
||||||
|
Torrents []FeikuaiTorrent `json:"torrents"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeikuaiTorrent 种子数据
|
||||||
|
type FeikuaiTorrent struct {
|
||||||
|
InfoHash string `json:"info_hash"`
|
||||||
|
Magnet string `json:"magnet"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
SizeBytes int64 `json:"size_bytes"`
|
||||||
|
SizeGB float64 `json:"size_gb"`
|
||||||
|
Seeders int `json:"seeders"`
|
||||||
|
Leechers int `json:"leechers"`
|
||||||
|
PublishedAt string `json:"published_at"`
|
||||||
|
PublishedAgo string `json:"published_ago"`
|
||||||
|
FilePath string `json:"file_path"`
|
||||||
|
FileExt string `json:"file_ext"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTorrent 解析种子数据为SearchResult
|
||||||
|
func (p *FeikuaiPlugin) parseTorrent(keyword string, item FeikuaiAPIItem, torrent FeikuaiTorrent) model.SearchResult {
|
||||||
|
// 构建唯一ID
|
||||||
|
uniqueID := fmt.Sprintf("%s-%s", p.Name(), torrent.InfoHash)
|
||||||
|
|
||||||
|
// 构建work_title
|
||||||
|
workTitle := p.buildWorkTitle(keyword, torrent.Name)
|
||||||
|
|
||||||
|
// 构建描述信息
|
||||||
|
content := p.buildContent(item, torrent)
|
||||||
|
|
||||||
|
// 解析发布时间
|
||||||
|
datetime := p.parsePublishedTime(torrent.PublishedAt)
|
||||||
|
|
||||||
|
// 构建标签
|
||||||
|
tags := p.extractTags(item.Title, torrent.Name)
|
||||||
|
|
||||||
|
// 构建磁力链接
|
||||||
|
links := []model.Link{
|
||||||
|
{
|
||||||
|
Type: "magnet",
|
||||||
|
URL: torrent.Magnet,
|
||||||
|
Password: "", // 磁力链接无密码
|
||||||
|
Datetime: datetime,
|
||||||
|
WorkTitle: workTitle,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.SearchResult{
|
||||||
|
UniqueID: uniqueID,
|
||||||
|
Title: workTitle, // 使用处理后的work_title作为标题
|
||||||
|
Content: content,
|
||||||
|
Links: links,
|
||||||
|
Tags: tags,
|
||||||
|
Channel: "", // 插件搜索结果Channel为空
|
||||||
|
Datetime: datetime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildWorkTitle 构建work_title(核心功能)
|
||||||
|
func (p *FeikuaiPlugin) buildWorkTitle(keyword, fileName string) string {
|
||||||
|
// 1. 清洗文件名
|
||||||
|
cleanedName := p.cleanFileName(fileName)
|
||||||
|
|
||||||
|
// 2. 检查是否包含关键词
|
||||||
|
if p.containsKeywords(keyword, cleanedName) {
|
||||||
|
return cleanedName
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 不包含关键词,拼接中文关键词
|
||||||
|
return fmt.Sprintf("%s-%s", keyword, cleanedName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanFileName 清洗文件名
|
||||||
|
func (p *FeikuaiPlugin) cleanFileName(fileName string) string {
|
||||||
|
// 去除文件扩展名
|
||||||
|
fileName = fileExtRegex.ReplaceAllString(fileName, "")
|
||||||
|
|
||||||
|
// 去除文件大小信息
|
||||||
|
fileName = fileSizeRegex.ReplaceAllString(fileName, "")
|
||||||
|
|
||||||
|
// 去除日期时间部分(@来源-日期 时间)
|
||||||
|
if idx := strings.Index(fileName, "@"); idx != -1 {
|
||||||
|
fileName = fileName[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsKeywords 检查文本是否包含关键词
|
||||||
|
func (p *FeikuaiPlugin) containsKeywords(keyword, text string) bool {
|
||||||
|
// 简化处理:分词并检查
|
||||||
|
keywords := p.splitKeywords(keyword)
|
||||||
|
lowerText := strings.ToLower(text)
|
||||||
|
|
||||||
|
for _, kw := range keywords {
|
||||||
|
if strings.Contains(lowerText, strings.ToLower(kw)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitKeywords 分词提取关键词
|
||||||
|
func (p *FeikuaiPlugin) splitKeywords(keyword string) []string {
|
||||||
|
// 移除标点符号和空格
|
||||||
|
keyword = strings.TrimSpace(keyword)
|
||||||
|
|
||||||
|
// 简单按空格、中文标点分割
|
||||||
|
separators := []string{" ", " ", ",", "。", "、", ";", ":", "!", "?", "-", "_"}
|
||||||
|
|
||||||
|
parts := []string{keyword}
|
||||||
|
for _, sep := range separators {
|
||||||
|
var newParts []string
|
||||||
|
for _, part := range parts {
|
||||||
|
if strings.Contains(part, sep) {
|
||||||
|
newParts = append(newParts, strings.Split(part, sep)...)
|
||||||
|
} else {
|
||||||
|
newParts = append(newParts, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts = newParts
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤空字符串和过短的词
|
||||||
|
var result []string
|
||||||
|
for _, part := range parts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if len(part) >= 2 { // 至少2个字符
|
||||||
|
result = append(result, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildContent 构建内容描述
|
||||||
|
func (p *FeikuaiPlugin) buildContent(item FeikuaiAPIItem, torrent FeikuaiTorrent) string {
|
||||||
|
var contentParts []string
|
||||||
|
|
||||||
|
// 文件名
|
||||||
|
contentParts = append(contentParts, fmt.Sprintf("文件名: %s", torrent.Name))
|
||||||
|
|
||||||
|
// 文件大小
|
||||||
|
contentParts = append(contentParts, fmt.Sprintf("大小: %.2f GB", torrent.SizeGB))
|
||||||
|
|
||||||
|
// 做种数和下载数
|
||||||
|
contentParts = append(contentParts, fmt.Sprintf("做种: %d", torrent.Seeders))
|
||||||
|
contentParts = append(contentParts, fmt.Sprintf("下载: %d", torrent.Leechers))
|
||||||
|
|
||||||
|
// 发布时间(人类可读格式)
|
||||||
|
if torrent.PublishedAgo != "" {
|
||||||
|
contentParts = append(contentParts, fmt.Sprintf("发布: %s", torrent.PublishedAgo))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(contentParts, " | ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractTags 提取标签
|
||||||
|
func (p *FeikuaiPlugin) extractTags(title, fileName string) []string {
|
||||||
|
var tags []string
|
||||||
|
combinedText := strings.ToUpper(title + " " + fileName)
|
||||||
|
|
||||||
|
// 分辨率标签
|
||||||
|
if strings.Contains(combinedText, "2160P") || strings.Contains(combinedText, "4K") {
|
||||||
|
tags = append(tags, "4K")
|
||||||
|
} else if strings.Contains(combinedText, "1080P") {
|
||||||
|
tags = append(tags, "1080P")
|
||||||
|
} else if strings.Contains(combinedText, "720P") {
|
||||||
|
tags = append(tags, "720P")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编码格式
|
||||||
|
if strings.Contains(combinedText, "H265") || strings.Contains(combinedText, "HEVC") {
|
||||||
|
tags = append(tags, "H265")
|
||||||
|
} else if strings.Contains(combinedText, "H264") || strings.Contains(combinedText, "AVC") {
|
||||||
|
tags = append(tags, "H264")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HDR标签
|
||||||
|
if strings.Contains(combinedText, "HDR") {
|
||||||
|
tags = append(tags, "HDR")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 60帧
|
||||||
|
if strings.Contains(combinedText, "60FPS") || strings.Contains(combinedText, "60HZ") {
|
||||||
|
tags = append(tags, "60fps")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePublishedTime 解析发布时间
|
||||||
|
func (p *FeikuaiPlugin) parsePublishedTime(timeStr string) time.Time {
|
||||||
|
if timeStr == "" {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析ISO 8601格式: "2025-11-18 00:54:20.659664+00"
|
||||||
|
layouts := []string{
|
||||||
|
"2006-01-02 15:04:05.999999-07",
|
||||||
|
"2006-01-02 15:04:05.999999+07",
|
||||||
|
"2006-01-02 15:04:05-07",
|
||||||
|
"2006-01-02 15:04:05+07",
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, layout := range layouts {
|
||||||
|
if t, err := time.Parse(layout, timeStr); err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析失败,返回当前时间
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// doRequestWithRetry 带重试的HTTP请求
|
||||||
|
func (p *FeikuaiPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*http.Response, error) {
|
||||||
|
maxRetries := 3
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
for i := 0; i < maxRetries; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
// 指数退避
|
||||||
|
backoff := time.Duration(1<<uint(i-1)) * 200 * time.Millisecond
|
||||||
|
time.Sleep(backoff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 克隆请求避免并发问题
|
||||||
|
reqClone := req.Clone(req.Context())
|
||||||
|
|
||||||
|
resp, err := client.Do(reqClone)
|
||||||
|
if err == nil {
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
lastErr = fmt.Errorf("HTTP状态码: %d", resp.StatusCode)
|
||||||
|
} else {
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("[%s] 重试 %d 次后仍然失败: %w", p.Name(), maxRetries, lastErr)
|
||||||
|
}
|
||||||
446
plugin/feikuai/html结构分析.md
Normal file
446
plugin/feikuai/html结构分析.md
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
# Feikuai网站 (飞快TV) HTML结构分析
|
||||||
|
|
||||||
|
## 网站信息
|
||||||
|
|
||||||
|
- **网站名称**: 飞快TV
|
||||||
|
- **域名**: `feikuai.tv`
|
||||||
|
- **搜索URL格式**: `https://feikuai.tv/vodsearch/-------------.html?wd={关键词}`
|
||||||
|
- **详情页URL格式**: `https://feikuai.tv/voddetail/{ID}.html`
|
||||||
|
- **主要特点**: 影视网盘资源站,支持多种网盘类型下载
|
||||||
|
|
||||||
|
## 搜索结果页面结构
|
||||||
|
|
||||||
|
搜索结果页面的主要内容位于 `.module-items.module-card-items` 元素内,每个搜索结果项包含在 `.module-card-item.module-item` 元素中。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="module-main module-page" id="ajaxRoot">
|
||||||
|
<div class="module-items module-card-items" id="resultList">
|
||||||
|
<div class="module-card-item module-item">
|
||||||
|
<!-- 单个搜索结果 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 单个搜索结果结构
|
||||||
|
|
||||||
|
每个搜索结果包含以下主要元素:
|
||||||
|
|
||||||
|
#### 1. 分类标签
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="module-card-item-class">剧集</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- 类型:电影、剧集、综艺、动漫
|
||||||
|
|
||||||
|
#### 2. 封面图片和详情页链接
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a href="/voddetail/157546.html" class="module-card-item-poster">
|
||||||
|
<div class="module-item-cover">
|
||||||
|
<div class="module-item-note">30集完结</div>
|
||||||
|
<div class="module-item-douban">豆瓣:9.3分</div>
|
||||||
|
<div class="module-item-pic">
|
||||||
|
<img class="lazy lazyload"
|
||||||
|
data-original="/upload/vod/20250727-1/5a8143a6b2e3fea89e11df8090bbdeff.jpg"
|
||||||
|
alt="凡人修仙传"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
src="/upload/mxprocms/20250310-1/4dd2e7fd412a71590c02b9514bf1805c.gif">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **详情页链接**: 从 `<a>` 标签的 `href` 属性提取
|
||||||
|
- **资源ID**: 从URL中提取(如 `157546`)
|
||||||
|
- **更新状态**: `.module-item-note` 包含集数信息
|
||||||
|
- **豆瓣评分**: `.module-item-douban` 包含评分(可选)
|
||||||
|
- **封面图片**: `img` 标签的 `data-original` 属性
|
||||||
|
|
||||||
|
#### 3. 标题和基本信息
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="module-card-item-info">
|
||||||
|
<div class="module-card-item-title">
|
||||||
|
<a href="/voddetail/157546.html"><strong>凡人修仙传</strong></a>
|
||||||
|
</div>
|
||||||
|
<div class="module-info-item">
|
||||||
|
<div class="module-info-item-content">2025 <span class="slash">/</span>中国大陆 <span class="slash">/</span> 奇幻,古装</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-info-item">
|
||||||
|
<div class="module-info-item-content">杨洋,金晨,汪铎,赵小棠,...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **标题**: `.module-card-item-title strong` 的文本内容
|
||||||
|
- **年份/地区/类型**: 第一个 `.module-info-item-content` 包含,用 `/` 分隔
|
||||||
|
- **演员信息**: 第二个 `.module-info-item-content` 包含演员列表
|
||||||
|
|
||||||
|
#### 4. 操作按钮
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="module-card-item-footer">
|
||||||
|
<a href="/vodplay/157546-1-1.html" class="play-btn icon-btn">
|
||||||
|
<i class="icon-play"></i><span>播放</span>
|
||||||
|
</a>
|
||||||
|
<a href="/voddetail/157546.html" class="play-btn-o"><span>详情</span></a>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 搜索结果数量
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="module-heading-search-result">
|
||||||
|
搜索 "<strong>凡人修仙传</strong>",
|
||||||
|
找到 <strong class="mac_total">26</strong> <span class="mac_suffix">部影片</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **搜索关键词**: `.module-heading-search-result strong` (第一个)
|
||||||
|
- **结果数量**: `.mac_total` 的文本内容
|
||||||
|
|
||||||
|
### 分页结构
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="resultPaging">
|
||||||
|
<div id="page">
|
||||||
|
<a href="/vodsearch/%E5%87%A1%E4%BA%BA%E4%BF%AE%E4%BB%99%E4%BC%A0----------1---.html" class="page-link page-previous">首页</a>
|
||||||
|
<span class="page-link page-number page-current display">1</span>
|
||||||
|
<a href="/vodsearch/%E5%87%A1%E4%BA%BA%E4%BF%AE%E4%BB%99%E4%BC%A0----------2---.html" class="page-link page-number display">2</a>
|
||||||
|
<a href="/vodsearch/%E5%87%A1%E4%BA%BA%E4%BF%AE%E4%BB%99%E4%BC%A0----------2---.html" class="page-link page-next">下一页</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 详情页面结构
|
||||||
|
|
||||||
|
### 1. 基本信息区域
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="module module-info">
|
||||||
|
<div class="module-main">
|
||||||
|
<div class="module-info-poster">
|
||||||
|
<div class="module-item-cover">
|
||||||
|
<div class="module-item-pic">
|
||||||
|
<img class="ls-is-cached lazy lazyload"
|
||||||
|
data-original="/upload/vod/20250727-1/5a8143a6b2e3fea89e11df8090bbdeff.jpg"
|
||||||
|
alt="凡人修仙传">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-info-main">
|
||||||
|
<div class="module-info-heading">
|
||||||
|
<h1>凡人修仙传</h1>
|
||||||
|
<div class="module-info-tag">
|
||||||
|
<div class="module-info-tag-link"><a title="2025" href="/vodshow/13-----------2025.html">2025</a></div>
|
||||||
|
<div class="module-info-tag-link"><a title="中国大陆" href="/vodshow/13-%E4%B8%AD%E5%9B%BD%E5%A4%A7%E9%99%86----------.html">中国大陆</a></div>
|
||||||
|
<div class="module-info-tag-link">
|
||||||
|
<a href="/vodshow/13---%E5%A5%87%E5%B9%BB--------.html">奇幻</a><span class="slash">/</span>
|
||||||
|
<a href="/vodshow/13---%E5%8F%A4%E8%A3%85--------.html">古装</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **标题**: `h1` 标签的文本内容
|
||||||
|
- **年份**: 第一个 `.module-info-tag-link a` 的 `title` 属性
|
||||||
|
- **地区**: 第二个 `.module-info-tag-link a` 的 `title` 属性
|
||||||
|
- **类型**: 第三个 `.module-info-tag-link` 内的所有 `a` 标签文本
|
||||||
|
|
||||||
|
### 2. 详细信息
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="module-info-content">
|
||||||
|
<div class="module-info-items">
|
||||||
|
<div class="module-info-item module-info-introduction">
|
||||||
|
<div class="module-info-introduction-content">
|
||||||
|
<p>该剧改编自忘语的同名小说...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-info-item">
|
||||||
|
<span class="module-info-item-title">导演:</span>
|
||||||
|
<div class="module-info-item-content">
|
||||||
|
<a href="/vodsearch/-----%E6%9D%A8%E9%98%B3--------.html" target="_blank">杨阳</a><span class="slash">/</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-info-item">
|
||||||
|
<span class="module-info-item-title">主演:</span>
|
||||||
|
<div class="module-info-item-content">
|
||||||
|
<a href="/vodsearch/-%E6%9D%A8%E6%B4%8B------------.html" target="_blank">杨洋</a><span class="slash">/</span>
|
||||||
|
<a href="/vodsearch/-%E9%87%91%E6%99%A8------------.html" target="_blank">金晨</a><span class="slash">/</span>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **剧情简介**: `.module-info-introduction-content p` 的文本内容
|
||||||
|
- **导演**: 查找包含 "导演:" 的 `.module-info-item-title`,然后提取 `.module-info-item-content` 中的演员链接
|
||||||
|
- **主演**: 查找包含 "主演:" 的 `.module-info-item-title`,然后提取 `.module-info-item-content` 中的演员链接
|
||||||
|
|
||||||
|
### 3. 网盘下载链接区域 ⭐ 核心
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="module" id="download-list" name="download-list">
|
||||||
|
<div class="module-heading player-heading">
|
||||||
|
<h2 class="module-title">影片下载</h2>
|
||||||
|
<div class="module-tab">
|
||||||
|
<div class="module-tab-items">
|
||||||
|
<div class="module-tab-items-box hisSwiper" id="y-downList">
|
||||||
|
<div class="module-tab-item tab-item selected active"
|
||||||
|
data-index="3"
|
||||||
|
data-dropdown-value="百度网盘">
|
||||||
|
<span>百度网盘</span>
|
||||||
|
<small>1</small>
|
||||||
|
</div>
|
||||||
|
<div class="module-tab-item tab-item"
|
||||||
|
data-index="2"
|
||||||
|
data-dropdown-value="夸克网盘">
|
||||||
|
<span>夸克网盘</span>
|
||||||
|
<small>1</small>
|
||||||
|
</div>
|
||||||
|
<!-- 更多网盘类型... -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 网盘类型标签
|
||||||
|
|
||||||
|
- **网盘类型**: `.module-tab-item span` 的文本内容
|
||||||
|
- **数量**: `.module-tab-item small` 的文本内容
|
||||||
|
- **网盘标识**: `data-dropdown-value` 属性或 `span` 文本
|
||||||
|
|
||||||
|
支持的网盘/链接类型:
|
||||||
|
- 百度网盘 (`baidu`)
|
||||||
|
- 夸克网盘 (`quark`)
|
||||||
|
- 迅雷云盘 (`xunlei`)
|
||||||
|
- 阿里云盘 (`aliyun`)
|
||||||
|
- 天翼云盘 (`tianyi`)
|
||||||
|
- UC网盘 (`uc`)
|
||||||
|
- 115网盘 (`115`)
|
||||||
|
- 123云盘 (`123`)
|
||||||
|
- 移动云盘 (`mobile`)
|
||||||
|
- 磁力链接 (`magnet`)
|
||||||
|
|
||||||
|
#### 下载链接列表
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="module-list module-player-list sort-list module-downlist">
|
||||||
|
<div class="tab-content selected" id="tab-content-3">
|
||||||
|
<div class="module-row-info">
|
||||||
|
<a class="module-row-text copy"
|
||||||
|
href="https://pan.baidu.com/s/1u9aaXsTkL1GdOMIH9qnPCA?pwd=B5B3"
|
||||||
|
target="_blank"
|
||||||
|
title="下载《凡人修仙传》">
|
||||||
|
<i class="icon-video-file"></i>
|
||||||
|
<div class="module-row-title-dlist">
|
||||||
|
<h4>凡人修仙传(2025)4K 高码率 更至EP169@一键搜片-2025-11-16 18:55:25</h4>
|
||||||
|
<p>https://pan.baidu.com/s/1u9aaXsTkL1GdOMIH9qnPCA?pwd=B5B3</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-content-2">
|
||||||
|
<div class="module-row-info">
|
||||||
|
<a class="module-row-text copy"
|
||||||
|
href="https://pan.quark.cn/s/063ce74fbf41"
|
||||||
|
target="_blank"
|
||||||
|
title="下载《凡人修仙传》">
|
||||||
|
<i class="icon-video-file"></i>
|
||||||
|
<div class="module-row-title-dlist">
|
||||||
|
<h4>凡人修仙传:外海风云篇 4K [更新至169集]@一键搜片-2025-11-16 18:55:25</h4>
|
||||||
|
<p>https://pan.quark.cn/s/063ce74fbf41</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-content-6">
|
||||||
|
<div class="module-row-info">
|
||||||
|
<a class="module-row-text copy"
|
||||||
|
href="magnet:?xt=urn:btih:C3A3A53C2408396D64450046361F00650CB9E53E&dn=Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv&xl=2458041664"
|
||||||
|
target="_blank"
|
||||||
|
title="下载《唐朝诡事录之长安》">
|
||||||
|
<i class="icon-video-file"></i>
|
||||||
|
<div class="module-row-title-dlist">
|
||||||
|
<h4>Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv · 2.29GB@一键搜片-2025-11-18 17:09:52</h4>
|
||||||
|
<p>magnet:?xt=urn:btih:C3A3A53C2408396D64450046361F00650CB9E53E&dn=Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv&xl=2458041664</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 链接数据提取
|
||||||
|
|
||||||
|
- **下载链接URL**: `.module-row-text` 的 `href` 属性 或 `.module-row-title-dlist p` 的文本内容
|
||||||
|
- **网盘/链接类型**: 根据链接URL自动识别
|
||||||
|
- 网盘链接:`baidu`, `quark`, `aliyun`, `xunlei`, `tianyi`, `uc`, `115`, `123`, `mobile`
|
||||||
|
- 磁力链接:`magnet:?xt=urn:btih:` 开头识别为 `magnet`
|
||||||
|
|
||||||
|
- **独立标题** (⭐ 重要 - 对应API的 `work_title` 字段):
|
||||||
|
- **基础提取**: 从 `.module-row-title-dlist h4` 提取文本内容
|
||||||
|
- **清洗处理**:
|
||||||
|
1. 去除末尾的日期时间部分(`@来源-日期 时间`)
|
||||||
|
2. 去除文件扩展名(如 `.mkv`, `.mp4` 等)
|
||||||
|
3. 去除文件大小信息(如 `· 2.29GB`)
|
||||||
|
- **标题拼接规则** (关键):
|
||||||
|
- 检查清洗后的独立标题是否包含详情页主标题的关键词
|
||||||
|
- **判断方法**: 将详情页标题分词,检查独立标题中是否包含任一关键词(忽略标点和空格)
|
||||||
|
- **需要拼接**: 如果不包含关键词,则拼接格式为 `{详情页主标题}-{独立标题}`
|
||||||
|
- **无需拼接**: 如果包含关键词,直接使用独立标题
|
||||||
|
- **示例**:
|
||||||
|
- 网盘链接:`凡人修仙传(2025)4K 高码率 更至EP169@一键搜片-2025-11-16 18:55:25`
|
||||||
|
→ 清洗后:`凡人修仙传(2025)4K 高码率 更至EP169`
|
||||||
|
→ 包含关键词"凡人修仙传",无需拼接
|
||||||
|
- 磁力链接:`Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv · 2.29GB@一键搜片-2025-11-18 17:09:52`
|
||||||
|
→ 详情页标题:`唐朝诡事录之长安`
|
||||||
|
→ 清洗后:`Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV`
|
||||||
|
→ 不包含关键词,需要拼接
|
||||||
|
→ 最终:`唐朝诡事录之长安-Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV`
|
||||||
|
|
||||||
|
- **日期提取** (对应API的 `datetime` 字段):
|
||||||
|
- 从独立标题中提取日期时间信息
|
||||||
|
- 日期格式:`@来源-YYYY-MM-DD HH:mm:ss`
|
||||||
|
- 正则表达式:`@[^-]+-(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})`
|
||||||
|
- 示例:从 `@一键搜片-2025-11-16 18:55:25` 提取 `2025-11-16 18:55:25`
|
||||||
|
|
||||||
|
## 提取逻辑
|
||||||
|
|
||||||
|
### 搜索结果页面提取逻辑
|
||||||
|
|
||||||
|
1. 定位所有的 `.module-card-item.module-item` 元素
|
||||||
|
2. 对于每个元素:
|
||||||
|
- 从 `.module-card-item-poster` 的 `href` 属性提取详情页链接
|
||||||
|
- 从链接中提取资源ID(如 `157546`)
|
||||||
|
- 从 `.module-card-item-title strong` 提取标题
|
||||||
|
- 从 `.module-card-item-class` 提取分类
|
||||||
|
- 从 `.module-item-note` 提取更新状态
|
||||||
|
- 从 `.module-item-douban` 提取豆瓣评分(可选)
|
||||||
|
- 从第一个 `.module-info-item-content` 提取年份/地区/类型
|
||||||
|
- 从第二个 `.module-info-item-content` 提取演员列表
|
||||||
|
- 从 `img` 的 `data-original` 属性提取封面图片URL
|
||||||
|
|
||||||
|
### 详情页面提取逻辑
|
||||||
|
|
||||||
|
1. 获取资源基本信息:
|
||||||
|
- 标题:`h1` 的文本内容
|
||||||
|
- 年份:第一个 `.module-info-tag-link a[title]` 的 `title` 属性
|
||||||
|
- 地区:第二个 `.module-info-tag-link a[title]` 的 `title` 属性
|
||||||
|
- 类型:第三个 `.module-info-tag-link` 内的所有 `a` 标签文本
|
||||||
|
- 封面图片:`.module-info-poster img` 的 `data-original` 属性
|
||||||
|
|
||||||
|
2. 提取详细信息:
|
||||||
|
- 剧情简介:`.module-info-introduction-content p` 的文本内容
|
||||||
|
- 导演:查找包含 "导演:" 的 `.module-info-item`,提取其中的 `a` 标签文本
|
||||||
|
- 主演:查找包含 "主演:" 的 `.module-info-item`,提取其中的 `a` 标签文本
|
||||||
|
|
||||||
|
3. 提取下载链接(⭐ 核心):
|
||||||
|
- 遍历所有 `.module-tab-item`,获取网盘类型和数量
|
||||||
|
- 对应每个 `.tab-content`,提取其中的 `.module-row-info`
|
||||||
|
- 对每个 `.module-row-info`:
|
||||||
|
- **链接URL**: 从 `.module-row-text` 的 `href` 属性或 `.module-row-title-dlist p` 提取
|
||||||
|
- **链接类型**: 根据链接URL自动识别(网盘类型或 `magnet`)
|
||||||
|
- **原始标题**: 从 `.module-row-title-dlist h4` 提取完整文本
|
||||||
|
- **独立标题** (`work_title`):
|
||||||
|
1. 清洗原始标题(去除日期、扩展名、文件大小)
|
||||||
|
2. 检查是否包含详情页主标题关键词
|
||||||
|
3. 如不包含,拼接为 `{详情页主标题}-{清洗后标题}`
|
||||||
|
- **日期时间** (`datetime`): 从原始标题中提取日期,使用正则 `@[^-]+-(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})`
|
||||||
|
- **密码**: 从URL参数中提取(如 `?pwd=xxx` 或 `?password=xxx`,仅适用于部分网盘)
|
||||||
|
|
||||||
|
## 网盘链接和磁力链接格式
|
||||||
|
|
||||||
|
| 类型 | URL特征 | 密码格式 |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| 百度网盘 | `pan.baidu.com` | `?pwd=` 参数 |
|
||||||
|
| 夸克网盘 | `pan.quark.cn` | 无密码或单独提供 |
|
||||||
|
| 阿里云盘 | `alipan.com` 或 `aliyundrive.com` | 无密码 |
|
||||||
|
| 迅雷网盘 | `pan.xunlei.com` | `?pwd=` 参数 |
|
||||||
|
| 天翼云盘 | `cloud.189.cn` | 无密码 |
|
||||||
|
| UC网盘 | `drive.uc.cn` | 无密码 |
|
||||||
|
| 115网盘 | `115cdn.com` | `?password=` 参数 |
|
||||||
|
| 123网盘 | `123684.com`, `123685.com`, `123912.com` | 无密码 |
|
||||||
|
| 移动云盘 | `caiyun.139.com` | 无密码 |
|
||||||
|
| 磁力链接 | `magnet:?xt=urn:btih:` | 无密码 |
|
||||||
|
|
||||||
|
## API字段映射
|
||||||
|
|
||||||
|
根据README的API文档,Link对象字段映射关系:
|
||||||
|
|
||||||
|
| API字段 | HTML提取位置 | 提取方法 | 示例 |
|
||||||
|
|---------|------------|---------|------|
|
||||||
|
| `type` | 链接URL | 自动识别URL特征 | `baidu`, `quark`, `tianyi`, `magnet` 等 |
|
||||||
|
| `url` | `.module-row-title-dlist p` 或 `href` | 文本内容或属性值 | `https://pan.baidu.com/s/xxx` 或 `magnet:?xt=...` |
|
||||||
|
| `password` | 链接URL参数 | 提取 `?pwd=` 或 `?password=` | `B5B3`, `yyds` (仅部分网盘) |
|
||||||
|
| `datetime` | `.module-row-title-dlist h4` | 正则提取日期时间 | `2025-11-16 18:55:25` |
|
||||||
|
| `work_title` | `.module-row-title-dlist h4` + 详情页主标题 | 清洗+关键词检查+拼接 | 见下方详细说明 |
|
||||||
|
|
||||||
|
**`work_title` 字段详细处理流程**:
|
||||||
|
|
||||||
|
1. **提取原始标题**: 从 `.module-row-title-dlist h4` 获取完整文本
|
||||||
|
- 示例1: `凡人修仙传(2025)4K 高码率 更至EP169@一键搜片-2025-11-16 18:55:25`
|
||||||
|
- 示例2: `Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv · 2.29GB@一键搜片-2025-11-18 17:09:52`
|
||||||
|
|
||||||
|
2. **清洗标题**:
|
||||||
|
- 去除日期时间部分: 删除 `@来源-日期 时间` 格式的后缀
|
||||||
|
- 去除文件扩展名: 删除 `.mkv`, `.mp4`, `.avi` 等
|
||||||
|
- 去除文件大小: 删除 `· 2.29GB` 等文件大小信息
|
||||||
|
- 清洗结果1: `凡人修仙传(2025)4K 高码率 更至EP169`
|
||||||
|
- 清洗结果2: `Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV`
|
||||||
|
|
||||||
|
3. **关键词检查与拼接**:
|
||||||
|
- 获取详情页主标题(如 `唐朝诡事录之长安`)
|
||||||
|
- 将主标题分词,提取关键词(忽略标点符号和空格)
|
||||||
|
- 检查清洗后的独立标题是否包含任一关键词
|
||||||
|
- **包含关键词**: 直接使用清洗后的标题
|
||||||
|
- 示例: `凡人修仙传(2025)4K 高码率 更至EP169` (包含"凡人修仙传")
|
||||||
|
- **不包含关键词**: 拼接格式为 `{详情页主标题}-{清洗后标题}`
|
||||||
|
- 示例: `唐朝诡事录之长安-Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV`
|
||||||
|
|
||||||
|
**其他字段说明**:
|
||||||
|
- `datetime`: 从原始 `h4` 标题中提取的时间戳,格式为 `YYYY-MM-DD HH:mm:ss`
|
||||||
|
- `password`: 部分网盘(百度、迅雷、115)的密码在URL参数中,需要单独提取;磁力链接无密码
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **图片延迟加载**: 封面图片使用了 `lazy lazyload` 类,实际图片URL在 `data-original` 属性中
|
||||||
|
|
||||||
|
2. **资源ID提取**: 从URL中提取ID的正则表达式:`/voddetail/(\d+)\.html`
|
||||||
|
|
||||||
|
3. **链接类型识别**:
|
||||||
|
- 网盘链接:通过域名识别(`pan.baidu.com`, `pan.quark.cn` 等)
|
||||||
|
- 磁力链接:通过 `magnet:?xt=urn:btih:` 前缀识别
|
||||||
|
|
||||||
|
4. **网盘链接密码**: 某些网盘的密码包含在URL参数中(如 `?pwd=B5B3`),需要分离链接和密码;磁力链接无密码
|
||||||
|
|
||||||
|
5. **独立标题处理** (⭐ 核心重点):
|
||||||
|
- 每个链接都有独立的 `h4` 标题,必须单独提取
|
||||||
|
- 需要清洗标题(去除日期、扩展名、文件大小)
|
||||||
|
- **关键词检查**: 必须检查清洗后标题是否包含详情页主标题的关键词
|
||||||
|
- **拼接规则**: 不包含关键词时,需拼接为 `{详情页主标题}-{清洗后标题}`
|
||||||
|
- 特别注意磁力链接的标题通常是英文文件名,大概率需要拼接中文标题
|
||||||
|
|
||||||
|
6. **日期时间提取** (重要):
|
||||||
|
- 从 `h4` 标题末尾提取日期时间
|
||||||
|
- 格式为 `@来源-YYYY-MM-DD HH:mm:ss`
|
||||||
|
- 正则表达式: `@[^-]+-(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})`
|
||||||
|
|
||||||
|
7. **多链接支持**: 一个资源可能有多个网盘和磁力链接,每个链接都有独立的标题、时间和密码
|
||||||
|
|
||||||
|
8. **分页处理**: 搜索结果有分页,URL格式为 `/vodsearch/{关键词}----------{页码}---.html`
|
||||||
|
|
||||||
|
9. **AJAX加载**: 网站使用AJAX动态加载搜索结果,需要注意异步请求处理
|
||||||
|
|
||||||
|
10. **反爬虫**: 图片设置了 `referrerpolicy="no-referrer"`,需要在请求头中处理
|
||||||
299
plugin/feikuai/json结构分析.md
Normal file
299
plugin/feikuai/json结构分析.md
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
# Feikuai API 数据结构分析
|
||||||
|
|
||||||
|
## 基本信息
|
||||||
|
- **数据源类型**: JSON API
|
||||||
|
- **网站名称**: 飞快TV (feikuai.tv)
|
||||||
|
- **API URL格式**: `https://feikuai.tv/t_search/bm_search.php?kw={URL编码的关键词}`
|
||||||
|
- **数据特点**: 磁力链接搜索API,提供结构化的BT/磁力资源数据
|
||||||
|
- **特殊说明**: 专注于磁力链接,包含详细的种子信息(做种数、下载数等)
|
||||||
|
|
||||||
|
## API响应结构
|
||||||
|
|
||||||
|
### 顶层结构
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0, // 状态码:0表示成功
|
||||||
|
"msg": "ok", // 响应消息
|
||||||
|
"keyword": "唐朝诡事录之长安", // 搜索关键词
|
||||||
|
"count": 8, // 搜索结果总数
|
||||||
|
"items": [] // 数据列表数组
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `items`数组中的数据项结构
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content_id": null, // 内容ID(通常为null)
|
||||||
|
"title": "【高清剧集网发布 www.BPHDTV.com】唐朝诡事录之长安[第07-08集][国语音轨+简繁英字幕].2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV",
|
||||||
|
"type": "movie", // 资源类型(通常为movie)
|
||||||
|
"year": null, // 年份(通常为null)
|
||||||
|
"torrents": [] // 磁力链接数组
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `torrents`数组中的种子数据结构
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"info_hash": "c3a3a53c2408396d64450046361f00650cb9e53e", // 种子哈希值
|
||||||
|
"magnet": "magnet:?xt=urn:btih:C3A3A53C2408396D64450046361F00650CB9E53E&dn=Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv&xl=2458041664",
|
||||||
|
"name": "Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv",
|
||||||
|
"size_bytes": 2458041664, // 文件大小(字节)
|
||||||
|
"size_gb": 2.29, // 文件大小(GB)
|
||||||
|
"seeders": 4, // 做种数
|
||||||
|
"leechers": 4, // 下载数
|
||||||
|
"published_at": "2025-11-18 00:54:20.659664+00", // 发布时间(带时区)
|
||||||
|
"published_ago": "约 8 小时前", // 发布时间(人类可读)
|
||||||
|
"file_path": "Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv",
|
||||||
|
"file_ext": "mkv" // 文件扩展名
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 插件所需字段映射
|
||||||
|
|
||||||
|
| 源字段 | 目标字段 | 说明 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| `content_id` 或基于 `info_hash` | `UniqueID` | 格式: `feikuai-{info_hash}` 或 `feikuai-{index}` |
|
||||||
|
| `title` | `Title` | 资源标题(包含发布组信息) |
|
||||||
|
| `title` + `name` + `size_gb` + `seeders` + `leechers` | `Content` | 组合描述信息 |
|
||||||
|
| 从 `title` 或 `name` 提取 | `Tags` | 标签数组(如分辨率、格式等) |
|
||||||
|
| `torrents` | `Links` | 解析为Link数组,每个种子对应一个Link |
|
||||||
|
| `""` | `Channel` | 插件搜索结果Channel为空 |
|
||||||
|
| `published_at` | `Datetime` | 磁力链接发布时间 |
|
||||||
|
|
||||||
|
## 下载链接解析
|
||||||
|
|
||||||
|
### 磁力链接特点
|
||||||
|
- **链接类型**: 全部为 `magnet` 类型
|
||||||
|
- **无需密码**: 磁力链接不需要提取码
|
||||||
|
- **多种子支持**: 一个资源(item)可能包含多个种子(torrents)
|
||||||
|
|
||||||
|
### 磁力链接格式
|
||||||
|
```
|
||||||
|
magnet:?xt=urn:btih:{INFO_HASH}&dn={URL编码的文件名}&xl={文件大小}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
magnet:?xt=urn:btih:C3A3A53C2408396D64450046361F00650CB9E53E&dn=Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv&xl=2458041664
|
||||||
|
```
|
||||||
|
|
||||||
|
### 种子信息提取
|
||||||
|
从 `torrents` 数组中,每个种子可提取以下信息:
|
||||||
|
- **磁力链接**: `magnet` 字段
|
||||||
|
- **文件名**: `name` 或 `file_path` 字段
|
||||||
|
- **文件大小**: `size_gb` (GB) 或 `size_bytes` (字节)
|
||||||
|
- **做种/下载数**: `seeders` / `leechers`
|
||||||
|
- **发布时间**: `published_at` 或 `published_ago`
|
||||||
|
|
||||||
|
## work_title 处理规则
|
||||||
|
|
||||||
|
根据HTML结构分析中的规则,需要对每个磁力链接的标题进行处理:
|
||||||
|
|
||||||
|
### 处理流程
|
||||||
|
1. **提取标题**: 从 `name` 或 `file_path` 字段获取文件名
|
||||||
|
2. **清洗标题**:
|
||||||
|
- 去除文件扩展名(`.mkv`, `.mp4` 等)
|
||||||
|
- 去除文件大小信息(如果在文件名中)
|
||||||
|
3. **关键词检查**:
|
||||||
|
- 检查清洗后的文件名是否包含搜索关键词
|
||||||
|
- 或检查是否包含 `title` 字段中的关键词
|
||||||
|
4. **拼接规则**:
|
||||||
|
- **包含关键词**: 直接使用清洗后的文件名
|
||||||
|
- **不包含关键词**: 拼接为 `{搜索关键词}-{清洗后文件名}`
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
**场景1: 英文文件名,不包含中文关键词**
|
||||||
|
```
|
||||||
|
搜索关键词: "唐朝诡事录之长安"
|
||||||
|
文件名: "Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv"
|
||||||
|
清洗后: "Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV"
|
||||||
|
work_title: "唐朝诡事录之长安-Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV"
|
||||||
|
```
|
||||||
|
|
||||||
|
**场景2: 中文文件名,包含关键词**
|
||||||
|
```
|
||||||
|
搜索关键词: "唐朝诡事录之长安"
|
||||||
|
文件名: "唐朝诡事录之长安.Horror.Stories.of.Tang.Dynasty.S03E05.2022.2160p.WEB-DL.H265.DDP5.1-ColorTV.mkv"
|
||||||
|
清洗后: "唐朝诡事录之长安.Horror.Stories.of.Tang.Dynasty.S03E05.2022.2160p.WEB-DL.H265.DDP5.1-ColorTV"
|
||||||
|
work_title: "唐朝诡事录之长安.Horror.Stories.of.Tang.Dynasty.S03E05.2022.2160p.WEB-DL.H265.DDP5.1-ColorTV"
|
||||||
|
(包含关键词,无需拼接)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 插件开发指导
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
```go
|
||||||
|
searchURL := fmt.Sprintf("https://feikuai.tv/t_search/bm_search.php?kw=%s", url.QueryEscape(keyword))
|
||||||
|
```
|
||||||
|
|
||||||
|
### SearchResult构建示例
|
||||||
|
```go
|
||||||
|
// 遍历items
|
||||||
|
for _, item := range apiResponse.Items {
|
||||||
|
// 遍历每个item的torrents
|
||||||
|
for _, torrent := range item.Torrents {
|
||||||
|
// 清洗文件名
|
||||||
|
cleanedName := cleanFileName(torrent.Name)
|
||||||
|
|
||||||
|
// 检查是否包含关键词并拼接
|
||||||
|
workTitle := buildWorkTitle(keyword, cleanedName)
|
||||||
|
|
||||||
|
// 构建SearchResult
|
||||||
|
result := model.SearchResult{
|
||||||
|
UniqueID: fmt.Sprintf("feikuai-%s", torrent.InfoHash),
|
||||||
|
Title: item.Title, // 或使用workTitle
|
||||||
|
Content: buildContent(item, torrent),
|
||||||
|
Links: []model.Link{
|
||||||
|
{
|
||||||
|
Type: "magnet",
|
||||||
|
URL: torrent.Magnet,
|
||||||
|
Password: "", // 磁力链接无密码
|
||||||
|
Datetime: parseTime(torrent.PublishedAt),
|
||||||
|
WorkTitle: workTitle, // ⭐ 重要:独立标题
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tags: extractTags(item.Title, torrent.Name),
|
||||||
|
Channel: "", // 插件搜索结果Channel为空
|
||||||
|
Datetime: parseTime(torrent.PublishedAt),
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键函数示例
|
||||||
|
|
||||||
|
#### 1. 清洗文件名
|
||||||
|
```go
|
||||||
|
func cleanFileName(fileName string) string {
|
||||||
|
// 去除文件扩展名
|
||||||
|
ext := filepath.Ext(fileName)
|
||||||
|
if ext != "" {
|
||||||
|
fileName = strings.TrimSuffix(fileName, ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去除文件大小信息(如果存在)
|
||||||
|
fileName = regexp.MustCompile(`\s*·\s*[\d.]+\s*[KMGT]B\s*$`).ReplaceAllString(fileName, "")
|
||||||
|
|
||||||
|
return strings.TrimSpace(fileName)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 构建work_title
|
||||||
|
```go
|
||||||
|
func buildWorkTitle(keyword, cleanedName string) string {
|
||||||
|
// 检查是否包含关键词(忽略大小写和标点)
|
||||||
|
if containsKeywords(keyword, cleanedName) {
|
||||||
|
return cleanedName
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不包含关键词,需要拼接
|
||||||
|
return fmt.Sprintf("%s-%s", keyword, cleanedName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsKeywords(keyword, text string) bool {
|
||||||
|
// 简单实现:分词后检查
|
||||||
|
keywords := splitKeywords(keyword)
|
||||||
|
for _, kw := range keywords {
|
||||||
|
if strings.Contains(strings.ToLower(text), strings.ToLower(kw)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 构建内容描述
|
||||||
|
```go
|
||||||
|
func buildContent(item FeikuaiAPIItem, torrent Torrent) string {
|
||||||
|
var contentParts []string
|
||||||
|
|
||||||
|
contentParts = append(contentParts, fmt.Sprintf("文件名: %s", torrent.Name))
|
||||||
|
contentParts = append(contentParts, fmt.Sprintf("大小: %.2f GB", torrent.SizeGB))
|
||||||
|
contentParts = append(contentParts, fmt.Sprintf("做种: %d", torrent.Seeders))
|
||||||
|
contentParts = append(contentParts, fmt.Sprintf("下载: %d", torrent.Leechers))
|
||||||
|
contentParts = append(contentParts, fmt.Sprintf("发布: %s", torrent.PublishedAgo))
|
||||||
|
|
||||||
|
return strings.Join(contentParts, " | ")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 提取标签
|
||||||
|
```go
|
||||||
|
func extractTags(title, fileName string) []string {
|
||||||
|
var tags []string
|
||||||
|
|
||||||
|
// 提取分辨率
|
||||||
|
if strings.Contains(title, "2160p") || strings.Contains(fileName, "2160p") {
|
||||||
|
tags = append(tags, "4K")
|
||||||
|
} else if strings.Contains(title, "1080p") || strings.Contains(fileName, "1080p") {
|
||||||
|
tags = append(tags, "1080p")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取编码格式
|
||||||
|
if strings.Contains(title, "H265") || strings.Contains(fileName, "H265") {
|
||||||
|
tags = append(tags, "H265")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取HDR
|
||||||
|
if strings.Contains(title, "HDR") || strings.Contains(fileName, "HDR") {
|
||||||
|
tags = append(tags, "HDR")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. 时间解析
|
||||||
|
```go
|
||||||
|
func parseTime(timeStr string) time.Time {
|
||||||
|
// 解析ISO 8601格式: "2025-11-18 00:54:20.659664+00"
|
||||||
|
t, err := time.Parse("2006-01-02 15:04:05.999999-07", timeStr)
|
||||||
|
if err != nil {
|
||||||
|
// 解析失败,返回当前时间
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API字段映射表
|
||||||
|
|
||||||
|
| API字段 | Link对象字段 | 提取方法 | 示例 |
|
||||||
|
|---------|-------------|---------|------|
|
||||||
|
| `magnet` | `URL` | 直接使用 | `magnet:?xt=urn:btih:...` |
|
||||||
|
| - | `Type` | 固定值 | `magnet` |
|
||||||
|
| - | `Password` | 固定值 | `""` (空字符串) |
|
||||||
|
| `published_at` | `Datetime` | 时间解析 | `2025-11-18T00:54:20Z` |
|
||||||
|
| `name` | `WorkTitle` | 清洗+关键词检查+拼接 | `唐朝诡事录之长安-Strange.Tales...` |
|
||||||
|
|
||||||
|
## 与其他插件的差异
|
||||||
|
|
||||||
|
| 特性 | feikuai | wanou/ouge/zhizhen | huban | 说明 |
|
||||||
|
|------|---------|-------------------|-------|------|
|
||||||
|
| **链接类型** | 仅磁力链接 | 网盘链接 | 网盘链接 | 专注BT资源 |
|
||||||
|
| **多链接** | 一对多 | 多对一 | 多对多 | 一个资源多个种子 |
|
||||||
|
| **种子信息** | 详细 | 无 | 无 | 包含做种数等 |
|
||||||
|
| **work_title** | 必需拼接 | 可选 | 可选 | 文件名通常不含中文 |
|
||||||
|
| **时间信息** | 精确 | 当前时间 | 当前时间 | API提供发布时间 |
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **磁力链接专用**: 此API仅返回磁力链接,不包含网盘链接
|
||||||
|
2. **多种子处理**: 一个资源可能有多个种子,需要全部提取
|
||||||
|
3. **文件名处理**: 文件名通常是英文,需要拼接中文关键词
|
||||||
|
4. **时区处理**: `published_at` 包含时区信息(+00),需要正确解析
|
||||||
|
5. **做种数排序**: 建议按做种数(seeders)降序排序,优先显示热门资源
|
||||||
|
6. **空值处理**: `content_id` 和 `year` 通常为 null,需要处理
|
||||||
|
7. **标题清洗**: `title` 字段包含发布组信息(如【高清剧集网发布 www.BPHDTV.com】),可选择性去除
|
||||||
|
|
||||||
|
## 开发建议
|
||||||
|
|
||||||
|
1. **独立实现**: 不能复用网盘类插件的代码,需要专门处理磁力链接
|
||||||
|
2. **work_title关键**: 文件名拼接中文关键词是核心功能
|
||||||
|
3. **种子排序**: 实现按做种数排序,提升用户体验
|
||||||
|
4. **时间解析**: 正确解析带时区的ISO 8601时间格式
|
||||||
|
5. **内容丰富**: 充分利用API提供的文件大小、做种数等信息
|
||||||
|
6. **错误处理**: API可能返回 `code != 0` 的错误状态
|
||||||
|
7. **测试覆盖**: 重点测试中英文文件名的work_title拼接逻辑
|
||||||
Reference in New Issue
Block a user