package convert import ( "fmt" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" "streamrip-go/internal/config" ) type profile struct { codecLib string ext string lossless bool } var profiles = map[string]profile{ "FLAC": {codecLib: "flac", ext: "flac", lossless: true}, "ALAC": {codecLib: "alac", ext: "m4a", lossless: true}, "OPUS": {codecLib: "libopus", ext: "opus", lossless: false}, "MP3": {codecLib: "libmp3lame", ext: "mp3", lossless: false}, "VORBIS": {codecLib: "libvorbis", ext: "ogg", lossless: false}, "AAC": {codecLib: "aac", ext: "m4a", lossless: false}, } func Convert(path string, cfg config.ConversionConfig) (string, error) { if !cfg.Enabled { return path, nil } if _, err := exec.LookPath("ffmpeg"); err != nil { return path, fmt.Errorf("ffmpeg not found: %w", err) } p, ok := profiles[strings.ToUpper(strings.TrimSpace(cfg.Codec))] if !ok { return path, fmt.Errorf("unsupported conversion codec: %s", cfg.Codec) } base := strings.TrimSuffix(path, filepath.Ext(path)) finalPath := base + "." + p.ext tmpPath := finalPath + ".tmp." + p.ext args := buildFFmpegArgs(path, tmpPath, p, cfg) cmd := exec.Command("ffmpeg", args...) output, err := cmd.CombinedOutput() if err != nil { _ = os.Remove(tmpPath) return path, fmt.Errorf("conversion failed: %w: %s", err, string(output)) } if path != finalPath { _ = os.Remove(path) } if err = os.Rename(tmpPath, finalPath); err != nil { _ = os.Remove(tmpPath) return path, err } return finalPath, nil } func buildFFmpegArgs(inputPath, outputPath string, p profile, cfg config.ConversionConfig) []string { args := []string{ "-y", "-i", inputPath, "-map", "0:a:0", "-map_metadata", "0", "-c:a", p.codecLib, } if p.lossless { filter := buildLosslessFilter(cfg) if filter != "" { args = append(args, "-af", filter) } } else { if cfg.LossyBitrate > 0 { args = append(args, "-b:a", strconv.Itoa(cfg.LossyBitrate)+"k") } } args = append(args, outputPath) return args } func buildLosslessFilter(cfg config.ConversionConfig) string { parts := make([]string, 0, 2) if cfg.SamplingRate > 0 { rates := allowedSampleRates(cfg.SamplingRate) if len(rates) > 0 { parts = append(parts, "sample_rates="+strings.Join(rates, "|")) } } if cfg.BitDepth == 16 { parts = append(parts, "sample_fmts=s16p|s16") } else if cfg.BitDepth == 24 || cfg.BitDepth == 32 { parts = append(parts, "sample_fmts=s16p|s16|s32p|s32") } if len(parts) == 0 { return "" } return "aformat=" + strings.Join(parts, ":") } func allowedSampleRates(max int) []string { all := []int{44100, 48000, 88200, 96000, 176400, 192000} out := make([]int, 0, len(all)) for _, r := range all { if r <= max { out = append(out, r) } } sort.Ints(out) str := make([]string, 0, len(out)) for _, r := range out { str = append(str, strconv.Itoa(r)) } return str }