package backend import ( "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "sync" ) type FlacInfo struct { Path string `json:"path"` SampleRate uint32 `json:"sample_rate"` BitsPerSample uint8 `json:"bits_per_sample"` } func GetFlacInfoBatch(paths []string) []FlacInfo { results := make([]FlacInfo, len(paths)) var wg sync.WaitGroup for i, path := range paths { wg.Add(1) go func(idx int, p string) { defer wg.Done() info := FlacInfo{Path: p} ffprobePath, err := GetFFprobePath() if err != nil { results[idx] = info return } args := []string{ "-v", "error", "-select_streams", "a:0", "-show_entries", "stream=sample_rate,bits_per_raw_sample,bits_per_sample", "-of", "default=noprint_wrappers=0", p, } cmd := exec.Command(ffprobePath, args...) setHideWindow(cmd) out, err := cmd.CombinedOutput() if err != nil { results[idx] = info return } kvMap := make(map[string]string) for _, line := range strings.Split(string(out), "\n") { if parts := strings.SplitN(line, "=", 2); len(parts) == 2 { kvMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) } } if v, ok := kvMap["sample_rate"]; ok { if s, err := strconv.Atoi(v); err == nil { info.SampleRate = uint32(s) } } bits := 0 if v, ok := kvMap["bits_per_raw_sample"]; ok && v != "N/A" && v != "" { bits, _ = strconv.Atoi(v) } if bits == 0 { if v, ok := kvMap["bits_per_sample"]; ok && v != "N/A" && v != "" { bits, _ = strconv.Atoi(v) } } info.BitsPerSample = uint8(bits) results[idx] = info }(i, path) } wg.Wait() return results } type ResampleRequest struct { InputFiles []string `json:"input_files"` SampleRate string `json:"sample_rate"` BitDepth string `json:"bit_depth"` } type ResampleResult struct { InputFile string `json:"input_file"` OutputFile string `json:"output_file"` Success bool `json:"success"` Error string `json:"error,omitempty"` } func buildFolderLabel(sampleRate, bitDepth string) string { var parts []string if bitDepth != "" { parts = append(parts, bitDepth+"bit") } switch sampleRate { case "44100": parts = append(parts, "44.1kHz") case "48000": parts = append(parts, "48kHz") case "96000": parts = append(parts, "96kHz") case "192000": parts = append(parts, "192kHz") default: if sampleRate != "" { parts = append(parts, sampleRate+"Hz") } } if len(parts) == 0 { return "Resampled" } return strings.Join(parts, " ") } func ResampleAudio(req ResampleRequest) ([]ResampleResult, error) { ffmpegPath, err := GetFFmpegPath() if err != nil { return nil, fmt.Errorf("failed to get ffmpeg path: %w", err) } if err := ValidateExecutable(ffmpegPath); err != nil { return nil, fmt.Errorf("invalid ffmpeg executable: %w", err) } installed, err := IsFFmpegInstalled() if err != nil || !installed { return nil, fmt.Errorf("ffmpeg is not installed") } if req.SampleRate == "" && req.BitDepth == "" { return nil, fmt.Errorf("at least one of sample rate or bit depth must be specified") } results := make([]ResampleResult, len(req.InputFiles)) var wg sync.WaitGroup var mu sync.Mutex folderLabel := buildFolderLabel(req.SampleRate, req.BitDepth) for i, inputFile := range req.InputFiles { wg.Add(1) go func(idx int, inputFile string) { defer wg.Done() result := ResampleResult{ InputFile: inputFile, } inputExt := strings.ToLower(filepath.Ext(inputFile)) baseName := strings.TrimSuffix(filepath.Base(inputFile), inputExt) inputDir := filepath.Dir(inputFile) outputDir := filepath.Join(inputDir, folderLabel) if err := os.MkdirAll(outputDir, 0755); err != nil { result.Error = fmt.Sprintf("failed to create output directory: %v", err) result.Success = false mu.Lock() results[idx] = result mu.Unlock() return } outputFile := filepath.Join(outputDir, baseName+".flac") result.OutputFile = outputFile args := []string{ "-i", inputFile, "-y", } if req.BitDepth != "" { switch req.BitDepth { case "16": args = append(args, "-c:a", "flac", "-sample_fmt", "s16") case "24": args = append(args, "-c:a", "flac", "-sample_fmt", "s32", "-bits_per_raw_sample", "24") default: args = append(args, "-c:a", "flac") } } else { args = append(args, "-c:a", "flac") } if req.SampleRate != "" { args = append(args, "-ar", req.SampleRate) } args = append(args, "-map_metadata", "0") args = append(args, outputFile) fmt.Printf("[Resample] %s -> %s\n", inputFile, outputFile) cmd := exec.Command(ffmpegPath, args...) setHideWindow(cmd) output, err := cmd.CombinedOutput() if err != nil { result.Error = fmt.Sprintf("resampling failed: %s - %s", err.Error(), string(output)) result.Success = false mu.Lock() results[idx] = result mu.Unlock() return } result.Success = true fmt.Printf("[Resample] Done: %s\n", outputFile) mu.Lock() results[idx] = result mu.Unlock() }(i, inputFile) } wg.Wait() return results, nil }