diff --git a/cmd/download.go b/cmd/download.go index 89fdff0..27d150f 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/mathismqn/godeez/internal/deezer" + "github.com/mathismqn/godeez/internal/tags" "github.com/spf13/cobra" ) @@ -102,7 +103,7 @@ var downloadCmd = &cobra.Command{ } fmt.Printf("\r Downloading %s... DONE\n", songTitle) - if err := deezer.AddTags(album, song, filePath); err != nil { + if err := tags.Add(album, song, filePath); 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 aaafce5..2c6c23b 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,9 @@ require ( require ( github.com/bogem/id3v2/v2 v2.1.4 + github.com/go-flac/flacpicture/v2 v2.0.2 + github.com/go-flac/flacvorbis/v2 v2.0.2 + github.com/go-flac/go-flac/v2 v2.0.1 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/crypto v0.28.0 diff --git a/go.sum b/go.sum index 0be5452..16c9e65 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,12 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-flac/flacpicture/v2 v2.0.2 h1:HCaJIVZpxnpdWs6G3ECEVRelzqS5xOi1Ba1AGmtXbzE= +github.com/go-flac/flacpicture/v2 v2.0.2/go.mod h1:DMZBPWPAmdLqNhqFSy5ZBs9wyBzOekXutGfP7/TFCuo= +github.com/go-flac/flacvorbis/v2 v2.0.2 h1:xCL3OhxrxWkHrbWUBvGNe+6FQ03yLmBbz0v5z4V2PoQ= +github.com/go-flac/flacvorbis/v2 v2.0.2/go.mod h1:SwTB5gs13VaM/N7rstwPoUsPibiMKklgwybYP9dYo2g= +github.com/go-flac/go-flac/v2 v2.0.1 h1:1zilNkbmmpK9DLsz2NbjLHG8avOmthYqUfVc9YKB/Ps= +github.com/go-flac/go-flac/v2 v2.0.1/go.mod h1:hvgeR2hElLbwk0Q1/vMazIDmIc2LAFSd9Bx/Fk6ViKo= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= diff --git a/internal/deezer/album.go b/internal/deezer/album.go index 8f9eca3..88385ac 100644 --- a/internal/deezer/album.go +++ b/internal/deezer/album.go @@ -51,3 +51,18 @@ func GetAlbumData(id string) (*Album, error) { return &album, nil } + +func (a *Album) GetCoverImage() ([]byte, error) { + url := fmt.Sprintf("https://e-cdn-images.dzcdn.net/images/cover/%s/500x500-000000-80-0-0.jpg", a.Data.CoverID) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return io.ReadAll(resp.Body) +} diff --git a/internal/deezer/song.go b/internal/deezer/song.go index 21b5c04..efe9484 100644 --- a/internal/deezer/song.go +++ b/internal/deezer/song.go @@ -20,12 +20,11 @@ type Song struct { Composers []string `json:"composer"` Authors []string `json:"author"` } `json:"SNG_CONTRIBUTORS"` - Duration string `json:"DURATION"` - Gain string `json:"GAIN"` - ISRC string `json:"ISRC"` - ExplicitLyrics string `json:"EXPLICIT_LYRICS"` - TrackNumber string `json:"TRACK_NUMBER"` - TrackToken string `json:"TRACK_TOKEN"` + Duration string `json:"DURATION"` + Gain string `json:"GAIN"` + ISRC string `json:"ISRC"` + TrackNumber string `json:"TRACK_NUMBER"` + TrackToken string `json:"TRACK_TOKEN"` } func (s *Song) GetMediaData(quality string) (*Media, error) { diff --git a/internal/deezer/tag.go b/internal/deezer/tag.go deleted file mode 100644 index cd10218..0000000 --- a/internal/deezer/tag.go +++ /dev/null @@ -1,101 +0,0 @@ -package deezer - -import ( - "fmt" - "io" - "net/http" - "path" - "strconv" - "strings" - - "github.com/bogem/id3v2/v2" -) - -func AddTags(album *Album, song *Song, pathFile string) error { - cover, err := getCoverImage(album.Data.CoverID) - if err != nil { - return err - } - - ext := path.Ext(pathFile) - if ext == ".mp3" { - tag, err := id3v2.Open(pathFile, id3v2.Options{Parse: true}) - if err != nil { - return err - } - defer tag.Close() - - var year string - duration, _ := strconv.Atoi(song.Duration) - dateParts := strings.Split(album.Data.PhysicalReleaseDate, "-") - if len(dateParts) == 3 { - year = dateParts[0] - } - - addTag(tag, "TALB", album.Data.Name) - addTag(tag, "TPE2", album.Data.Artist) - addTag(tag, "TPUB", album.Data.Label) - addTag(tag, "TDOR", album.Data.OriginalReleaseDate) - addTag(tag, "COMM", album.Data.ProducerLine) - if year != "" { - addTag(tag, "TYER", year) - } - - addTag(tag, "TIT2", song.Title) - addTag(tag, "TPE1", strings.Join(song.Contributors.MainArtists, " / ")) - addTag(tag, "TCOM", strings.Join(song.Contributors.Composers, " / ")) - addTag(tag, "TEXT", strings.Join(song.Contributors.Authors, " / ")) - addTag(tag, "TRCK", song.TrackNumber) - if duration > 0 { - addTag(tag, "TLEN", fmt.Sprintf("%d", duration*1000)) - } - addTXXXTag(tag, "GAIN", song.Gain) - addTXXXTag(tag, "ISRC", song.ISRC) - addTXXXTag(tag, "Explicit lyrics", song.ExplicitLyrics) - - frame := id3v2.PictureFrame{ - Encoding: tag.DefaultEncoding(), - MimeType: "image/jpeg", - PictureType: id3v2.PTFrontCover, - Description: "Cover", - Picture: cover, - } - tag.AddAttachedPicture(frame) - - return tag.Save() - } - - return nil -} - -func getCoverImage(id string) ([]byte, error) { - url := fmt.Sprintf("https://e-cdn-images.dzcdn.net/images/cover/%s/500x500-000000-80-0-0.jpg", id) - resp, err := http.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - return io.ReadAll(resp.Body) -} - -func addTag(tag *id3v2.Tag, name, value string) { - if value != "" { - tag.AddTextFrame(name, tag.DefaultEncoding(), value) - } -} - -func addTXXXTag(tag *id3v2.Tag, description, value string) { - if value != "" { - udf := id3v2.UserDefinedTextFrame{ - Encoding: tag.DefaultEncoding(), - Description: description, - Value: value, - } - tag.AddUserDefinedTextFrame(udf) - } -} diff --git a/internal/tags/flac/tag.go b/internal/tags/flac/tag.go new file mode 100644 index 0000000..b20fbb2 --- /dev/null +++ b/internal/tags/flac/tag.go @@ -0,0 +1,86 @@ +package flac + +import ( + "os" + "strings" + + "github.com/go-flac/flacpicture/v2" + "github.com/go-flac/flacvorbis/v2" + "github.com/go-flac/go-flac/v2" + "github.com/mathismqn/godeez/internal/deezer" +) + +func AddTags(album *deezer.Album, song *deezer.Song, cover []byte, path string) error { + file, err := flac.ParseFile(path) + if err != nil { + return err + } + cmts, idx, err := extractFLACComment(file) + if err != nil { + return err + } + if cmts == nil && idx > 0 { + cmts = flacvorbis.New() + } + + addTag(cmts, "ALBUM", album.Data.Name) + addTag(cmts, "ALBUMARTIST", album.Data.Artist) + addTag(cmts, "PUBLISHER", album.Data.Label) + addTag(cmts, "ORIGINALDATE", album.Data.OriginalReleaseDate) + addTag(cmts, "DATE", album.Data.PhysicalReleaseDate) + addTag(cmts, "COMMENT", album.Data.ProducerLine) + + addTag(cmts, "TITLE", song.Title) + addTag(cmts, "ARTIST", strings.Join(song.Contributors.MainArtists, " / ")) + addTag(cmts, "COMPOSER", strings.Join(song.Contributors.Composers, " / ")) + addTag(cmts, "LYRICIST", strings.Join(song.Contributors.Authors, " / ")) + addTag(cmts, "TRACKNUMBER", song.TrackNumber) + addTag(cmts, "REPLAYGAIN_TRACK_GAIN", song.Gain) + addTag(cmts, "ISRC", song.ISRC) + + cmtsmeta := cmts.Marshal() + if idx > 0 { + file.Meta[idx] = &cmtsmeta + } else { + file.Meta = append(file.Meta, &cmtsmeta) + } + + picture, err := flacpicture.NewFromImageData(flacpicture.PictureTypeFrontCover, "Front cover", cover, "image/jpeg") + if err != nil { + return err + } + picturemeta := picture.Marshal() + file.Meta = append(file.Meta, &picturemeta) + + return saveTags(file, path) +} + +func addTag(cmts *flacvorbis.MetaDataBlockVorbisComment, name, value string) { + if value != "" { + cmts.Add(name, value) + } +} + +func extractFLACComment(file *flac.File) (*flacvorbis.MetaDataBlockVorbisComment, int, error) { + var cmt *flacvorbis.MetaDataBlockVorbisComment + var cmtIdx int + var err error + for idx, meta := range file.Meta { + if meta.Type == flac.VorbisComment { + cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta) + cmtIdx = idx + if err != nil { + return nil, 0, err + } + } + } + + return cmt, cmtIdx, nil +} + +func saveTags(file *flac.File, path string) error { + tempPath := path + ".tmp" + file.Save(tempPath) + + return os.Rename(tempPath, path) +} diff --git a/internal/tags/id3v2/tag.go b/internal/tags/id3v2/tag.go new file mode 100644 index 0000000..04ebbce --- /dev/null +++ b/internal/tags/id3v2/tag.go @@ -0,0 +1,60 @@ +package id3v2 + +import ( + "strings" + + "github.com/bogem/id3v2/v2" + "github.com/mathismqn/godeez/internal/deezer" +) + +func AddTags(album *deezer.Album, song *deezer.Song, cover []byte, path string) error { + tag, err := id3v2.Open(path, id3v2.Options{Parse: true}) + if err != nil { + return err + } + defer tag.Close() + + addTag(tag, "TALB", album.Data.Name) + addTag(tag, "TPE2", album.Data.Artist) + addTag(tag, "TPUB", album.Data.Label) + addTag(tag, "TDOR", album.Data.OriginalReleaseDate) + addTag(tag, "TYER", album.Data.PhysicalReleaseDate) + addTag(tag, "COMM", album.Data.ProducerLine) + + addTag(tag, "TIT2", song.Title) + addTag(tag, "TPE1", strings.Join(song.Contributors.MainArtists, " / ")) + addTag(tag, "TCOM", strings.Join(song.Contributors.Composers, " / ")) + addTag(tag, "TEXT", strings.Join(song.Contributors.Authors, " / ")) + addTag(tag, "TRCK", song.TrackNumber) + addTag(tag, "TLEN", song.Duration) + addTXXXTag(tag, "GAIN", song.Gain) + addTXXXTag(tag, "ISRC", song.ISRC) + + frame := id3v2.PictureFrame{ + Encoding: tag.DefaultEncoding(), + MimeType: "image/jpeg", + PictureType: id3v2.PTFrontCover, + Description: "Cover", + Picture: cover, + } + tag.AddAttachedPicture(frame) + + return tag.Save() +} + +func addTag(tag *id3v2.Tag, name, value string) { + if value != "" { + tag.AddTextFrame(name, tag.DefaultEncoding(), value) + } +} + +func addTXXXTag(tag *id3v2.Tag, description, value string) { + if value != "" { + udf := id3v2.UserDefinedTextFrame{ + Encoding: tag.DefaultEncoding(), + Description: description, + Value: value, + } + tag.AddUserDefinedTextFrame(udf) + } +} diff --git a/internal/tags/tag.go b/internal/tags/tag.go new file mode 100644 index 0000000..7eae30b --- /dev/null +++ b/internal/tags/tag.go @@ -0,0 +1,33 @@ +package tags + +import ( + "fmt" + "path" + "strconv" + "strings" + + "github.com/mathismqn/godeez/internal/deezer" + "github.com/mathismqn/godeez/internal/tags/flac" + "github.com/mathismqn/godeez/internal/tags/id3v2" +) + +func Add(album *deezer.Album, song *deezer.Song, pathFile string) error { + cover, err := album.GetCoverImage() + if err != nil { + return err + } + + duration, _ := strconv.Atoi(song.Duration) + song.Duration = fmt.Sprintf("%d", duration*1000) + dateParts := strings.Split(album.Data.PhysicalReleaseDate, "-") + if len(dateParts) == 3 { + album.Data.PhysicalReleaseDate = dateParts[0] + } + + ext := path.Ext(pathFile) + if ext == ".mp3" { + return id3v2.AddTags(album, song, cover, pathFile) + } + + return flac.AddTags(album, song, cover, pathFile) +}