.redownlaod with suffix, isrc variable

This commit is contained in:
afkarxyz
2026-04-13 21:53:47 +07:00
parent 7792a69d33
commit db8f82aa17
15 changed files with 298 additions and 62 deletions
+35 -9
View File
@@ -159,6 +159,7 @@ type DownloadRequest struct {
SpotifyDiscNumber int `json:"spotify_disc_number,omitempty"` SpotifyDiscNumber int `json:"spotify_disc_number,omitempty"`
SpotifyTotalTracks int `json:"spotify_total_tracks,omitempty"` SpotifyTotalTracks int `json:"spotify_total_tracks,omitempty"`
SpotifyTotalDiscs int `json:"spotify_total_discs,omitempty"` SpotifyTotalDiscs int `json:"spotify_total_discs,omitempty"`
ISRC string `json:"isrc,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"` Composer string `json:"composer,omitempty"`
@@ -363,6 +364,9 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
if req.FilenameFormat == "" { if req.FilenameFormat == "" {
req.FilenameFormat = "title-artist" req.FilenameFormat = "title-artist"
} }
if req.ISRC == "" && strings.Contains(req.FilenameFormat, "{isrc}") && req.SpotifyID != "" {
req.ISRC = backend.ResolveTrackISRC(req.SpotifyID)
}
itemID := req.ItemID itemID := req.ItemID
if itemID == "" { if itemID == "" {
@@ -449,9 +453,10 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
} }
if req.TrackName != "" && req.ArtistName != "" { if req.TrackName != "" && req.ArtistName != "" {
expectedFilename := backend.BuildExpectedFilename(req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.SpotifyDiscNumber, req.UseAlbumTrackNumber) expectedFilename := backend.BuildExpectedFilename(req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.SpotifyDiscNumber, req.UseAlbumTrackNumber, req.ISRC)
expectedPath := filepath.Join(req.OutputDir, expectedFilename) expectedPath := filepath.Join(req.OutputDir, expectedFilename)
if !backend.GetRedownloadWithSuffixSetting() {
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 { if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 {
backend.SkipDownloadItem(itemID, expectedPath) backend.SkipDownloadItem(itemID, expectedPath)
@@ -464,6 +469,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
}, nil }, nil
} }
} }
}
lyricsChan := make(chan string, 1) lyricsChan := make(chan string, 1)
isrcChan := make(chan string, 1) isrcChan := make(chan string, 1)
@@ -506,32 +512,35 @@ 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, req.Composer, metadataSeparator, 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, req.ISRC, 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, req.Composer, metadataSeparator, 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, req.ISRC, 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, req.Composer, metadataSeparator, 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, req.ISRC, 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, req.Composer, metadataSeparator, 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, req.ISRC, 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, req.Composer, metadataSeparator, 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, req.ISRC, 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, req.Composer, metadataSeparator, 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, req.ISRC, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
} }
} }
case "qobuz": case "qobuz":
isrc := strings.TrimSpace(req.ISRC)
if isrc == "" {
fmt.Println("Waiting for ISRC (Qobuz dependency)...") fmt.Println("Waiting for ISRC (Qobuz dependency)...")
isrc := <-isrcChan isrc = <-isrcChan
}
downloader := backend.NewQobuzDownloader() downloader := backend.NewQobuzDownloader()
quality := req.AudioFormat quality := req.AudioFormat
if quality == "" { if quality == "" {
@@ -957,6 +966,7 @@ type LyricsDownloadRequest struct {
AlbumName string `json:"album_name"` AlbumName string `json:"album_name"`
AlbumArtist string `json:"album_artist"` AlbumArtist string `json:"album_artist"`
ReleaseDate string `json:"release_date"` ReleaseDate string `json:"release_date"`
ISRC string `json:"isrc,omitempty"`
OutputDir string `json:"output_dir"` OutputDir string `json:"output_dir"`
FilenameFormat string `json:"filename_format"` FilenameFormat string `json:"filename_format"`
TrackNumber bool `json:"track_number"` TrackNumber bool `json:"track_number"`
@@ -981,6 +991,7 @@ func (a *App) DownloadLyrics(req LyricsDownloadRequest) (backend.LyricsDownloadR
AlbumName: req.AlbumName, AlbumName: req.AlbumName,
AlbumArtist: req.AlbumArtist, AlbumArtist: req.AlbumArtist,
ReleaseDate: req.ReleaseDate, ReleaseDate: req.ReleaseDate,
ISRC: req.ISRC,
OutputDir: req.OutputDir, OutputDir: req.OutputDir,
FilenameFormat: req.FilenameFormat, FilenameFormat: req.FilenameFormat,
TrackNumber: req.TrackNumber, TrackNumber: req.TrackNumber,
@@ -1404,6 +1415,7 @@ type CheckFileExistenceRequest struct {
AlbumName string `json:"album_name,omitempty"` AlbumName string `json:"album_name,omitempty"`
AlbumArtist string `json:"album_artist,omitempty"` AlbumArtist string `json:"album_artist,omitempty"`
ReleaseDate string `json:"release_date,omitempty"` ReleaseDate string `json:"release_date,omitempty"`
ISRC string `json:"isrc,omitempty"`
TrackNumber int `json:"track_number,omitempty"` TrackNumber int `json:"track_number,omitempty"`
DiscNumber int `json:"disc_number,omitempty"` DiscNumber int `json:"disc_number,omitempty"`
Position int `json:"position,omitempty"` Position int `json:"position,omitempty"`
@@ -1433,6 +1445,7 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
} }
defaultFilenameFormat := "title-artist" defaultFilenameFormat := "title-artist"
redownloadWithSuffix := backend.GetRedownloadWithSuffixSetting()
type result struct { type result struct {
index int index int
@@ -1483,6 +1496,10 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
if filenameFormat == "" { if filenameFormat == "" {
filenameFormat = defaultFilenameFormat filenameFormat = defaultFilenameFormat
} }
isrc := strings.TrimSpace(t.ISRC)
if isrc == "" && strings.Contains(filenameFormat, "{isrc}") && t.SpotifyID != "" {
isrc = backend.ResolveTrackISRC(t.SpotifyID)
}
trackNumber := t.Position trackNumber := t.Position
if t.UseAlbumTrackNumber && t.TrackNumber > 0 { if t.UseAlbumTrackNumber && t.TrackNumber > 0 {
@@ -1507,6 +1524,7 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
trackNumber, trackNumber,
t.DiscNumber, t.DiscNumber,
t.UseAlbumTrackNumber, t.UseAlbumTrackNumber,
isrc,
) )
expectedFilename := strings.TrimSuffix(expectedFilenameBase, ".flac") + fileExt expectedFilename := strings.TrimSuffix(expectedFilenameBase, ".flac") + fileExt
@@ -1517,7 +1535,10 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
} }
expectedPath := filepath.Join(targetDir, expectedFilename) expectedPath := filepath.Join(targetDir, expectedFilename)
if redownloadWithSuffix {
expectedPath, _ = backend.ResolveOutputPathForDownload(expectedPath, true)
res.FilePath = filepath.Base(expectedPath)
} else {
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 { if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 {
res.Exists = true res.Exists = true
res.FilePath = expectedPath res.FilePath = expectedPath
@@ -1525,6 +1546,7 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
res.FilePath = expectedFilename res.FilePath = expectedFilename
} }
}
resultsChan <- result{index: idx, result: res} resultsChan <- result{index: idx, result: res}
}(i, track) }(i, track)
@@ -1573,6 +1595,10 @@ func (a *App) SkipDownloadItem(itemID, filePath string) {
backend.SkipDownloadItem(itemID, filePath) backend.SkipDownloadItem(itemID, filePath)
} }
func (a *App) GetTrackISRC(spotifyTrackID string) string {
return backend.ResolveTrackISRC(spotifyTrackID)
}
func (a *App) GetPreviewURL(trackID string) (string, error) { func (a *App) GetPreviewURL(trackID string) (string, error) {
return backend.GetPreviewURL(trackID) return backend.GetPreviewURL(trackID)
} }
+13 -5
View File
@@ -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, spotifyComposer, metadataSeparator, 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, isrcOverride, 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 {
@@ -219,14 +219,16 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
filenameArtist = GetFirstArtist(spotifyArtistName) filenameArtist = GetFirstArtist(spotifyArtistName)
filenameAlbumArtist = GetFirstArtist(spotifyAlbumArtist) filenameAlbumArtist = GetFirstArtist(spotifyAlbumArtist)
} }
expectedFilename := BuildExpectedFilename(spotifyTrackName, filenameArtist, spotifyAlbumName, filenameAlbumArtist, spotifyReleaseDate, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyDiscNumber, false) expectedFilename := BuildExpectedFilename(spotifyTrackName, filenameArtist, spotifyAlbumName, filenameAlbumArtist, spotifyReleaseDate, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyDiscNumber, false, isrcOverride)
expectedPath := filepath.Join(outputDir, expectedFilename) expectedPath := filepath.Join(outputDir, expectedFilename)
if !GetRedownloadWithSuffixSetting() {
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 0 { if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 0 {
fmt.Printf("File already exists: %s (%.2f MB)\n", expectedPath, float64(fileInfo.Size())/(1024*1024)) fmt.Printf("File already exists: %s (%.2f MB)\n", expectedPath, float64(fileInfo.Size())/(1024*1024))
return "EXISTS:" + expectedPath, nil return "EXISTS:" + expectedPath, nil
} }
} }
}
type mbResult struct { type mbResult struct {
ISRC string ISRC string
@@ -271,11 +273,13 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
return "", err return "", err
} }
var isrc string isrc := strings.TrimSpace(isrcOverride)
var mbMeta Metadata var mbMeta Metadata
if spotifyURL != "" { if spotifyURL != "" {
result := <-metaChan result := <-metaChan
if isrc == "" {
isrc = result.ISRC isrc = result.ISRC
}
mbMeta = result.Metadata mbMeta = result.Metadata
} }
@@ -309,6 +313,7 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
newFilename = strings.ReplaceAll(newFilename, "{album_artist}", safeAlbumArtist) newFilename = strings.ReplaceAll(newFilename, "{album_artist}", safeAlbumArtist)
newFilename = strings.ReplaceAll(newFilename, "{year}", year) newFilename = strings.ReplaceAll(newFilename, "{year}", year)
newFilename = strings.ReplaceAll(newFilename, "{date}", SanitizeFilename(spotifyReleaseDate)) newFilename = strings.ReplaceAll(newFilename, "{date}", SanitizeFilename(spotifyReleaseDate))
newFilename = strings.ReplaceAll(newFilename, "{isrc}", SanitizeOptionalFilename(isrc))
if spotifyDiscNumber > 0 { if spotifyDiscNumber > 0 {
newFilename = strings.ReplaceAll(newFilename, "{disc}", fmt.Sprintf("%d", spotifyDiscNumber)) newFilename = strings.ReplaceAll(newFilename, "{disc}", fmt.Sprintf("%d", spotifyDiscNumber))
@@ -346,6 +351,9 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
} }
newFilename = newFilename + ext newFilename = newFilename + ext
newFilePath := filepath.Join(outputDir, newFilename) newFilePath := filepath.Join(outputDir, newFilename)
if GetRedownloadWithSuffixSetting() {
newFilePath, _ = ResolveOutputPathForDownload(newFilePath, true)
}
if err := os.Rename(filePath, newFilePath); err != nil { if err := os.Rename(filePath, newFilePath); err != nil {
fmt.Printf("Warning: Failed to rename file: %v\n", err) fmt.Printf("Warning: Failed to rename file: %v\n", err)
@@ -420,7 +428,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, spotifyComposer, metadataSeparator, 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, isrcOverride, spotifyURL string,
useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool,
) (string, error) { ) (string, error) {
@@ -429,5 +437,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, spotifyComposer, metadataSeparator, 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, isrcOverride, spotifyURL, useFirstArtistOnly, useSingleGenre, embedGenre)
} }
+10
View File
@@ -69,6 +69,16 @@ func GetSpotFetchAPISettings() (bool, string) {
return true, apiURL return true, apiURL
} }
func GetRedownloadWithSuffixSetting() bool {
settings, err := LoadConfigSettings()
if err != nil || settings == nil {
return false
}
enabled, _ := settings["redownloadWithSuffix"].(bool)
return enabled
}
func GetLinkResolverSetting() string { func GetLinkResolverSetting() string {
settings, err := LoadConfigSettings() settings, err := LoadConfigSettings()
if err != nil || settings == nil { if err != nil || settings == nil {
+12
View File
@@ -30,6 +30,7 @@ type AudioMetadata struct {
TrackNumber int `json:"track_number"` TrackNumber int `json:"track_number"`
DiscNumber int `json:"disc_number"` DiscNumber int `json:"disc_number"`
Year string `json:"year"` Year string `json:"year"`
ISRC string `json:"isrc"`
} }
type RenamePreview struct { type RenamePreview struct {
@@ -175,6 +176,8 @@ func readFlacMetadata(filePath string) (*AudioMetadata, error) {
} }
case "DATE", "YEAR": case "DATE", "YEAR":
metadata.Year = value metadata.Year = value
case "ISRC", "TSRC":
metadata.ISRC = value
} }
} }
} }
@@ -221,6 +224,12 @@ func readMp3Metadata(filePath string) (*AudioMetadata, error) {
} }
} }
if frames := tag.GetFrames("TSRC"); len(frames) > 0 {
if textFrame, ok := frames[0].(id3v2.TextFrame); ok {
metadata.ISRC = textFrame.Text
}
}
return metadata, nil return metadata, nil
} }
@@ -301,6 +310,8 @@ func readMetadataWithFFprobe(filePath string) (*AudioMetadata, error) {
if metadata.Year == "" || len(value) > len(metadata.Year) { if metadata.Year == "" || len(value) > len(metadata.Year) {
metadata.Year = value metadata.Year = value
} }
case "isrc", "tsrc":
metadata.ISRC = value
} }
} }
@@ -333,6 +344,7 @@ func GenerateFilename(metadata *AudioMetadata, format string, ext string) string
result = strings.ReplaceAll(result, "{album_artist}", sanitizeFilenameForRename(metadata.AlbumArtist)) result = strings.ReplaceAll(result, "{album_artist}", sanitizeFilenameForRename(metadata.AlbumArtist))
result = strings.ReplaceAll(result, "{year}", sanitizeFilenameForRename(year)) result = strings.ReplaceAll(result, "{year}", sanitizeFilenameForRename(year))
result = strings.ReplaceAll(result, "{date}", sanitizeFilenameForRename(metadata.Year)) result = strings.ReplaceAll(result, "{date}", sanitizeFilenameForRename(metadata.Year))
result = strings.ReplaceAll(result, "{isrc}", sanitizeFilenameForRename(metadata.ISRC))
if metadata.TrackNumber > 0 { if metadata.TrackNumber > 0 {
result = strings.ReplaceAll(result, "{track}", fmt.Sprintf("%02d", metadata.TrackNumber)) result = strings.ReplaceAll(result, "{track}", fmt.Sprintf("%02d", metadata.TrackNumber))
+52 -3
View File
@@ -2,6 +2,7 @@ package backend
import ( import (
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
@@ -9,12 +10,12 @@ import (
"unicode/utf8" "unicode/utf8"
) )
func BuildExpectedFilename(trackName, artistName, albumName, albumArtist, releaseDate, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position, discNumber int, useAlbumTrackNumber bool) string { func buildFormattedFilenameBase(trackName, artistName, albumName, albumArtist, releaseDate, filenameFormat, playlistName, playlistOwner, isrc string, includeTrackNumber bool, position, discNumber int, useAlbumTrackNumber bool) string {
safeTitle := SanitizeFilename(trackName) safeTitle := SanitizeFilename(trackName)
safeArtist := SanitizeFilename(artistName) safeArtist := SanitizeFilename(artistName)
safeAlbum := SanitizeFilename(albumName) safeAlbum := SanitizeFilename(albumName)
safeAlbumArtist := SanitizeFilename(albumArtist) safeAlbumArtist := SanitizeFilename(albumArtist)
safeISRC := SanitizeOptionalFilename(isrc)
safePlaylist := SanitizeFilename(playlistName) safePlaylist := SanitizeFilename(playlistName)
safeCreator := SanitizeFilename(playlistOwner) safeCreator := SanitizeFilename(playlistOwner)
@@ -36,6 +37,7 @@ func BuildExpectedFilename(trackName, artistName, albumName, albumArtist, releas
filename = strings.ReplaceAll(filename, "{date}", SanitizeFilename(releaseDate)) filename = strings.ReplaceAll(filename, "{date}", SanitizeFilename(releaseDate))
filename = strings.ReplaceAll(filename, "{playlist}", safePlaylist) filename = strings.ReplaceAll(filename, "{playlist}", safePlaylist)
filename = strings.ReplaceAll(filename, "{creator}", safeCreator) filename = strings.ReplaceAll(filename, "{creator}", safeCreator)
filename = strings.ReplaceAll(filename, "{isrc}", safeISRC)
if discNumber > 0 { if discNumber > 0 {
filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber)) filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber))
@@ -67,7 +69,47 @@ func BuildExpectedFilename(trackName, artistName, albumName, albumArtist, releas
} }
} }
return filename + ".flac" return filename
}
func BuildExpectedFilename(trackName, artistName, albumName, albumArtist, releaseDate, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position, discNumber int, useAlbumTrackNumber bool, extra ...string) string {
isrc := ""
if len(extra) > 0 {
isrc = extra[0]
}
return buildFormattedFilenameBase(trackName, artistName, albumName, albumArtist, releaseDate, filenameFormat, playlistName, playlistOwner, isrc, includeTrackNumber, position, discNumber, useAlbumTrackNumber) + ".flac"
}
func ResolveOutputPathForDownload(path string, redownloadWithSuffix bool) (string, bool) {
if !redownloadWithSuffix {
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
return path, true
}
return path, false
}
if info, err := os.Stat(path); err != nil || info.Size() == 0 {
return path, false
}
ext := filepath.Ext(path)
base := strings.TrimSuffix(path, ext)
for i := 1; ; i++ {
candidate := fmt.Sprintf("%s_%02d%s", base, i, ext)
if info, err := os.Stat(candidate); err != nil || info.Size() == 0 {
return candidate, false
}
}
}
func mustFileSize(path string) int64 {
info, err := os.Stat(path)
if err != nil {
return 0
}
return info.Size()
} }
func SanitizeFilename(name string) string { func SanitizeFilename(name string) string {
@@ -188,3 +230,10 @@ func sanitizeFolderName(name string) string { return SanitizeFilename(name) }
func sanitizeFilename(name string) string { func sanitizeFilename(name string) string {
return SanitizeFilename(name) return SanitizeFilename(name)
} }
func SanitizeOptionalFilename(name string) string {
if strings.TrimSpace(name) == "" {
return ""
}
return SanitizeFilename(name)
}
+22
View File
@@ -0,0 +1,22 @@
package backend
import "strings"
func ResolveTrackISRC(spotifyTrackID string) string {
spotifyTrackID = strings.TrimSpace(spotifyTrackID)
if spotifyTrackID == "" {
return ""
}
if cachedISRC, err := GetCachedISRC(spotifyTrackID); err == nil && cachedISRC != "" {
return strings.ToUpper(strings.TrimSpace(cachedISRC))
}
client := NewSongLinkClient()
isrc, err := client.GetISRCDirect(spotifyTrackID)
if err != nil {
return ""
}
return strings.ToUpper(strings.TrimSpace(isrc))
}
+11 -3
View File
@@ -44,6 +44,7 @@ type LyricsDownloadRequest struct {
AlbumName string `json:"album_name"` AlbumName string `json:"album_name"`
AlbumArtist string `json:"album_artist"` AlbumArtist string `json:"album_artist"`
ReleaseDate string `json:"release_date"` ReleaseDate string `json:"release_date"`
ISRC string `json:"isrc"`
OutputDir string `json:"output_dir"` OutputDir string `json:"output_dir"`
FilenameFormat string `json:"filename_format"` FilenameFormat string `json:"filename_format"`
TrackNumber bool `json:"track_number"` TrackNumber bool `json:"track_number"`
@@ -363,11 +364,12 @@ func msToLRCTimestamp(msStr string) string {
return fmt.Sprintf("[%02d:%02d.%02d]", minutes, seconds, centiseconds) return fmt.Sprintf("[%02d:%02d.%02d]", minutes, seconds, centiseconds)
} }
func buildLyricsFilename(trackName, artistName, albumName, albumArtist, releaseDate, filenameFormat string, includeTrackNumber bool, position, discNumber int) string { func buildLyricsFilename(trackName, artistName, albumName, albumArtist, releaseDate, filenameFormat, isrc string, includeTrackNumber bool, position, discNumber int) string {
safeTitle := sanitizeFilename(trackName) safeTitle := sanitizeFilename(trackName)
safeArtist := sanitizeFilename(artistName) safeArtist := sanitizeFilename(artistName)
safeAlbum := sanitizeFilename(albumName) safeAlbum := sanitizeFilename(albumName)
safeAlbumArtist := sanitizeFilename(albumArtist) safeAlbumArtist := sanitizeFilename(albumArtist)
safeISRC := SanitizeOptionalFilename(isrc)
year := "" year := ""
if len(releaseDate) >= 4 { if len(releaseDate) >= 4 {
@@ -384,6 +386,7 @@ func buildLyricsFilename(trackName, artistName, albumName, albumArtist, releaseD
filename = strings.ReplaceAll(filename, "{album_artist}", safeAlbumArtist) filename = strings.ReplaceAll(filename, "{album_artist}", safeAlbumArtist)
filename = strings.ReplaceAll(filename, "{year}", year) filename = strings.ReplaceAll(filename, "{year}", year)
filename = strings.ReplaceAll(filename, "{date}", sanitizeFilename(releaseDate)) filename = strings.ReplaceAll(filename, "{date}", sanitizeFilename(releaseDate))
filename = strings.ReplaceAll(filename, "{isrc}", safeISRC)
if discNumber > 0 { if discNumber > 0 {
filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber)) filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber))
@@ -485,10 +488,15 @@ func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloa
if filenameFormat == "" { if filenameFormat == "" {
filenameFormat = "title-artist" filenameFormat = "title-artist"
} }
filename := buildLyricsFilename(req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, filenameFormat, req.TrackNumber, req.Position, req.DiscNumber) resolvedISRC := strings.TrimSpace(req.ISRC)
if resolvedISRC == "" && strings.Contains(filenameFormat, "{isrc}") {
resolvedISRC = ResolveTrackISRC(req.SpotifyID)
}
filename := buildLyricsFilename(req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, filenameFormat, resolvedISRC, req.TrackNumber, req.Position, req.DiscNumber)
filePath := filepath.Join(outputDir, filename) filePath := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(filePath); err == nil && fileInfo.Size() > 0 { filePath, alreadyExists := ResolveOutputPathForDownload(filePath, GetRedownloadWithSuffixSetting())
if alreadyExists {
return &LyricsDownloadResponse{ return &LyricsDownloadResponse{
Success: true, Success: true,
Message: "Lyrics file already exists", Message: "Lyrics file already exists",
+10 -5
View File
@@ -326,8 +326,12 @@ func (q *QobuzDownloader) DownloadCoverArt(coverURL, filepath string) error {
return err return err
} }
func buildQobuzFilename(title, artist, album, albumArtist, releaseDate string, trackNumber, discNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string { func buildQobuzFilename(title, artist, album, albumArtist, releaseDate string, trackNumber, discNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool, extra ...string) string {
var filename string var filename string
isrc := ""
if len(extra) > 0 {
isrc = SanitizeOptionalFilename(extra[0])
}
numberToUse := position numberToUse := position
if useAlbumTrackNumber && trackNumber > 0 { if useAlbumTrackNumber && trackNumber > 0 {
@@ -347,6 +351,7 @@ func buildQobuzFilename(title, artist, album, albumArtist, releaseDate string, t
filename = strings.ReplaceAll(filename, "{album_artist}", albumArtist) filename = strings.ReplaceAll(filename, "{album_artist}", albumArtist)
filename = strings.ReplaceAll(filename, "{year}", year) filename = strings.ReplaceAll(filename, "{year}", year)
filename = strings.ReplaceAll(filename, "{date}", SanitizeFilename(releaseDate)) filename = strings.ReplaceAll(filename, "{date}", SanitizeFilename(releaseDate))
filename = strings.ReplaceAll(filename, "{isrc}", isrc)
if discNumber > 0 { if discNumber > 0 {
filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber)) filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber))
@@ -467,11 +472,11 @@ func (q *QobuzDownloader) DownloadTrackWithISRC(isrc, outputDir, quality, filena
safeTitle := sanitizeFilename(trackTitle) safeTitle := sanitizeFilename(trackTitle)
safeAlbum := sanitizeFilename(albumTitle) safeAlbum := sanitizeFilename(albumTitle)
filename := buildQobuzFilename(safeTitle, safeArtist, safeAlbum, safeAlbumArtist, spotifyReleaseDate, spotifyTrackNumber, spotifyDiscNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) filename := buildQobuzFilename(safeTitle, safeArtist, safeAlbum, safeAlbumArtist, spotifyReleaseDate, spotifyTrackNumber, spotifyDiscNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber, isrc)
filepath := filepath.Join(outputDir, filename) filepath := filepath.Join(outputDir, filename)
filepath, alreadyExists := ResolveOutputPathForDownload(filepath, GetRedownloadWithSuffixSetting())
if fileInfo, err := os.Stat(filepath); err == nil && fileInfo.Size() > 0 { if alreadyExists {
fmt.Printf("File already exists: %s (%.2f MB)\n", filepath, float64(fileInfo.Size())/(1024*1024)) fmt.Printf("File already exists: %s (%.2f MB)\n", filepath, float64(mustFileSize(filepath))/(1024*1024))
return "EXISTS:" + filepath, nil return "EXISTS:" + filepath, nil
} }
+24 -13
View File
@@ -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, spotifyComposer, metadataSeparator, 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, isrcOverride, 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)
@@ -449,11 +449,12 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
trackTitleForFile := sanitizeFilename(trackTitle) trackTitleForFile := sanitizeFilename(trackTitle)
albumTitleForFile := sanitizeFilename(albumTitle) albumTitleForFile := sanitizeFilename(albumTitle)
filename := buildTidalFilename(trackTitleForFile, artistNameForFile, albumTitleForFile, albumArtistForFile, spotifyReleaseDate, spotifyTrackNumber, spotifyDiscNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) filename := buildTidalFilename(trackTitleForFile, artistNameForFile, albumTitleForFile, albumArtistForFile, spotifyReleaseDate, spotifyTrackNumber, spotifyDiscNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber, isrcOverride)
outputFilename := filepath.Join(outputDir, filename) outputFilename := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 { outputFilename, alreadyExists := ResolveOutputPathForDownload(outputFilename, GetRedownloadWithSuffixSetting())
fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(fileInfo.Size())/(1024*1024)) if alreadyExists {
fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(mustFileSize(outputFilename))/(1024*1024))
return "EXISTS:" + outputFilename, nil return "EXISTS:" + outputFilename, nil
} }
@@ -511,11 +512,13 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
return "", err return "", err
} }
var isrc string isrc := strings.TrimSpace(isrcOverride)
var mbMeta Metadata var mbMeta Metadata
if spotifyURL != "" { if spotifyURL != "" {
result := <-metaChan result := <-metaChan
if isrc == "" {
isrc = result.ISRC isrc = result.ISRC
}
mbMeta = result.Metadata mbMeta = result.Metadata
} }
@@ -572,7 +575,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, spotifyComposer, metadataSeparator, 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, isrcOverride, 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)
@@ -610,11 +613,12 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
trackTitleForFile := sanitizeFilename(trackTitle) trackTitleForFile := sanitizeFilename(trackTitle)
albumTitleForFile := sanitizeFilename(albumTitle) albumTitleForFile := sanitizeFilename(albumTitle)
filename := buildTidalFilename(trackTitleForFile, artistNameForFile, albumTitleForFile, albumArtistForFile, spotifyReleaseDate, spotifyTrackNumber, spotifyDiscNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) filename := buildTidalFilename(trackTitleForFile, artistNameForFile, albumTitleForFile, albumArtistForFile, spotifyReleaseDate, spotifyTrackNumber, spotifyDiscNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber, isrcOverride)
outputFilename := filepath.Join(outputDir, filename) outputFilename := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 { outputFilename, alreadyExists := ResolveOutputPathForDownload(outputFilename, GetRedownloadWithSuffixSetting())
fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(fileInfo.Size())/(1024*1024)) if alreadyExists {
fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(mustFileSize(outputFilename))/(1024*1024))
return "EXISTS:" + outputFilename, nil return "EXISTS:" + outputFilename, nil
} }
@@ -673,11 +677,13 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
return "", err return "", err
} }
var isrc string isrc := strings.TrimSpace(isrcOverride)
var mbMeta Metadata var mbMeta Metadata
if spotifyURL != "" { if spotifyURL != "" {
result := <-metaChan result := <-metaChan
if isrc == "" {
isrc = result.ISRC isrc = result.ISRC
}
mbMeta = result.Metadata mbMeta = result.Metadata
} }
@@ -734,14 +740,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, spotifyComposer, metadataSeparator, 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, isrcOverride, 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, spotifyComposer, metadataSeparator, 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, isrcOverride, spotifyURL, allowFallback, useFirstArtistOnly, useSingleGenre, embedGenre)
} }
type SegmentTemplate struct { type SegmentTemplate struct {
@@ -981,8 +987,12 @@ func getDownloadURLRotated(apis []string, trackID int64, quality string) (string
return "", "", fmt.Errorf("all %d APIs failed. Last error: %v", len(apis), lastError) return "", "", fmt.Errorf("all %d APIs failed. Last error: %v", len(apis), lastError)
} }
func buildTidalFilename(title, artist, album, albumArtist, releaseDate string, trackNumber, discNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string { func buildTidalFilename(title, artist, album, albumArtist, releaseDate string, trackNumber, discNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool, extra ...string) string {
var filename string var filename string
isrc := ""
if len(extra) > 0 {
isrc = SanitizeOptionalFilename(extra[0])
}
numberToUse := position numberToUse := position
if useAlbumTrackNumber && trackNumber > 0 { if useAlbumTrackNumber && trackNumber > 0 {
@@ -1002,6 +1012,7 @@ func buildTidalFilename(title, artist, album, albumArtist, releaseDate string, t
filename = strings.ReplaceAll(filename, "{album_artist}", albumArtist) filename = strings.ReplaceAll(filename, "{album_artist}", albumArtist)
filename = strings.ReplaceAll(filename, "{year}", year) filename = strings.ReplaceAll(filename, "{year}", year)
filename = strings.ReplaceAll(filename, "{date}", SanitizeFilename(releaseDate)) filename = strings.ReplaceAll(filename, "{date}", SanitizeFilename(releaseDate))
filename = strings.ReplaceAll(filename, "{isrc}", isrc)
if discNumber > 0 { if discNumber > 0 {
filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber)) filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber))
+4 -2
View File
@@ -36,6 +36,7 @@ interface FileMetadata {
track_number: number; track_number: number;
disc_number: number; disc_number: number;
year: string; year: string;
isrc?: string;
} }
type TabType = "track" | "lyric" | "cover"; type TabType = "track" | "lyric" | "cover";
const FORMAT_PRESETS: Record<string, { const FORMAT_PRESETS: Record<string, {
@@ -549,7 +550,7 @@ export function FileManagerPage() {
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help"/> <Info className="h-3.5 w-3.5 text-muted-foreground cursor-help"/>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right"> <TooltipContent side="right">
<p className="text-xs whitespace-nowrap">Variables: {"{title}"}, {"{artist}"}, {"{album}"}, {"{album_artist}"}, {"{track}"}, {"{disc}"}, {"{year}"}, {"{date}"}</p> <p className="text-xs whitespace-nowrap">Variables: {"{title}"}, {"{artist}"}, {"{album}"}, {"{album_artist}"}, {"{track}"}, {"{disc}"}, {"{year}"}, {"{date}"}, {"{isrc}"}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
@@ -571,7 +572,7 @@ export function FileManagerPage() {
</Tooltip> </Tooltip>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Preview: <span className="font-mono">{renameFormat.replace(/\{title\}/g, "All The Stars").replace(/\{artist\}/g, "Kendrick Lamar, SZA").replace(/\{album\}/g, "Black Panther").replace(/\{album_artist\}/g, "Kendrick Lamar").replace(/\{track\}/g, "01").replace(/\{disc\}/g, "1").replace(/\{year\}/g, "2018").replace(/\{date\}/g, "2018-02-09")}.flac</span> Preview: <span className="font-mono">{renameFormat.replace(/\{title\}/g, "All The Stars").replace(/\{artist\}/g, "Kendrick Lamar, SZA").replace(/\{album\}/g, "Black Panther").replace(/\{album_artist\}/g, "Kendrick Lamar").replace(/\{track\}/g, "01").replace(/\{disc\}/g, "1").replace(/\{year\}/g, "2018").replace(/\{date\}/g, "2018-02-09").replace(/\{isrc\}/g, "USUM71801234")}.flac</span>
</p> </p>
</div>)} </div>)}
@@ -660,6 +661,7 @@ export function FileManagerPage() {
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm"><span className="text-muted-foreground">Track</span><span>{metadataInfo.track_number || "-"}</span></div> <div className="grid grid-cols-[100px_1fr] gap-2 text-sm"><span className="text-muted-foreground">Track</span><span>{metadataInfo.track_number || "-"}</span></div>
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm"><span className="text-muted-foreground">Disc</span><span>{metadataInfo.disc_number || "-"}</span></div> <div className="grid grid-cols-[100px_1fr] gap-2 text-sm"><span className="text-muted-foreground">Disc</span><span>{metadataInfo.disc_number || "-"}</span></div>
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm"><span className="text-muted-foreground">Year</span><span>{metadataInfo.year ? metadataInfo.year.substring(0, 4) : "-"}</span></div> <div className="grid grid-cols-[100px_1fr] gap-2 text-sm"><span className="text-muted-foreground">Year</span><span>{metadataInfo.year ? metadataInfo.year.substring(0, 4) : "-"}</span></div>
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm"><span className="text-muted-foreground">ISRC</span><span>{metadataInfo.isrc || "-"}</span></div>
</div>) : (<div className="text-center py-4 text-muted-foreground">No metadata available</div>)} </div>) : (<div className="text-center py-4 text-muted-foreground">No metadata available</div>)}
<DialogFooter><Button onClick={() => setShowMetadata(false)}>Close</Button></DialogFooter> <DialogFooter><Button onClick={() => setShowMetadata(false)}>Close</Button></DialogFooter>
</DialogContent> </DialogContent>
+14 -2
View File
@@ -568,7 +568,8 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
.replace(/\{track\}/g, "01") .replace(/\{track\}/g, "01")
.replace(/\{disc\}/g, "1") .replace(/\{disc\}/g, "1")
.replace(/\{year\}/g, "2018") .replace(/\{year\}/g, "2018")
.replace(/\{date\}/g, "2018-02-09")} .replace(/\{date\}/g, "2018-02-09")
.replace(/\{isrc\}/g, "USUM71801234")}
/ /
</span> </span>
</p>)} </p>)}
@@ -614,6 +615,16 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
</Label> </Label>
</div> </div>
<div className="flex items-center gap-3">
<Switch id="redownload-with-suffix" checked={tempSettings.redownloadWithSuffix} onCheckedChange={(checked) => setTempSettings((prev) => ({
...prev,
redownloadWithSuffix: checked,
}))}/>
<Label htmlFor="redownload-with-suffix" className="text-sm cursor-pointer font-normal">
Redownload With Suffix
</Label>
</div>
</div> </div>
@@ -686,7 +697,8 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
.replace(/\{track\}/g, "01") .replace(/\{track\}/g, "01")
.replace(/\{disc\}/g, "1") .replace(/\{disc\}/g, "1")
.replace(/\{year\}/g, "2018") .replace(/\{year\}/g, "2018")
.replace(/\{date\}/g, "2018-02-09")} .replace(/\{date\}/g, "2018-02-09")
.replace(/\{isrc\}/g, "USUM71801234")}
.flac .flac
</span> </span>
</p>)} </p>)}
+32
View File
@@ -12,6 +12,7 @@ interface CheckFileExistenceRequest {
album_name?: string; album_name?: string;
album_artist?: string; album_artist?: string;
release_date?: string; release_date?: string;
isrc?: string;
track_number?: number; track_number?: number;
disc_number?: number; disc_number?: number;
position?: number; position?: number;
@@ -31,6 +32,24 @@ interface FileExistenceResult {
const CheckFilesExistence = (outputDir: string, rootDir: string, tracks: CheckFileExistenceRequest[]): Promise<FileExistenceResult[]> => (window as any)["go"]["main"]["App"]["CheckFilesExistence"](outputDir, rootDir, tracks); const CheckFilesExistence = (outputDir: string, rootDir: string, tracks: CheckFileExistenceRequest[]): Promise<FileExistenceResult[]> => (window as any)["go"]["main"]["App"]["CheckFilesExistence"](outputDir, rootDir, tracks);
const SkipDownloadItem = (itemID: string, filePath: string): Promise<void> => (window as any)["go"]["main"]["App"]["SkipDownloadItem"](itemID, filePath); const SkipDownloadItem = (itemID: string, filePath: string): Promise<void> => (window as any)["go"]["main"]["App"]["SkipDownloadItem"](itemID, filePath);
const CreateM3U8File = (playlistName: string, outputDir: string, filePaths: string[]): Promise<void> => (window as any)["go"]["main"]["App"]["CreateM3U8File"](playlistName, outputDir, filePaths); const CreateM3U8File = (playlistName: string, outputDir: string, filePaths: string[]): Promise<void> => (window as any)["go"]["main"]["App"]["CreateM3U8File"](playlistName, outputDir, filePaths);
const GetTrackISRC = (spotifyId: string): Promise<string> => (window as any)["go"]["main"]["App"]["GetTrackISRC"](spotifyId);
async function resolveTemplateISRC(settings: { folderTemplate?: string; filenameTemplate?: string }, spotifyId?: string): Promise<string> {
if (!spotifyId) {
return "";
}
const folderTemplate = settings.folderTemplate || "";
const filenameTemplate = settings.filenameTemplate || "";
if (!folderTemplate.includes("{isrc}") && !filenameTemplate.includes("{isrc}")) {
return "";
}
try {
return await GetTrackISRC(spotifyId);
}
catch {
return "";
}
}
export function useDownload(region: string) { export function useDownload(region: string) {
const [downloadProgress, setDownloadProgress] = useState<number>(0); const [downloadProgress, setDownloadProgress] = useState<number>(0);
const [isDownloading, setIsDownloading] = useState(false); const [isDownloading, setIsDownloading] = useState(false);
@@ -81,11 +100,13 @@ export function useDownload(region: string) {
const displayAlbumArtist = settings.useFirstArtistOnly && albumArtist const displayAlbumArtist = settings.useFirstArtistOnly && albumArtist
? getFirstArtist(albumArtist) ? getFirstArtist(albumArtist)
: albumArtist; : albumArtist;
const resolvedTemplateISRC = await resolveTemplateISRC(settings, spotifyId || id);
const templateData: TemplateData = { const templateData: TemplateData = {
artist: displayArtist?.replace(/\//g, placeholder), artist: displayArtist?.replace(/\//g, placeholder),
album: albumName?.replace(/\//g, placeholder), album: albumName?.replace(/\//g, placeholder),
album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder), album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
title: trackName?.replace(/\//g, placeholder), title: trackName?.replace(/\//g, placeholder),
isrc: resolvedTemplateISRC?.replace(/\//g, placeholder),
track: trackNumberForTemplate, track: trackNumberForTemplate,
year: yearValue, year: yearValue,
date: releaseDate, date: releaseDate,
@@ -117,6 +138,7 @@ export function useDownload(region: string) {
album_name: albumName, album_name: albumName,
album_artist: displayAlbumArtist, album_artist: displayAlbumArtist,
release_date: finalReleaseDate || releaseDate, release_date: finalReleaseDate || releaseDate,
isrc: resolvedTemplateISRC || undefined,
track_number: finalTrackNumber || spotifyTrackNumber || 0, track_number: finalTrackNumber || spotifyTrackNumber || 0,
disc_number: spotifyDiscNumber || 0, disc_number: spotifyDiscNumber || 0,
position: trackNumberForTemplate, position: trackNumberForTemplate,
@@ -193,6 +215,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber, spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks, spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs, spotify_total_discs: spotifyTotalDiscs,
isrc: resolvedTemplateISRC || undefined,
copyright: copyright, copyright: copyright,
publisher: publisher, publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly, use_first_artist_only: settings.useFirstArtistOnly,
@@ -240,6 +263,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber, spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks, spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs, spotify_total_discs: spotifyTotalDiscs,
isrc: resolvedTemplateISRC || undefined,
copyright: copyright, copyright: copyright,
publisher: publisher, publisher: publisher,
use_single_genre: settings.useSingleGenre, use_single_genre: settings.useSingleGenre,
@@ -286,6 +310,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber, spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks, spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs, spotify_total_discs: spotifyTotalDiscs,
isrc: resolvedTemplateISRC || undefined,
copyright: copyright, copyright: copyright,
publisher: publisher, publisher: publisher,
use_single_genre: settings.useSingleGenre, use_single_genre: settings.useSingleGenre,
@@ -350,6 +375,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber, spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks, spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs, spotify_total_discs: spotifyTotalDiscs,
isrc: resolvedTemplateISRC || undefined,
copyright: copyright, copyright: copyright,
publisher: publisher, publisher: publisher,
use_single_genre: settings.useSingleGenre, use_single_genre: settings.useSingleGenre,
@@ -395,11 +421,13 @@ export function useDownload(region: string) {
const displayAlbumArtist = settings.useFirstArtistOnly && albumArtist const displayAlbumArtist = settings.useFirstArtistOnly && albumArtist
? getFirstArtist(albumArtist) ? getFirstArtist(albumArtist)
: albumArtist; : albumArtist;
const resolvedTemplateISRC = await resolveTemplateISRC(settings, spotifyId);
const templateData: TemplateData = { const templateData: TemplateData = {
artist: displayArtist?.replace(/\//g, placeholder), artist: displayArtist?.replace(/\//g, placeholder),
album: albumName?.replace(/\//g, placeholder), album: albumName?.replace(/\//g, placeholder),
album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder), album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
title: trackName?.replace(/\//g, placeholder), title: trackName?.replace(/\//g, placeholder),
isrc: resolvedTemplateISRC?.replace(/\//g, placeholder),
track: trackNumberForTemplate, track: trackNumberForTemplate,
year: yearValue, year: yearValue,
date: releaseDate, date: releaseDate,
@@ -468,6 +496,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber, spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks, spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs, spotify_total_discs: spotifyTotalDiscs,
isrc: resolvedTemplateISRC || undefined,
copyright: copyright, copyright: copyright,
publisher: publisher, publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly, use_first_artist_only: settings.useFirstArtistOnly,
@@ -515,6 +544,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber, spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks, spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs, spotify_total_discs: spotifyTotalDiscs,
isrc: resolvedTemplateISRC || undefined,
copyright: copyright, copyright: copyright,
publisher: publisher, publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly, use_first_artist_only: settings.useFirstArtistOnly,
@@ -563,6 +593,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber, spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks, spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs, spotify_total_discs: spotifyTotalDiscs,
isrc: resolvedTemplateISRC || undefined,
copyright: copyright, copyright: copyright,
publisher: publisher, publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly, use_first_artist_only: settings.useFirstArtistOnly,
@@ -624,6 +655,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber, spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks, spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs, spotify_total_discs: spotifyTotalDiscs,
isrc: resolvedTemplateISRC || undefined,
copyright: copyright, copyright: copyright,
publisher: publisher, publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly, use_first_artist_only: settings.useFirstArtistOnly,
+24
View File
@@ -5,6 +5,24 @@ import { toastWithSound as toast } from "@/lib/toast-with-sound";
import { joinPath, sanitizePath, getFirstArtist } from "@/lib/utils"; import { joinPath, sanitizePath, getFirstArtist } from "@/lib/utils";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
import type { TrackMetadata } from "@/types/api"; import type { TrackMetadata } from "@/types/api";
const GetTrackISRC = (spotifyId: string): Promise<string> => (window as any)["go"]["main"]["App"]["GetTrackISRC"](spotifyId);
async function resolveTemplateISRC(settings: { folderTemplate?: string; filenameTemplate?: string }, spotifyId?: string): Promise<string> {
if (!spotifyId) {
return "";
}
const folderTemplate = settings.folderTemplate || "";
const filenameTemplate = settings.filenameTemplate || "";
if (!folderTemplate.includes("{isrc}") && !filenameTemplate.includes("{isrc}")) {
return "";
}
try {
return await GetTrackISRC(spotifyId);
}
catch {
return "";
}
}
export function useLyrics() { export function useLyrics() {
const [downloadingLyricsTrack, setDownloadingLyricsTrack] = useState<string | null>(null); const [downloadingLyricsTrack, setDownloadingLyricsTrack] = useState<string | null>(null);
const [downloadedLyrics, setDownloadedLyrics] = useState<Set<string>>(new Set()); const [downloadedLyrics, setDownloadedLyrics] = useState<Set<string>>(new Set());
@@ -28,11 +46,13 @@ export function useLyrics() {
const yearValue = releaseDate?.substring(0, 4); const yearValue = releaseDate?.substring(0, 4);
const displayArtist = settings.useFirstArtistOnly && artistName ? getFirstArtist(artistName) : artistName; const displayArtist = settings.useFirstArtistOnly && artistName ? getFirstArtist(artistName) : artistName;
const displayAlbumArtist = settings.useFirstArtistOnly && albumArtist ? getFirstArtist(albumArtist) : albumArtist; const displayAlbumArtist = settings.useFirstArtistOnly && albumArtist ? getFirstArtist(albumArtist) : albumArtist;
const resolvedTemplateISRC = await resolveTemplateISRC(settings, spotifyId);
const templateData: TemplateData = { const templateData: TemplateData = {
artist: displayArtist?.replace(/\//g, placeholder), artist: displayArtist?.replace(/\//g, placeholder),
album: albumName?.replace(/\//g, placeholder), album: albumName?.replace(/\//g, placeholder),
album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder), album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
title: trackName?.replace(/\//g, placeholder), title: trackName?.replace(/\//g, placeholder),
isrc: resolvedTemplateISRC?.replace(/\//g, placeholder),
track: position, track: position,
year: yearValue, year: yearValue,
date: releaseDate, date: releaseDate,
@@ -61,6 +81,7 @@ export function useLyrics() {
album_name: albumName, album_name: albumName,
album_artist: displayAlbumArtist, album_artist: displayAlbumArtist,
release_date: releaseDate, release_date: releaseDate,
isrc: resolvedTemplateISRC || undefined,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate || "{title}", filename_format: settings.filenameTemplate || "{title}",
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -129,11 +150,13 @@ export function useLyrics() {
const yearValue = track.release_date?.substring(0, 4); const yearValue = track.release_date?.substring(0, 4);
const displayArtist = settings.useFirstArtistOnly && track.artists ? getFirstArtist(track.artists) : track.artists; const displayArtist = settings.useFirstArtistOnly && track.artists ? getFirstArtist(track.artists) : track.artists;
const displayAlbumArtist = settings.useFirstArtistOnly && track.album_artist ? getFirstArtist(track.album_artist) : track.album_artist; const displayAlbumArtist = settings.useFirstArtistOnly && track.album_artist ? getFirstArtist(track.album_artist) : track.album_artist;
const resolvedTemplateISRC = await resolveTemplateISRC(settings, id);
const templateData: TemplateData = { const templateData: TemplateData = {
artist: displayArtist?.replace(/\//g, placeholder), artist: displayArtist?.replace(/\//g, placeholder),
album: track.album_name?.replace(/\//g, placeholder), album: track.album_name?.replace(/\//g, placeholder),
album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder), album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
title: track.name?.replace(/\//g, placeholder), title: track.name?.replace(/\//g, placeholder),
isrc: resolvedTemplateISRC?.replace(/\//g, placeholder),
track: trackPosition, track: trackPosition,
year: yearValue, year: yearValue,
date: track.release_date, date: track.release_date,
@@ -161,6 +184,7 @@ export function useLyrics() {
album_name: track.album_name, album_name: track.album_name,
album_artist: displayAlbumArtist, album_artist: displayAlbumArtist,
release_date: track.release_date, release_date: track.release_date,
isrc: resolvedTemplateISRC || undefined,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate || "{title}", filename_format: settings.filenameTemplate || "{title}",
track_number: settings.trackNumber, track_number: settings.trackNumber,
+11
View File
@@ -36,6 +36,7 @@ export interface Settings {
useFirstArtistOnly: boolean; useFirstArtistOnly: boolean;
useSingleGenre: boolean; useSingleGenre: boolean;
embedGenre: boolean; embedGenre: boolean;
redownloadWithSuffix: boolean;
separator: "comma" | "semicolon"; separator: "comma" | "semicolon";
} }
export const FOLDER_PRESETS: Record<FolderPreset, { export const FOLDER_PRESETS: Record<FolderPreset, {
@@ -85,6 +86,7 @@ export const TEMPLATE_VARIABLES = [
{ key: "{disc}", description: "Disc number", example: "1" }, { key: "{disc}", description: "Disc number", example: "1" },
{ key: "{year}", description: "Release year", example: "2014" }, { key: "{year}", description: "Release year", example: "2014" },
{ key: "{date}", description: "Release date (YYYY-MM-DD)", example: "2014-10-27" }, { key: "{date}", description: "Release date (YYYY-MM-DD)", example: "2014-10-27" },
{ key: "{isrc}", description: "Track ISRC", example: "USUM71412345" },
]; ];
function detectOS(): "Windows" | "linux/MacOS" { function detectOS(): "Windows" | "linux/MacOS" {
const platform = window.navigator.platform.toLowerCase(); const platform = window.navigator.platform.toLowerCase();
@@ -124,6 +126,7 @@ export const DEFAULT_SETTINGS: Settings = {
useFirstArtistOnly: false, useFirstArtistOnly: false,
useSingleGenre: false, useSingleGenre: false,
embedGenre: true, embedGenre: true,
redownloadWithSuffix: false,
separator: "semicolon" separator: "semicolon"
}; };
export const FONT_OPTIONS: { export const FONT_OPTIONS: {
@@ -243,6 +246,9 @@ function getSettingsFromLocalStorage(): Settings {
if (!('separator' in parsed)) { if (!('separator' in parsed)) {
parsed.separator = "semicolon"; parsed.separator = "semicolon";
} }
if (!('redownloadWithSuffix' in parsed)) {
parsed.redownloadWithSuffix = false;
}
return { ...DEFAULT_SETTINGS, ...parsed }; return { ...DEFAULT_SETTINGS, ...parsed };
} }
} }
@@ -346,6 +352,9 @@ export async function loadSettings(): Promise<Settings> {
if (!('separator' in parsed)) { if (!('separator' in parsed)) {
parsed.separator = "semicolon"; parsed.separator = "semicolon";
} }
if (!('redownloadWithSuffix' in parsed)) {
parsed.redownloadWithSuffix = false;
}
cachedSettings = { ...DEFAULT_SETTINGS, ...parsed }; cachedSettings = { ...DEFAULT_SETTINGS, ...parsed };
return cachedSettings!; return cachedSettings!;
} }
@@ -368,6 +377,7 @@ export interface TemplateData {
album?: string; album?: string;
album_artist?: string; album_artist?: string;
title?: string; title?: string;
isrc?: string;
track?: number; track?: number;
disc?: number; disc?: number;
year?: string; year?: string;
@@ -382,6 +392,7 @@ export function parseTemplate(template: string, data: TemplateData): string {
result = result.replace(/\{artist\}/g, data.artist || "Unknown Artist"); result = result.replace(/\{artist\}/g, data.artist || "Unknown Artist");
result = result.replace(/\{album\}/g, data.album || "Unknown Album"); result = result.replace(/\{album\}/g, data.album || "Unknown Album");
result = result.replace(/\{album_artist\}/g, data.album_artist || data.artist || "Unknown Artist"); result = result.replace(/\{album_artist\}/g, data.album_artist || data.artist || "Unknown Artist");
result = result.replace(/\{isrc\}/g, data.isrc || "");
result = result.replace(/\{track\}/g, data.track ? String(data.track).padStart(2, "0") : "00"); result = result.replace(/\{track\}/g, data.track ? String(data.track).padStart(2, "0") : "00");
result = result.replace(/\{disc\}/g, data.disc ? String(data.disc) : "1"); result = result.replace(/\{disc\}/g, data.disc ? String(data.disc) : "1");
result = result.replace(/\{year\}/g, data.year || "0000"); result = result.replace(/\{year\}/g, data.year || "0000");
+4
View File
@@ -23,6 +23,7 @@ export interface TrackMetadata {
artist_id?: string; artist_id?: string;
artist_url?: string; artist_url?: string;
artists_data?: ArtistSimple[]; artists_data?: ArtistSimple[];
isrc?: string;
copyright?: string; copyright?: string;
publisher?: string; publisher?: string;
plays?: string; plays?: string;
@@ -134,6 +135,7 @@ export interface DownloadRequest {
spotify_disc_number?: number; spotify_disc_number?: number;
spotify_total_tracks?: number; spotify_total_tracks?: number;
spotify_total_discs?: number; spotify_total_discs?: number;
isrc?: string;
copyright?: string; copyright?: string;
publisher?: string; publisher?: string;
spotify_url?: string; spotify_url?: string;
@@ -190,6 +192,7 @@ export interface LyricsDownloadRequest {
album_name?: string; album_name?: string;
album_artist?: string; album_artist?: string;
release_date?: string; release_date?: string;
isrc?: string;
output_dir?: string; output_dir?: string;
filename_format?: string; filename_format?: string;
track_number?: boolean; track_number?: boolean;
@@ -278,4 +281,5 @@ export interface AudioMetadata {
track_number: number; track_number: number;
disc_number: number; disc_number: number;
year: string; year: string;
isrc?: string;
} }