mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
124 lines
2.9 KiB
Go
124 lines
2.9 KiB
Go
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
|
|
}
|