From 16c71c8688fedc1691dac6b244ac5ec75f080773 Mon Sep 17 00:00:00 2001 From: Mathis Maquenne <124215603+mathismqn@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:37:59 +0200 Subject: [PATCH] refactor: decouple tag writers from deezer models --- internal/downloader/client.go | 4 +- internal/downloader/tagmeta.go | 40 ++++++++++++++++ internal/tag/flac.go | 84 ++++++++++++++++++++++++++++++++++ internal/tag/id3v2.go | 70 ++++++++++++++++++++++++++++ internal/tag/tag.go | 69 ++++++++++++++++++++++++++++ internal/tags/flac.go | 83 --------------------------------- internal/tags/id3v2.go | 72 ----------------------------- internal/tags/tags.go | 44 ------------------ 8 files changed, 265 insertions(+), 201 deletions(-) create mode 100644 internal/downloader/tagmeta.go create mode 100644 internal/tag/flac.go create mode 100644 internal/tag/id3v2.go create mode 100644 internal/tag/tag.go delete mode 100644 internal/tags/flac.go delete mode 100644 internal/tags/id3v2.go delete mode 100644 internal/tags/tags.go diff --git a/internal/downloader/client.go b/internal/downloader/client.go index e5bb651..d6620a7 100644 --- a/internal/downloader/client.go +++ b/internal/downloader/client.go @@ -16,7 +16,7 @@ import ( "github.com/mathismqn/godeez/internal/deezer" "github.com/mathismqn/godeez/internal/fileutil" "github.com/mathismqn/godeez/internal/store" - "github.com/mathismqn/godeez/internal/tags" + "github.com/mathismqn/godeez/internal/tag" ) const chunkSize = 2048 @@ -234,7 +234,7 @@ func (c *Client) streamToFile(ctx context.Context, stream io.ReadCloser, outputP func (c *Client) finalizeDownload(resource deezer.Resource, track *deezer.Track, outputPath, mediaFormat, genre string, cover []byte, bpmKey bpmKey) []string { var warnings []string - if err := tags.AddTags(resource, track, cover, outputPath, bpmKey.BPM, bpmKey.Key, genre); err != nil { + if err := tag.Write(outputPath, buildTagMetadata(resource, track, cover, bpmKey, genre)); err != nil { warnings = append(warnings, fmt.Sprintf("failed to add tags: %v", err)) } diff --git a/internal/downloader/tagmeta.go b/internal/downloader/tagmeta.go new file mode 100644 index 0000000..526d081 --- /dev/null +++ b/internal/downloader/tagmeta.go @@ -0,0 +1,40 @@ +package downloader + +import ( + "strings" + + "github.com/mathismqn/godeez/internal/deezer" + "github.com/mathismqn/godeez/internal/tag" +) + +func buildTagMetadata(resource deezer.Resource, track *deezer.Track, cover []byte, bpm bpmKey, genre string) tag.Metadata { + m := tag.Metadata{ + Title: track.GetTitle(), + Artists: strings.Join(track.Contributors.MainArtists, ", "), + Composers: strings.Join(track.Contributors.Composers, ", "), + Lyricists: strings.Join(track.Contributors.Authors, ", "), + Genre: genre, + BPM: bpm.BPM, + Key: bpm.Key, + TrackNumber: track.TrackNumber, + Duration: track.Duration, + Gain: track.Gain, + ISRC: track.ISRC, + Cover: cover, + } + + if album, ok := resource.(*deezer.Album); ok { + data := album.Results.Data + m.Album = &tag.AlbumMetadata{ + Artist: data.Artist, + Title: data.Title, + Label: data.Label, + OriginalReleaseDate: data.OriginalReleaseDate, + ReleaseDate: data.PhysicalReleaseDate, + ProducerLine: data.ProducerLine, + Copyright: data.Copyright, + } + } + + return m +} diff --git a/internal/tag/flac.go b/internal/tag/flac.go new file mode 100644 index 0000000..acb64f1 --- /dev/null +++ b/internal/tag/flac.go @@ -0,0 +1,84 @@ +package tag + +import ( + "os" + "strings" + + "github.com/go-flac/flacpicture/v2" + "github.com/go-flac/flacvorbis/v2" + "github.com/go-flac/go-flac/v2" +) + +type flacTagger struct { + file *flac.File + cmts *flacvorbis.MetaDataBlockVorbisComment + index int + path string +} + +func (t *flacTagger) write(m Metadata) error { + if m.Album != nil { + date := m.Album.ReleaseDate + if parts := strings.Split(date, "-"); len(parts) == 3 { + date = parts[0] + } + + t.addTag("TRACKNUMBER", m.TrackNumber) + t.addTag("ALBUMARTIST", m.Album.Artist) + t.addTag("ALBUM", m.Album.Title) + t.addTag("PUBLISHER", m.Album.Label) + t.addTag("ORIGINALDATE", m.Album.OriginalReleaseDate) + t.addTag("DATE", date) + t.addTag("COMMENT", m.Album.ProducerLine) + t.addTag("COPYRIGHT", m.Album.Copyright) + } + + t.addTag("ARTIST", m.Artists) + t.addTag("TITLE", m.Title) + t.addTag("COMPOSER", m.Composers) + t.addTag("LYRICIST", m.Lyricists) + t.addTag("GENRE", m.Genre) + t.addTag("REPLAYGAIN_TRACK_GAIN", m.Gain) + t.addTag("ISRC", m.ISRC) + t.addTag("BPM", m.BPM) + t.addTag("KEY", m.Key) + t.addTag("INITIALKEY", m.Key) + + 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", m.Cover, "image/jpeg") + if err != nil { + return err + } + pictureMeta := picture.Marshal() + t.file.Meta = append(t.file.Meta, &pictureMeta) + + tmpPath := t.path + ".tmp" + if err := t.file.Save(tmpPath); err != nil { + return err + } + return os.Rename(tmpPath, t.path) +} + +func (t *flacTagger) addTag(name, value string) { + if value != "" { + t.cmts.Add(name, value) + } +} + +func extractFLACComment(file *flac.File) (*flacvorbis.MetaDataBlockVorbisComment, int) { + for idx, meta := range file.Meta { + if meta.Type == flac.VorbisComment { + cmt, err := flacvorbis.ParseFromMetaDataBlock(*meta) + if err == nil { + return cmt, idx + } + } + } + return nil, 0 +} diff --git a/internal/tag/id3v2.go b/internal/tag/id3v2.go new file mode 100644 index 0000000..625b391 --- /dev/null +++ b/internal/tag/id3v2.go @@ -0,0 +1,70 @@ +package tag + +import ( + "fmt" + "strconv" + + "github.com/bogem/id3v2/v2" +) + +type id3v2Tagger struct { + tag *id3v2.Tag +} + +func (t *id3v2Tagger) write(m Metadata) error { + defer t.tag.Close() + + duration, err := strconv.Atoi(m.Duration) + if err != nil { + return err + } + length := fmt.Sprintf("%d", duration*1000) + + if m.Album != nil { + t.addTag("TRCK", m.TrackNumber) + t.addTag("TPE2", m.Album.Artist) + t.addTag("TALB", m.Album.Title) + t.addTag("TPUB", m.Album.Label) + t.addTag("TDOR", m.Album.OriginalReleaseDate) + t.addTag("TYER", m.Album.ReleaseDate) + t.addTag("COMM", m.Album.ProducerLine) + t.addTag("TCOP", m.Album.Copyright) + } + + t.addTag("TPE1", m.Artists) + t.addTag("TIT2", m.Title) + t.addTag("TCOM", m.Composers) + t.addTag("TEXT", m.Lyricists) + t.addTag("TCON", m.Genre) + t.addTag("TLEN", length) + t.addTag("TBPM", m.BPM) + t.addTag("TKEY", m.Key) + t.addTXXX("GAIN", m.Gain) + t.addTXXX("ISRC", m.ISRC) + + t.tag.AddAttachedPicture(id3v2.PictureFrame{ + Encoding: t.tag.DefaultEncoding(), + MimeType: "image/jpeg", + PictureType: id3v2.PTFrontCover, + Description: "Cover", + Picture: m.Cover, + }) + + return t.tag.Save() +} + +func (t *id3v2Tagger) addTag(name, value string) { + if value != "" { + t.tag.AddTextFrame(name, t.tag.DefaultEncoding(), value) + } +} + +func (t *id3v2Tagger) addTXXX(description, value string) { + if value != "" { + t.tag.AddUserDefinedTextFrame(id3v2.UserDefinedTextFrame{ + Encoding: t.tag.DefaultEncoding(), + Description: description, + Value: value, + }) + } +} diff --git a/internal/tag/tag.go b/internal/tag/tag.go new file mode 100644 index 0000000..517166a --- /dev/null +++ b/internal/tag/tag.go @@ -0,0 +1,69 @@ +package tag + +import ( + "path" + + "github.com/bogem/id3v2/v2" + "github.com/go-flac/flacvorbis/v2" + "github.com/go-flac/go-flac/v2" +) + +type AlbumMetadata struct { + Artist string + Title string + Label string + OriginalReleaseDate string + ReleaseDate string + ProducerLine string + Copyright string +} + +type Metadata struct { + Title string + Artists string + Composers string + Lyricists string + Genre string + BPM string + Key string + TrackNumber string + Duration string + Gain string + ISRC string + Cover []byte + Album *AlbumMetadata +} + +type tagger interface { + write(m Metadata) error +} + +func newTagger(filePath string) (tagger, error) { + if path.Ext(filePath) == ".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 := extractFLACComment(file) + if cmts == nil { + cmts = flacvorbis.New() + } + + return &flacTagger{file: file, cmts: cmts, index: idx, path: filePath}, nil +} + +func Write(filePath string, m Metadata) error { + t, err := newTagger(filePath) + if err != nil { + return err + } + return t.write(m) +} diff --git a/internal/tags/flac.go b/internal/tags/flac.go deleted file mode 100644 index b5c7849..0000000 --- a/internal/tags/flac.go +++ /dev/null @@ -1,83 +0,0 @@ -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, track *deezer.Track, cover []byte, path, tempo, key, genre string) error { - if album, ok := resource.(*deezer.Album); ok { - if parts := strings.Split(album.Results.Data.PhysicalReleaseDate, "-"); len(parts) == 3 { - album.Results.Data.PhysicalReleaseDate = parts[0] - } - - t.addTag("TRACKNUMBER", track.TrackNumber) - t.addTag("ALBUMARTIST", album.Results.Data.Artist) - t.addTag("ALBUM", album.Results.Data.Title) - t.addTag("PUBLISHER", album.Results.Data.Label) - t.addTag("ORIGINALDATE", album.Results.Data.OriginalReleaseDate) - t.addTag("DATE", album.Results.Data.PhysicalReleaseDate) - t.addTag("COMMENT", album.Results.Data.ProducerLine) - t.addTag("COPYRIGHT", album.Results.Data.Copyright) - } - - t.addTag("ARTIST", strings.Join(track.Contributors.MainArtists, ", ")) - t.addTag("TITLE", track.GetTitle()) - t.addTag("COMPOSER", strings.Join(track.Contributors.Composers, ", ")) - t.addTag("LYRICIST", strings.Join(track.Contributors.Authors, ", ")) - t.addTag("GENRE", genre) - t.addTag("REPLAYGAIN_TRACK_GAIN", track.Gain) - t.addTag("ISRC", track.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 - } 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) - - tmpPath := path + ".tmp" - if err := t.file.Save(tmpPath); err != nil { - return err - } - return os.Rename(tmpPath, path) -} - -func (t *flacTagger) addTag(name, value string) { - if value != "" { - t.cmts.Add(name, value) - } -} - -func extractFLACComment(file *flac.File) (*flacvorbis.MetaDataBlockVorbisComment, int) { - for idx, meta := range file.Meta { - if meta.Type == flac.VorbisComment { - cmt, err := flacvorbis.ParseFromMetaDataBlock(*meta) - if err == nil { - return cmt, idx - } - } - } - return nil, 0 -} diff --git a/internal/tags/id3v2.go b/internal/tags/id3v2.go deleted file mode 100644 index ca40d10..0000000 --- a/internal/tags/id3v2.go +++ /dev/null @@ -1,72 +0,0 @@ -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, track *deezer.Track, cover []byte, path, tempo, key, genre string) error { - defer t.tag.Close() - - duration, err := strconv.Atoi(track.Duration) - if err != nil { - return err - } - track.Duration = fmt.Sprintf("%d", duration*1000) - - if album, ok := resource.(*deezer.Album); ok { - t.addTag("TRCK", track.TrackNumber) - t.addTag("TPE2", album.Results.Data.Artist) - t.addTag("TALB", album.Results.Data.Title) - t.addTag("TPUB", album.Results.Data.Label) - t.addTag("TDOR", album.Results.Data.OriginalReleaseDate) - t.addTag("TYER", album.Results.Data.PhysicalReleaseDate) - t.addTag("COMM", album.Results.Data.ProducerLine) - t.addTag("TCOP", album.Results.Data.Copyright) - } - - t.addTag("TPE1", strings.Join(track.Contributors.MainArtists, ", ")) - t.addTag("TIT2", track.GetTitle()) - t.addTag("TCOM", strings.Join(track.Contributors.Composers, ", ")) - t.addTag("TEXT", strings.Join(track.Contributors.Authors, ", ")) - t.addTag("TCON", genre) - t.addTag("TLEN", track.Duration) - t.addTag("TBPM", tempo) - t.addTag("TKEY", key) - t.addTXXX("GAIN", track.Gain) - t.addTXXX("ISRC", track.ISRC) - - t.tag.AddAttachedPicture(id3v2.PictureFrame{ - Encoding: t.tag.DefaultEncoding(), - MimeType: "image/jpeg", - PictureType: id3v2.PTFrontCover, - Description: "Cover", - Picture: cover, - }) - - return t.tag.Save() -} - -func (t *id3v2Tagger) addTag(name, value string) { - if value != "" { - t.tag.AddTextFrame(name, t.tag.DefaultEncoding(), value) - } -} - -func (t *id3v2Tagger) addTXXX(description, value string) { - if value != "" { - t.tag.AddUserDefinedTextFrame(id3v2.UserDefinedTextFrame{ - Encoding: t.tag.DefaultEncoding(), - Description: description, - Value: value, - }) - } -} diff --git a/internal/tags/tags.go b/internal/tags/tags.go deleted file mode 100644 index 8a927fd..0000000 --- a/internal/tags/tags.go +++ /dev/null @@ -1,44 +0,0 @@ -package tags - -import ( - "path" - - "github.com/bogem/id3v2/v2" - "github.com/go-flac/flacvorbis/v2" - "github.com/go-flac/go-flac/v2" - "github.com/mathismqn/godeez/internal/deezer" -) - -type tagger interface { - addTags(resource deezer.Resource, track *deezer.Track, cover []byte, filePath, tempo, key, genre string) error -} - -func newTagger(filePath string) (tagger, error) { - if path.Ext(filePath) == ".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 := extractFLACComment(file) - if cmts == nil { - cmts = flacvorbis.New() - } - - return &flacTagger{file: file, cmts: cmts, index: idx}, nil -} - -func AddTags(resource deezer.Resource, track *deezer.Track, cover []byte, filePath, tempo, key, genre string) error { - t, err := newTagger(filePath) - if err != nil { - return err - } - return t.addTags(resource, track, cover, filePath, tempo, key, genre) -}