package transcoder import ( "context" "errors" "fmt" "io" "os" "os/exec" "strconv" "strings" "github.com/navidrome/navidrome/log" ) type Transcoder interface { Start(ctx context.Context, command, path string, maxBitRate int) (io.ReadCloser, error) } func New() Transcoder { return &externalTranscoder{} } type externalTranscoder struct{} func (e *externalTranscoder) Start(ctx context.Context, command, path string, maxBitRate int) (io.ReadCloser, error) { args := createTranscodeCommand(command, path, maxBitRate) log.Trace(ctx, "Executing transcoding command", "cmd", args) j := &Cmd{ctx: ctx, args: args} j.PipeReader, j.out = io.Pipe() err := j.start() if err != nil { return nil, err } go j.wait() return j, nil } type Cmd struct { *io.PipeReader out *io.PipeWriter ctx context.Context args []string cmd *exec.Cmd } func (j *Cmd) start() error { cmd := exec.CommandContext(j.ctx, j.args[0], j.args[1:]...) // #nosec cmd.Stdout = j.out cmd.Stderr = os.Stderr j.cmd = cmd if err := cmd.Start(); err != nil { return fmt.Errorf("starting cmd: %w", err) } return nil } func (j *Cmd) wait() { if err := j.cmd.Wait(); err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { _ = j.out.CloseWithError(fmt.Errorf("%s exited with non-zero status code: %d", j.args[0], exitErr.ExitCode())) } else { _ = j.out.CloseWithError(fmt.Errorf("waiting %s cmd: %w", j.args[0], err)) } return } if j.ctx.Err() != nil { _ = j.out.CloseWithError(j.ctx.Err()) return } _ = j.out.Close() } // Path will always be an absolute path func createTranscodeCommand(cmd, path string, maxBitRate int) []string { split := strings.Split(cmd, " ") for i, s := range split { s = strings.ReplaceAll(s, "%s", path) s = strings.ReplaceAll(s, "%b", strconv.Itoa(maxBitRate)) split[i] = s } return split }