package fqapi import ( "crypto/rand" "encoding/base64" "encoding/hex" "fmt" "io" "net/http" "path/filepath" "strings" "time" ) // generateRandomKey 生成随机key func generateRandomKey(length int) string { b := make([]byte, length/2) rand.Read(b) return hex.EncodeToString(b) } // extractDeviceParam 从deviceHeader字符串中提取指定参数 func extractDeviceParam(deviceHeader, paramName string) string { // deviceHeader 格式: device_id=xxx&device_type=xxx&iid=xxx&version_name=xxx parts := strings.Split(deviceHeader, "&") for _, part := range parts { kv := strings.Split(part, "=") if len(kv) == 2 && kv[0] == paramName { return kv[1] } } return "" } // downloadMediaWithRetry 带重试的媒体下载,自动切换设备 func downloadMediaWithRetry(url, deviceID string, headers map[string]string, maxRetries int) ([]byte, string, error) { var lastErr error currentDeviceID := deviceID for i := 0; i <= maxRetries; i++ { data, contentType, err := downloadMedia(url, currentDeviceID, headers) if err == nil && len(data) > 0 { return data, contentType, nil } lastErr = err if i < maxRetries { // 重试时切换到新的有效设备 newDevice, err := GetValidDevice() if err == nil { currentDeviceID = newDevice.DeviceHeader } // 重试间隔递增(2秒、4秒、6秒) time.Sleep(time.Duration(i*2+2) * time.Second) } } return nil, "", fmt.Errorf("下载失败(已重试%d次): %w", maxRetries, lastErr) } // downloadMedia 下载媒体内容 func downloadMedia(url, deviceID string, headers map[string]string) ([]byte, string, error) { // 创建专用的媒体下载客户端,超时时间更长 client := &http.Client{ Timeout: 300 * time.Second, // 5分钟超时,适合大文件 } req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, "", err } // 设置CDN请求头,模拟安卓客户端请求 req.Header.Set("User-Agent", "com.dragon.read/7.9.9.32 (Linux; U; Android 13; zh_CN; M2102K1AC; Build/TKQ1.220807.001; Cronet/113.0.5672.62)") req.Header.Set("Accept", "*/*") req.Header.Set("Accept-Encoding", "identity") req.Header.Set("Connection", "keep-alive") req.Header.Set("Host", req.URL.Host) // 添加设备信息(如果提供了deviceID) if deviceID != "" { req.Header.Set("X-Device-ID", extractDeviceParam(deviceID, "device_id")) req.Header.Set("X-Device-Type", extractDeviceParam(deviceID, "device_type")) req.Header.Set("X-IID", extractDeviceParam(deviceID, "iid")) req.Header.Set("X-Version-Name", extractDeviceParam(deviceID, "version_name")) } // 添加自定义请求头 for k, v := range headers { req.Header.Set(k, v) } resp, err := client.Do(req) if err != nil { return nil, "", fmt.Errorf("下载失败: %v", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, "", fmt.Errorf("HTTP状态码: %d", resp.StatusCode) } // 读取内容 data, err := io.ReadAll(resp.Body) if err != nil { return nil, "", fmt.Errorf("读取内容失败: %w", err) } // 获取Content-Type contentType := resp.Header.Get("Content-Type") if contentType == "" { // 根据文件后缀推断 contentType = getContentTypeFromURL(url) } return data, contentType, nil } // getContentTypeFromURL 根据URL推断Content-Type func getContentTypeFromURL(url string) string { ext := strings.ToLower(filepath.Ext(url)) switch ext { case ".mp3", ".m4a", ".aac": return "audio/mpeg" case ".mp4", ".m4v", ".mov": return "video/mp4" case ".flv": return "video/x-flv" case ".ts": return "video/MP2T" case ".webm": return "video/webm" case ".ogg": return "audio/ogg" default: return "application/octet-stream" } } // DetectMediaType 检测媒体类型 func DetectMediaType(data []byte) string { if len(data) < 4 { return "unknown" } // 检测音频格式 if data[0] == 0xFF && (data[1] == 0xFB || data[1] == 0xF3 || data[1] == 0xF2) { return "audio/mpeg" // MP3 } if string(data[:4]) == "ftyp" || string(data[:4]) == "moov" || string(data[:4]) == "mdat" { return "video/mp4" // MP4/M4A } if string(data[:4]) == "OggS" { return "audio/ogg" // OGG } if string(data[:4]) == "RIFF" && string(data[8:12]) == "WAVE" { return "audio/wav" // WAV } // 检测视频格式 if string(data[:3]) == "FLV" { return "video/x-flv" // FLV } if string(data[:4]) == "webm" { return "video/webm" // WebM } return "unknown" } // processMediaURL 处理媒体URL,根据output模式返回不同的URL(仅支持音频) func processMediaURL(originalURL, deviceID string, headers map[string]string, outputMode string, mediaType string) interface{} { switch outputMode { case "proxy": // 生成代理URL if mediaType == "audio" { return GenerateAudioProxyURL(originalURL, deviceID, headers) } // 其他类型返回原始URL return originalURL case "data_uri": // 直接下载并返回base64 data, contentType, err := downloadMediaWithRetry(originalURL, deviceID, headers, 2) if err != nil { return originalURL } return fmt.Sprintf("data:%s;base64,%s", contentType, base64.StdEncoding.EncodeToString(data)) default: // 直接返回原始URL return originalURL } }