diff --git a/app/choose.go b/app/choose.go index 525e18b..da98672 100644 --- a/app/choose.go +++ b/app/choose.go @@ -20,6 +20,7 @@ func PickNewFile() media.Probe { } func SetFile(path string) media.Probe { + resetTags() f := media.ProbeFile(path) file = &f copyTagsFromFile() @@ -71,6 +72,7 @@ func PickAgain() { } file = nil tmpfile = nil + resetTags() } func Finish() { diff --git a/app/convert.go b/app/convert.go new file mode 100644 index 0000000..e6d708d --- /dev/null +++ b/app/convert.go @@ -0,0 +1,16 @@ +package app + +import ( + "fmt" + + "codeberg.org/danjones000/strip-beats/media" + "codeberg.org/danjones000/strip-beats/utils" +) + +func convert() { + in := utils.Tern(tmpfile == nil, file, tmpfile) + out, _ := media.ConvertAndTag(*in, tags) + fmt.Println(out) + + quit() +} diff --git a/app/fade.go b/app/fade.go index 290e0f4..64882d3 100644 --- a/app/fade.go +++ b/app/fade.go @@ -2,6 +2,8 @@ package app import ( "os" + p "path/filepath" + s "strings" "codeberg.org/danjones000/strip-beats/media" "codeberg.org/danjones000/strip-beats/utils" @@ -31,7 +33,10 @@ func validateNumber(input string, lastChar rune) bool { } func fadeFile() error { - tmp, err := os.CreateTemp("", "audio.*.mka") + base := p.Base(file.Format.Path) + ext := p.Ext(base) + base = s.TrimSuffix(base, ext) + tmp, err := os.CreateTemp("", base+".*.mka") if err != nil { return err } diff --git a/app/menu.go b/app/menu.go index 5a6e5ff..96b3ac0 100644 --- a/app/menu.go +++ b/app/menu.go @@ -10,7 +10,7 @@ func (st AppStep) Title() string { mustpick := "You need to pick a file" switch st { case Pick: - return "Pick a new show" + return "Pick a new file" case Watch: if file == nil { return mustpick @@ -26,6 +26,11 @@ func (st AppStep) Title() string { return mustpick } return fmt.Sprintf("Should we try to identify %s", file.ShortPath()) + case Convert: + if file == nil { + return mustpick + } + return fmt.Sprintf("Convert %s?", file.ShortPath()) case Restart: return "Forget current selection" case Quit: @@ -54,6 +59,8 @@ func (st AppStep) Rune() rune { return 'r' case Print: return 'a' + case Convert: + return 'c' case Quit: return 'q' default: @@ -70,7 +77,7 @@ func mainMenu() AppStep { if file == nil { steps = []list.Option{Pick, Quit} } else { - steps = []list.Option{Pick, Watch, Fade, Print, Quit} + steps = []list.Option{Pick, Watch, Fade, Print, Convert, Quit} } step := list.List("What would you like to do next?", steps, nil) diff --git a/app/run.go b/app/run.go index 066f6c6..cf01523 100644 --- a/app/run.go +++ b/app/run.go @@ -16,6 +16,7 @@ const ( Watch Fade Print + Convert Restart Quit ) @@ -94,6 +95,9 @@ func Run(step AppStep) { case Print: print() step = mainMenu() + case Convert: + convert() + step = mainMenu() case Quit: quit() default: diff --git a/app/tags.go b/app/tags.go index 98f2fb3..cd25b4d 100644 --- a/app/tags.go +++ b/app/tags.go @@ -8,6 +8,10 @@ import ( var tags t.Tags +func resetTags() { + tags = t.Tags{} +} + func copyTagsFromFile() { if file == nil { panic(errors.New("Missing file")) diff --git a/config/config.go b/config/config.go index 7c4d5fa..fbd3af9 100644 --- a/config/config.go +++ b/config/config.go @@ -12,7 +12,7 @@ import ( const ( AppName string = "strip-beats" - Version string = "0.1.0" + Version string = "0.1.1" Url string = "https://codeberg.org/danjones000/strip-beats" Email string = "danjones@goodevilgenius.org" UserAgent string = AppName + "/" + Version + " (" + Url + "; " + Email + ")" diff --git a/media/ffmpeg.go b/media/ffmpeg.go index 9dfa4c2..5ff6c3e 100644 --- a/media/ffmpeg.go +++ b/media/ffmpeg.go @@ -3,12 +3,142 @@ package media import ( "errors" "fmt" + p "path/filepath" "strconv" + s "strings" "codeberg.org/danjones000/strip-beats/config" + t "codeberg.org/danjones000/strip-beats/media/tags" + "codeberg.org/danjones000/strip-beats/utils" + "github.com/google/uuid" ffmpeg "github.com/u2takey/ffmpeg-go" ) +func ConvertAndTag(in Probe, tags t.Tags) (string, error) { + st := in.GetFirstAcceptableAudio() + if st == nil { + st = in.GetFirstAudio() + } + if st == nil { + return "", errors.New("Can't find an audio stream") + } + + conf := config.GetConfig() + codec := utils.Tern(st.isAcceptableCodec(), "copy", conf.FfEncoder) + target := utils.Tern(st.isAcceptableCodec(), st.CodecName, conf.Codec) + + base := p.Base(in.Format.Path) + ext := p.Ext(base) + base = s.TrimSuffix(base, ext) + ext = conf.CodecExt[target] + + out := p.Join(conf.SavePath, base+"."+ext) + + input := ffmpeg.Input(in.Format.Path).Get(strconv.Itoa(st.Index)) + args := ffmpeg.KwArgs{"c:a": codec} + output := input.Output(out, args, getMetadataArgs(ext, tags)).GlobalArgs("-y") + + return out, output.Run() +} + +func getMetadataArgs(ext string, tags t.Tags) ffmpeg.KwArgs { + var meta []string + args := ffmpeg.KwArgs{} + switch ext { + case "opus": + fallthrough + case "flac": + fallthrough + case "ogg": + meta = append(meta, mapMetaKeys(getOggMeta(tags))...) + case "m4a": + fallthrough + case "mp4": + meta = append(meta, mapMetaKeys(getMp4Meta(tags))...) + case "mp3": + // @todo meta = append(meta, mapMetaKeys(getMp3Meta(tags))...) + } + + meta = append(meta, "comment=Processed by "+config.UserAgent) + args["metadata"] = meta + return args +} + +func mapMetaKeys(meta map[string]string) []string { + out := []string{} + for k, v := range meta { + if v != "" { + out = append(out, fmt.Sprintf("%s=%s", k, v)) + } + } + return out +} + +func getOggMeta(tags t.Tags) map[string]string { + meta := map[string]string{} + meta["TITLE"] = tags.Title + meta["ARTIST"] = tags.Artist + meta["ALBUM_ARTIST"] = tags.AlbumArtist + meta["ALBUMARTIST"] = tags.AlbumArtist + meta["ALBUM"] = tags.Album + meta["DATE"] = tags.Date + year, _, _ := s.Cut(tags.Date, "-") + meta["YEAR"] = year + meta["URL"] = tags.Url + meta["PURL"] = tags.Url + meta["URI"] = tags.Url + meta["ARTISTSORT"] = tags.ArtistSort + meta["ALBUMARTISTSORT"] = tags.AlbumArtistSort + meta["RELEASECOUNTRY"] = tags.ReleaseCountry + meta["LABEL"] = tags.Label + meta["GENRE"] = tags.Genre + meta["DISC"] = utils.Tern(tags.Disc > 0, strconv.Itoa(tags.Disc), "") + meta["DISCTOTAL"] = utils.Tern(tags.DiscCount > 0, strconv.Itoa(tags.DiscCount), "") + meta["TRACK"] = utils.Tern(tags.Track > 0, strconv.Itoa(tags.Track), "") + meta["TRACKTOTAL"] = utils.Tern(tags.TrackCount > 0, strconv.Itoa(tags.TrackCount), "") + if tags.AcoustidId != uuid.Nil { + meta["ACOUSTID_ID"] = tags.AcoustidId.String() + } + if tags.MusicbrainzReleaseGroupId != uuid.Nil { + meta["MUSICBRAINZ_RELEASEGROUPID"] = tags.MusicbrainzReleaseGroupId.String() + } + if tags.MusicbrainzAlbumId != uuid.Nil { + meta["MUSICBRAINZ_ALBUMID"] = tags.MusicbrainzAlbumId.String() + } + if tags.MusicbrainzAlbumArtistId != uuid.Nil { + meta["MUSICBRAINZ_ALBUMARTISTID"] = tags.MusicbrainzAlbumArtistId.String() + } + if tags.MusicbrainzRecordingId != uuid.Nil { + meta["MUSICBRAINZ_RECORDINGID"] = tags.MusicbrainzRecordingId.String() + } + if tags.MusicBrainzLabelId != uuid.Nil { + meta["MUSICBRAINZ_LABELID"] = tags.MusicBrainzLabelId.String() + } + + return meta +} + +func getMp4Meta(tags t.Tags) map[string]string { + meta := map[string]string{} + meta["title"] = tags.Title + meta["artist"] = tags.Artist + meta["album_artist"] = tags.AlbumArtist + meta["album"] = tags.Album + year, _, _ := s.Cut(tags.Date, "-") + meta["year"] = year + meta["description"] = tags.Url + meta["genre"] = tags.Genre + meta["network"] = tags.Label + if tags.Disc > 0 { + meta["disc"] = fmt.Sprintf("%d", tags.Disc) + utils.Tern(tags.DiscCount > 0, fmt.Sprintf("/%d", tags.DiscCount), "") + } + if tags.Track > 0 { + meta["track"] = fmt.Sprintf("%d", tags.Track) + utils.Tern(tags.TrackCount > 0, fmt.Sprintf("/%d", tags.TrackCount), "") + } + + return meta +} + func TrimWithFade(in Probe, out string, start, stop, up, down float64) error { // -ss (start) -t (end) -af afade=t=in:st=(start):d=(up),afade=t=out:st=(downstart):d=(down) st := in.GetFirstAcceptableAudio()