新增插件feikuai

This commit is contained in:
www.xueximeng.com
2025-11-18 18:11:53 +08:00
parent 28d566771d
commit 3d61e6e33e
5 changed files with 1550 additions and 2 deletions

View File

@@ -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层过滤控制详解
#### 构造函数选择 #### 构造函数选择

View File

@@ -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
View 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)
}

View 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>凡人修仙传20254K 高码率 更至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`
- **标题拼接规则** (关键):
- 检查清洗后的独立标题是否包含详情页主标题的关键词
- **判断方法**: 将详情页标题分词,检查独立标题中是否包含任一关键词(忽略标点和空格)
- **需要拼接**: 如果不包含关键词,则拼接格式为 `{详情页主标题}-{独立标题}`
- **无需拼接**: 如果包含关键词,直接使用独立标题
- **示例**:
- 网盘链接:`凡人修仙传20254K 高码率 更至EP169@一键搜片-2025-11-16 18:55:25`
→ 清洗后:`凡人修仙传20254K 高码率 更至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: `凡人修仙传20254K 高码率 更至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: `凡人修仙传20254K 高码率 更至EP169`
- 清洗结果2: `Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV`
3. **关键词检查与拼接**:
- 获取详情页主标题(如 `唐朝诡事录之长安`
- 将主标题分词,提取关键词(忽略标点符号和空格)
- 检查清洗后的独立标题是否包含任一关键词
- **包含关键词**: 直接使用清洗后的标题
- 示例: `凡人修仙传20254K 高码率 更至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"`,需要在请求头中处理

View 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拼接逻辑