feat: download playlists
This commit is contained in:
+129
-102
@@ -11,111 +11,138 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var outputDir string
|
||||||
|
var quality string
|
||||||
|
|
||||||
var downloadCmd = &cobra.Command{
|
var downloadCmd = &cobra.Command{
|
||||||
Use: "download [album_id...]",
|
Use: "download",
|
||||||
Short: "Download songs from one or more albums",
|
Short: "Download songs from Deezer",
|
||||||
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")
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(downloadCmd)
|
rootCmd.AddCommand(downloadCmd)
|
||||||
downloadCmd.Flags().StringP("output", "o", "", "output directory (default is current directory)")
|
downloadCmd.PersistentFlags().StringVarP(&outputDir, "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(&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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
+16
-43
@@ -3,16 +3,13 @@ package deezer
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"path"
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Album struct {
|
type Album struct {
|
||||||
Data struct {
|
Data struct {
|
||||||
Name string `json:"ALB_TITLE"`
|
Title string `json:"ALB_TITLE"`
|
||||||
Artist string `json:"ART_NAME"`
|
Artist string `json:"ART_NAME"`
|
||||||
CoverID string `json:"ALB_PICTURE"`
|
|
||||||
OriginalReleaseDate string `json:"ORIGINAL_RELEASE_DATE"`
|
OriginalReleaseDate string `json:"ORIGINAL_RELEASE_DATE"`
|
||||||
PhysicalReleaseDate string `json:"PHYSICAL_RELEASE_DATE"`
|
PhysicalReleaseDate string `json:"PHYSICAL_RELEASE_DATE"`
|
||||||
Label string `json:"LABEL_NAME"`
|
Label string `json:"LABEL_NAME"`
|
||||||
@@ -23,46 +20,22 @@ type Album struct {
|
|||||||
} `json:"SONGS"`
|
} `json:"SONGS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAlbumData(id string) (*Album, error) {
|
func (a *Album) GetURL(id string) string {
|
||||||
url := fmt.Sprintf("https://www.deezer.com/en/album/%s", id)
|
return "https://www.deezer.com/en/album/" + 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) GetCoverImage() ([]byte, error) {
|
func (a *Album) UnmarshalData(data []byte) error {
|
||||||
url := fmt.Sprintf("https://e-cdn-images.dzcdn.net/images/cover/%s/500x500-000000-80-0-0.jpg", a.Data.CoverID)
|
return json.Unmarshal(data, a)
|
||||||
resp, err := http.Get(url)
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
func (a *Album) GetSongs() []*Song {
|
||||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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]))
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ type Song struct {
|
|||||||
Artist string `json:"ART_NAME"`
|
Artist string `json:"ART_NAME"`
|
||||||
Title string `json:"SNG_TITLE"`
|
Title string `json:"SNG_TITLE"`
|
||||||
Version string `json:"VERSION"`
|
Version string `json:"VERSION"`
|
||||||
|
Cover string `json:"ALB_PICTURE"`
|
||||||
Contributors struct {
|
Contributors struct {
|
||||||
MainArtists []string `json:"main_artist"`
|
MainArtists []string `json:"main_artist"`
|
||||||
Composers []string `json:"composer"`
|
Composers []string `json:"composer"`
|
||||||
@@ -70,3 +71,18 @@ func (s *Song) GetMediaData(quality string) (*Media, error) {
|
|||||||
|
|
||||||
return &media, nil
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -1,33 +1,52 @@
|
|||||||
package tags
|
package tags
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"path"
|
"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/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 {
|
type Tagger interface {
|
||||||
cover, err := album.GetCoverImage()
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
duration, _ := strconv.Atoi(song.Duration)
|
return tagger.AddTags(resource, song, cover, filePath)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user