From f247a0f71ea930dac0e6f85f79f2ec23d2cf6141 Mon Sep 17 00:00:00 2001 From: Mathis Maquenne <124215603+mathismqn@users.noreply.github.com> Date: Sun, 13 Oct 2024 02:03:24 +0200 Subject: [PATCH] feat: download playlists --- cmd/download.go | 231 ++++++++++++++++++++---------------- cmd/download_album.go | 17 +++ cmd/download_playlist.go | 17 +++ internal/deezer/album.go | 59 +++------ internal/deezer/playlist.go | 35 ++++++ internal/deezer/resource.go | 42 +++++++ internal/deezer/song.go | 16 +++ internal/tags/flac.go | 87 ++++++++++++++ internal/tags/flac/tag.go | 86 -------------- internal/tags/id3v2.go | 67 +++++++++++ internal/tags/id3v2/tag.go | 60 ---------- internal/tags/tag.go | 59 +++++---- 12 files changed, 465 insertions(+), 311 deletions(-) create mode 100644 cmd/download_album.go create mode 100644 cmd/download_playlist.go create mode 100644 internal/deezer/playlist.go create mode 100644 internal/deezer/resource.go create mode 100644 internal/tags/flac.go delete mode 100644 internal/tags/flac/tag.go create mode 100644 internal/tags/id3v2.go delete mode 100644 internal/tags/id3v2/tag.go diff --git a/cmd/download.go b/cmd/download.go index 27d150f..1a16772 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -11,111 +11,138 @@ import ( "github.com/spf13/cobra" ) +var outputDir string +var quality string + var downloadCmd = &cobra.Command{ - Use: "download [album_id...]", - Short: "Download songs from one or more albums", - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - quality, _ := cmd.Flags().GetString("quality") - nAlbums := len(args) - - if quality == "" { - quality = "best" - } - validQualities := map[string]bool{ - "mp3_128": true, - "mp3_320": true, - "flac": true, - "best": true, - } - if !validQualities[quality] { - fmt.Fprintf(os.Stderr, "invalid quality option: %s\n", quality) - os.Exit(1) - } - - separator := "--------------------------------------------------" - for i, id := range args { - fmt.Println(separator) - fmt.Printf("[%d/%d] Getting data for album %s...", i+1, nAlbums, id) - - album, err := deezer.GetAlbumData(id) - if err != nil { - fmt.Printf("\r[%d/%d] Getting data for album %s... FAILED\n", i+1, nAlbums, id) - fmt.Fprintf(os.Stderr, "Error: could not get album data: %v\n", err) - continue - } - - fmt.Printf("\r[%d/%d] Getting data for album %s... DONE\n", i+1, nAlbums, id) - - output = path.Join(output, fmt.Sprintf("%s - %s", album.Data.Artist, album.Data.Name)) - if _, err := os.Stat(output); os.IsNotExist(err) { - if err := os.MkdirAll(output, 0755); err != nil { - fmt.Fprintf(os.Stderr, "Error: could not create output directory: %v\n", err) - continue - } - } - - fmt.Printf("Starting download of album: %s\n", album.Data.Name) - - for _, song := range album.Songs.Data { - songTitle := song.Title - if song.Version != "" { - songTitle = fmt.Sprintf("%s %s", song.Title, song.Version) - } - - fmt.Printf(" Downloading %s...", songTitle) - - media, err := song.GetMediaData(quality) - if err != nil { - fmt.Printf("\r Downloading %s... FAILED\n", songTitle) - fmt.Fprintf(os.Stderr, "Error: could not get media data: %v\n", err) - if err.Error() == "invalid license token" { - os.Exit(1) - } - continue - } - - if len(media.Data) == 0 || len(media.Data[0].Media) == 0 || len(media.Data[0].Media[0].Sources) == 0 { - fmt.Printf("\r Downloading %s... FAILED\n", songTitle) - fmt.Fprintf(os.Stderr, "Error: could not get media sources\n") - continue - } - - url := media.Data[0].Media[0].Sources[0].URL - for _, source := range media.Data[0].Media[0].Sources { - if source.Provider == "ak" { - url = source.URL - break - } - } - ext := "mp3" - if media.Data[0].Media[0].Format == "FLAC" { - ext = "flac" - } - - filePath := path.Join(output, fmt.Sprintf("%s. %s - %s.%s", song.TrackNumber, songTitle, strings.Join(song.Contributors.MainArtists, ", "), ext)) - err = media.Download(url, filePath, song.ID) - if err != nil { - fmt.Printf("\r Downloading %s... FAILED\n", songTitle) - fmt.Fprintf(os.Stderr, "Error: could not download song: %v\n", err) - continue - } - fmt.Printf("\r Downloading %s... DONE\n", songTitle) - - if err := tags.Add(album, song, filePath); err != nil { - fmt.Fprintf(os.Stderr, "Error: could not add tags to song: %v\n", err) - } - } - } - - fmt.Println(separator) - fmt.Println("All downloads completed") - }, + Use: "download", + Short: "Download songs from Deezer", } func init() { rootCmd.AddCommand(downloadCmd) - downloadCmd.Flags().StringP("output", "o", "", "output directory (default is current directory)") - downloadCmd.Flags().StringP("quality", "q", "", "download quality [mp3_128, mp3_320, flac, best] (default is best)") + downloadCmd.PersistentFlags().StringVarP(&outputDir, "output", "o", "", "output directory (default is current directory)") + downloadCmd.PersistentFlags().StringVarP(&quality, "quality", "q", "", "download quality [mp3_128, mp3_320, flac, best] (default is best)") +} + +func validateInput() { + if quality == "" { + quality = "best" + } + + validQualities := map[string]bool{ + "mp3_128": true, + "mp3_320": true, + "flac": true, + "best": true, + } + if !validQualities[quality] { + fmt.Fprintf(os.Stderr, "invalid quality option: %s\n", quality) + os.Exit(1) + } +} + +func downloadContent(contentType string, args []string) { + nArgs := len(args) + separator := "--------------------------------------------------" + + for i, id := range args { + fmt.Println(separator) + fmt.Printf("[%d/%d] Getting data for %s %s...", i+1, nArgs, contentType, id) + + var resource deezer.Resource + var songs []*deezer.Song + + switch contentType { + case "album": + album := &deezer.Album{} + if err := deezer.GetData(album, id); err != nil { + fmt.Printf("\r[%d/%d] Getting data for album %s... FAILED\n", i+1, nArgs, id) + fmt.Fprintf(os.Stderr, "Error: could not get album data: %v\n", err) + continue + } + resource = album + songs = album.GetSongs() + case "playlist": + playlist := &deezer.Playlist{} + if err := deezer.GetData(playlist, id); err != nil { + fmt.Printf("\r[%d/%d] Getting data for playlist %s... FAILED\n", i+1, nArgs, id) + fmt.Fprintf(os.Stderr, "Error: could not get playlist data: %v\n", err) + continue + } + resource = playlist + songs = playlist.GetSongs() + } + + fmt.Printf("\r[%d/%d] Getting data for %s %s... DONE\n", i+1, nArgs, contentType, id) + + output := resource.GetOutputPath(outputDir) + if _, err := os.Stat(output); os.IsNotExist(err) { + if err := os.MkdirAll(output, 0755); err != nil { + fmt.Fprintf(os.Stderr, "Error: could not create output directory: %v\n", err) + continue + } + } + + title := resource.GetTitle() + fmt.Printf("Starting download of %s: %s\n", contentType, title) + + for _, song := range songs { + songTitle := song.Title + if song.Version != "" { + songTitle = fmt.Sprintf("%s %s", song.Title, song.Version) + } + + fmt.Printf(" Downloading %s...", songTitle) + + media, err := song.GetMediaData(quality) + if err != nil { + fmt.Printf("\r Downloading %s... FAILED\n", songTitle) + fmt.Fprintf(os.Stderr, "Error: could not get media data: %v\n", err) + if err.Error() == "invalid license token" { + os.Exit(1) + } + continue + } + + if len(media.Data) == 0 || len(media.Data[0].Media) == 0 || len(media.Data[0].Media[0].Sources) == 0 { + fmt.Printf("\r Downloading %s... FAILED\n", songTitle) + fmt.Fprintf(os.Stderr, "Error: could not get media sources\n") + continue + } + + url := media.Data[0].Media[0].Sources[0].URL + for _, source := range media.Data[0].Media[0].Sources { + if source.Provider == "ak" { + url = source.URL + break + } + } + + ext := "mp3" + if media.Data[0].Media[0].Format == "FLAC" { + ext = "flac" + } + trackNumber := "" + if contentType == "album" { + trackNumber = song.TrackNumber + "." + } + + filePath := path.Join(output, fmt.Sprintf("%s %s - %s.%s", trackNumber, songTitle, strings.Join(song.Contributors.MainArtists, ", "), ext)) + err = media.Download(url, filePath, song.ID) + if err != nil { + fmt.Printf("\r Downloading %s... FAILED\n", songTitle) + fmt.Fprintf(os.Stderr, "Error: could not download song: %v\n", err) + continue + } + fmt.Printf("\r Downloading %s... DONE\n", songTitle) + + if err := tags.AddTags(resource, song, filePath); err != nil { + fmt.Fprintf(os.Stderr, "Error: could not add tags to song: %v\n", err) + } + } + } + + fmt.Println(separator) + fmt.Println("All downloads completed") } diff --git a/cmd/download_album.go b/cmd/download_album.go new file mode 100644 index 0000000..1cb9ce5 --- /dev/null +++ b/cmd/download_album.go @@ -0,0 +1,17 @@ +package cmd + +import "github.com/spf13/cobra" + +var albumCmd = &cobra.Command{ + Use: "album [album_id...]", + Short: "Download songs from one or more albums", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + validateInput() + downloadContent("album", args) + }, +} + +func init() { + downloadCmd.AddCommand(albumCmd) +} diff --git a/cmd/download_playlist.go b/cmd/download_playlist.go new file mode 100644 index 0000000..698cf66 --- /dev/null +++ b/cmd/download_playlist.go @@ -0,0 +1,17 @@ +package cmd + +import "github.com/spf13/cobra" + +var playlistCmd = &cobra.Command{ + Use: "playlist [playlist_id...]", + Short: "Download songs from one or more playlists", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + validateInput() + downloadContent("playlist", args) + }, +} + +func init() { + downloadCmd.AddCommand(playlistCmd) +} diff --git a/internal/deezer/album.go b/internal/deezer/album.go index 88385ac..c152bcb 100644 --- a/internal/deezer/album.go +++ b/internal/deezer/album.go @@ -3,16 +3,13 @@ package deezer import ( "encoding/json" "fmt" - "io" - "net/http" - "regexp" + "path" ) type Album struct { Data struct { - Name string `json:"ALB_TITLE"` + Title string `json:"ALB_TITLE"` Artist string `json:"ART_NAME"` - CoverID string `json:"ALB_PICTURE"` OriginalReleaseDate string `json:"ORIGINAL_RELEASE_DATE"` PhysicalReleaseDate string `json:"PHYSICAL_RELEASE_DATE"` Label string `json:"LABEL_NAME"` @@ -23,46 +20,22 @@ type Album struct { } `json:"SONGS"` } -func GetAlbumData(id string) (*Album, error) { - url := fmt.Sprintf("https://www.deezer.com/en/album/%s", 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) - } - - body, _ := io.ReadAll(resp.Body) - re := regexp.MustCompile(`window\.__DZR_APP_STATE__ = (\{.*\})`) - matches := re.FindStringSubmatch(string(body)) - if len(matches) != 2 { - return nil, fmt.Errorf("error parsing response") - } - - var album Album - err = json.Unmarshal([]byte(matches[1]), &album) - if err != nil { - return nil, err - } - - return &album, nil +func (a *Album) GetURL(id string) string { + return "https://www.deezer.com/en/album/" + id } -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() +func (a *Album) UnmarshalData(data []byte) error { + return json.Unmarshal(data, a) +} - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } +func (a *Album) GetSongs() []*Song { + return a.Songs.Data +} - return io.ReadAll(resp.Body) +func (a *Album) GetOutputPath(outputDir string) string { + return path.Join(outputDir, fmt.Sprintf("%s - %s", a.Data.Artist, a.Data.Title)) +} + +func (a *Album) GetTitle() string { + return a.Data.Title } diff --git a/internal/deezer/playlist.go b/internal/deezer/playlist.go new file mode 100644 index 0000000..ea06cdd --- /dev/null +++ b/internal/deezer/playlist.go @@ -0,0 +1,35 @@ +package deezer + +import ( + "encoding/json" + "path" +) + +type Playlist struct { + Data struct { + Title string `json:"TITLE"` + } `json:"DATA"` + Songs struct { + Data []*Song `json:"data"` + } `json:"SONGS"` +} + +func (p *Playlist) GetURL(id string) string { + return "https://www.deezer.com/en/playlist/" + id +} + +func (p *Playlist) UnmarshalData(data []byte) error { + return json.Unmarshal(data, p) +} + +func (p *Playlist) GetSongs() []*Song { + return p.Songs.Data +} + +func (p *Playlist) GetOutputPath(outputDir string) string { + return path.Join(outputDir, p.Data.Title) +} + +func (p *Playlist) GetTitle() string { + return p.Data.Title +} diff --git a/internal/deezer/resource.go b/internal/deezer/resource.go new file mode 100644 index 0000000..766a50b --- /dev/null +++ b/internal/deezer/resource.go @@ -0,0 +1,42 @@ +package deezer + +import ( + "fmt" + "io" + "net/http" + "regexp" +) + +type Resource interface { + GetURL(id string) string + UnmarshalData(data []byte) error + GetSongs() []*Song + GetOutputPath(outputDir string) string + GetTitle() string +} + +func GetData(r Resource, id string) error { + url := r.GetURL(id) + + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("resource not found") + } + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + re := regexp.MustCompile(`window\.__DZR_APP_STATE__ = (\{.*\})`) + matches := re.FindStringSubmatch(string(body)) + if len(matches) != 2 { + return fmt.Errorf("error parsing response") + } + + return r.UnmarshalData([]byte(matches[1])) +} diff --git a/internal/deezer/song.go b/internal/deezer/song.go index efe9484..7de5805 100644 --- a/internal/deezer/song.go +++ b/internal/deezer/song.go @@ -15,6 +15,7 @@ type Song struct { Artist string `json:"ART_NAME"` Title string `json:"SNG_TITLE"` Version string `json:"VERSION"` + Cover string `json:"ALB_PICTURE"` Contributors struct { MainArtists []string `json:"main_artist"` Composers []string `json:"composer"` @@ -70,3 +71,18 @@ func (s *Song) GetMediaData(quality string) (*Media, error) { return &media, nil } + +func (s *Song) GetCoverImage() ([]byte, error) { + url := fmt.Sprintf("https://e-cdn-images.dzcdn.net/images/cover/%s/500x500-000000-80-0-0.jpg", s.Cover) + 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/tags/flac.go b/internal/tags/flac.go new file mode 100644 index 0000000..2b2f806 --- /dev/null +++ b/internal/tags/flac.go @@ -0,0 +1,87 @@ +package tags + +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" +) + +type FLACTagger struct { + File *flac.File + Cmts *flacvorbis.MetaDataBlockVorbisComment + Index int +} + +func (t *FLACTagger) AddTags(resource deezer.Resource, song *deezer.Song, cover []byte, path string) error { + if album, ok := resource.(*deezer.Album); ok { + dateParts := strings.Split(album.Data.PhysicalReleaseDate, "-") + if len(dateParts) == 3 { + album.Data.PhysicalReleaseDate = dateParts[0] + } + + t.addTag("ALBUM", album.Data.Title) + t.addTag("ALBUMARTIST", album.Data.Artist) + t.addTag("PUBLISHER", album.Data.Label) + t.addTag("ORIGINALDATE", album.Data.OriginalReleaseDate) + t.addTag("DATE", album.Data.PhysicalReleaseDate) + t.addTag("COMMENT", album.Data.ProducerLine) + t.addTag("TRACKNUMBER", song.TrackNumber) + } + + t.addTag("TITLE", song.Title) + t.addTag("ARTIST", strings.Join(song.Contributors.MainArtists, " / ")) + t.addTag("COMPOSER", strings.Join(song.Contributors.Composers, " / ")) + t.addTag("LYRICIST", strings.Join(song.Contributors.Authors, " / ")) + t.addTag("REPLAYGAIN_TRACK_GAIN", song.Gain) + t.addTag("ISRC", song.ISRC) + + cmtsmeta := t.Cmts.Marshal() + if t.Index > 0 { + t.File.Meta[t.Index] = &cmtsmeta + } else { + t.File.Meta = append(t.File.Meta, &cmtsmeta) + } + + picture, err := flacpicture.NewFromImageData(flacpicture.PictureTypeFrontCover, "Front cover", cover, "image/jpeg") + if err != nil { + return err + } + picturemeta := picture.Marshal() + t.File.Meta = append(t.File.Meta, &picturemeta) + + return t.saveTags(path) +} + +func (t *FLACTagger) addTag(name, value string) { + if value != "" { + t.Cmts.Add(name, value) + } +} + +func (t *FLACTagger) saveTags(path string) error { + tempPath := path + ".tmp" + t.File.Save(tempPath) + + return os.Rename(tempPath, path) +} + +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 +} diff --git a/internal/tags/flac/tag.go b/internal/tags/flac/tag.go deleted file mode 100644 index b20fbb2..0000000 --- a/internal/tags/flac/tag.go +++ /dev/null @@ -1,86 +0,0 @@ -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.go b/internal/tags/id3v2.go new file mode 100644 index 0000000..baf6be4 --- /dev/null +++ b/internal/tags/id3v2.go @@ -0,0 +1,67 @@ +package tags + +import ( + "fmt" + "strconv" + "strings" + + "github.com/bogem/id3v2/v2" + "github.com/mathismqn/godeez/internal/deezer" +) + +type ID3v2Tagger struct { + Tag *id3v2.Tag +} + +func (t *ID3v2Tagger) AddTags(resource deezer.Resource, song *deezer.Song, cover []byte, path string) error { + defer t.Tag.Close() + + duration, _ := strconv.Atoi(song.Duration) + song.Duration = fmt.Sprintf("%d", duration*1000) + + if album, ok := resource.(*deezer.Album); ok { + t.addTag("TALB", album.Data.Title) + t.addTag("TPE2", album.Data.Artist) + t.addTag("TPUB", album.Data.Label) + t.addTag("TDOR", album.Data.OriginalReleaseDate) + t.addTag("TYER", album.Data.PhysicalReleaseDate) + t.addTag("COMM", album.Data.ProducerLine) + t.addTag("TRCK", song.TrackNumber) + } + + t.addTag("TIT2", song.Title) + t.addTag("TPE1", strings.Join(song.Contributors.MainArtists, " / ")) + t.addTag("TCOM", strings.Join(song.Contributors.Composers, " / ")) + t.addTag("TEXT", strings.Join(song.Contributors.Authors, " / ")) + t.addTag("TLEN", song.Duration) + t.addTXXXTag("GAIN", song.Gain) + t.addTXXXTag("ISRC", song.ISRC) + + frame := id3v2.PictureFrame{ + Encoding: t.Tag.DefaultEncoding(), + MimeType: "image/jpeg", + PictureType: id3v2.PTFrontCover, + Description: "Cover", + Picture: cover, + } + t.Tag.AddAttachedPicture(frame) + + return t.Tag.Save() +} + +func (t *ID3v2Tagger) addTag(name, value string) { + if value != "" { + t.Tag.AddTextFrame(name, t.Tag.DefaultEncoding(), value) + } +} + +func (t *ID3v2Tagger) addTXXXTag(description, value string) { + if value != "" { + udf := id3v2.UserDefinedTextFrame{ + Encoding: t.Tag.DefaultEncoding(), + Description: description, + Value: value, + } + t.Tag.AddUserDefinedTextFrame(udf) + } +} diff --git a/internal/tags/id3v2/tag.go b/internal/tags/id3v2/tag.go deleted file mode 100644 index 04ebbce..0000000 --- a/internal/tags/id3v2/tag.go +++ /dev/null @@ -1,60 +0,0 @@ -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 index 7eae30b..bcedd1e 100644 --- a/internal/tags/tag.go +++ b/internal/tags/tag.go @@ -1,33 +1,52 @@ package tags import ( - "fmt" "path" - "strconv" - "strings" + "github.com/bogem/id3v2/v2" + "github.com/go-flac/flacvorbis/v2" + "github.com/go-flac/go-flac/v2" "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() +type Tagger interface { + AddTags(resource deezer.Resource, song *deezer.Song, cover []byte, path string) error +} + +func NewTagger(filePath string) (Tagger, error) { + ext := path.Ext(filePath) + if ext == ".mp3" { + tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true}) + if err != nil { + return nil, err + } + return &ID3v2Tagger{Tag: tag}, nil + } + + file, err := flac.ParseFile(filePath) + if err != nil { + return nil, err + } + cmts, idx, err := extractFLACComment(file) + if err != nil { + return nil, err + } + if cmts == nil && idx > 0 { + cmts = flacvorbis.New() + } + + return &FLACTagger{File: file, Cmts: cmts, Index: idx}, nil +} + +func AddTags(resource deezer.Resource, song *deezer.Song, filePath string) error { + tagger, err := NewTagger(filePath) + if err != nil { + return err + } + cover, err := song.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) + return tagger.AddTags(resource, song, cover, filePath) }