diff --git a/LICENSE b/LICENSE index e69de29..c2984bc 100644 --- a/LICENSE +++ b/LICENSE @@ -0,0 +1,7 @@ +© 2023 Dan Jones + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc4e3a9 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# strip-beats + +Imagine you have a folder full of music videos. You want to go through those videos, turn it into an audio file, tag them, and add it to your music collection. + +This app aims to streamline that. + +## Pre-requisites + +- ffmpeg +- fpcalc (for fingerprinting audio so that we can identify it and automatically pull tags) +- mpv (for watching the video to ensure you're stipping out non-music elements, like an intro or outro) +- An account on acousticid.org (for querying the fingerprint to get data) + +## Install + +- Pull the repo +- `go install .` + +Hopefully, I'll have binaries downloadable soon. + +## Disclaimer + +This does not work yet! Some features are implemented, but most are not yet. Have a look around at the code, but don't actually use this at all. diff --git a/app/choose.go b/app/choose.go index 4d07959..da98672 100644 --- a/app/choose.go +++ b/app/choose.go @@ -5,7 +5,7 @@ import ( "os" "codeberg.org/danjones000/strip-beats/files" - "codeberg.org/danjones000/strip-beats/input/boolean" + "codeberg.org/danjones000/strip-beats/io/boolean" "codeberg.org/danjones000/strip-beats/media" "codeberg.org/danjones000/strip-beats/utils" "github.com/rkoesters/xdg/trash" @@ -20,8 +20,10 @@ func PickNewFile() media.Probe { } func SetFile(path string) media.Probe { + resetTags() f := media.ProbeFile(path) file = &f + copyTagsFromFile() return f } @@ -70,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 2e5cdd1..96b3ac0 100644 --- a/app/menu.go +++ b/app/menu.go @@ -3,14 +3,14 @@ package app import ( "fmt" - "codeberg.org/danjones000/strip-beats/input/list" + "codeberg.org/danjones000/strip-beats/io/list" ) 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/print.go b/app/print.go index 7ce26dc..1a87c68 100644 --- a/app/print.go +++ b/app/print.go @@ -3,15 +3,48 @@ package app import ( "fmt" + "codeberg.org/danjones000/strip-beats/io/boolean" + "codeberg.org/danjones000/strip-beats/io/list" + m "codeberg.org/danjones000/strip-beats/io/message" "codeberg.org/danjones000/strip-beats/media" "codeberg.org/danjones000/strip-beats/media/brainz" + "codeberg.org/danjones000/strip-beats/utils" + "github.com/akrennmair/slice" ) +type recOpt struct { + rec brainz.Recording + r rune + score float64 +} + +func (o recOpt) Title() string { + return o.rec.Title +} + +func (o recOpt) Text() string { + return fmt.Sprintf( + "(Score %.2f) By %s - First Released %s - %s %s", + 100*o.score, + o.rec.FirstArtist().Name, + o.rec.FirstReleaseDate, + o.rec.FirstGenre().Name, + utils.Tern(o.rec.Video, "(Video)", "")) +} + +func (o recOpt) Rune() rune { + return o.r +} + +func (o recOpt) Selected() func() { + return nil +} + func print() { if file == nil { PickFileWithConf() } - fp, err := media.Fingerprint("/home/drj/MyFiles/Videos/WebShows/YouTube/Dolly_Parton_-_Topic/Just_Because_I_m_a_Woman.Dolly_Parton_-_Topic.Fmv-XQerVkM.webm") + fp, err := media.Fingerprint(file.Format.Path) if err != nil { panic(err) } @@ -19,17 +52,136 @@ func print() { if err != nil { panic(err) } - var recs []brainz.Recording + rec, ok := chooseRec(ids) + if !ok { + m.Message("Couldn't find a marching recording") + return + } + + fmt.Println(rec.Title) + + tags.Title = rec.Title + + tags.MusicbrainzRecordingId = rec.Id + tags.Date = rec.FirstReleaseDate + tags.Genre = rec.FirstGenre().Name + tags.Artist = rec.FirstArtist().Name + tags.ArtistSort = rec.FirstArtist().SortName + tags.AcoustidId = rec.AcousticId + + rel := findFirstRelease(rec) + if !boolean.Choose(fmt.Sprintf("Is %s album (%s %s) correct?", rel.Title, rel.Country, rel.Date)) { + rel = chooseRel(rec) + } + media := rel.Media[0] + track := media.Tracks[0] + full, err := brainz.GetReleaseWithMedia(rel.Id) + if err != nil { + panic(err) + } + albumArtist := full.ArtistCredit[0] + + tags.MusicbrainzReleaseGroupId = full.ReleaseGroup.Id + tags.MusicbrainzAlbumId = rel.Id + tags.MusicbrainzAlbumArtistId = albumArtist.Artist.Id + tags.AlbumArtist = albumArtist.Name + tags.Album = rel.Title + if rel.Date != "" { + tags.Date = rel.Date + } + tags.AlbumArtistSort = albumArtist.Artist.SortName + tags.ReleaseCountry = rel.Country + if len(full.LabelInfo) > 0 { + tags.Label = full.LabelInfo[0].Label.Name + tags.MusicBrainzLabelId = full.LabelInfo[0].Label.Id + } + if len(rel.Genres) > 0 && rel.Genres[0].Name != "" { + tags.Genre = rel.Genres[0].Name + } + tags.Disc = media.Position + tags.DiscCount = len(full.Media) + tags.Track = track.Position + tags.TrackCount = media.TrackCount +} + +func chooseRec(ids media.IdResults) (brainz.Recording, bool) { + var recs []list.Option + var rec brainz.Recording + var err error + i := 'a' for _, res := range ids.Results { - for _, rec := range res.Recordings { + for _, rec = range res.Recordings { err = brainz.FillRecording(&rec) if err != nil { panic(err) } - recs = append(recs, rec) - + rec.AcousticId = res.Id + recs = append(recs, recOpt{rec, i, res.Score}) + if rec.Title != "" { + // Empty titles will be filtered out, so we don't need to increment them + i = i + 1 + } } } + recs = slice.Filter(recs, func(opt list.Option) bool { + return opt.Title() != "" + }) - fmt.Printf("%+v\n", recs) + if len(recs) < 1 { + return rec, false + } + + return list.List("Which recording is the correct one?", recs, nil).(recOpt).rec, true +} + +func findFirstRelease(rec brainz.Recording) brainz.Release { + var rel brainz.Release + for _, rel = range rec.Releases { + if rel.Date == rec.FirstReleaseDate { + return rel + } + } + if len(rec.Releases) > 0 { + return rec.Releases[0] + } + return brainz.Release{} +} + +func chooseRel(rec brainz.Recording) brainz.Release { + var rels []list.Option + var rel brainz.Release + i := 'a' + for _, rel = range rec.Releases { + rels = append(rels, relOpt{rel, i}) + if rel.Title != "" { + // Empty titles will be filtered out, so we don't need to increment them + i = i + 1 + } + } + rels = slice.Filter(rels, func(opt list.Option) bool { + return opt.Title() != "" + }) + + return list.List("Which releases is the correct one?", rels, nil).(relOpt).rel +} + +type relOpt struct { + rel brainz.Release + r rune +} + +func (o relOpt) Title() string { + return o.rel.Title +} + +func (o relOpt) Text() string { + return fmt.Sprintf("%s %s", o.rel.Country, o.rel.Date) +} + +func (o relOpt) Rune() rune { + return o.r +} + +func (o relOpt) Selected() func() { + return nil } diff --git a/app/run.go b/app/run.go index a42b1ab..cf01523 100644 --- a/app/run.go +++ b/app/run.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "codeberg.org/danjones000/strip-beats/input/boolean" + "codeberg.org/danjones000/strip-beats/io/boolean" "codeberg.org/danjones000/strip-beats/media" "codeberg.org/danjones000/strip-beats/media/brainz" ) @@ -16,6 +16,7 @@ const ( Watch Fade Print + Convert Restart Quit ) @@ -51,14 +52,17 @@ func testMb() { } func testPrint() { - SetFile("/home/drj/MyFiles/Videos/WebShows/YouTube/Dolly_Parton_-_Topic/Just_Because_I_m_a_Woman.Dolly_Parton_-_Topic.Fmv-XQerVkM.webm") + // SetFile("/home/drj/MyFiles/Videos/WebShows/YouTube/Dolly_Parton_-_Topic/Just_Because_I_m_a_Woman.Dolly_Parton_-_Topic.Fmv-XQerVkM.webm") + // SetFile("/home/drj/MyFiles/Videos/WebShows/YouTube/Whitney_Houston/I_Will_Always_Love_You_Ultimate_Collection_Edit.Whitney_Houston.rB7z_l8mBxw.mp4") + SetFile("/home/drj/MyFiles/Music/Original_Broadway_Cast_of_Hamilton/Hamilton/d02t12-We_Know.m4a") + // SetFile("/home/drj/MyFiles/Videos/WebShows/YouTube/KaceyMusgravesVEVO/Kacey_Musgraves_-_Biscuits-nGIUtLO_x8g.mp4") + // SetFile("/home/drj/MyFiles/Videos/WebShows/YouTube/Willie_Nelson_-_Topic/Too_Sick_To_Pray.Willie_Nelson_-_Topic.8QgBXo41j2E.webm") print() quit() } func Run(step AppStep) { - testPrint() for step < Quit { switch step { case Pick: @@ -91,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 new file mode 100644 index 0000000..cd25b4d --- /dev/null +++ b/app/tags.go @@ -0,0 +1,21 @@ +package app + +import ( + "errors" + + t "codeberg.org/danjones000/strip-beats/media/tags" +) + +var tags t.Tags + +func resetTags() { + tags = t.Tags{} +} + +func copyTagsFromFile() { + if file == nil { + panic(errors.New("Missing file")) + } + + tags = file.FullTags() +} diff --git a/cmd/root.go b/cmd/root.go index 13aaf4b..a04a3ce 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 NAME HERE +Copyright © 2023 Dan Jones */ package cmd 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/go.mod b/go.mod index 093ef9b..ab3c23e 100644 --- a/go.mod +++ b/go.mod @@ -13,21 +13,25 @@ require ( github.com/rivo/tview v0.0.0-20230826224341-9754ab44dc1c github.com/rkoesters/xdg v0.0.1 github.com/spf13/cobra v1.7.0 + github.com/stretchr/testify v1.7.0 github.com/u2takey/ffmpeg-go v0.5.0 golang.org/x/term v0.11.0 ) require ( github.com/aws/aws-sdk-go v1.38.20 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/tcell/v2 v2.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/u2takey/go-utils v0.3.1 // indirect golang.org/x/sys v0.11.0 // indirect golang.org/x/text v0.7.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6e6d58e..a93a960 100644 --- a/go.sum +++ b/go.sum @@ -117,6 +117,7 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/input/boolean/boolean.go b/io/boolean/boolean.go similarity index 100% rename from input/boolean/boolean.go rename to io/boolean/boolean.go diff --git a/input/list/list.go b/io/list/list.go similarity index 100% rename from input/list/list.go rename to io/list/list.go diff --git a/io/message/message.go b/io/message/message.go new file mode 100644 index 0000000..c73ffcd --- /dev/null +++ b/io/message/message.go @@ -0,0 +1,20 @@ +package message + +import "github.com/rivo/tview" + +func Message(text string) { + app := tview.NewApplication() + modal := tview.NewModal() + if text != "" { + modal.SetText(text) + } + + modal.AddButtons([]string{"Ok"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + app.Stop() + }) + + if err := app.SetRoot(modal, false).EnableMouse(true).Run(); err != nil { + panic(err) + } +} diff --git a/main.go b/main.go index e7b5f1c..a2c4aef 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,5 @@ /* -Copyright © 2023 NAME HERE - +Copyright © 2023 Dan Jones */ package main diff --git a/media/brainz/brainz.go b/media/brainz/brainz.go index da9d999..47bd4c9 100644 --- a/media/brainz/brainz.go +++ b/media/brainz/brainz.go @@ -12,6 +12,7 @@ import ( type Recording struct { Id uuid.UUID + AcousticId uuid.UUID Isrcs []string FirstReleaseDate string `json:"first-release-date"` Length int @@ -19,6 +20,45 @@ type Recording struct { Video bool Releases []Release Genres []Genre + ArtistCredit []ArtistCredit `json:"artist-credit"` +} + +func (r Recording) FirstArtist() Artist { + var a Artist + for _, ac := range r.ArtistCredit { + if ac.Artist.Name != "" { + return ac.Artist + } + } + for _, rel := range r.Releases { + for _, ac := range rel.ArtistCredit { + if ac.Artist.Name != "" { + return ac.Artist + } + } + } + + if a.Name == "" { + a.Name = "Unknown Artist" + } + return a +} + +func (r Recording) FirstGenre() Genre { + var g Genre + for _, g = range r.Genres { + if g.Name != "" { + return g + } + } + for _, rel := range r.Releases { + for _, g = range rel.Genres { + if g.Name != "" { + return g + } + } + } + return g } type Genre struct { @@ -27,21 +67,57 @@ type Genre struct { } type Release struct { - Id uuid.UUID - Country string - Date string - Media []Media - Status string - StatusId uuid.UUID `json:"status-id"` - ArtistCredit []ArtistCredit `json:"artist-credit"` - Title string - Genres []Genre + Id uuid.UUID + Asin string + Barcode string + Country string + Date string + Disambiguation string + Media []Media + Packaging string + PackagingId uuid.UUID `json:"packaging-id"` + Quality string + Status string + StatusId uuid.UUID `json:"status-id"` + ArtistCredit []ArtistCredit `json:"artist-credit"` + Title string + Genres []Genre + ReleaseGroup ReleaseGroup `json:"release-group"` + LabelInfo []LabelInfo `json:"label-info"` // ReleaseEvents []ReleaseEvent `json:"release-events"` } +type LabelInfo struct { + CatalogNumber string `json:"catalog-number"` + Label Label +} + +type Label struct { + Id uuid.UUID + Name string + SortName string `json:"sort-name"` + Disambiguation string + TypeId string `json:"type-id"` + Type string + LabelCode int `json:"label-code"` +} + +type ReleaseGroup struct { + Id uuid.UUID + Title string + ArtistCredit []ArtistCredit `json:"artist-credit"` + Disambiguation string + FirstReleaseDate string `json:"first-release-date"` + PrimaryType string `json:"primary-type"` + PrimaryTypeId uuid.UUID `json:"primary-type-id"` + SecondaryTypes []string `json:"secondary-types"` + SecondaryTypeIds []uuid.UUID `json:"secondary-type-ids"` +} + type ArtistCredit struct { - Name string - Artist Artist + Name string + Artist Artist + JoinPhrase string } type Artist struct { @@ -56,6 +132,7 @@ type Artist struct { type Media struct { FormatId uuid.UUID `json:"format-id"` Position int + Title string TrackOffset int `json:"track-offset"` Format string TrackCount int `json:"track-count"` @@ -70,6 +147,25 @@ type Track struct { Length int } +func GetReleaseWithMedia(id uuid.UUID) (Release, error) { + rel := Release{Id: id} + url := fmt.Sprintf("https://musicbrainz.org/ws/2/release/%s", id) + resp, err := h.GetWithQuery(url, u.Values{ + "fmt": []string{"json"}, + "inc": []string{"artist-credits+discids+labels+release-groups"}}) + if err != nil { + return rel, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return rel, err + } + err = json.Unmarshal(body, &rel) + + return rel, err +} + func GetRecording(id string) (Recording, error) { u, err := uuid.Parse(id) rec := Recording{Id: u} 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() diff --git a/media/fingerprint.go b/media/fingerprint.go index 2b4d1ad..5952bad 100644 --- a/media/fingerprint.go +++ b/media/fingerprint.go @@ -12,6 +12,7 @@ import ( "codeberg.org/danjones000/strip-beats/config" "codeberg.org/danjones000/strip-beats/media/brainz" h "codeberg.org/danjones000/strip-beats/utils/http" + "github.com/google/uuid" ) type FPrint struct { @@ -45,7 +46,7 @@ type IdResults struct { } type IdResult struct { - Id string + Id uuid.UUID Score float64 Recordings []brainz.Recording } diff --git a/media/probe.go b/media/probe.go index 6419b93..b3b4f29 100644 --- a/media/probe.go +++ b/media/probe.go @@ -4,6 +4,7 @@ import ( "encoding/json" "codeberg.org/danjones000/strip-beats/config" + "codeberg.org/danjones000/strip-beats/media/tags" "codeberg.org/danjones000/strip-beats/utils" "dario.cat/mergo" "github.com/akrennmair/slice" @@ -99,7 +100,7 @@ type Stream struct { StartTime float64 `json:"start_time,string"` Duration float64 `json:",string"` DurationTs int `json:"duration_ts"` - Tags Tags + Tags tags.Tags } func (st Stream) isWantedCodec() bool { @@ -124,26 +125,10 @@ type Format struct { Size int `json:",string"` BitRate int `json:"bit_rate,string"` Score int `json:"probe_score"` - Tags Tags + Tags tags.Tags } -type Tags struct { - MajorBrand string `json:"major_brand"` - Title string - Artist string - AlbumArtist string `json:"album_artist"` - Album string - Date string - Encoder string - Comment string - Description string - Composer string - Genre string - Disc string - Track string -} - -func (pr Probe) FullTags() Tags { +func (pr Probe) FullTags() tags.Tags { t := pr.Format.Tags s := pr.WantedAudioStream() if s != nil { diff --git a/media/tags/tags.go b/media/tags/tags.go new file mode 100644 index 0000000..eeb13fd --- /dev/null +++ b/media/tags/tags.go @@ -0,0 +1,35 @@ +package tags + +import ( + "github.com/google/uuid" +) + +type Tags struct { + Title string + Artist string + AlbumArtist string `json:"album_artist"` + Album string + Date string + Comment string + Description string + Synopsis string + Url string `json:"purl"` + AcoustidId uuid.UUID `json:"ACOUSTID_ID"` + MusicbrainzReleaseGroupId uuid.UUID `json:"MUSICBRAINZ_RELEASEGROUPID"` + MusicbrainzAlbumId uuid.UUID `json:"MUSICBRAINZ_ALBUMID"` + MusicbrainzAlbumArtistId uuid.UUID `json:"MUSICBRAINZ_ALBUMARTISTID"` + MusicbrainzRecordingId uuid.UUID + MusicBrainzLabelId uuid.UUID + ArtistSort string + AlbumArtistSort string + ReleaseCountry string + Label string + Composer string + Genre string + Disc int `json:"-"` + DiscCount int + Track int `json:"-"` + TrackCount int + TrackStr string `json:"track"` + DiscStr string `json:"disc"` +} diff --git a/utils/utils.go b/utils/utils.go index 1f40e98..17d76d5 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -29,3 +29,17 @@ func HourMinSecToSeconds(time string) (float64, error) { dur, _ := t.ParseDuration(f) return dur.Seconds(), nil } + +func Tern[V any](choice bool, one, two V) V { + if choice { + return one + } + return two +} + +func TernCall[V any](choice bool, one, two func() V) V { + if choice { + return one() + } + return two() +} diff --git a/utils/utils_tern_test.go b/utils/utils_tern_test.go new file mode 100644 index 0000000..5a9280c --- /dev/null +++ b/utils/utils_tern_test.go @@ -0,0 +1,23 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTern(t *testing.T) { + assert.Equal(t, Tern(true, 5, 2), 5) + assert.Equal(t, Tern(false, 5, 2), 2) +} + +func TestTernCall(t *testing.T) { + five := func() int { + return 5 + } + two := func() int { + return 2 + } + assert.Equal(t, TernCall(true, five, two), 5) + assert.Equal(t, TernCall(false, five, two), 2) +}