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; }