Files
chenxin 1a4997e031 ✨ feat(api): 统一响应格式并完善参数体系与文档
- 正文参数支持语义值(plain/html/keep/remove)与别名(img_mode/review_mode),URL 参数优先于配置
- 修复纯文本被段评强制包回 HTML、HTML 模式段评注入失效的问题
- 修复 PKCS7 填充弱校验导致漫画/正文偶发解密乱码返回 200 的问题
- 搜索结果展平:去 search_tabs 层与 selected_tab_idx/server_ab_params,提升 has_more/next_offset
- 全接口解包字符串形式的 JSON 字段(category_schema 等),缓存出口幂等处理
- 序列化关闭 HTML 转义,video/audio 播放链接可直接复制播放
- video 响应透传 CENC 加密状态(key_seed/kid/encryption_method)
- 新增 gzip 中间件,JSON/文本响应压缩约 81%,二进制透传
- 漫画默认输出模式改为 proxy(url 模式链接 404 且内容加密不可用)
- openapi.yaml:补全 23 个路径参数定义、tab_type 完整枚举、CDN 签名说明,示例更新
2026-10-09 21:01:18 +08:00

133 lines
4.0 KiB
Go

package fqapi
import (
"fanqie/internal/common"
"fmt"
)
var audioLogger = common.NewLogger("audio", "INFO", "")
// HandleAudio 处理获取音频
func HandleAudio(itemIDs interface{}, toneID interface{}, output interface{}) (string, error) {
if itemIDs == nil || itemIDs == "" {
return BuildSuccessResponse([]interface{}{}), nil
}
// 处理itemIDs - 使用ParseStringParam
itemIDStr := ParseStringParam(itemIDs, "")
// 处理toneID - 使用ParseIntParam
toneIDInt := ParseIntParam(toneID, 1)
// 处理output参数
outputStr := ParseStringParam(output, "url")
// 使用 BuildURL 添加设备参数
url := BuildURL("https://reading.snssdk.com/reading/reader/audio/playinfo/",
fmt.Sprintf("item_ids=%s", itemIDStr),
fmt.Sprintf("tone_id=%d", toneIDInt),
"pv_player=-1",
)
// 使用 GorgonRequest 添加签名和设备信息
resp, err := GorgonRequest(url, map[string]interface{}{
"method": "GET",
"headers": map[string]string{
"User-Agent": "com.dragon.read",
},
})
if err != nil {
audioLogger.Error(fmt.Sprintf("获取音频失败: %v", err))
return BuildErrorResponse("获取音频失败"), err
}
respJSON, err := resp.JSON()
if err != nil {
audioLogger.Error(fmt.Sprintf("解析响应失败: %v", err))
return BuildErrorResponse("解析响应失败"), err
}
// 检查响应码
var respCode float64
var okCode bool
if code, ok := respJSON["code"].(float64); ok {
respCode, okCode = code, code == 0
} else if code, ok := respJSON["Code"].(float64); ok {
respCode, okCode = code, code == 0
}
if okCode {
// 转换响应格式:只修改 code 和 message,保留原始数据结构
respJSON["code"] = 200
respJSON["message"] = "SUCCESS"
// 处理output参数,替换音频URL
if outputStr != "url" {
// 获取当前使用的设备信息
currentDevice := GetCurrentDevice()
deviceID := ""
if currentDevice != nil {
deviceID = currentDevice.DeviceHeader
}
// 准备请求头
headers := map[string]string{
"User-Agent": "com.dragon.read",
}
// 处理音频URL,支持多种可能的结构
if dataArray, ok := respJSON["data"].([]interface{}); ok {
// 情况1:data是数组格式(实际返回的格式)
for _, item := range dataArray {
if audioItem, ok := item.(map[string]interface{}); ok {
// 处理main_url
if mainURL, ok := audioItem["main_url"].(string); ok && mainURL != "" {
processedURL := processMediaURL(mainURL, deviceID, headers, outputStr, "audio")
audioItem["main_url"] = processedURL
}
// 处理backup_url
if backupURL, ok := audioItem["backup_url"].(string); ok && backupURL != "" {
processedURL := processMediaURL(backupURL, deviceID, headers, outputStr, "audio")
audioItem["backup_url"] = processedURL
}
}
}
} else if data, ok := respJSON["data"].(map[string]interface{}); ok {
// 情况2:单个音频在audio_info下
if audioInfo, ok := data["audio_info"].(map[string]interface{}); ok {
if playURL, ok := audioInfo["play_url"].(string); ok && playURL != "" {
processedURL := processMediaURL(playURL, deviceID, headers, outputStr, "audio")
audioInfo["play_url"] = processedURL
}
}
// 情况3:音频列表在audio_list下
if audioList, ok := data["audio_list"].([]interface{}); ok {
for _, item := range audioList {
if audioItem, ok := item.(map[string]interface{}); ok {
if playURL, ok := audioItem["play_url"].(string); ok && playURL != "" {
processedURL := processMediaURL(playURL, deviceID, headers, outputStr, "audio")
audioItem["play_url"] = processedURL
}
}
}
}
}
}
return marshalNoEscape(UnwrapJSONStrings(respJSON)), nil
}
// 返回错误信息
errMsg := "获取音频失败"
if msg, ok := respJSON["message"].(string); ok {
errMsg = msg
} else if msg, ok := respJSON["Message"].(string); ok {
errMsg = msg
}
audioLogger.Error(fmt.Sprintf("获取音频失败: code=%v, msg=%s", respCode, errMsg))
return BuildErrorResponse(errMsg), nil
}