From d0599f654e585321e953019776a167b1fa4d8036 Mon Sep 17 00:00:00 2001 From: Mathis Maquenne <124215603+mathismqn@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:34:25 +0200 Subject: [PATCH] feat(tags): add tempo and key --- .gitignore | 3 ++ cmd/download.go | 10 ++++- go.mod | 3 ++ go.sum | 17 +++++++++ internal/deezer/song.go | 83 +++++++++++++++++++++++++++++++++++++++++ internal/tags/flac.go | 6 ++- internal/tags/id3v2.go | 5 ++- internal/tags/tag.go | 6 +-- 8 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09a5631 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +build.sh +builds/ diff --git a/cmd/download.go b/cmd/download.go index 879f988..a6c9ef4 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -147,7 +147,15 @@ func downloadContent(contentType string, args []string) { } fmt.Printf("\r Downloading %s... DONE\n", songTitle) - if err := tags.AddTags(resource, song, filePath); err != nil { + tempo, key, err := song.GetTempoAndKey() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: could not get tempo and key: %v\n", err) + } else { + fmt.Printf(" Tempo: %s\n", tempo) + fmt.Printf(" Key: %s\n", key) + } + + if err := tags.AddTags(resource, song, filePath, tempo, key); err != nil { fmt.Fprintf(os.Stderr, "Error: could not add tags to song: %v\n", err) } } diff --git a/go.mod b/go.mod index d107a2e..f505774 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( ) require ( + github.com/andybalholm/cascadia v1.3.2 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -22,6 +23,7 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.29.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect @@ -29,6 +31,7 @@ require ( ) require ( + github.com/PuerkitoBio/goquery v1.10.0 github.com/bogem/id3v2/v2 v2.1.4 github.com/flytam/filenamify v1.2.0 github.com/go-flac/flacpicture/v2 v2.0.2 diff --git a/go.sum b/go.sum index 2ef2287..fbf9919 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= +github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/bogem/id3v2/v2 v2.1.4 h1:CEwe+lS2p6dd9UZRlPc1zbFNIha2mb2qzT1cCEoNWoI= github.com/bogem/id3v2/v2 v2.1.4/go.mod h1:l+gR8MZ6rc9ryPTPkX77smS5Me/36gxkMgDayZ9G1vY= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -79,29 +83,42 @@ golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= diff --git a/internal/deezer/song.go b/internal/deezer/song.go index e4a4782..7d1853d 100644 --- a/internal/deezer/song.go +++ b/internal/deezer/song.go @@ -6,7 +6,11 @@ import ( "fmt" "io" "net/http" + "net/url" + "strconv" + "strings" + "github.com/PuerkitoBio/goquery" "github.com/mathismqn/godeez/internal/config" ) @@ -93,3 +97,82 @@ func (s *Song) GetCoverImage() ([]byte, error) { return io.ReadAll(resp.Body) } + +func (s *Song) GetTempoAndKey() (string, string, error) { + reqUrl := "https://songbpm.com/searches" + values := url.Values{} + values.Add("query", fmt.Sprintf("%s %s %s", s.Artist, s.Title, s.Version)) + + client := &http.Client{} + req, err := http.NewRequest("POST", reqUrl, bytes.NewBufferString(values.Encode())) + if err != nil { + return "", "", err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Origin", "https://songbpm.com") + + resp, err := client.Do(req) + if err != nil { + return "", "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + doc, err := goquery.NewDocumentFromReader(resp.Body) + if err != nil { + return "", "", err + } + + var key, bpm string + var found bool + + doc.Find("a.flex.flex-col").Each(func(i int, selection *goquery.Selection) { + if found { + return + } + + if strings.Contains(selection.Text(), s.Title) && strings.Contains(selection.Text(), s.Artist) { + foundArtist := selection.Find("p.text-sm.font-light.uppercase").Text() + foundTitle := selection.Find("p.pr-2.text-lg").Text() + + if strings.Contains(strings.ToLower(foundArtist), strings.ToLower(s.Artist)) && strings.Contains(strings.ToLower(foundTitle), strings.ToLower(s.Title)) { + durationStr := strings.TrimSpace(selection.Find("div.flex-1.flex-col.items-center").Eq(1).Find("span.text-2xl").Text()) + parts := strings.Split(durationStr, ":") + if len(parts) != 2 { + return + } + minutes, err := strconv.Atoi(parts[0]) + if err != nil { + fmt.Println(err) + return + } + seconds, err := strconv.Atoi(parts[1]) + if err != nil { + return + } + + foundDuration := minutes*60 + seconds + duration, err := strconv.Atoi(s.Duration) + if err != nil { + return + } + + if foundDuration > (duration-2) || foundDuration < (duration+2) { + key = selection.Find("div.flex-1.flex-col.items-center").Eq(0).Find("span.text-2xl").Text() + bpm = selection.Find("div.flex-1.flex-col.items-center").Eq(2).Find("span.text-2xl").Text() + + found = true + } + } + } + }) + + if !found { + return "", "", fmt.Errorf("no data found") + } + + return bpm, key, nil +} diff --git a/internal/tags/flac.go b/internal/tags/flac.go index 97004c4..d9a0c45 100644 --- a/internal/tags/flac.go +++ b/internal/tags/flac.go @@ -16,7 +16,7 @@ type FLACTagger struct { Index int } -func (t *FLACTagger) AddTags(resource deezer.Resource, song *deezer.Song, cover []byte, path string) error { +func (t *FLACTagger) AddTags(resource deezer.Resource, song *deezer.Song, cover []byte, path, tempo, key string) error { if album, ok := resource.(*deezer.Album); ok { dateParts := strings.Split(album.Data.PhysicalReleaseDate, "-") if len(dateParts) == 3 { @@ -39,6 +39,10 @@ func (t *FLACTagger) AddTags(resource deezer.Resource, song *deezer.Song, cover t.addTag("REPLAYGAIN_TRACK_GAIN", song.Gain) t.addTag("ISRC", song.ISRC) + t.addTag("BPM", tempo) + t.addTag("KEY", key) + t.addTag("INITIALKEY", key) + cmtsmeta := t.Cmts.Marshal() if t.Index > 0 { t.File.Meta[t.Index] = &cmtsmeta diff --git a/internal/tags/id3v2.go b/internal/tags/id3v2.go index 76657bb..cbc3ddd 100644 --- a/internal/tags/id3v2.go +++ b/internal/tags/id3v2.go @@ -13,7 +13,7 @@ type ID3v2Tagger struct { Tag *id3v2.Tag } -func (t *ID3v2Tagger) AddTags(resource deezer.Resource, song *deezer.Song, cover []byte, path string) error { +func (t *ID3v2Tagger) AddTags(resource deezer.Resource, song *deezer.Song, cover []byte, path, tempo, key string) error { defer t.Tag.Close() duration, _ := strconv.Atoi(song.Duration) @@ -37,6 +37,9 @@ func (t *ID3v2Tagger) AddTags(resource deezer.Resource, song *deezer.Song, cover t.addTXXXTag("GAIN", song.Gain) t.addTXXXTag("ISRC", song.ISRC) + t.addTag("TBPM", tempo) + t.addTag("TKEY", key) + frame := id3v2.PictureFrame{ Encoding: t.Tag.DefaultEncoding(), MimeType: "image/jpeg", diff --git a/internal/tags/tag.go b/internal/tags/tag.go index bcedd1e..7ea4f43 100644 --- a/internal/tags/tag.go +++ b/internal/tags/tag.go @@ -10,7 +10,7 @@ import ( ) type Tagger interface { - AddTags(resource deezer.Resource, song *deezer.Song, cover []byte, path string) error + AddTags(resource deezer.Resource, song *deezer.Song, cover []byte, path, tempo, key string) error } func NewTagger(filePath string) (Tagger, error) { @@ -38,7 +38,7 @@ func NewTagger(filePath string) (Tagger, error) { return &FLACTagger{File: file, Cmts: cmts, Index: idx}, nil } -func AddTags(resource deezer.Resource, song *deezer.Song, filePath string) error { +func AddTags(resource deezer.Resource, song *deezer.Song, filePath, tempo, key string) error { tagger, err := NewTagger(filePath) if err != nil { return err @@ -48,5 +48,5 @@ func AddTags(resource deezer.Resource, song *deezer.Song, filePath string) error return err } - return tagger.AddTags(resource, song, cover, filePath) + return tagger.AddTags(resource, song, cover, filePath, tempo, key) }