feat: download playlists

This commit is contained in:
Mathis Maquenne
2024-10-14 16:22:44 +02:00
parent cfa53da6ae
commit f247a0f71e
12 changed files with 465 additions and 311 deletions
+16 -43
View File
@@ -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
}
+35
View File
@@ -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
}
+42
View File
@@ -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]))
}
+16
View File
@@ -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)
}
+87
View File
@@ -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
}
-86
View File
@@ -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)
}
+67
View File
@@ -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)
}
}
-60
View File
@@ -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)
}
}
+39 -20
View File
@@ -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)
}