package fqapi import ( "encoding/base64" "encoding/json" "fanqie/internal/common" "fmt" "math" ) var videoLogger = common.NewLogger("video", "INFO", "") // HandleVideo 处理获取视频(支持批量和单个接口) func HandleVideo(itemIDs interface{}) (string, error) { if itemIDs == nil || itemIDs == "" { return BuildSuccessResponse([]interface{}{}), nil } // 处理itemIDs - 使用ParseStringParam itemIDStr := ParseStringParam(itemIDs, "") // 判断是单个ID还是多个ID(用逗号分隔) isSingle := true if str, ok := itemIDs.(string); ok { for _, ch := range str { if ch == ',' { isSingle = false break } } } // 根据ID数量选择不同的接口 var url string if isSingle { // 单个视频接口 - 使用带签名的URL url = BuildURL("https://api5-normal-sinfonlineb.fqnovel.com/novel/player/video_model/v1") } else { // 批量视频接口 url = BuildURL("https://reading.snssdk.com/novel/player/multi_video_model/v1/") } // 构建请求体,添加更多设备参数,请求所有清晰度 requestBody := fmt.Sprintf(`{"biz_param":{"device_level":3,"need_all_video_definition":true,"need_mp4_align":false,"use_os_player":false,"video_platform":1024},"video_id":"%s"}`, itemIDStr) // 使用 GorgonRequest 添加签名和设备信息 resp, err := GorgonRequest(url, map[string]interface{}{ "method": "POST", "body": requestBody, "headers": map[string]string{ "User-Agent": "com.dragon.read", "Content-Type": "application/json", }, }) if err != nil { videoLogger.Error(fmt.Sprintf("获取视频失败: %v", err)) return BuildErrorResponse("FAILED"), err } respJSON, err := resp.JSON() if err != nil { videoLogger.Error(fmt.Sprintf("解析响应失败: %v", err)) return BuildErrorResponse("FAILED"), err } // 检查响应码(可能返回 Code 或 code) 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 { // 尝试获取 data(可能在不同位置) var data map[string]interface{} if d, ok := respJSON["data"].(map[string]interface{}); ok { data = d } else if d, ok := respJSON["Data"].(map[string]interface{}); ok { data = d } if data != nil { if videoModelStr, ok := data["video_model"].(string); ok && isSingle { decodedModel := decodeVideoModelAsJSON(videoModelStr) // 单个接口直接返回 video_model response := map[string]interface{}{ "code": 200, "message": "SUCCESS", "data": decodedModel, } resultBytes, _ := json.Marshal(response) return string(resultBytes), nil } else { // 批量接口:data 是一个map,key是ID,value包含video_model result := make(map[string]interface{}) for itemID, itemData := range data { if itemMap, ok := itemData.(map[string]interface{}); ok { if videoModelStr, ok := itemMap["video_model"].(string); ok { decodedModel := decodeVideoModelAsJSON(videoModelStr) result[itemID] = map[string]interface{}{ "video_model": decodedModel, } } } } // 构建返回结果 response := map[string]interface{}{ "code": 200, "message": "SUCCESS", "data": result, } resultBytes, _ := json.Marshal(response) return string(resultBytes), nil } } } // 返回错误信息 errMsg := "获取视频失败" if msg, ok := respJSON["message"].(string); ok { errMsg = msg } else if msg, ok := respJSON["Message"].(string); ok { errMsg = msg } videoLogger.Error(fmt.Sprintf("获取视频失败: code=%v, msg=%s", respCode, errMsg)) return BuildErrorResponse(errMsg), nil } // decodeVideoModelAsJSON 解析并解码 video_model,返回关键字段的 JSON 对象 func decodeVideoModelAsJSON(videoModelStr string) interface{} { // 解析 video_model JSON 字符串 var videoModel map[string]interface{} if err := json.Unmarshal([]byte(videoModelStr), &videoModel); err != nil { // 解析失败,返回空对象 return map[string]interface{}{} } // 构建精简的返回结果,只包含关键字段 result := make(map[string]interface{}) // 提取关键字段 if videoID, ok := videoModel["video_id"].(string); ok { result["video_id"] = videoID } if duration, ok := videoModel["video_duration"].(float64); ok { result["video_duration"] = formatDuration(duration) } if posterURL, ok := videoModel["poster_url"].(string); ok { result["poster_url"] = posterURL } // 处理 video_list if videoList, ok := videoModel["video_list"].(map[string]interface{}); ok { processedVideoList := make(map[string]interface{}) for quality, qualityData := range videoList { if qualityMap, ok := qualityData.(map[string]interface{}); ok { // 只保留关键字段 qualityInfo := make(map[string]interface{}) if definition, ok := qualityMap["definition"].(string); ok { qualityInfo["definition"] = definition } if vtype, ok := qualityMap["vtype"].(string); ok { qualityInfo["vtype"] = vtype } if vwidth, ok := qualityMap["vwidth"].(float64); ok { qualityInfo["vwidth"] = int(vwidth) } if vheight, ok := qualityMap["vheight"].(float64); ok { qualityInfo["vheight"] = int(vheight) } if bitrate, ok := qualityMap["bitrate"].(float64); ok { qualityInfo["bitrate"] = formatBitrate(bitrate) } if size, ok := qualityMap["size"].(float64); ok { qualityInfo["size"] = formatFileSize(size) } if fps, ok := qualityMap["fps"].(float64); ok { qualityInfo["fps"] = int(fps) } // 解码 main_url if mainURL, ok := qualityMap["main_url"].(string); ok { if decoded, err := base64.StdEncoding.DecodeString(mainURL); err == nil { qualityInfo["main_url"] = string(decoded) } } // 解码 backup_url_1 if backupURL, ok := qualityMap["backup_url_1"].(string); ok { if decoded, err := base64.StdEncoding.DecodeString(backupURL); err == nil { qualityInfo["backup_url_1"] = string(decoded) } } processedVideoList[quality] = qualityInfo } } result["video_list"] = processedVideoList } return result } // formatDuration 将秒数转换为可读的时间格式 func formatDuration(seconds float64) string { totalSeconds := int(math.Round(seconds)) hours := totalSeconds / 3600 minutes := (totalSeconds % 3600) / 60 secs := totalSeconds % 60 if hours > 0 { return fmt.Sprintf("%d小时%d分%d秒", hours, minutes, secs) } else if minutes > 0 { return fmt.Sprintf("%d分%d秒", minutes, secs) } return fmt.Sprintf("%d秒", secs) } // formatFileSize 将字节数转换为可读的文件大小格式 func formatFileSize(bytes float64) string { const unit = 1024.0 if bytes < unit { return fmt.Sprintf("%.0f B", bytes) } kb := bytes / unit if kb < unit { return fmt.Sprintf("%.2f KB", kb) } mb := kb / unit if mb < unit { return fmt.Sprintf("%.2f MB", mb) } gb := mb / unit return fmt.Sprintf("%.2f GB", gb) } // formatBitrate 将比特率转换为可读的格式 func formatBitrate(bitrate float64) string { const unit = 1000.0 kbps := bitrate / unit if kbps < unit { return fmt.Sprintf("%.0f Kbps", kbps) } mbps := kbps / unit return fmt.Sprintf("%.2f Mbps", mbps) }