.add composer and fix multiple value tag
This commit is contained in:
@@ -161,6 +161,7 @@ type DownloadRequest struct {
|
|||||||
SpotifyTotalDiscs int `json:"spotify_total_discs,omitempty"`
|
SpotifyTotalDiscs int `json:"spotify_total_discs,omitempty"`
|
||||||
Copyright string `json:"copyright,omitempty"`
|
Copyright string `json:"copyright,omitempty"`
|
||||||
Publisher string `json:"publisher,omitempty"`
|
Publisher string `json:"publisher,omitempty"`
|
||||||
|
Composer string `json:"composer,omitempty"`
|
||||||
PlaylistName string `json:"playlist_name,omitempty"`
|
PlaylistName string `json:"playlist_name,omitempty"`
|
||||||
PlaylistOwner string `json:"playlist_owner,omitempty"`
|
PlaylistOwner string `json:"playlist_owner,omitempty"`
|
||||||
AllowFallback bool `json:"allow_fallback"`
|
AllowFallback bool `json:"allow_fallback"`
|
||||||
@@ -384,11 +385,6 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
spotifyURL = fmt.Sprintf("https://open.spotify.com/track/%s", req.SpotifyID)
|
spotifyURL = fmt.Sprintf("https://open.spotify.com/track/%s", req.SpotifyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.SpotifyID != "" && (req.Copyright == "" || req.Publisher == "" || req.SpotifyTotalDiscs == 0 || req.ReleaseDate == "" || req.SpotifyTotalTracks == 0 || req.SpotifyTrackNumber == 0) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
trackURL := fmt.Sprintf("https://open.spotify.com/track/%s", req.SpotifyID)
|
|
||||||
metadataSeparator := req.Separator
|
metadataSeparator := req.Separator
|
||||||
if metadataSeparator == "" {
|
if metadataSeparator == "" {
|
||||||
metadataSeparator = ", "
|
metadataSeparator = ", "
|
||||||
@@ -403,6 +399,12 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.SpotifyID != "" && (req.Copyright == "" || req.Publisher == "" || req.Composer == "" || req.SpotifyTotalDiscs == 0 || req.ReleaseDate == "" || req.SpotifyTotalTracks == 0 || req.SpotifyTrackNumber == 0) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
trackURL := fmt.Sprintf("https://open.spotify.com/track/%s", req.SpotifyID)
|
||||||
trackData, err := backend.GetFilteredSpotifyData(ctx, trackURL, false, 0, metadataSeparator, nil)
|
trackData, err := backend.GetFilteredSpotifyData(ctx, trackURL, false, 0, metadataSeparator, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
||||||
@@ -410,6 +412,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
Track struct {
|
Track struct {
|
||||||
Copyright string `json:"copyright"`
|
Copyright string `json:"copyright"`
|
||||||
Publisher string `json:"publisher"`
|
Publisher string `json:"publisher"`
|
||||||
|
Composer string `json:"composer"`
|
||||||
TotalDiscs int `json:"total_discs"`
|
TotalDiscs int `json:"total_discs"`
|
||||||
TotalTracks int `json:"total_tracks"`
|
TotalTracks int `json:"total_tracks"`
|
||||||
TrackNumber int `json:"track_number"`
|
TrackNumber int `json:"track_number"`
|
||||||
@@ -425,6 +428,9 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
if req.Publisher == "" && trackResp.Track.Publisher != "" {
|
if req.Publisher == "" && trackResp.Track.Publisher != "" {
|
||||||
req.Publisher = trackResp.Track.Publisher
|
req.Publisher = trackResp.Track.Publisher
|
||||||
}
|
}
|
||||||
|
if req.Composer == "" && trackResp.Track.Composer != "" {
|
||||||
|
req.Composer = trackResp.Track.Composer
|
||||||
|
}
|
||||||
if req.SpotifyTotalDiscs == 0 && trackResp.Track.TotalDiscs > 0 {
|
if req.SpotifyTotalDiscs == 0 && trackResp.Track.TotalDiscs > 0 {
|
||||||
req.SpotifyTotalDiscs = trackResp.Track.TotalDiscs
|
req.SpotifyTotalDiscs = trackResp.Track.TotalDiscs
|
||||||
}
|
}
|
||||||
@@ -500,25 +506,25 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
|
|
||||||
downloader := backend.NewAmazonDownloader()
|
downloader := backend.NewAmazonDownloader()
|
||||||
if req.ServiceURL != "" {
|
if req.ServiceURL != "" {
|
||||||
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, spotifyURL, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||||
} else {
|
} else {
|
||||||
filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, spotifyURL, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "tidal":
|
case "tidal":
|
||||||
if req.ApiURL == "" || req.ApiURL == "auto" {
|
if req.ApiURL == "" || req.ApiURL == "auto" {
|
||||||
downloader := backend.NewTidalDownloader("")
|
downloader := backend.NewTidalDownloader("")
|
||||||
if req.ServiceURL != "" {
|
if req.ServiceURL != "" {
|
||||||
filename, err = downloader.DownloadByURLWithFallback(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
filename, err = downloader.DownloadByURLWithFallback(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||||
} else {
|
} else {
|
||||||
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
downloader := backend.NewTidalDownloader(req.ApiURL)
|
downloader := backend.NewTidalDownloader(req.ApiURL)
|
||||||
if req.ServiceURL != "" {
|
if req.ServiceURL != "" {
|
||||||
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||||
} else {
|
} else {
|
||||||
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,7 +537,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
if quality == "" {
|
if quality == "" {
|
||||||
quality = "6"
|
quality = "6"
|
||||||
}
|
}
|
||||||
filename, err = downloader.DownloadTrackWithISRC(isrc, req.OutputDir, quality, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
filename, err = downloader.DownloadTrackWithISRC(isrc, req.OutputDir, quality, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return DownloadResponse{
|
return DownloadResponse{
|
||||||
|
|||||||
+5
-3
@@ -204,7 +204,7 @@ func (a *AmazonDownloader) DownloadFromService(amazonURL, outputDir, quality str
|
|||||||
return a.DownloadFromAfkarXYZ(amazonURL, outputDir, quality)
|
return a.DownloadFromAfkarXYZ(amazonURL, outputDir, quality)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, spotifyURL string, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
||||||
|
|
||||||
if outputDir != "." {
|
if outputDir != "." {
|
||||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
@@ -390,6 +390,8 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
|
|||||||
Comment: spotifyURL,
|
Comment: spotifyURL,
|
||||||
Copyright: spotifyCopyright,
|
Copyright: spotifyCopyright,
|
||||||
Publisher: spotifyPublisher,
|
Publisher: spotifyPublisher,
|
||||||
|
Composer: spotifyComposer,
|
||||||
|
Separator: metadataSeparator,
|
||||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||||
ISRC: isrc,
|
ISRC: isrc,
|
||||||
Genre: mbMeta.Genre,
|
Genre: mbMeta.Genre,
|
||||||
@@ -418,7 +420,7 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
|
|||||||
return filePath, nil
|
return filePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, quality, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string,
|
func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, quality, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, spotifyURL string,
|
||||||
useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool,
|
useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
|
|
||||||
@@ -427,5 +429,5 @@ func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, qualit
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.DownloadByURL(amazonURL, outputDir, quality, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, embedMaxQualityCover, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL, useFirstArtistOnly, useSingleGenre, embedGenre)
|
return a.DownloadByURL(amazonURL, outputDir, quality, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, embedMaxQualityCover, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, spotifyURL, useFirstArtistOnly, useSingleGenre, embedGenre)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func normalizeArtistSeparator(separator string) string {
|
||||||
|
separator = strings.TrimSpace(separator)
|
||||||
|
if separator == "," || separator == ";" {
|
||||||
|
return separator
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitArtistSegment(segment string, separator string) []string {
|
||||||
|
segment = strings.TrimSpace(segment)
|
||||||
|
if segment == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(segment, "|||SEP|||") {
|
||||||
|
return strings.Split(segment, "|||SEP|||")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := []string{segment}
|
||||||
|
|
||||||
|
if separator = normalizeArtistSeparator(separator); separator != "" {
|
||||||
|
var separated []string
|
||||||
|
for _, part := range parts {
|
||||||
|
for _, item := range strings.Split(part, separator) {
|
||||||
|
separated = append(separated, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts = separated
|
||||||
|
} else if strings.Contains(segment, ";") {
|
||||||
|
var separated []string
|
||||||
|
for _, part := range parts {
|
||||||
|
for _, item := range strings.Split(part, ";") {
|
||||||
|
separated = append(separated, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts = separated
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitArtistCredits(artistStr, separator string) []string {
|
||||||
|
rawParts := splitArtistSegment(artistStr, separator)
|
||||||
|
if len(rawParts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}, len(rawParts))
|
||||||
|
result := make([]string, 0, len(rawParts))
|
||||||
|
for _, part := range rawParts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[part]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[part] = struct{}{}
|
||||||
|
result = append(result, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitMetadataValues(value, separator string) []string {
|
||||||
|
rawParts := splitArtistSegment(value, separator)
|
||||||
|
if len(rawParts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}, len(rawParts))
|
||||||
|
result := make([]string, 0, len(rawParts))
|
||||||
|
for _, part := range rawParts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[part]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[part] = struct{}{}
|
||||||
|
result = append(result, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
+133
-19
@@ -21,6 +21,7 @@ type Metadata struct {
|
|||||||
Artist string
|
Artist string
|
||||||
Album string
|
Album string
|
||||||
AlbumArtist string
|
AlbumArtist string
|
||||||
|
Separator string
|
||||||
Date string
|
Date string
|
||||||
ReleaseDate string
|
ReleaseDate string
|
||||||
TrackNumber int
|
TrackNumber int
|
||||||
@@ -31,12 +32,72 @@ type Metadata struct {
|
|||||||
Comment string
|
Comment string
|
||||||
Copyright string
|
Copyright string
|
||||||
Publisher string
|
Publisher string
|
||||||
|
Composer string
|
||||||
Lyrics string
|
Lyrics string
|
||||||
Description string
|
Description string
|
||||||
ISRC string
|
ISRC string
|
||||||
Genre string
|
Genre string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveMetadataSeparator(separator string) string {
|
||||||
|
if normalized := normalizeArtistSeparator(separator); normalized != "" {
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeArtistSeparator(GetSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayMetadataSeparator(separator string) string {
|
||||||
|
if resolved := resolveMetadataSeparator(separator); resolved != "" {
|
||||||
|
return resolved + " "
|
||||||
|
}
|
||||||
|
|
||||||
|
return "; "
|
||||||
|
}
|
||||||
|
|
||||||
|
func addVorbisTagValues(cmt *flacvorbis.MetaDataBlockVorbisComment, key string, values []string) {
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = cmt.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addMP3TextFrame(tag *id3v2.Tag, frameID string, value string) {
|
||||||
|
tag.DeleteFrames(frameID)
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.AddTextFrame(frameID, id3v2.EncodingUTF8, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinMultiValueText(values []string, separator string, nullSeparated bool) string {
|
||||||
|
cleaned := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value != "" {
|
||||||
|
cleaned = append(cleaned, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cleaned) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(cleaned) == 1 {
|
||||||
|
return cleaned[0]
|
||||||
|
}
|
||||||
|
if nullSeparated {
|
||||||
|
return strings.Join(cleaned, "\x00")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(cleaned, displayMetadataSeparator(separator))
|
||||||
|
}
|
||||||
|
|
||||||
func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
||||||
f, err := flac.ParseFile(filepath)
|
f, err := flac.ParseFile(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -52,17 +113,22 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmt := flacvorbis.New()
|
cmt := flacvorbis.New()
|
||||||
|
separator := resolveMetadataSeparator(metadata.Separator)
|
||||||
|
|
||||||
if metadata.Title != "" {
|
if metadata.Title != "" {
|
||||||
_ = cmt.Add(flacvorbis.FIELD_TITLE, metadata.Title)
|
_ = cmt.Add(flacvorbis.FIELD_TITLE, metadata.Title)
|
||||||
}
|
}
|
||||||
if metadata.Artist != "" {
|
if artistValues := SplitArtistCredits(metadata.Artist, separator); len(artistValues) > 0 {
|
||||||
|
addVorbisTagValues(cmt, flacvorbis.FIELD_ARTIST, artistValues)
|
||||||
|
} else if metadata.Artist != "" {
|
||||||
_ = cmt.Add(flacvorbis.FIELD_ARTIST, metadata.Artist)
|
_ = cmt.Add(flacvorbis.FIELD_ARTIST, metadata.Artist)
|
||||||
}
|
}
|
||||||
if metadata.Album != "" {
|
if metadata.Album != "" {
|
||||||
_ = cmt.Add(flacvorbis.FIELD_ALBUM, metadata.Album)
|
_ = cmt.Add(flacvorbis.FIELD_ALBUM, metadata.Album)
|
||||||
}
|
}
|
||||||
if metadata.AlbumArtist != "" {
|
if albumArtistValues := SplitArtistCredits(metadata.AlbumArtist, separator); len(albumArtistValues) > 0 {
|
||||||
|
addVorbisTagValues(cmt, "ALBUMARTIST", albumArtistValues)
|
||||||
|
} else if metadata.AlbumArtist != "" {
|
||||||
_ = cmt.Add("ALBUMARTIST", metadata.AlbumArtist)
|
_ = cmt.Add("ALBUMARTIST", metadata.AlbumArtist)
|
||||||
}
|
}
|
||||||
if metadata.Date != "" {
|
if metadata.Date != "" {
|
||||||
@@ -86,6 +152,11 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
|||||||
if metadata.Publisher != "" {
|
if metadata.Publisher != "" {
|
||||||
_ = cmt.Add("PUBLISHER", metadata.Publisher)
|
_ = cmt.Add("PUBLISHER", metadata.Publisher)
|
||||||
}
|
}
|
||||||
|
if composerValues := SplitArtistCredits(metadata.Composer, separator); len(composerValues) > 0 {
|
||||||
|
addVorbisTagValues(cmt, "COMPOSER", composerValues)
|
||||||
|
} else if metadata.Composer != "" {
|
||||||
|
_ = cmt.Add("COMPOSER", metadata.Composer)
|
||||||
|
}
|
||||||
if metadata.Description != "" {
|
if metadata.Description != "" {
|
||||||
_ = cmt.Add("DESCRIPTION", metadata.Description)
|
_ = cmt.Add("DESCRIPTION", metadata.Description)
|
||||||
}
|
}
|
||||||
@@ -97,7 +168,9 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
|||||||
_ = cmt.Add("ISRC", metadata.ISRC)
|
_ = cmt.Add("ISRC", metadata.ISRC)
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata.Genre != "" {
|
if genreValues := SplitMetadataValues(metadata.Genre, separator); len(genreValues) > 0 {
|
||||||
|
addVorbisTagValues(cmt, "GENRE", genreValues)
|
||||||
|
} else if metadata.Genre != "" {
|
||||||
_ = cmt.Add("GENRE", metadata.Genre)
|
_ = cmt.Add("GENRE", metadata.Genre)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -901,6 +974,10 @@ func ExtractFullMetadataFromFile(filePath string) (Metadata, error) {
|
|||||||
metadata.Copyright = value
|
metadata.Copyright = value
|
||||||
case "publisher", "tpub", "label":
|
case "publisher", "tpub", "label":
|
||||||
metadata.Publisher = value
|
metadata.Publisher = value
|
||||||
|
case "composer", "writer", "wm/composer", "©wrt":
|
||||||
|
metadata.Composer = value
|
||||||
|
case "genre", "tcon":
|
||||||
|
metadata.Genre = value
|
||||||
case "url":
|
case "url":
|
||||||
metadata.URL = value
|
metadata.URL = value
|
||||||
case "comment", "comments":
|
case "comment", "comments":
|
||||||
@@ -940,15 +1017,13 @@ func embedMetadataToMP3(filePath string, metadata Metadata, coverPath string) er
|
|||||||
return fmt.Errorf("failed to open MP3 file: %w", err)
|
return fmt.Errorf("failed to open MP3 file: %w", err)
|
||||||
}
|
}
|
||||||
defer tag.Close()
|
defer tag.Close()
|
||||||
|
separator := resolveMetadataSeparator(metadata.Separator)
|
||||||
|
|
||||||
tag.DeleteFrames("TXXX")
|
tag.DeleteFrames("TXXX")
|
||||||
|
|
||||||
if metadata.Title != "" {
|
if metadata.Title != "" {
|
||||||
tag.SetTitle(metadata.Title)
|
tag.SetTitle(metadata.Title)
|
||||||
}
|
}
|
||||||
if metadata.Artist != "" {
|
|
||||||
tag.SetArtist(metadata.Artist)
|
|
||||||
}
|
|
||||||
if metadata.Album != "" {
|
if metadata.Album != "" {
|
||||||
tag.SetAlbum(metadata.Album)
|
tag.SetAlbum(metadata.Album)
|
||||||
}
|
}
|
||||||
@@ -960,10 +1035,17 @@ func embedMetadataToMP3(filePath string, metadata Metadata, coverPath string) er
|
|||||||
tag.SetYear(year)
|
tag.SetYear(year)
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata.AlbumArtist != "" {
|
artistText := joinMultiValueText(SplitArtistCredits(metadata.Artist, separator), separator, true)
|
||||||
tag.DeleteFrames("TPE2")
|
if artistText == "" {
|
||||||
tag.AddTextFrame("TPE2", id3v2.EncodingUTF8, metadata.AlbumArtist)
|
artistText = strings.TrimSpace(metadata.Artist)
|
||||||
}
|
}
|
||||||
|
addMP3TextFrame(tag, "TPE1", artistText)
|
||||||
|
|
||||||
|
albumArtistText := joinMultiValueText(SplitArtistCredits(metadata.AlbumArtist, separator), separator, true)
|
||||||
|
if albumArtistText == "" {
|
||||||
|
albumArtistText = strings.TrimSpace(metadata.AlbumArtist)
|
||||||
|
}
|
||||||
|
addMP3TextFrame(tag, "TPE2", albumArtistText)
|
||||||
|
|
||||||
if metadata.TrackNumber > 0 {
|
if metadata.TrackNumber > 0 {
|
||||||
tag.DeleteFrames(tag.CommonID("Track number/Position in set"))
|
tag.DeleteFrames(tag.CommonID("Track number/Position in set"))
|
||||||
@@ -984,18 +1066,21 @@ func embedMetadataToMP3(filePath string, metadata Metadata, coverPath string) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
if metadata.Copyright != "" {
|
if metadata.Copyright != "" {
|
||||||
tag.DeleteFrames("TCOP")
|
addMP3TextFrame(tag, "TCOP", metadata.Copyright)
|
||||||
tag.AddTextFrame("TCOP", id3v2.EncodingUTF8, metadata.Copyright)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata.Publisher != "" {
|
if metadata.Publisher != "" {
|
||||||
tag.DeleteFrames("TPUB")
|
addMP3TextFrame(tag, "TPUB", metadata.Publisher)
|
||||||
tag.AddTextFrame("TPUB", id3v2.EncodingUTF8, metadata.Publisher)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composerText := joinMultiValueText(SplitArtistCredits(metadata.Composer, separator), separator, true)
|
||||||
|
if composerText == "" {
|
||||||
|
composerText = strings.TrimSpace(metadata.Composer)
|
||||||
|
}
|
||||||
|
addMP3TextFrame(tag, "TCOM", composerText)
|
||||||
|
|
||||||
if metadata.ISRC != "" {
|
if metadata.ISRC != "" {
|
||||||
tag.DeleteFrames("TSRC")
|
addMP3TextFrame(tag, "TSRC", metadata.ISRC)
|
||||||
tag.AddTextFrame("TSRC", id3v2.EncodingUTF8, metadata.ISRC)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if comment := resolveMetadataComment(metadata); comment != "" {
|
if comment := resolveMetadataComment(metadata); comment != "" {
|
||||||
@@ -1027,6 +1112,12 @@ func embedMetadataToMP3(filePath string, metadata Metadata, coverPath string) er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
genreText := joinMultiValueText(SplitMetadataValues(metadata.Genre, separator), separator, true)
|
||||||
|
if genreText == "" {
|
||||||
|
genreText = strings.TrimSpace(metadata.Genre)
|
||||||
|
}
|
||||||
|
addMP3TextFrame(tag, "TCON", genreText)
|
||||||
|
|
||||||
if err := tag.Save(); err != nil {
|
if err := tag.Save(); err != nil {
|
||||||
return fmt.Errorf("failed to save MP3 tags: %w", err)
|
return fmt.Errorf("failed to save MP3 tags: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1048,6 +1139,7 @@ func embedMetadataToM4A(filePath string, metadata Metadata, coverPath string) er
|
|||||||
"-i", filePath,
|
"-i", filePath,
|
||||||
"-y",
|
"-y",
|
||||||
}
|
}
|
||||||
|
separator := resolveMetadataSeparator(metadata.Separator)
|
||||||
|
|
||||||
if coverPath != "" && fileExists(coverPath) {
|
if coverPath != "" && fileExists(coverPath) {
|
||||||
args = append(args, "-i", coverPath)
|
args = append(args, "-i", coverPath)
|
||||||
@@ -1059,14 +1151,22 @@ func embedMetadataToM4A(filePath string, metadata Metadata, coverPath string) er
|
|||||||
if metadata.Title != "" {
|
if metadata.Title != "" {
|
||||||
args = append(args, "-metadata", "title="+metadata.Title)
|
args = append(args, "-metadata", "title="+metadata.Title)
|
||||||
}
|
}
|
||||||
if metadata.Artist != "" {
|
artistText := joinMultiValueText(SplitArtistCredits(metadata.Artist, separator), separator, false)
|
||||||
args = append(args, "-metadata", "artist="+metadata.Artist)
|
if artistText == "" {
|
||||||
|
artistText = strings.TrimSpace(metadata.Artist)
|
||||||
|
}
|
||||||
|
if artistText != "" {
|
||||||
|
args = append(args, "-metadata", "artist="+artistText)
|
||||||
}
|
}
|
||||||
if metadata.Album != "" {
|
if metadata.Album != "" {
|
||||||
args = append(args, "-metadata", "album="+metadata.Album)
|
args = append(args, "-metadata", "album="+metadata.Album)
|
||||||
}
|
}
|
||||||
if metadata.AlbumArtist != "" {
|
albumArtistText := joinMultiValueText(SplitArtistCredits(metadata.AlbumArtist, separator), separator, false)
|
||||||
args = append(args, "-metadata", "album_artist="+metadata.AlbumArtist)
|
if albumArtistText == "" {
|
||||||
|
albumArtistText = strings.TrimSpace(metadata.AlbumArtist)
|
||||||
|
}
|
||||||
|
if albumArtistText != "" {
|
||||||
|
args = append(args, "-metadata", "album_artist="+albumArtistText)
|
||||||
}
|
}
|
||||||
if metadata.Date != "" {
|
if metadata.Date != "" {
|
||||||
args = append(args, "-metadata", "date="+metadata.Date)
|
args = append(args, "-metadata", "date="+metadata.Date)
|
||||||
@@ -1091,9 +1191,23 @@ func embedMetadataToM4A(filePath string, metadata Metadata, coverPath string) er
|
|||||||
if metadata.Publisher != "" {
|
if metadata.Publisher != "" {
|
||||||
args = append(args, "-metadata", "publisher="+metadata.Publisher)
|
args = append(args, "-metadata", "publisher="+metadata.Publisher)
|
||||||
}
|
}
|
||||||
|
composerText := joinMultiValueText(SplitArtistCredits(metadata.Composer, separator), separator, false)
|
||||||
|
if composerText == "" {
|
||||||
|
composerText = strings.TrimSpace(metadata.Composer)
|
||||||
|
}
|
||||||
|
if composerText != "" {
|
||||||
|
args = append(args, "-metadata", "composer="+composerText)
|
||||||
|
}
|
||||||
if metadata.ISRC != "" {
|
if metadata.ISRC != "" {
|
||||||
args = append(args, "-metadata", "isrc="+metadata.ISRC)
|
args = append(args, "-metadata", "isrc="+metadata.ISRC)
|
||||||
}
|
}
|
||||||
|
genreText := joinMultiValueText(SplitMetadataValues(metadata.Genre, separator), separator, false)
|
||||||
|
if genreText == "" {
|
||||||
|
genreText = strings.TrimSpace(metadata.Genre)
|
||||||
|
}
|
||||||
|
if genreText != "" {
|
||||||
|
args = append(args, "-metadata", "genre="+genreText)
|
||||||
|
}
|
||||||
if comment := resolveMetadataComment(metadata); comment != "" {
|
if comment := resolveMetadataComment(metadata); comment != "" {
|
||||||
args = append(args, "-metadata", "comment="+comment)
|
args = append(args, "-metadata", "comment="+comment)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -381,7 +381,7 @@ func buildQobuzFilename(title, artist, album, albumArtist, releaseDate string, t
|
|||||||
return filename + ".flac"
|
return filename + ".flac"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QobuzDownloader) DownloadTrack(spotifyID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
func (q *QobuzDownloader) DownloadTrack(spotifyID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
||||||
var isrc string
|
var isrc string
|
||||||
if spotifyID != "" {
|
if spotifyID != "" {
|
||||||
linkClient := NewSongLinkClient()
|
linkClient := NewSongLinkClient()
|
||||||
@@ -394,10 +394,10 @@ func (q *QobuzDownloader) DownloadTrack(spotifyID, outputDir, quality, filenameF
|
|||||||
return "", fmt.Errorf("spotify ID is required for Qobuz download")
|
return "", fmt.Errorf("spotify ID is required for Qobuz download")
|
||||||
}
|
}
|
||||||
|
|
||||||
return q.DownloadTrackWithISRC(isrc, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL, allowFallback, useFirstArtistOnly, useSingleGenre, embedGenre)
|
return q.DownloadTrackWithISRC(isrc, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, spotifyURL, allowFallback, useFirstArtistOnly, useSingleGenre, embedGenre)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QobuzDownloader) DownloadTrackWithISRC(isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
func (q *QobuzDownloader) DownloadTrackWithISRC(isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
||||||
fmt.Printf("Fetching track info for ISRC: %s\n", isrc)
|
fmt.Printf("Fetching track info for ISRC: %s\n", isrc)
|
||||||
|
|
||||||
metaChan := make(chan Metadata, 1)
|
metaChan := make(chan Metadata, 1)
|
||||||
@@ -522,6 +522,8 @@ func (q *QobuzDownloader) DownloadTrackWithISRC(isrc, outputDir, quality, filena
|
|||||||
Comment: spotifyURL,
|
Comment: spotifyURL,
|
||||||
Copyright: spotifyCopyright,
|
Copyright: spotifyCopyright,
|
||||||
Publisher: spotifyPublisher,
|
Publisher: spotifyPublisher,
|
||||||
|
Composer: spotifyComposer,
|
||||||
|
Separator: metadataSeparator,
|
||||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||||
ISRC: isrc,
|
ISRC: isrc,
|
||||||
Genre: mbMeta.Genre,
|
Genre: mbMeta.Genre,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ type TrackMetadata struct {
|
|||||||
ArtistsData []ArtistSimple `json:"artists_data,omitempty"`
|
ArtistsData []ArtistSimple `json:"artists_data,omitempty"`
|
||||||
Copyright string `json:"copyright,omitempty"`
|
Copyright string `json:"copyright,omitempty"`
|
||||||
Publisher string `json:"publisher,omitempty"`
|
Publisher string `json:"publisher,omitempty"`
|
||||||
|
Composer string `json:"composer,omitempty"`
|
||||||
Plays string `json:"plays,omitempty"`
|
Plays string `json:"plays,omitempty"`
|
||||||
PreviewURL string `json:"preview_url,omitempty"`
|
PreviewURL string `json:"preview_url,omitempty"`
|
||||||
IsExplicit bool `json:"is_explicit,omitempty"`
|
IsExplicit bool `json:"is_explicit,omitempty"`
|
||||||
@@ -193,6 +194,7 @@ type apiTrackResponse struct {
|
|||||||
Disc int `json:"disc"`
|
Disc int `json:"disc"`
|
||||||
Discs int `json:"discs"`
|
Discs int `json:"discs"`
|
||||||
Copyright string `json:"copyright"`
|
Copyright string `json:"copyright"`
|
||||||
|
Composer string `json:"composer,omitempty"`
|
||||||
Plays string `json:"plays"`
|
Plays string `json:"plays"`
|
||||||
Album struct {
|
Album struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -496,6 +498,10 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
filteredData := FilterTrack(data, c.Separator, albumFetchData)
|
filteredData := FilterTrack(data, c.Separator, albumFetchData)
|
||||||
|
composer, composerErr := c.fetchTrackComposerWithClient(ctx, client, trackID)
|
||||||
|
if composerErr == nil && composer != "" {
|
||||||
|
filteredData["composer"] = composer
|
||||||
|
}
|
||||||
|
|
||||||
jsonData, err := json.Marshal(filteredData)
|
jsonData, err := json.Marshal(filteredData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -510,6 +516,89 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string)
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectTrackCreditNamesByRole(items []interface{}, role string) []string {
|
||||||
|
role = strings.TrimSpace(role)
|
||||||
|
if role == "" || len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}, len(items))
|
||||||
|
names := make([]string, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
itemMap, ok := item.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.EqualFold(strings.TrimSpace(getString(itemMap, "role")), role) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimSpace(getString(itemMap, "name"))
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[name]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[name] = struct{}{}
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SpotifyMetadataClient) fetchTrackComposerWithClient(ctx context.Context, client *SpotifyClient, trackID string) (string, error) {
|
||||||
|
_ = ctx
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"variables": map[string]interface{}{
|
||||||
|
"trackUri": fmt.Sprintf("spotify:track:%s", trackID),
|
||||||
|
"contributorsLimit": 100,
|
||||||
|
"contributorsOffset": 0,
|
||||||
|
},
|
||||||
|
"operationName": "queryTrackCreditsModal",
|
||||||
|
"extensions": map[string]interface{}{
|
||||||
|
"persistedQuery": map[string]interface{}{
|
||||||
|
"version": 1,
|
||||||
|
"sha256Hash": "e2ca40d46cf1fde36562261ccec754f23fb31b561877252e9fe0d6834aabb84b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := client.Query(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to query track credits: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
creditItems := getSlice(
|
||||||
|
getMap(
|
||||||
|
getMap(
|
||||||
|
getMap(
|
||||||
|
getMap(data, "data"),
|
||||||
|
"trackUnion",
|
||||||
|
),
|
||||||
|
"creditsTrait",
|
||||||
|
),
|
||||||
|
"contributors",
|
||||||
|
),
|
||||||
|
"items",
|
||||||
|
)
|
||||||
|
|
||||||
|
composerNames := collectTrackCreditNamesByRole(creditItems, "Composer")
|
||||||
|
if len(composerNames) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
separator := strings.TrimSpace(c.Separator)
|
||||||
|
if separator == "" {
|
||||||
|
separator = ", "
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(composerNames, separator), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *SpotifyMetadataClient) fetchAlbum(ctx context.Context, albumID string, callback MetadataCallback) (*apiAlbumResponse, error) {
|
func (c *SpotifyMetadataClient) fetchAlbum(ctx context.Context, albumID string, callback MetadataCallback) (*apiAlbumResponse, error) {
|
||||||
client := NewSpotifyClient()
|
client := NewSpotifyClient()
|
||||||
if err := client.Initialize(); err != nil {
|
if err := client.Initialize(); err != nil {
|
||||||
@@ -963,6 +1052,7 @@ func (c *SpotifyMetadataClient) formatTrackData(raw *apiTrackResponse) TrackResp
|
|||||||
ArtistsData: artistsData,
|
ArtistsData: artistsData,
|
||||||
Copyright: raw.Copyright,
|
Copyright: raw.Copyright,
|
||||||
Publisher: raw.Album.Label,
|
Publisher: raw.Album.Label,
|
||||||
|
Composer: raw.Composer,
|
||||||
Plays: raw.Plays,
|
Plays: raw.Plays,
|
||||||
IsExplicit: raw.IsExplicit,
|
IsExplicit: raw.IsExplicit,
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-4
@@ -416,7 +416,7 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
||||||
if outputDir != "." {
|
if outputDir != "." {
|
||||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
return "", fmt.Errorf("directory error: %w", err)
|
return "", fmt.Errorf("directory error: %w", err)
|
||||||
@@ -554,6 +554,8 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
|
|||||||
Comment: spotifyURL,
|
Comment: spotifyURL,
|
||||||
Copyright: spotifyCopyright,
|
Copyright: spotifyCopyright,
|
||||||
Publisher: spotifyPublisher,
|
Publisher: spotifyPublisher,
|
||||||
|
Composer: spotifyComposer,
|
||||||
|
Separator: metadataSeparator,
|
||||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||||
ISRC: isrc,
|
ISRC: isrc,
|
||||||
Genre: mbMeta.Genre,
|
Genre: mbMeta.Genre,
|
||||||
@@ -570,7 +572,7 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
|
|||||||
return outputFilename, nil
|
return outputFilename, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
||||||
apis, err := t.GetAvailableAPIs()
|
apis, err := t.GetAvailableAPIs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("no APIs available for fallback: %w", err)
|
return "", fmt.Errorf("no APIs available for fallback: %w", err)
|
||||||
@@ -714,6 +716,8 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
|
|||||||
Comment: spotifyURL,
|
Comment: spotifyURL,
|
||||||
Copyright: spotifyCopyright,
|
Copyright: spotifyCopyright,
|
||||||
Publisher: spotifyPublisher,
|
Publisher: spotifyPublisher,
|
||||||
|
Composer: spotifyComposer,
|
||||||
|
Separator: metadataSeparator,
|
||||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||||
ISRC: isrc,
|
ISRC: isrc,
|
||||||
Genre: mbMeta.Genre,
|
Genre: mbMeta.Genre,
|
||||||
@@ -730,14 +734,14 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
|
|||||||
return outputFilename, nil
|
return outputFilename, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
||||||
|
|
||||||
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
|
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("songlink couldn't find Tidal URL: %w", err)
|
return "", fmt.Errorf("songlink couldn't find Tidal URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL, allowFallback, useFirstArtistOnly, useSingleGenre, embedGenre)
|
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, spotifyURL, allowFallback, useFirstArtistOnly, useSingleGenre, embedGenre)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SegmentTemplate struct {
|
type SegmentTemplate struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user