initial Go port of streamrip

This commit is contained in:
2026-04-19 21:11:38 +02:00
commit 97e8b758b3
32 changed files with 7008 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
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
}