mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
initial Go port of streamrip
This commit is contained in:
123
internal/audio/convert/convert.go
Normal file
123
internal/audio/convert/convert.go
Normal 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
|
||||
}
|
||||
43
internal/audio/convert/convert_test.go
Normal file
43
internal/audio/convert/convert_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"streamrip-go/internal/config"
|
||||
)
|
||||
|
||||
func TestAllowedSampleRates(t *testing.T) {
|
||||
got := allowedSampleRates(96000)
|
||||
want := []string{"44100", "48000", "88200", "96000"}
|
||||
if strings.Join(got, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("rates=%v want=%v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFFmpegArgsLossless(t *testing.T) {
|
||||
cfg := config.ConversionConfig{Enabled: true, Codec: "FLAC", SamplingRate: 48000, BitDepth: 16}
|
||||
args := buildFFmpegArgs("in.flac", "out.flac", profiles["FLAC"], cfg)
|
||||
joined := strings.Join(args, " ")
|
||||
if !strings.Contains(joined, "-c:a flac") {
|
||||
t.Fatalf("missing flac codec args=%s", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "sample_rates=44100|48000") {
|
||||
t.Fatalf("missing sample rate filter args=%s", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "sample_fmts=s16p|s16") {
|
||||
t.Fatalf("missing bit depth filter args=%s", joined)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFFmpegArgsLossy(t *testing.T) {
|
||||
cfg := config.ConversionConfig{Enabled: true, Codec: "MP3", LossyBitrate: 320}
|
||||
args := buildFFmpegArgs("in.flac", "out.mp3", profiles["MP3"], cfg)
|
||||
joined := strings.Join(args, " ")
|
||||
if !strings.Contains(joined, "-c:a libmp3lame") {
|
||||
t.Fatalf("missing mp3 codec args=%s", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "-b:a 320k") {
|
||||
t.Fatalf("missing bitrate args=%s", joined)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user