diff --git a/streamer/audio/ffprobe.go b/streamer/audio/ffprobe.go index e5df6163..7c7f1911 100644 --- a/streamer/audio/ffprobe.go +++ b/streamer/audio/ffprobe.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "io" + "os" "os/exec" "regexp" "strconv" @@ -53,13 +54,14 @@ type Info struct { Title string Artist string Album string + Comment string Bitrate int } var ffprobeTextArgs = []string{ "-loglevel", "fatal", "-hide_banner", - "-show_entries", "format_tags=title,artist,album:stream_tags=title,artist,album:stream=duration,bit_rate:format=format_name,bit_rate", + "-show_entries", "format_tags=title,artist,album,comment:stream_tags=title,artist,album,comment:stream=duration,bit_rate:format=format_name,bit_rate", "-of", "default=noprint_wrappers=1", } @@ -77,6 +79,21 @@ func ProbeText(ctx context.Context, filename string) (*Info, error) { return parseProbeText(ctx, bytes.NewReader(out)) } +func probeText(ctx context.Context, file *os.File) (*Info, error) { + const op errors.Op = "streamer/audio.Probe" + + cmd := exec.CommandContext(ctx, "ffprobe", + append(ffprobeTextArgs, "-i", "-")...) + cmd.Stdin = file + + out, err := cmd.Output() + if err != nil { + return nil, errors.E(op, err, errors.Info(cmd.String())) + } + + return parseProbeText(ctx, bytes.NewReader(out)) +} + func parseProbeText(ctx context.Context, out io.Reader) (*Info, error) { const op errors.Op = "streamer/audio.parseProbeText" var err error @@ -107,6 +124,8 @@ func parseProbeText(ctx context.Context, out io.Reader) (*Info, error) { info.Artist = value case "album": info.Album = value + case "comment": + info.Comment = value case "bit_rate": if value != "N/A" { // could not exist info.Bitrate, err = strconv.Atoi(value) diff --git a/streamer/audio/metadata.go b/streamer/audio/metadata.go new file mode 100644 index 00000000..af96fb6a --- /dev/null +++ b/streamer/audio/metadata.go @@ -0,0 +1,71 @@ +package audio + +import ( + "context" + "io" + "os/exec" + "path/filepath" + "strings" + + radio "github.com/R-a-dio/valkyrie" + "github.com/R-a-dio/valkyrie/errors" + "github.com/spf13/afero" +) + +// DeleteID3Tags runs `id3v2 --delete-all ` this removes any id3 tags +// in the file. +func DeleteID3Tags(ctx context.Context, filename string) { + exec.CommandContext(ctx, "id3v2", "--delete-all", filename) +} + +// WithMetadata puts the metadata of song into the file given by filename and returns +// it as an in-memory file. +func WriteMetadata(ctx context.Context, f afero.File, song radio.Song) (*MemoryBuffer, error) { + const op errors.Op = "streamer/audio.WriteMetadata" + + args := []string{ + "-hide_banner", + "-loglevel", "error", + "-i", "-", + "-c:a", "copy", // copy audio stream + "-id3v2_version", "3", // windows apparently doesn't support v2.4 + } + if song.Title != "" { + args = append(args, "-metadata", "title="+song.Title) + } + if song.Artist != "" { + args = append(args, "-metadata", "artist="+song.Artist) + } + if song.Album != "" { + args = append(args, "-metadata", "album="+song.Album) + } + if song.Tags != "" { + args = append(args, "-metadata", "comment="+song.Tags) + } + + switch strings.ToLower(filepath.Ext(f.Name())) { + case ".flac": + args = append(args, "-f", "flac") + case ".mp3": + args = append(args, "-f", "mp3") + case ".ogg": + args = append(args, "-f", "ogg") + default: + return nil, errors.E(op, errors.InvalidArgument) + } + args = append(args, "-") + + ff, err := newFFmpegCmd("metadata-"+song.TrackID.String(), args) + if err != nil { + return nil, errors.E(op, err) + } + ff.Cmd.Stdin = f + + out, err := ff.Run() + if err != nil { + return nil, errors.E(op, err) + } + // seek the output to the start + _, _ = out.Seek(0, io.SeekStart) + return out, nil +} diff --git a/streamer/audio/metadata_test.go b/streamer/audio/metadata_test.go new file mode 100644 index 00000000..d5d72a22 --- /dev/null +++ b/streamer/audio/metadata_test.go @@ -0,0 +1,38 @@ +package audio + +import ( + "context" + "testing" + + radio "github.com/R-a-dio/valkyrie" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWriteMetadata(t *testing.T) { + fsys := afero.NewOsFs() + f, err := fsys.Open("testdata/MP3_2MG.mp3") + require.NoError(t, err) + + song := radio.Song{ + DatabaseTrack: &radio.DatabaseTrack{ + Title: "test", + Artist: "some kind of artist", + Album: "a kind of album", + Tags: "test effective very", + }, + } + + out, err := WriteMetadata(context.Background(), f, song) + require.NoError(t, err) + + // probe the output file + info, err := probeText(context.Background(), out.Memfd.File) + require.NoError(t, err) + + assert.Equal(t, song.Title, info.Title) + assert.Equal(t, song.Artist, info.Artist) + assert.Equal(t, song.Album, info.Album) + assert.Equal(t, song.Tags, info.Comment) +}