From b514c5635b70df6d0707b97e8f47e05985ce1ceb Mon Sep 17 00:00:00 2001 From: "www.xueximeng.com" Date: Mon, 27 Oct 2025 18:28:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8F=92=E4=BB=B6nsgame?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- main.go | 1 + plugin/nsgame/json结构分析.md | 335 ++++++++++++++++++++++++++++++++++ plugin/nsgame/nsgame.go | 299 ++++++++++++++++++++++++++++++ 4 files changed, 636 insertions(+), 1 deletion(-) create mode 100644 plugin/nsgame/json结构分析.md create mode 100644 plugin/nsgame/nsgame.go diff --git a/README.md b/README.md index 9417385..b39b0f3 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ susu,thepiratebay,wanou,xuexizhinan,panyq,zhizhen,labi,muou,ouge,shandian, duoduo,huban,cyg,erxiao,miaoso,fox4k,pianku,clmao,wuji,cldi,xiaozhang, libvio,leijing,xb6v,xys,ddys,hdmoli,yuhuage,u3c3,javdb,clxiong,jutoushe, sdso,xiaoji,xdyh,haisou,bixin,djgou,nyaa,xinjuc,aikanzy,qupanshe,xdpan, -discourse,yunsou,ahhhhfs +discourse,yunsou,ahhhhfs,nsgame diff --git a/main.go b/main.go index de2667b..88f114f 100644 --- a/main.go +++ b/main.go @@ -79,6 +79,7 @@ import ( _ "pansou/plugin/discourse" _ "pansou/plugin/yunsou" _ "pansou/plugin/ahhhhfs" + _ "pansou/plugin/nsgame" ) // 全局缓存写入管理器 diff --git a/plugin/nsgame/json结构分析.md b/plugin/nsgame/json结构分析.md new file mode 100644 index 0000000..863e9bb --- /dev/null +++ b/plugin/nsgame/json结构分析.md @@ -0,0 +1,335 @@ +# NSGame API JSON 结构分析 + +## 概述 + +NSGame (NS游戏网) 是一个专门提供 Nintendo Switch 游戏资源的搜索平台,提供 RESTful API 接口进行游戏资源搜索。本文档详细说明 NSGame API 的请求格式和响应结构。 + +## API 接口信息 + +### 请求地址 +- **URL**: `https://nsthwj.com/thwj/game/query` +- **方法**: GET +- **参数**: + - `pageNum`: 页码(从1开始) + - `pageSize`: 每页大小(建议100) + - `type`: 游戏类型(可选,空字符串表示全部) + - `queryName`: 搜索关键词(URL编码) + +### 请求示例 +``` +GET https://nsthwj.com/thwj/game/query?pageNum=1&pageSize=100&type=&queryName=%E9%A9%AC%E9%87%8C%E5%A5%A5 +``` + +### 请求头设置 +```http +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 +Accept: application/json, text/plain, */* +Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 +Referer: https://nsthwj.com/ +``` + +## 响应数据结构 + +### 根级响应结构 + +```json +{ + "success": true, + "data": { + "pageData": { + "totalCount": 27, + "pageNum": 0, + "data": [] + }, + "pageView": null + }, + "code": "200", + "message": null +} +``` + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `success` | boolean | 请求是否成功 | +| `data` | object | 响应数据对象 | +| `code` | string | 状态码,"200"表示成功 | +| `message` | string/null | 错误消息,成功时为null | + +### data 对象结构 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `pageData` | object | 分页数据对象 | +| `pageView` | null | 页面视图信息(通常为null) | + +### pageData 对象结构 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `totalCount` | number | 搜索结果总数 | +| `pageNum` | number | 当前页码 | +| `data` | array | 游戏资源列表 | + +### data 数组中的游戏资源项结构 + +每个游戏资源项包含以下字段: + +```json +{ + "name": "马里奥奥德赛|Super Mario Odyssey中文", + "url": "https://pan.baidu.com/s/1ZNTxWN-Vn7TUb6vq0QoIVA?pwd=thwj\n[夸克网盘]:https://pan.quark.cn/s/2dab74360187\n[UC网盘]:https://drive.uc.cn/s/843e8385fbb34", + "password": "最新版本:1.4.1\n含1.4.1金手指" +} +``` + +| 字段名 | 类型 | 必填 | 示例值 | 说明 | +|--------|------|------|--------|------| +| `name` | string | ✓ | `"马里奥奥德赛\|Super Mario Odyssey中文"` | 游戏名称(中文\|英文) | +| `url` | string | ✓ | 多行链接文本 | 网盘链接(换行符分隔) | +| `password` | string | ✓ | `"最新版本:1.4.1\n含1.4.1金手指"` | 版本信息和金手指说明 | + +## 特殊数据格式说明 + +### 1. url 字段格式 ⭐ 重要 + +`url` 字段包含多个网盘链接,使用换行符 `\n` 分隔: + +``` +https://pan.baidu.com/s/1ZNTxWN-Vn7TUb6vq0QoIVA?pwd=thwj +[夸克网盘]:https://pan.quark.cn/s/2dab74360187 +[UC网盘]:https://drive.uc.cn/s/843e8385fbb34 +``` + +**格式规则**: +- **百度网盘**: 直接链接,密码在URL参数中 `?pwd=xxxx` +- **夸克网盘**: 格式 `[夸克网盘]:{链接}`,无密码 +- **UC网盘**: 格式 `[UC网盘]:{链接}`,无密码 + +**提取方法**: +1. 按 `\n` 分割字符串 +2. 逐行解析链接 +3. 识别链接类型并提取 + +### 2. password 字段格式 + +`password` 字段实际上不是网盘提取码,而是游戏版本信息: + +``` +最新版本:1.4.1 +含1.4.1金手指 +``` + +**内容说明**: +- 第一行:游戏的最新版本号 +- 第二行:金手指信息(如果有) + +### 3. name 字段格式 + +游戏名称使用竖线 `|` 分隔中英文: + +``` +马里奥奥德赛|Super Mario Odyssey中文 +``` + +**格式规则**: +- 中文名称 `|` 英文名称 +- 可能包含语言标识(中文、汉化等) + +## 支持的网盘平台 + +| 平台标识 | 平台名称 | 域名特征 | 密码位置 | +|----------|----------|----------|----------| +| `baidu` | 百度网盘 | `pan.baidu.com` | URL参数 `?pwd=` | +| `quark` | 夸克网盘 | `pan.quark.cn` | 无密码 | +| `uc` | UC网盘 | `drive.uc.cn` | 无密码 | + +## 插件实现要点 + +### 1. 插件配置 +- **插件名称**: `nsgame` +- **优先级**: 建议设置为 2(高质量游戏资源) +- **Service层过滤**: 启用(标准资源搜索插件) +- **特点**: Nintendo Switch 游戏专属 + +### 2. 数据转换映射 + +| NSGame字段 | PanSou SearchResult字段 | 转换说明 | +|------------|-------------------------|----------| +| `name` | `UniqueID` | 格式:`nsgame-{游戏名hash}` | +| `name` | `Title` | 游戏名称(中英文) | +| `password` | `Content` | 版本信息和金手指说明 | +| - | `Datetime` | 使用当前时间 | +| `url` | `Links` | 解析多行链接文本 | +| - | `Tags` | 添加"NS游戏"、"Switch"标签 | +| - | `Channel` | 设置为空字符串(插件搜索结果) | + +### 3. 链接解析逻辑 + +```go +// 解析 url 字段中的多个网盘链接 +func parseMultipleLinks(urlText string) []model.Link { + var links []model.Link + + // 按换行符分割 + lines := strings.Split(urlText, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // 提取链接和类型 + var url, cloudType, password string + + if strings.Contains(line, "[夸克网盘]") { + // 夸克网盘格式 + url = extractURL(line) + cloudType = "quark" + } else if strings.Contains(line, "[UC网盘]") { + // UC网盘格式 + url = extractURL(line) + cloudType = "uc" + } else if strings.Contains(line, "pan.baidu.com") { + // 百度网盘格式 + url, password = extractBaiduLink(line) + cloudType = "baidu" + } + + if url != "" { + links = append(links, model.Link{ + Type: cloudType, + URL: url, + Password: password, + }) + } + } + + return links +} +``` + +### 4. 百度网盘密码提取 + +百度网盘的密码在URL参数中,需要单独提取: + +```go +// 从百度网盘URL中提取链接和密码 +func extractBaiduLink(line string) (url, password string) { + // 提取完整URL + re := regexp.MustCompile(`https://pan\.baidu\.com/s/[^?\s]+(\?pwd=[a-zA-Z0-9]+)?`) + matches := re.FindStringSubmatch(line) + if len(matches) > 0 { + fullURL := matches[0] + + // 提取密码参数 + if strings.Contains(fullURL, "?pwd=") { + parts := strings.Split(fullURL, "?pwd=") + url = parts[0] + password = parts[1] + } else { + url = fullURL + } + } + + return +} +``` + +### 5. 唯一ID生成 + +由于API返回数据没有唯一ID字段,需要基于游戏名称生成: + +```go +import "crypto/md5" + +func generateUniqueID(gameName string) string { + // 使用游戏名称的MD5哈希的前12位 + hash := md5.Sum([]byte(gameName)) + return fmt.Sprintf("nsgame-%x", hash)[:20] +} +``` + +## 错误处理 + +### 常见错误类型 +1. **API请求失败**: 网络连接失败或服务器错误 +2. **JSON解析错误**: 响应格式不符合预期 +3. **链接格式异常**: url字段格式不符合预期 +4. **空结果**: 关键词搜索无结果 + +### 容错机制 +- **部分失败容忍**: 单个链接解析失败不影响其他链接 +- **数据验证**: 验证必填字段存在性 +- **默认值处理**: 缺失字段使用合理默认值 +- **日志记录**: 详细记录异常情况 + +## 性能优化建议 + +1. **分页控制**: 默认每页100条,避免过多请求 +2. **缓存策略**: 游戏资源更新不频繁,可设置较长缓存时间 +3. **超时设置**: 合理设置请求超时时间(建议10秒) +4. **连接复用**: 使用HTTP连接池 +5. **关键词过滤**: 使用 `FilterResultsByKeyword` 提高相关性 + +## 开发注意事项 + +1. **链接解析**: 正确处理url字段中的多行链接文本 +2. **密码位置**: 百度网盘密码在URL参数中,不在password字段 +3. **版本信息**: password字段是版本信息,应作为Content展示 +4. **游戏名称**: name字段包含中英文,用竖线分隔 +5. **标签设置**: 添加"NS游戏"、"Switch"等标签帮助分类 +6. **唯一ID**: 基于游戏名称生成稳定的唯一标识 +7. **字符编码**: 确保正确处理中文字符 +8. **请求头**: 设置合适的User-Agent避免反爬虫 + +## 示例代码结构 + +```go +// API响应结构 +type NSGameResponse struct { + Success bool `json:"success"` + Data struct { + PageData struct { + TotalCount int `json:"totalCount"` + PageNum int `json:"pageNum"` + Data []NSGameItem `json:"data"` + } `json:"pageData"` + PageView interface{} `json:"pageView"` + } `json:"data"` + Code string `json:"code"` + Message interface{} `json:"message"` +} + +// 游戏资源项 +type NSGameItem struct { + Name string `json:"name"` // 游戏名称 + URL string `json:"url"` // 网盘链接(多行文本) + Password string `json:"password"` // 版本信息 +} +``` + +## API调用示例 + +### 搜索马里奥游戏 +```bash +curl "https://nsthwj.com/thwj/game/query?pageNum=1&pageSize=100&type=&queryName=%E9%A9%AC%E9%87%8C%E5%A5%A5" +``` + +### 搜索塞尔达游戏 +```bash +curl "https://nsthwj.com/thwj/game/query?pageNum=1&pageSize=100&type=&queryName=%E5%A1%9E%E5%B0%94%E8%BE%BE" +``` + +## 总结 + +NSGame API 的主要特点: +- ✅ 专注于 Nintendo Switch 游戏资源 +- ✅ 支持多种主流网盘(百度、夸克、UC) +- ✅ 提供详细的版本信息和金手指说明 +- ✅ 简单的分页接口设计 +- ⚠️ url字段格式特殊,需要特殊解析 +- ⚠️ password字段不是提取码,是版本信息 + +实现此插件的关键在于正确解析 `url` 字段中的多行链接文本,并正确识别各网盘类型和提取密码。 + diff --git a/plugin/nsgame/nsgame.go b/plugin/nsgame/nsgame.go new file mode 100644 index 0000000..17f0dc4 --- /dev/null +++ b/plugin/nsgame/nsgame.go @@ -0,0 +1,299 @@ +package nsgame + +import ( + "context" + "crypto/md5" + "fmt" + "io" + "net/http" + "net/url" + "pansou/model" + "pansou/plugin" + "pansou/util/json" + "regexp" + "strings" + "time" +) + +const ( + // 插件名称 + pluginName = "nsgame" + + // API地址 + apiURL = "https://nsthwj.com/thwj/game/query" + + // 优先级 + defaultPriority = 2 + + // 超时时间 + defaultTimeout = 10 * time.Second + + // 每页大小 + pageSize = 1000 +) + +// 预编译的正则表达式 +var ( + // 提取URL的正则表达式 + urlRegex = regexp.MustCompile(`https?://[^\s]+`) + + // 百度网盘链接和密码提取 + baiduLinkRegex = regexp.MustCompile(`https://pan\.baidu\.com/s/[^?\s]+`) + baiduPwdRegex = regexp.MustCompile(`\?pwd=([a-zA-Z0-9]+)`) +) + +// NSGameAsyncPlugin NSGame异步插件 +type NSGameAsyncPlugin struct { + *plugin.BaseAsyncPlugin +} + +// NSGameResponse API响应结构 +type NSGameResponse struct { + Success bool `json:"success"` + Data struct { + PageData struct { + TotalCount int `json:"totalCount"` + PageNum int `json:"pageNum"` + Data []NSGameItem `json:"data"` + } `json:"pageData"` + PageView interface{} `json:"pageView"` + } `json:"data"` + Code string `json:"code"` + Message interface{} `json:"message"` +} + +// NSGameItem 游戏资源项 +type NSGameItem struct { + Name string `json:"name"` // 游戏名称 + URL string `json:"url"` // 网盘链接(多行文本) + Password string `json:"password"` // 版本信息 +} + +// 在init函数中注册插件 +func init() { + plugin.RegisterGlobalPlugin(NewNSGamePlugin()) +} + +// NewNSGamePlugin 创建新的NSGame异步插件 +func NewNSGamePlugin() *NSGameAsyncPlugin { + return &NSGameAsyncPlugin{ + BaseAsyncPlugin: plugin.NewBaseAsyncPlugin(pluginName, defaultPriority), + } +} + +// Search 执行搜索并返回结果(兼容性方法) +func (p *NSGameAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { + result, err := p.SearchWithResult(keyword, ext) + if err != nil { + return nil, err + } + return result.Results, nil +} + +// SearchWithResult 执行搜索并返回包含IsFinal标记的结果 +func (p *NSGameAsyncPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) { + return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext) +} + +// searchImpl 实现具体的搜索逻辑 +func (p *NSGameAsyncPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { + // 1. 构建搜索URL + searchURL := fmt.Sprintf("%s?pageNum=1&pageSize=%d&type=&queryName=%s", + apiURL, pageSize, url.QueryEscape(keyword)) + + // 2. 创建带超时的上下文 + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) + defer cancel() + + // 3. 创建请求 + req, err := http.NewRequestWithContext(ctx, "GET", searchURL, nil) + if err != nil { + return nil, fmt.Errorf("[%s] 创建请求失败: %w", p.Name(), err) + } + + // 4. 设置请求头 + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 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("Referer", "https://nsthwj.com/") + + // 5. 发送请求(带重试机制) + 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) + } + + // 6. 读取响应体 + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("[%s] 读取响应失败: %w", p.Name(), err) + } + + // 7. 解析JSON响应 + var apiResp NSGameResponse + if err := json.Unmarshal(body, &apiResp); err != nil { + return nil, fmt.Errorf("[%s] JSON解析失败: %w", p.Name(), err) + } + + // 8. 检查响应状态 + if !apiResp.Success || apiResp.Code != "200" { + return nil, fmt.Errorf("[%s] API返回错误: success=%v, code=%s", p.Name(), apiResp.Success, apiResp.Code) + } + + // 9. 转换为标准格式 + var results []model.SearchResult + for _, item := range apiResp.Data.PageData.Data { + // 解析网盘链接 + links := p.parseLinks(item.URL) + if len(links) == 0 { + continue + } + + // 生成唯一ID + uniqueID := p.generateUniqueID(item.Name) + + // 将版本信息拼接到标题中 + title := item.Name + if item.Password != "" { + // 将换行符替换为空格,使标题更紧凑 + versionInfo := strings.ReplaceAll(item.Password, "\n", " ") + title = fmt.Sprintf("%s(%s)", item.Name, versionInfo) + } + + // 构建结果 + result := model.SearchResult{ + UniqueID: uniqueID, + Title: title, // 标题包含版本信息 + Content: item.Password, // 保留原始版本信息在Content中 + Links: links, + Tags: []string{"NS游戏", "Switch"}, + Channel: "", // 插件搜索结果 Channel 必须为空 + Datetime: time.Now(), + } + + results = append(results, result) + } + + // 10. 关键词过滤 + return plugin.FilterResultsByKeyword(results, keyword), nil +} + +// parseLinks 解析url字段中的多个网盘链接 +func (p *NSGameAsyncPlugin) parseLinks(urlText string) []model.Link { + var links []model.Link + + // 按换行符分割 + lines := strings.Split(urlText, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // 判断链接类型并提取 + if strings.Contains(line, "[夸克网盘]") { + // 夸克网盘格式: [夸克网盘]:https://pan.quark.cn/s/xxx + if url := p.extractURL(line); url != "" && strings.Contains(url, "pan.quark.cn") { + links = append(links, model.Link{ + Type: "quark", + URL: url, + Password: "", + }) + } + } else if strings.Contains(line, "[UC网盘]") { + // UC网盘格式: [UC网盘]:https://drive.uc.cn/s/xxx + if url := p.extractURL(line); url != "" && strings.Contains(url, "drive.uc.cn") { + links = append(links, model.Link{ + Type: "uc", + URL: url, + Password: "", + }) + } + } else if strings.Contains(line, "pan.baidu.com") { + // 百度网盘格式: https://pan.baidu.com/s/xxx?pwd=xxxx + url, password := p.extractBaiduLink(line) + if url != "" { + links = append(links, model.Link{ + Type: "baidu", + URL: url, + Password: password, + }) + } + } + } + + return links +} + +// extractURL 从文本中提取URL +func (p *NSGameAsyncPlugin) extractURL(text string) string { + matches := urlRegex.FindString(text) + return strings.TrimSpace(matches) +} + +// extractBaiduLink 从百度网盘链接中提取URL和密码 +func (p *NSGameAsyncPlugin) extractBaiduLink(line string) (url, password string) { + // 提取完整URL + fullURL := urlRegex.FindString(line) + if fullURL == "" { + return + } + + // 提取基础链接 + linkMatches := baiduLinkRegex.FindString(fullURL) + if linkMatches == "" { + return + } + url = linkMatches + + // 提取密码 + pwdMatches := baiduPwdRegex.FindStringSubmatch(fullURL) + if len(pwdMatches) >= 2 { + password = pwdMatches[1] + } + + return +} + +// generateUniqueID 基于游戏名称生成唯一ID +func (p *NSGameAsyncPlugin) generateUniqueID(gameName string) string { + // 使用MD5哈希生成稳定的唯一ID + hash := md5.Sum([]byte(gameName)) + return fmt.Sprintf("%s-%x", p.Name(), hash)[:28] +} + +// doRequestWithRetry 带重试机制的HTTP请求 +func (p *NSGameAsyncPlugin) 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<