From 6d27cc45020c3ae3d7b8129dc52b0d9c9fc6361d Mon Sep 17 00:00:00 2001 From: Superredstone Date: Thu, 19 Feb 2026 18:49:55 +0100 Subject: [PATCH] feat: fix metadata embedding --- go.mod | 9 +++-- go.sum | 33 ++++-------------- lib/app.go | 7 +++- lib/download.go | 12 ++++++- lib/metadata.go | 90 +++++++++++++++++++++++++++++++++++++++--------- lib/spotfetch.go | 40 +++++++++++++++++++++ lib/types.go | 16 +++++++-- lib/utils.go | 10 ++++++ 8 files changed, 165 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index f1a7d8a..86dec54 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,11 @@ go 1.24.4 replace github.com/Superredstone/spotiflac-cli/lib => ./lib 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.4 github.com/pquerna/otp v1.5.0 github.com/urfave/cli/v3 v3.6.2 ) -require ( - github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect - golang.org/x/text v0.3.8 // indirect -) +require github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect diff --git a/go.sum b/go.sum index 1e0d80e..52b029e 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,14 @@ -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/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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.4 h1:atf/kFa8U9idtkA//NO22XGr+MzQLeXZecnmP9sYBf0= +github.com/go-flac/go-flac/v2 v2.0.4/go.mod h1:sYOlTKxutMW0RDYF+KlD6Zn+VOCZlIFQG/r/usPveCs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= @@ -15,30 +19,5 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -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/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/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/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/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 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lib/app.go b/lib/app.go index 9d15798..2a2c45a 100644 --- a/lib/app.go +++ b/lib/app.go @@ -4,12 +4,13 @@ type App struct { UserAgent string // User agent used for scraping requests SelectedTidalApiUrl string Verbose bool + SpotifyClient *SpotifyClient } func NewApp() App { return App{ UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36", - Verbose: false, + Verbose: false, } } @@ -19,5 +20,9 @@ func (app *App) Init() error { return err } + if err := app.InitSpotifyClient(); err != nil { + return err + } + return nil } diff --git a/lib/download.go b/lib/download.go index 8bf2a95..bc6921a 100644 --- a/lib/download.go +++ b/lib/download.go @@ -1,6 +1,7 @@ package lib import ( + "errors" "io" "net/http" "os" @@ -38,9 +39,18 @@ func (app *App) Download(url string, outputFile string, service string, quality if err := app.DownloadTrack(url, outputFile, service, quality); err != nil { return err } + + return nil + case UrlTypePlaylist: + _, err := app.GetPlaylistMetadata(url) + if err != nil { + return err + } + + return nil } - return nil + return errors.New("Invalid URL type.") } func (app *App) DownloadTrack(url string, outputFile, service string, quality string) error { diff --git a/lib/metadata.go b/lib/metadata.go index 0efc077..684ae58 100644 --- a/lib/metadata.go +++ b/lib/metadata.go @@ -3,21 +3,47 @@ package lib import ( "encoding/json" "errors" + "io" + "net/http" - id3v2 "github.com/bogem/id3v2/v2" + "github.com/go-flac/flacpicture/v2" + "github.com/go-flac/flacvorbis/v2" + "github.com/go-flac/go-flac/v2" ) -func (app *App) GetTrackMetadata(url string) (TrackMetadata, error) { - app.log("Getting metadata for " + url) +func (app *App) GetPlaylistMetadata(url string) (PlaylistMetadata, error) { + app.log("Fetching playlist metadata") - client := NewSpotifyClient() - var result TrackMetadata - - err := client.Initialize() + var result PlaylistMetadata + playlistId, err := ParseTrackId(url) if err != nil { - return result, errors.New("Unable to fetch Spotify metadata.") + return result, err } + payload := BuildSpotifyReqPayloadPlaylist(playlistId) + + rawMetadata, err := app.SpotifyClient.Query(payload) + if err != nil { + return result, err + } + + byteMetadata, err := json.Marshal(rawMetadata) + if err != nil { + return result, err + } + + if err := json.Unmarshal(byteMetadata, &result); err != nil { + return result, err + } + + return result, nil +} + +func (app *App) GetTrackMetadata(url string) (TrackMetadata, error) { + app.log("Fetching metadata for " + url) + + var result TrackMetadata + trackId, err := ParseTrackId(url) if err != nil { return result, err @@ -25,7 +51,7 @@ func (app *App) GetTrackMetadata(url string) (TrackMetadata, error) { payload := BuildSpotifyReqPayloadTrack(trackId) - rawMetadata, err := client.Query(payload) + rawMetadata, err := app.SpotifyClient.Query(payload) if err != nil { return result, err } @@ -43,10 +69,10 @@ func (app *App) PrintMetadata(url string) error { return errors.New("Unimplemented.") } -func (app *App) EmbedMetadata(file string, metadata TrackMetadata) error { +func (app *App) EmbedMetadata(fileName string, metadata TrackMetadata) error { app.log("Embedding metadata") - tag, err := id3v2.Open(file, id3v2.Options{Parse: true}) + file, err := flac.ParseFile(fileName) if err != nil { return err } @@ -56,14 +82,46 @@ func (app *App) EmbedMetadata(file string, metadata TrackMetadata) error { return err } - tag.SetArtist(artists) - tag.SetTitle(metadata.Data.TrackUnion.Name) - tag.SetYear(string(metadata.Data.TrackUnion.AlbumOfTrack.Date.Year)) - tag.SetAlbum(metadata.Data.TrackUnion.AlbumOfTrack.Name) + cmt := flacvorbis.New() + cmt.Add(flacvorbis.FIELD_ALBUM, metadata.Data.TrackUnion.AlbumOfTrack.Name) + cmt.Add(flacvorbis.FIELD_DATE, string(metadata.Data.TrackUnion.AlbumOfTrack.Date.IsoString.Year())) + cmt.Add(flacvorbis.FIELD_ARTIST, artists) + cmt.Add(flacvorbis.FIELD_TITLE, metadata.Data.TrackUnion.Name) + cmtBlock := cmt.Marshal() + file.Meta = append(file.Meta, &cmtBlock) - if err = tag.Save(); err != nil { + cover, err := app.GetAlbumCover(metadata) + if err != nil { return err } + picture, err := flacpicture.NewFromImageData( + flacpicture.PictureTypeFrontCover, "Front cover", cover, "image/jpeg") + + pictureMeta := picture.Marshal() + file.Meta = append(file.Meta, &pictureMeta) + file.Save(fileName) + return nil } + +func (app *App) GetAlbumCover(metadata TrackMetadata) ([]byte, error) { + app.log("Embedding cover") + + for _, source := range metadata.Data.TrackUnion.AlbumOfTrack.CoverArt.Sources { + rawResponse, err := http.Get(source.Url) + if err != nil { + continue + } + defer rawResponse.Body.Close() + + response, err := io.ReadAll(rawResponse.Body) + if err != nil { + continue + } + + return response, nil + } + + return []byte{}, errors.New("Unable to download album cover for " + metadata.Data.TrackUnion.Name + ".") +} diff --git a/lib/spotfetch.go b/lib/spotfetch.go index f4a5fd2..c1345cb 100644 --- a/lib/spotfetch.go +++ b/lib/spotfetch.go @@ -1835,3 +1835,43 @@ func BuildSpotifyReqPayloadTrack(trackId string) SpotifyPayload { return payload } + +func BuildSpotifyReqPayloadPlaylist(playlistId string) SpotifyPayload { + payload := map[string]interface{}{ + "variables": map[string]interface{}{ + "uri": fmt.Sprintf("spotify:playlist:%s", playlistId), + "offset": 0, // No one wants to download from their playlists starting from song 158th, right? + "limit": 5000, // Hope that this does not limit anyone + "enableWatchFeedEntrypoint": false, + }, + "operationName": "fetchPlaylist", + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "bb67e0af06e8d6f52b531f97468ee4acd44cd0f82b988e15c2ea47b1148efc77", + }, + }, + } + + return payload +} + +func BuildSpotifyReqPayloadAlbum(albumId string) SpotifyPayload { + payload := map[string]interface{}{ + "variables": map[string]interface{}{ + "uri": fmt.Sprintf("spotify:album:%s", albumId), + "locale": "", + "offset": 0, // No one wants to download from an album from song number 9 + "limit": 5000, // No album will ever have more than 5000 songs, i hope + }, + "operationName": "getAlbum", + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "b9bfabef66ed756e5e13f68a942deb60bd4125ec1f1be8cc42769dc0259b4b10", + }, + }, + } + + return payload +} diff --git a/lib/types.go b/lib/types.go index 0045098..9715208 100644 --- a/lib/types.go +++ b/lib/types.go @@ -16,8 +16,12 @@ type ExtractedColors struct { } type CoverArt struct { - ExtractedColors ExtractedColors `json:"extractedColors"` - Sources []map[string]interface{} `json:"sources"` + ExtractedColors ExtractedColors `json:"extractedColors"` + Sources []struct { + Height int `json:"height"` + Width int `json:"width"` + Url string `json:"url"` + } `json:"sources"` } type Date struct { @@ -119,3 +123,11 @@ type Data struct { type TrackMetadata struct { Data Data `json:"data"` } + +type PlaylistMetadata struct { + Data struct { + Playlist struct { + Name string `json:"name"` + } `json:"playlistV2"` + } `json:"data"` +} diff --git a/lib/utils.go b/lib/utils.go index 900c2f6..d47b497 100644 --- a/lib/utils.go +++ b/lib/utils.go @@ -121,3 +121,13 @@ func FileExists(file string) (bool, error) { return false, err } + +func (app *App) InitSpotifyClient() error { + app.SpotifyClient = NewSpotifyClient() + + if err := app.SpotifyClient.Initialize(); err != nil { + return errors.New("Unable to fetch Spotify metadata.") + } + + return nil +}