From db8f82aa17077e802c02967881f3adaa5900961f Mon Sep 17 00:00:00 2001
From: afkarxyz
Date: Mon, 13 Apr 2026 21:53:47 +0700
Subject: [PATCH] .redownlaod with suffix, isrc variable
---
app.go | 72 ++++++++++++++-------
backend/amazon.go | 26 +++++---
backend/config.go | 10 +++
backend/filemanager.go | 12 ++++
backend/filename.go | 55 +++++++++++++++-
backend/isrc_helper.go | 22 +++++++
backend/lyrics.go | 14 +++-
backend/qobuz.go | 15 +++--
backend/tidal.go | 41 +++++++-----
frontend/src/components/FileManagerPage.tsx | 6 +-
frontend/src/components/SettingsPage.tsx | 16 ++++-
frontend/src/hooks/useDownload.ts | 32 +++++++++
frontend/src/hooks/useLyrics.ts | 24 +++++++
frontend/src/lib/settings.ts | 11 ++++
frontend/src/types/api.ts | 4 ++
15 files changed, 298 insertions(+), 62 deletions(-)
create mode 100644 backend/isrc_helper.go
diff --git a/app.go b/app.go
index 425f519..835faf4 100644
--- a/app.go
+++ b/app.go
@@ -159,6 +159,7 @@ type DownloadRequest struct {
SpotifyDiscNumber int `json:"spotify_disc_number,omitempty"`
SpotifyTotalTracks int `json:"spotify_total_tracks,omitempty"`
SpotifyTotalDiscs int `json:"spotify_total_discs,omitempty"`
+ ISRC string `json:"isrc,omitempty"`
Copyright string `json:"copyright,omitempty"`
Publisher string `json:"publisher,omitempty"`
Composer string `json:"composer,omitempty"`
@@ -363,6 +364,9 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
if req.FilenameFormat == "" {
req.FilenameFormat = "title-artist"
}
+ if req.ISRC == "" && strings.Contains(req.FilenameFormat, "{isrc}") && req.SpotifyID != "" {
+ req.ISRC = backend.ResolveTrackISRC(req.SpotifyID)
+ }
itemID := req.ItemID
if itemID == "" {
@@ -449,19 +453,21 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
}
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)
- if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 {
+ if !backend.GetRedownloadWithSuffixSetting() {
+ if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 {
- backend.SkipDownloadItem(itemID, expectedPath)
- return DownloadResponse{
- Success: true,
- Message: "File already exists",
- File: expectedPath,
- AlreadyExists: true,
- ItemID: itemID,
- }, nil
+ backend.SkipDownloadItem(itemID, expectedPath)
+ return DownloadResponse{
+ Success: true,
+ Message: "File already exists",
+ File: expectedPath,
+ AlreadyExists: true,
+ ItemID: itemID,
+ }, nil
+ }
}
}
@@ -506,32 +512,35 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
downloader := backend.NewAmazonDownloader()
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 {
- 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":
if req.ApiURL == "" || req.ApiURL == "auto" {
downloader := backend.NewTidalDownloader("")
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 {
- 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 {
downloader := backend.NewTidalDownloader(req.ApiURL)
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 {
- 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":
- fmt.Println("Waiting for ISRC (Qobuz dependency)...")
- isrc := <-isrcChan
+ isrc := strings.TrimSpace(req.ISRC)
+ if isrc == "" {
+ fmt.Println("Waiting for ISRC (Qobuz dependency)...")
+ isrc = <-isrcChan
+ }
downloader := backend.NewQobuzDownloader()
quality := req.AudioFormat
if quality == "" {
@@ -957,6 +966,7 @@ type LyricsDownloadRequest struct {
AlbumName string `json:"album_name"`
AlbumArtist string `json:"album_artist"`
ReleaseDate string `json:"release_date"`
+ ISRC string `json:"isrc,omitempty"`
OutputDir string `json:"output_dir"`
FilenameFormat string `json:"filename_format"`
TrackNumber bool `json:"track_number"`
@@ -981,6 +991,7 @@ func (a *App) DownloadLyrics(req LyricsDownloadRequest) (backend.LyricsDownloadR
AlbumName: req.AlbumName,
AlbumArtist: req.AlbumArtist,
ReleaseDate: req.ReleaseDate,
+ ISRC: req.ISRC,
OutputDir: req.OutputDir,
FilenameFormat: req.FilenameFormat,
TrackNumber: req.TrackNumber,
@@ -1404,6 +1415,7 @@ type CheckFileExistenceRequest struct {
AlbumName string `json:"album_name,omitempty"`
AlbumArtist string `json:"album_artist,omitempty"`
ReleaseDate string `json:"release_date,omitempty"`
+ ISRC string `json:"isrc,omitempty"`
TrackNumber int `json:"track_number,omitempty"`
DiscNumber int `json:"disc_number,omitempty"`
Position int `json:"position,omitempty"`
@@ -1433,6 +1445,7 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
}
defaultFilenameFormat := "title-artist"
+ redownloadWithSuffix := backend.GetRedownloadWithSuffixSetting()
type result struct {
index int
@@ -1483,6 +1496,10 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
if filenameFormat == "" {
filenameFormat = defaultFilenameFormat
}
+ isrc := strings.TrimSpace(t.ISRC)
+ if isrc == "" && strings.Contains(filenameFormat, "{isrc}") && t.SpotifyID != "" {
+ isrc = backend.ResolveTrackISRC(t.SpotifyID)
+ }
trackNumber := t.Position
if t.UseAlbumTrackNumber && t.TrackNumber > 0 {
@@ -1507,6 +1524,7 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
trackNumber,
t.DiscNumber,
t.UseAlbumTrackNumber,
+ isrc,
)
expectedFilename := strings.TrimSuffix(expectedFilenameBase, ".flac") + fileExt
@@ -1517,13 +1535,17 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
}
expectedPath := filepath.Join(targetDir, expectedFilename)
-
- if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 {
- res.Exists = true
- res.FilePath = expectedPath
+ 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 {
+ res.Exists = true
+ res.FilePath = expectedPath
+ } else {
- res.FilePath = expectedFilename
+ res.FilePath = expectedFilename
+ }
}
resultsChan <- result{index: idx, result: res}
@@ -1573,6 +1595,10 @@ func (a *App) SkipDownloadItem(itemID, filePath string) {
backend.SkipDownloadItem(itemID, filePath)
}
+func (a *App) GetTrackISRC(spotifyTrackID string) string {
+ return backend.ResolveTrackISRC(spotifyTrackID)
+}
+
func (a *App) GetPreviewURL(trackID string) (string, error) {
return backend.GetPreviewURL(trackID)
}
diff --git a/backend/amazon.go b/backend/amazon.go
index 4beac37..7c33917 100644
--- a/backend/amazon.go
+++ b/backend/amazon.go
@@ -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, 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 err := os.MkdirAll(outputDir, 0755); err != nil {
@@ -219,12 +219,14 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
filenameArtist = GetFirstArtist(spotifyArtistName)
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)
- 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))
- return "EXISTS:" + expectedPath, nil
+ if !GetRedownloadWithSuffixSetting() {
+ 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))
+ return "EXISTS:" + expectedPath, nil
+ }
}
}
@@ -271,11 +273,13 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
return "", err
}
- var isrc string
+ isrc := strings.TrimSpace(isrcOverride)
var mbMeta Metadata
if spotifyURL != "" {
result := <-metaChan
- isrc = result.ISRC
+ if isrc == "" {
+ isrc = result.ISRC
+ }
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, "{year}", year)
newFilename = strings.ReplaceAll(newFilename, "{date}", SanitizeFilename(spotifyReleaseDate))
+ newFilename = strings.ReplaceAll(newFilename, "{isrc}", SanitizeOptionalFilename(isrc))
if spotifyDiscNumber > 0 {
newFilename = strings.ReplaceAll(newFilename, "{disc}", fmt.Sprintf("%d", spotifyDiscNumber))
@@ -346,6 +351,9 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
}
newFilename = newFilename + ext
newFilePath := filepath.Join(outputDir, newFilename)
+ if GetRedownloadWithSuffixSetting() {
+ newFilePath, _ = ResolveOutputPathForDownload(newFilePath, true)
+ }
if err := os.Rename(filePath, newFilePath); err != nil {
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
}
-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,
) (string, error) {
@@ -429,5 +437,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, 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)
}
diff --git a/backend/config.go b/backend/config.go
index 5f1b562..d382536 100644
--- a/backend/config.go
+++ b/backend/config.go
@@ -69,6 +69,16 @@ func GetSpotFetchAPISettings() (bool, string) {
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 {
settings, err := LoadConfigSettings()
if err != nil || settings == nil {
diff --git a/backend/filemanager.go b/backend/filemanager.go
index 9b915fb..162713d 100644
--- a/backend/filemanager.go
+++ b/backend/filemanager.go
@@ -30,6 +30,7 @@ type AudioMetadata struct {
TrackNumber int `json:"track_number"`
DiscNumber int `json:"disc_number"`
Year string `json:"year"`
+ ISRC string `json:"isrc"`
}
type RenamePreview struct {
@@ -175,6 +176,8 @@ func readFlacMetadata(filePath string) (*AudioMetadata, error) {
}
case "DATE", "YEAR":
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
}
@@ -301,6 +310,8 @@ func readMetadataWithFFprobe(filePath string) (*AudioMetadata, error) {
if metadata.Year == "" || len(value) > len(metadata.Year) {
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, "{year}", sanitizeFilenameForRename(year))
result = strings.ReplaceAll(result, "{date}", sanitizeFilenameForRename(metadata.Year))
+ result = strings.ReplaceAll(result, "{isrc}", sanitizeFilenameForRename(metadata.ISRC))
if metadata.TrackNumber > 0 {
result = strings.ReplaceAll(result, "{track}", fmt.Sprintf("%02d", metadata.TrackNumber))
diff --git a/backend/filename.go b/backend/filename.go
index 0c8a373..91ae94d 100644
--- a/backend/filename.go
+++ b/backend/filename.go
@@ -2,6 +2,7 @@ package backend
import (
"fmt"
+ "os"
"path/filepath"
"regexp"
"strings"
@@ -9,12 +10,12 @@ import (
"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)
safeArtist := SanitizeFilename(artistName)
safeAlbum := SanitizeFilename(albumName)
safeAlbumArtist := SanitizeFilename(albumArtist)
+ safeISRC := SanitizeOptionalFilename(isrc)
safePlaylist := SanitizeFilename(playlistName)
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, "{playlist}", safePlaylist)
filename = strings.ReplaceAll(filename, "{creator}", safeCreator)
+ filename = strings.ReplaceAll(filename, "{isrc}", safeISRC)
if discNumber > 0 {
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 {
@@ -188,3 +230,10 @@ func sanitizeFolderName(name string) string { return SanitizeFilename(name) }
func sanitizeFilename(name string) string {
return SanitizeFilename(name)
}
+
+func SanitizeOptionalFilename(name string) string {
+ if strings.TrimSpace(name) == "" {
+ return ""
+ }
+ return SanitizeFilename(name)
+}
diff --git a/backend/isrc_helper.go b/backend/isrc_helper.go
new file mode 100644
index 0000000..2f8283b
--- /dev/null
+++ b/backend/isrc_helper.go
@@ -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))
+}
diff --git a/backend/lyrics.go b/backend/lyrics.go
index 3feba0d..16e025a 100644
--- a/backend/lyrics.go
+++ b/backend/lyrics.go
@@ -44,6 +44,7 @@ type LyricsDownloadRequest struct {
AlbumName string `json:"album_name"`
AlbumArtist string `json:"album_artist"`
ReleaseDate string `json:"release_date"`
+ ISRC string `json:"isrc"`
OutputDir string `json:"output_dir"`
FilenameFormat string `json:"filename_format"`
TrackNumber bool `json:"track_number"`
@@ -363,11 +364,12 @@ func msToLRCTimestamp(msStr string) string {
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)
safeArtist := sanitizeFilename(artistName)
safeAlbum := sanitizeFilename(albumName)
safeAlbumArtist := sanitizeFilename(albumArtist)
+ safeISRC := SanitizeOptionalFilename(isrc)
year := ""
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, "{year}", year)
filename = strings.ReplaceAll(filename, "{date}", sanitizeFilename(releaseDate))
+ filename = strings.ReplaceAll(filename, "{isrc}", safeISRC)
if discNumber > 0 {
filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber))
@@ -485,10 +488,15 @@ func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloa
if filenameFormat == "" {
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)
- if fileInfo, err := os.Stat(filePath); err == nil && fileInfo.Size() > 0 {
+ filePath, alreadyExists := ResolveOutputPathForDownload(filePath, GetRedownloadWithSuffixSetting())
+ if alreadyExists {
return &LyricsDownloadResponse{
Success: true,
Message: "Lyrics file already exists",
diff --git a/backend/qobuz.go b/backend/qobuz.go
index 9d84682..de7229a 100644
--- a/backend/qobuz.go
+++ b/backend/qobuz.go
@@ -326,8 +326,12 @@ func (q *QobuzDownloader) DownloadCoverArt(coverURL, filepath string) error {
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
+ isrc := ""
+ if len(extra) > 0 {
+ isrc = SanitizeOptionalFilename(extra[0])
+ }
numberToUse := position
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, "{year}", year)
filename = strings.ReplaceAll(filename, "{date}", SanitizeFilename(releaseDate))
+ filename = strings.ReplaceAll(filename, "{isrc}", isrc)
if discNumber > 0 {
filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber))
@@ -467,11 +472,11 @@ func (q *QobuzDownloader) DownloadTrackWithISRC(isrc, outputDir, quality, filena
safeTitle := sanitizeFilename(trackTitle)
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)
-
- if fileInfo, err := os.Stat(filepath); err == nil && fileInfo.Size() > 0 {
- fmt.Printf("File already exists: %s (%.2f MB)\n", filepath, float64(fileInfo.Size())/(1024*1024))
+ filepath, alreadyExists := ResolveOutputPathForDownload(filepath, GetRedownloadWithSuffixSetting())
+ if alreadyExists {
+ fmt.Printf("File already exists: %s (%.2f MB)\n", filepath, float64(mustFileSize(filepath))/(1024*1024))
return "EXISTS:" + filepath, nil
}
diff --git a/backend/tidal.go b/backend/tidal.go
index b8d9428..cd8fa1c 100644
--- a/backend/tidal.go
+++ b/backend/tidal.go
@@ -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, 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 err := os.MkdirAll(outputDir, 0755); err != nil {
return "", fmt.Errorf("directory error: %w", err)
@@ -449,11 +449,12 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
trackTitleForFile := sanitizeFilename(trackTitle)
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)
- if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 {
- fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(fileInfo.Size())/(1024*1024))
+ outputFilename, alreadyExists := ResolveOutputPathForDownload(outputFilename, GetRedownloadWithSuffixSetting())
+ if alreadyExists {
+ fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(mustFileSize(outputFilename))/(1024*1024))
return "EXISTS:" + outputFilename, nil
}
@@ -511,11 +512,13 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
return "", err
}
- var isrc string
+ isrc := strings.TrimSpace(isrcOverride)
var mbMeta Metadata
if spotifyURL != "" {
result := <-metaChan
- isrc = result.ISRC
+ if isrc == "" {
+ isrc = result.ISRC
+ }
mbMeta = result.Metadata
}
@@ -572,7 +575,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, 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()
if err != nil {
return "", fmt.Errorf("no APIs available for fallback: %w", err)
@@ -610,11 +613,12 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
trackTitleForFile := sanitizeFilename(trackTitle)
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)
- if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 {
- fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(fileInfo.Size())/(1024*1024))
+ outputFilename, alreadyExists := ResolveOutputPathForDownload(outputFilename, GetRedownloadWithSuffixSetting())
+ if alreadyExists {
+ fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(mustFileSize(outputFilename))/(1024*1024))
return "EXISTS:" + outputFilename, nil
}
@@ -673,11 +677,13 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
return "", err
}
- var isrc string
+ isrc := strings.TrimSpace(isrcOverride)
var mbMeta Metadata
if spotifyURL != "" {
result := <-metaChan
- isrc = result.ISRC
+ if isrc == "" {
+ isrc = result.ISRC
+ }
mbMeta = result.Metadata
}
@@ -734,14 +740,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, 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)
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, 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 {
@@ -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)
}
-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
+ isrc := ""
+ if len(extra) > 0 {
+ isrc = SanitizeOptionalFilename(extra[0])
+ }
numberToUse := position
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, "{year}", year)
filename = strings.ReplaceAll(filename, "{date}", SanitizeFilename(releaseDate))
+ filename = strings.ReplaceAll(filename, "{isrc}", isrc)
if discNumber > 0 {
filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber))
diff --git a/frontend/src/components/FileManagerPage.tsx b/frontend/src/components/FileManagerPage.tsx
index b0c8a31..37b511f 100644
--- a/frontend/src/components/FileManagerPage.tsx
+++ b/frontend/src/components/FileManagerPage.tsx
@@ -36,6 +36,7 @@ interface FileMetadata {
track_number: number;
disc_number: number;
year: string;
+ isrc?: string;
}
type TabType = "track" | "lyric" | "cover";
const FORMAT_PRESETS: Record
- Variables: {"{title}"}, {"{artist}"}, {"{album}"}, {"{album_artist}"}, {"{track}"}, {"{disc}"}, {"{year}"}, {"{date}"}
+ Variables: {"{title}"}, {"{artist}"}, {"{album}"}, {"{album_artist}"}, {"{track}"}, {"{disc}"}, {"{year}"}, {"{date}"}, {"{isrc}"}
@@ -571,7 +572,7 @@ export function FileManagerPage() {
- Preview: {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
+ Preview: {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
)}
@@ -660,6 +661,7 @@ export function FileManagerPage() {
Track{metadataInfo.track_number || "-"}
Disc{metadataInfo.disc_number || "-"}
Year{metadataInfo.year ? metadataInfo.year.substring(0, 4) : "-"}
+ ISRC{metadataInfo.isrc || "-"}
) : (No metadata available
)}
diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx
index d02588f..d8aba2c 100644
--- a/frontend/src/components/SettingsPage.tsx
+++ b/frontend/src/components/SettingsPage.tsx
@@ -568,7 +568,8 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
.replace(/\{track\}/g, "01")
.replace(/\{disc\}/g, "1")
.replace(/\{year\}/g, "2018")
- .replace(/\{date\}/g, "2018-02-09")}
+ .replace(/\{date\}/g, "2018-02-09")
+ .replace(/\{isrc\}/g, "USUM71801234")}
/
)}
@@ -614,6 +615,16 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
+
+ setTempSettings((prev) => ({
+ ...prev,
+ redownloadWithSuffix: checked,
+ }))}/>
+
+
+
@@ -686,7 +697,8 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
.replace(/\{track\}/g, "01")
.replace(/\{disc\}/g, "1")
.replace(/\{year\}/g, "2018")
- .replace(/\{date\}/g, "2018-02-09")}
+ .replace(/\{date\}/g, "2018-02-09")
+ .replace(/\{isrc\}/g, "USUM71801234")}
.flac
)}
diff --git a/frontend/src/hooks/useDownload.ts b/frontend/src/hooks/useDownload.ts
index 0a94439..1bfaf5a 100644
--- a/frontend/src/hooks/useDownload.ts
+++ b/frontend/src/hooks/useDownload.ts
@@ -12,6 +12,7 @@ interface CheckFileExistenceRequest {
album_name?: string;
album_artist?: string;
release_date?: string;
+ isrc?: string;
track_number?: number;
disc_number?: number;
position?: number;
@@ -31,6 +32,24 @@ interface FileExistenceResult {
const CheckFilesExistence = (outputDir: string, rootDir: string, tracks: CheckFileExistenceRequest[]): Promise => (window as any)["go"]["main"]["App"]["CheckFilesExistence"](outputDir, rootDir, tracks);
const SkipDownloadItem = (itemID: string, filePath: string): Promise => (window as any)["go"]["main"]["App"]["SkipDownloadItem"](itemID, filePath);
const CreateM3U8File = (playlistName: string, outputDir: string, filePaths: string[]): Promise => (window as any)["go"]["main"]["App"]["CreateM3U8File"](playlistName, outputDir, filePaths);
+const GetTrackISRC = (spotifyId: string): Promise => (window as any)["go"]["main"]["App"]["GetTrackISRC"](spotifyId);
+
+async function resolveTemplateISRC(settings: { folderTemplate?: string; filenameTemplate?: string }, spotifyId?: string): Promise {
+ 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) {
const [downloadProgress, setDownloadProgress] = useState(0);
const [isDownloading, setIsDownloading] = useState(false);
@@ -81,11 +100,13 @@ export function useDownload(region: string) {
const displayAlbumArtist = settings.useFirstArtistOnly && albumArtist
? getFirstArtist(albumArtist)
: albumArtist;
+ const resolvedTemplateISRC = await resolveTemplateISRC(settings, spotifyId || id);
const templateData: TemplateData = {
artist: displayArtist?.replace(/\//g, placeholder),
album: albumName?.replace(/\//g, placeholder),
album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
title: trackName?.replace(/\//g, placeholder),
+ isrc: resolvedTemplateISRC?.replace(/\//g, placeholder),
track: trackNumberForTemplate,
year: yearValue,
date: releaseDate,
@@ -117,6 +138,7 @@ export function useDownload(region: string) {
album_name: albumName,
album_artist: displayAlbumArtist,
release_date: finalReleaseDate || releaseDate,
+ isrc: resolvedTemplateISRC || undefined,
track_number: finalTrackNumber || spotifyTrackNumber || 0,
disc_number: spotifyDiscNumber || 0,
position: trackNumberForTemplate,
@@ -193,6 +215,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs,
+ isrc: resolvedTemplateISRC || undefined,
copyright: copyright,
publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
@@ -240,6 +263,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs,
+ isrc: resolvedTemplateISRC || undefined,
copyright: copyright,
publisher: publisher,
use_single_genre: settings.useSingleGenre,
@@ -286,6 +310,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs,
+ isrc: resolvedTemplateISRC || undefined,
copyright: copyright,
publisher: publisher,
use_single_genre: settings.useSingleGenre,
@@ -350,6 +375,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs,
+ isrc: resolvedTemplateISRC || undefined,
copyright: copyright,
publisher: publisher,
use_single_genre: settings.useSingleGenre,
@@ -395,11 +421,13 @@ export function useDownload(region: string) {
const displayAlbumArtist = settings.useFirstArtistOnly && albumArtist
? getFirstArtist(albumArtist)
: albumArtist;
+ const resolvedTemplateISRC = await resolveTemplateISRC(settings, spotifyId);
const templateData: TemplateData = {
artist: displayArtist?.replace(/\//g, placeholder),
album: albumName?.replace(/\//g, placeholder),
album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
title: trackName?.replace(/\//g, placeholder),
+ isrc: resolvedTemplateISRC?.replace(/\//g, placeholder),
track: trackNumberForTemplate,
year: yearValue,
date: releaseDate,
@@ -468,6 +496,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs,
+ isrc: resolvedTemplateISRC || undefined,
copyright: copyright,
publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
@@ -515,6 +544,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs,
+ isrc: resolvedTemplateISRC || undefined,
copyright: copyright,
publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
@@ -563,6 +593,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs,
+ isrc: resolvedTemplateISRC || undefined,
copyright: copyright,
publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
@@ -624,6 +655,7 @@ export function useDownload(region: string) {
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs,
+ isrc: resolvedTemplateISRC || undefined,
copyright: copyright,
publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
diff --git a/frontend/src/hooks/useLyrics.ts b/frontend/src/hooks/useLyrics.ts
index 900ee3c..bf7c396 100644
--- a/frontend/src/hooks/useLyrics.ts
+++ b/frontend/src/hooks/useLyrics.ts
@@ -5,6 +5,24 @@ import { toastWithSound as toast } from "@/lib/toast-with-sound";
import { joinPath, sanitizePath, getFirstArtist } from "@/lib/utils";
import { logger } from "@/lib/logger";
import type { TrackMetadata } from "@/types/api";
+const GetTrackISRC = (spotifyId: string): Promise => (window as any)["go"]["main"]["App"]["GetTrackISRC"](spotifyId);
+
+async function resolveTemplateISRC(settings: { folderTemplate?: string; filenameTemplate?: string }, spotifyId?: string): Promise {
+ 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() {
const [downloadingLyricsTrack, setDownloadingLyricsTrack] = useState(null);
const [downloadedLyrics, setDownloadedLyrics] = useState>(new Set());
@@ -28,11 +46,13 @@ export function useLyrics() {
const yearValue = releaseDate?.substring(0, 4);
const displayArtist = settings.useFirstArtistOnly && artistName ? getFirstArtist(artistName) : artistName;
const displayAlbumArtist = settings.useFirstArtistOnly && albumArtist ? getFirstArtist(albumArtist) : albumArtist;
+ const resolvedTemplateISRC = await resolveTemplateISRC(settings, spotifyId);
const templateData: TemplateData = {
artist: displayArtist?.replace(/\//g, placeholder),
album: albumName?.replace(/\//g, placeholder),
album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
title: trackName?.replace(/\//g, placeholder),
+ isrc: resolvedTemplateISRC?.replace(/\//g, placeholder),
track: position,
year: yearValue,
date: releaseDate,
@@ -61,6 +81,7 @@ export function useLyrics() {
album_name: albumName,
album_artist: displayAlbumArtist,
release_date: releaseDate,
+ isrc: resolvedTemplateISRC || undefined,
output_dir: outputDir,
filename_format: settings.filenameTemplate || "{title}",
track_number: settings.trackNumber,
@@ -129,11 +150,13 @@ export function useLyrics() {
const yearValue = track.release_date?.substring(0, 4);
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 resolvedTemplateISRC = await resolveTemplateISRC(settings, id);
const templateData: TemplateData = {
artist: displayArtist?.replace(/\//g, placeholder),
album: track.album_name?.replace(/\//g, placeholder),
album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
title: track.name?.replace(/\//g, placeholder),
+ isrc: resolvedTemplateISRC?.replace(/\//g, placeholder),
track: trackPosition,
year: yearValue,
date: track.release_date,
@@ -161,6 +184,7 @@ export function useLyrics() {
album_name: track.album_name,
album_artist: displayAlbumArtist,
release_date: track.release_date,
+ isrc: resolvedTemplateISRC || undefined,
output_dir: outputDir,
filename_format: settings.filenameTemplate || "{title}",
track_number: settings.trackNumber,
diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts
index 7667b79..c4be6ae 100644
--- a/frontend/src/lib/settings.ts
+++ b/frontend/src/lib/settings.ts
@@ -36,6 +36,7 @@ export interface Settings {
useFirstArtistOnly: boolean;
useSingleGenre: boolean;
embedGenre: boolean;
+ redownloadWithSuffix: boolean;
separator: "comma" | "semicolon";
}
export const FOLDER_PRESETS: Record {
if (!('separator' in parsed)) {
parsed.separator = "semicolon";
}
+ if (!('redownloadWithSuffix' in parsed)) {
+ parsed.redownloadWithSuffix = false;
+ }
cachedSettings = { ...DEFAULT_SETTINGS, ...parsed };
return cachedSettings!;
}
@@ -368,6 +377,7 @@ export interface TemplateData {
album?: string;
album_artist?: string;
title?: string;
+ isrc?: string;
track?: number;
disc?: number;
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(/\{album\}/g, data.album || "Unknown Album");
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(/\{disc\}/g, data.disc ? String(data.disc) : "1");
result = result.replace(/\{year\}/g, data.year || "0000");
diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts
index 03cd42d..3811142 100644
--- a/frontend/src/types/api.ts
+++ b/frontend/src/types/api.ts
@@ -23,6 +23,7 @@ export interface TrackMetadata {
artist_id?: string;
artist_url?: string;
artists_data?: ArtistSimple[];
+ isrc?: string;
copyright?: string;
publisher?: string;
plays?: string;
@@ -134,6 +135,7 @@ export interface DownloadRequest {
spotify_disc_number?: number;
spotify_total_tracks?: number;
spotify_total_discs?: number;
+ isrc?: string;
copyright?: string;
publisher?: string;
spotify_url?: string;
@@ -190,6 +192,7 @@ export interface LyricsDownloadRequest {
album_name?: string;
album_artist?: string;
release_date?: string;
+ isrc?: string;
output_dir?: string;
filename_format?: string;
track_number?: boolean;
@@ -278,4 +281,5 @@ export interface AudioMetadata {
track_number: number;
disc_number: number;
year: string;
+ isrc?: string;
}