.add composer and fix multiple value tag
This commit is contained in:
+5
-3
@@ -204,7 +204,7 @@ func (a *AmazonDownloader) DownloadFromService(amazonURL, outputDir, quality str
|
||||
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 err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
@@ -390,6 +390,8 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
|
||||
Comment: spotifyURL,
|
||||
Copyright: spotifyCopyright,
|
||||
Publisher: spotifyPublisher,
|
||||
Composer: spotifyComposer,
|
||||
Separator: metadataSeparator,
|
||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||
ISRC: isrc,
|
||||
Genre: mbMeta.Genre,
|
||||
@@ -418,7 +420,7 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
|
||||
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,
|
||||
) (string, error) {
|
||||
|
||||
@@ -427,5 +429,5 @@ func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, qualit
|
||||
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
|
||||
Album string
|
||||
AlbumArtist string
|
||||
Separator string
|
||||
Date string
|
||||
ReleaseDate string
|
||||
TrackNumber int
|
||||
@@ -31,12 +32,72 @@ type Metadata struct {
|
||||
Comment string
|
||||
Copyright string
|
||||
Publisher string
|
||||
Composer string
|
||||
Lyrics string
|
||||
Description string
|
||||
ISRC 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 {
|
||||
f, err := flac.ParseFile(filepath)
|
||||
if err != nil {
|
||||
@@ -52,17 +113,22 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
||||
}
|
||||
|
||||
cmt := flacvorbis.New()
|
||||
separator := resolveMetadataSeparator(metadata.Separator)
|
||||
|
||||
if 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)
|
||||
}
|
||||
if 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)
|
||||
}
|
||||
if metadata.Date != "" {
|
||||
@@ -86,6 +152,11 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
||||
if 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 != "" {
|
||||
_ = cmt.Add("DESCRIPTION", metadata.Description)
|
||||
}
|
||||
@@ -97,7 +168,9 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
||||
_ = 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)
|
||||
}
|
||||
|
||||
@@ -901,6 +974,10 @@ func ExtractFullMetadataFromFile(filePath string) (Metadata, error) {
|
||||
metadata.Copyright = value
|
||||
case "publisher", "tpub", "label":
|
||||
metadata.Publisher = value
|
||||
case "composer", "writer", "wm/composer", "©wrt":
|
||||
metadata.Composer = value
|
||||
case "genre", "tcon":
|
||||
metadata.Genre = value
|
||||
case "url":
|
||||
metadata.URL = value
|
||||
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)
|
||||
}
|
||||
defer tag.Close()
|
||||
separator := resolveMetadataSeparator(metadata.Separator)
|
||||
|
||||
tag.DeleteFrames("TXXX")
|
||||
|
||||
if metadata.Title != "" {
|
||||
tag.SetTitle(metadata.Title)
|
||||
}
|
||||
if metadata.Artist != "" {
|
||||
tag.SetArtist(metadata.Artist)
|
||||
}
|
||||
if metadata.Album != "" {
|
||||
tag.SetAlbum(metadata.Album)
|
||||
}
|
||||
@@ -960,10 +1035,17 @@ func embedMetadataToMP3(filePath string, metadata Metadata, coverPath string) er
|
||||
tag.SetYear(year)
|
||||
}
|
||||
|
||||
if metadata.AlbumArtist != "" {
|
||||
tag.DeleteFrames("TPE2")
|
||||
tag.AddTextFrame("TPE2", id3v2.EncodingUTF8, metadata.AlbumArtist)
|
||||
artistText := joinMultiValueText(SplitArtistCredits(metadata.Artist, separator), separator, true)
|
||||
if artistText == "" {
|
||||
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 {
|
||||
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 != "" {
|
||||
tag.DeleteFrames("TCOP")
|
||||
tag.AddTextFrame("TCOP", id3v2.EncodingUTF8, metadata.Copyright)
|
||||
addMP3TextFrame(tag, "TCOP", metadata.Copyright)
|
||||
}
|
||||
|
||||
if metadata.Publisher != "" {
|
||||
tag.DeleteFrames("TPUB")
|
||||
tag.AddTextFrame("TPUB", id3v2.EncodingUTF8, metadata.Publisher)
|
||||
addMP3TextFrame(tag, "TPUB", metadata.Publisher)
|
||||
}
|
||||
|
||||
composerText := joinMultiValueText(SplitArtistCredits(metadata.Composer, separator), separator, true)
|
||||
if composerText == "" {
|
||||
composerText = strings.TrimSpace(metadata.Composer)
|
||||
}
|
||||
addMP3TextFrame(tag, "TCOM", composerText)
|
||||
|
||||
if metadata.ISRC != "" {
|
||||
tag.DeleteFrames("TSRC")
|
||||
tag.AddTextFrame("TSRC", id3v2.EncodingUTF8, metadata.ISRC)
|
||||
addMP3TextFrame(tag, "TSRC", metadata.ISRC)
|
||||
}
|
||||
|
||||
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 {
|
||||
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,
|
||||
"-y",
|
||||
}
|
||||
separator := resolveMetadataSeparator(metadata.Separator)
|
||||
|
||||
if coverPath != "" && fileExists(coverPath) {
|
||||
args = append(args, "-i", coverPath)
|
||||
@@ -1059,14 +1151,22 @@ func embedMetadataToM4A(filePath string, metadata Metadata, coverPath string) er
|
||||
if metadata.Title != "" {
|
||||
args = append(args, "-metadata", "title="+metadata.Title)
|
||||
}
|
||||
if metadata.Artist != "" {
|
||||
args = append(args, "-metadata", "artist="+metadata.Artist)
|
||||
artistText := joinMultiValueText(SplitArtistCredits(metadata.Artist, separator), separator, false)
|
||||
if artistText == "" {
|
||||
artistText = strings.TrimSpace(metadata.Artist)
|
||||
}
|
||||
if artistText != "" {
|
||||
args = append(args, "-metadata", "artist="+artistText)
|
||||
}
|
||||
if metadata.Album != "" {
|
||||
args = append(args, "-metadata", "album="+metadata.Album)
|
||||
}
|
||||
if metadata.AlbumArtist != "" {
|
||||
args = append(args, "-metadata", "album_artist="+metadata.AlbumArtist)
|
||||
albumArtistText := joinMultiValueText(SplitArtistCredits(metadata.AlbumArtist, separator), separator, false)
|
||||
if albumArtistText == "" {
|
||||
albumArtistText = strings.TrimSpace(metadata.AlbumArtist)
|
||||
}
|
||||
if albumArtistText != "" {
|
||||
args = append(args, "-metadata", "album_artist="+albumArtistText)
|
||||
}
|
||||
if 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 != "" {
|
||||
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 != "" {
|
||||
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 != "" {
|
||||
args = append(args, "-metadata", "comment="+comment)
|
||||
}
|
||||
|
||||
+5
-3
@@ -381,7 +381,7 @@ func buildQobuzFilename(title, artist, album, albumArtist, releaseDate string, t
|
||||
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
|
||||
if spotifyID != "" {
|
||||
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 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)
|
||||
|
||||
metaChan := make(chan Metadata, 1)
|
||||
@@ -522,6 +522,8 @@ func (q *QobuzDownloader) DownloadTrackWithISRC(isrc, outputDir, quality, filena
|
||||
Comment: spotifyURL,
|
||||
Copyright: spotifyCopyright,
|
||||
Publisher: spotifyPublisher,
|
||||
Composer: spotifyComposer,
|
||||
Separator: metadataSeparator,
|
||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||
ISRC: isrc,
|
||||
Genre: mbMeta.Genre,
|
||||
|
||||
@@ -53,6 +53,7 @@ type TrackMetadata struct {
|
||||
ArtistsData []ArtistSimple `json:"artists_data,omitempty"`
|
||||
Copyright string `json:"copyright,omitempty"`
|
||||
Publisher string `json:"publisher,omitempty"`
|
||||
Composer string `json:"composer,omitempty"`
|
||||
Plays string `json:"plays,omitempty"`
|
||||
PreviewURL string `json:"preview_url,omitempty"`
|
||||
IsExplicit bool `json:"is_explicit,omitempty"`
|
||||
@@ -193,6 +194,7 @@ type apiTrackResponse struct {
|
||||
Disc int `json:"disc"`
|
||||
Discs int `json:"discs"`
|
||||
Copyright string `json:"copyright"`
|
||||
Composer string `json:"composer,omitempty"`
|
||||
Plays string `json:"plays"`
|
||||
Album struct {
|
||||
ID string `json:"id"`
|
||||
@@ -496,6 +498,10 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -510,6 +516,89 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string)
|
||||
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) {
|
||||
client := NewSpotifyClient()
|
||||
if err := client.Initialize(); err != nil {
|
||||
@@ -963,6 +1052,7 @@ func (c *SpotifyMetadataClient) formatTrackData(raw *apiTrackResponse) TrackResp
|
||||
ArtistsData: artistsData,
|
||||
Copyright: raw.Copyright,
|
||||
Publisher: raw.Album.Label,
|
||||
Composer: raw.Composer,
|
||||
Plays: raw.Plays,
|
||||
IsExplicit: raw.IsExplicit,
|
||||
}
|
||||
|
||||
+8
-4
@@ -416,7 +416,7 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e
|
||||
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 err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("directory error: %w", err)
|
||||
@@ -554,6 +554,8 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
|
||||
Comment: spotifyURL,
|
||||
Copyright: spotifyCopyright,
|
||||
Publisher: spotifyPublisher,
|
||||
Composer: spotifyComposer,
|
||||
Separator: metadataSeparator,
|
||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||
ISRC: isrc,
|
||||
Genre: mbMeta.Genre,
|
||||
@@ -570,7 +572,7 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
|
||||
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()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no APIs available for fallback: %w", err)
|
||||
@@ -714,6 +716,8 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
|
||||
Comment: spotifyURL,
|
||||
Copyright: spotifyCopyright,
|
||||
Publisher: spotifyPublisher,
|
||||
Composer: spotifyComposer,
|
||||
Separator: metadataSeparator,
|
||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||
ISRC: isrc,
|
||||
Genre: mbMeta.Genre,
|
||||
@@ -730,14 +734,14 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user