v7.0.9
This commit is contained in:
@@ -8,7 +8,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"spotiflac/backend"
|
"spotiflac/backend"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -17,12 +16,6 @@ import (
|
|||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
var isrcRegex = regexp.MustCompile(`^[A-Z]{2}[A-Z0-9]{3}\d{2}\d{5}$`)
|
|
||||||
|
|
||||||
func isValidISRC(isrc string) bool {
|
|
||||||
return isrcRegex.MatchString(isrc)
|
|
||||||
}
|
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
@@ -31,6 +24,19 @@ func NewApp() *App {
|
|||||||
return &App{}
|
return &App{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) getFirstArtist(artistString string) string {
|
||||||
|
if artistString == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
delimiters := []string{", ", " & ", " feat. ", " ft. ", " featuring "}
|
||||||
|
for _, d := range delimiters {
|
||||||
|
if idx := strings.Index(strings.ToLower(artistString), d); idx != -1 {
|
||||||
|
return strings.TrimSpace(artistString[:idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return artistString
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) startup(ctx context.Context) {
|
func (a *App) startup(ctx context.Context) {
|
||||||
a.ctx = ctx
|
a.ctx = ctx
|
||||||
|
|
||||||
@@ -51,7 +57,6 @@ type SpotifyMetadataRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DownloadRequest struct {
|
type DownloadRequest struct {
|
||||||
ISRC string `json:"isrc"`
|
|
||||||
Service string `json:"service"`
|
Service string `json:"service"`
|
||||||
Query string `json:"query,omitempty"`
|
Query string `json:"query,omitempty"`
|
||||||
TrackName string `json:"track_name,omitempty"`
|
TrackName string `json:"track_name,omitempty"`
|
||||||
@@ -82,6 +87,7 @@ type DownloadRequest struct {
|
|||||||
PlaylistName string `json:"playlist_name,omitempty"`
|
PlaylistName string `json:"playlist_name,omitempty"`
|
||||||
PlaylistOwner string `json:"playlist_owner,omitempty"`
|
PlaylistOwner string `json:"playlist_owner,omitempty"`
|
||||||
AllowFallback bool `json:"allow_fallback"`
|
AllowFallback bool `json:"allow_fallback"`
|
||||||
|
UseFirstArtistOnly bool `json:"use_first_artist_only,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DownloadResponse struct {
|
type DownloadResponse struct {
|
||||||
@@ -210,7 +216,7 @@ func (a *App) SearchSpotifyByType(req SpotifySearchByTypeRequest) ([]backend.Sea
|
|||||||
|
|
||||||
func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||||
|
|
||||||
if req.Service == "qobuz" && req.ISRC == "" && req.SpotifyID == "" {
|
if req.Service == "qobuz" && req.SpotifyID == "" {
|
||||||
return DownloadResponse{
|
return DownloadResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: "Spotify ID is required for Qobuz",
|
Error: "Spotify ID is required for Qobuz",
|
||||||
@@ -326,89 +332,72 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lyricsChan := make(chan string, 1)
|
||||||
|
isrcChan := make(chan string, 1)
|
||||||
|
|
||||||
|
if req.SpotifyID != "" {
|
||||||
|
if req.EmbedLyrics {
|
||||||
|
go func() {
|
||||||
|
client := backend.NewLyricsClient()
|
||||||
|
resp, _, err := client.FetchLyricsAllSources(req.SpotifyID, req.TrackName, req.ArtistName, req.Duration)
|
||||||
|
if err == nil && resp != nil && len(resp.Lines) > 0 {
|
||||||
|
lrc := client.ConvertToLRC(resp, req.TrackName, req.ArtistName)
|
||||||
|
lyricsChan <- lrc
|
||||||
|
} else {
|
||||||
|
lyricsChan <- ""
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
close(lyricsChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
client := backend.NewSongLinkClient()
|
||||||
|
isrc, _ := client.GetISRC(req.SpotifyID)
|
||||||
|
isrcChan <- isrc
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
close(lyricsChan)
|
||||||
|
close(isrcChan)
|
||||||
|
}
|
||||||
|
|
||||||
switch req.Service {
|
switch req.Service {
|
||||||
case "amazon":
|
case "amazon":
|
||||||
|
|
||||||
downloader := backend.NewAmazonDownloader()
|
downloader := backend.NewAmazonDownloader()
|
||||||
if req.ServiceURL != "" {
|
if req.ServiceURL != "" {
|
||||||
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL)
|
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.UseFirstArtistOnly)
|
||||||
} else {
|
} else {
|
||||||
if req.SpotifyID == "" {
|
filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.UseFirstArtistOnly)
|
||||||
return DownloadResponse{
|
|
||||||
Success: false,
|
|
||||||
Error: "Spotify ID is required for Amazon Music",
|
|
||||||
}, fmt.Errorf("spotify ID is required for Amazon Music")
|
|
||||||
}
|
|
||||||
filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "tidal":
|
case "tidal":
|
||||||
if req.ApiURL == "" || req.ApiURL == "auto" {
|
if req.ApiURL == "" || req.ApiURL == "auto" {
|
||||||
downloader := backend.NewTidalDownloader("")
|
downloader := backend.NewTidalDownloader("")
|
||||||
if req.ServiceURL != "" {
|
if req.ServiceURL != "" {
|
||||||
filename, err = downloader.DownloadByURLWithFallback(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback)
|
filename, err = downloader.DownloadByURLWithFallback(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly)
|
||||||
} else {
|
} else {
|
||||||
if req.SpotifyID == "" {
|
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly)
|
||||||
return DownloadResponse{
|
|
||||||
Success: false,
|
|
||||||
Error: "Spotify ID is required for Tidal",
|
|
||||||
}, fmt.Errorf("spotify ID is required for Tidal")
|
|
||||||
}
|
|
||||||
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
downloader := backend.NewTidalDownloader(req.ApiURL)
|
downloader := backend.NewTidalDownloader(req.ApiURL)
|
||||||
if req.ServiceURL != "" {
|
if req.ServiceURL != "" {
|
||||||
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback)
|
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly)
|
||||||
} else {
|
} else {
|
||||||
if req.SpotifyID == "" {
|
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly)
|
||||||
return DownloadResponse{
|
|
||||||
Success: false,
|
|
||||||
Error: "Spotify ID is required for Tidal",
|
|
||||||
}, fmt.Errorf("spotify ID is required for Tidal")
|
|
||||||
}
|
|
||||||
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "qobuz":
|
case "qobuz":
|
||||||
downloader := backend.NewQobuzDownloader()
|
|
||||||
|
|
||||||
|
fmt.Println("Waiting for ISRC (Qobuz dependency)...")
|
||||||
|
isrc := <-isrcChan
|
||||||
|
downloader := backend.NewQobuzDownloader()
|
||||||
quality := req.AudioFormat
|
quality := req.AudioFormat
|
||||||
if quality == "" {
|
if quality == "" {
|
||||||
quality = "6"
|
quality = "6"
|
||||||
}
|
}
|
||||||
|
filename, err = downloader.DownloadTrackWithISRC(isrc, req.SpotifyID, req.OutputDir, quality, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly)
|
||||||
deezerISRC := req.ISRC
|
|
||||||
|
|
||||||
if len(deezerISRC) != 12 || !isValidISRC(deezerISRC) {
|
|
||||||
deezerISRC = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if deezerISRC == "" && req.SpotifyID != "" {
|
|
||||||
|
|
||||||
songlinkClient := backend.NewSongLinkClient()
|
|
||||||
deezerURL, err := songlinkClient.GetDeezerURLFromSpotify(req.SpotifyID)
|
|
||||||
if err != nil {
|
|
||||||
return DownloadResponse{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Sprintf("Failed to get Deezer URL: %v", err),
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
deezerISRC, err = backend.GetDeezerISRC(deezerURL)
|
|
||||||
if err != nil {
|
|
||||||
return DownloadResponse{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Sprintf("Failed to get ISRC from Deezer: %v", err),
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if deezerISRC == "" {
|
|
||||||
return DownloadResponse{
|
|
||||||
Success: false,
|
|
||||||
Error: "ISRC is required for Qobuz (could not fetch from Deezer)",
|
|
||||||
}, fmt.Errorf("ISRC is required for Qobuz")
|
|
||||||
}
|
|
||||||
filename, err = downloader.DownloadByISRC(deezerISRC, req.OutputDir, quality, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return DownloadResponse{
|
return DownloadResponse{
|
||||||
@@ -443,53 +432,30 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
filename = strings.TrimPrefix(filename, "EXISTS:")
|
filename = strings.TrimPrefix(filename, "EXISTS:")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !alreadyExists && req.SpotifyID != "" && req.EmbedLyrics && strings.HasSuffix(filename, ".flac") {
|
if !alreadyExists && req.SpotifyID != "" && req.EmbedLyrics && (strings.HasSuffix(filename, ".flac") || strings.HasSuffix(filename, ".mp3") || strings.HasSuffix(filename, ".m4a")) {
|
||||||
go func(filePath, spotifyID, trackName, artistName string) {
|
fmt.Printf("\nWaiting for lyrics fetch to complete...\n")
|
||||||
fmt.Printf("\n========== LYRICS FETCH START ==========\n")
|
lyrics := <-lyricsChan
|
||||||
fmt.Printf("Spotify ID: %s\n", spotifyID)
|
if lyrics != "" {
|
||||||
fmt.Printf("Track: %s\n", trackName)
|
|
||||||
fmt.Printf("Artist: %s\n", artistName)
|
|
||||||
fmt.Println("Searching all sources...")
|
|
||||||
|
|
||||||
lyricsClient := backend.NewLyricsClient()
|
|
||||||
|
|
||||||
lyricsResp, source, err := lyricsClient.FetchLyricsAllSources(spotifyID, trackName, artistName, 0)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("All sources failed: %v\n", err)
|
|
||||||
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if lyricsResp == nil || len(lyricsResp.Lines) == 0 {
|
|
||||||
fmt.Println("No lyrics content found")
|
|
||||||
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Lyrics found from: %s\n", source)
|
|
||||||
fmt.Printf("Sync type: %s\n", lyricsResp.SyncType)
|
|
||||||
fmt.Printf("Total lines: %d\n", len(lyricsResp.Lines))
|
|
||||||
|
|
||||||
lyrics := lyricsClient.ConvertToLRC(lyricsResp, trackName, artistName)
|
|
||||||
if lyrics == "" {
|
|
||||||
fmt.Println("No lyrics content to embed")
|
|
||||||
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\n--- Full LRC Content ---\n")
|
fmt.Printf("\n--- Full LRC Content ---\n")
|
||||||
fmt.Println(lyrics)
|
fmt.Println(lyrics)
|
||||||
fmt.Printf("--- End LRC Content ---\n\n")
|
fmt.Printf("--- End LRC Content ---\n\n")
|
||||||
|
|
||||||
fmt.Printf("Embedding into: %s\n", filePath)
|
fmt.Printf("Embedding into: %s\n", filename)
|
||||||
if err := backend.EmbedLyricsOnly(filePath, lyrics); err != nil {
|
|
||||||
|
if err := backend.EmbedLyricsOnlyUniversal(filename, lyrics); err != nil {
|
||||||
fmt.Printf("Failed to embed lyrics: %v\n", err)
|
fmt.Printf("Failed to embed lyrics: %v\n", err)
|
||||||
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Lyrics embedded successfully!\n")
|
fmt.Printf("Lyrics embedded successfully!\n")
|
||||||
fmt.Printf("========== LYRICS FETCH END (SUCCESS) ==========\n\n")
|
|
||||||
}
|
}
|
||||||
}(filename, req.SpotifyID, req.TrackName, req.ArtistName)
|
} else {
|
||||||
|
fmt.Println("No lyrics found to embed.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-lyricsChan:
|
||||||
|
default:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message := "Download completed successfully"
|
message := "Download completed successfully"
|
||||||
@@ -599,9 +565,9 @@ func (a *App) ClearAllDownloads() {
|
|||||||
backend.ClearAllDownloads()
|
backend.ClearAllDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) AddToDownloadQueue(isrc, trackName, artistName, albumName string) string {
|
func (a *App) AddToDownloadQueue(spotifyID, trackName, artistName, albumName string) string {
|
||||||
itemID := fmt.Sprintf("%s-%d", isrc, time.Now().UnixNano())
|
itemID := fmt.Sprintf("%s-%d", spotifyID, time.Now().UnixNano())
|
||||||
backend.AddToQueue(itemID, trackName, artistName, albumName, isrc)
|
backend.AddToQueue(itemID, trackName, artistName, albumName, "")
|
||||||
return itemID
|
return itemID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,11 +610,9 @@ func (a *App) ExportFailedDownloads() (string, error) {
|
|||||||
failedItems = append(failedItems, line)
|
failedItems = append(failedItems, line)
|
||||||
failedItems = append(failedItems, fmt.Sprintf(" Error: %s", item.ErrorMessage))
|
failedItems = append(failedItems, fmt.Sprintf(" Error: %s", item.ErrorMessage))
|
||||||
|
|
||||||
if item.ISRC != "" {
|
if item.SpotifyID != "" {
|
||||||
failedItems = append(failedItems, fmt.Sprintf(" ID: %s", item.ISRC))
|
failedItems = append(failedItems, fmt.Sprintf(" ID: %s", item.SpotifyID))
|
||||||
if !strings.HasPrefix(item.ISRC, "http") {
|
failedItems = append(failedItems, fmt.Sprintf(" URL: https://open.spotify.com/track/%s", item.SpotifyID))
|
||||||
failedItems = append(failedItems, fmt.Sprintf(" URL: https://open.spotify.com/track/%s", item.ISRC))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
failedItems = append(failedItems, "")
|
failedItems = append(failedItems, "")
|
||||||
}
|
}
|
||||||
@@ -979,13 +943,13 @@ func (a *App) DownloadAvatar(req AvatarDownloadRequest) (backend.AvatarDownloadR
|
|||||||
return *resp, nil
|
return *resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) CheckTrackAvailability(spotifyTrackID string, isrc string) (string, error) {
|
func (a *App) CheckTrackAvailability(spotifyTrackID string) (string, error) {
|
||||||
if spotifyTrackID == "" {
|
if spotifyTrackID == "" {
|
||||||
return "", fmt.Errorf("spotify track ID is required")
|
return "", fmt.Errorf("spotify track ID is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
client := backend.NewSongLinkClient()
|
client := backend.NewSongLinkClient()
|
||||||
availability, err := client.CheckTrackAvailability(spotifyTrackID, isrc)
|
availability, err := client.CheckTrackAvailability(spotifyTrackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-5
@@ -261,7 +261,7 @@ func (a *AmazonDownloader) DownloadFromService(amazonURL, outputDir, quality str
|
|||||||
return a.DownloadFromAfkarXYZ(amazonURL, outputDir, quality)
|
return a.DownloadFromAfkarXYZ(amazonURL, outputDir, quality)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string) (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, spotifyURL string, useFirstArtistOnly bool) (string, error) {
|
||||||
|
|
||||||
if outputDir != "." {
|
if outputDir != "." {
|
||||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
@@ -270,7 +270,13 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
|
|||||||
}
|
}
|
||||||
|
|
||||||
if spotifyTrackName != "" && spotifyArtistName != "" {
|
if spotifyTrackName != "" && spotifyArtistName != "" {
|
||||||
expectedFilename := BuildExpectedFilename(spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyDiscNumber, false)
|
filenameArtist := spotifyArtistName
|
||||||
|
filenameAlbumArtist := spotifyAlbumArtist
|
||||||
|
if useFirstArtistOnly {
|
||||||
|
filenameArtist = GetFirstArtist(spotifyArtistName)
|
||||||
|
filenameAlbumArtist = GetFirstArtist(spotifyAlbumArtist)
|
||||||
|
}
|
||||||
|
expectedFilename := BuildExpectedFilename(spotifyTrackName, filenameArtist, spotifyAlbumName, filenameAlbumArtist, spotifyReleaseDate, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyDiscNumber, false)
|
||||||
expectedPath := filepath.Join(outputDir, expectedFilename)
|
expectedPath := filepath.Join(outputDir, expectedFilename)
|
||||||
|
|
||||||
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 0 {
|
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 0 {
|
||||||
@@ -279,6 +285,26 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isrcChan := make(chan string, 1)
|
||||||
|
if spotifyURL != "" {
|
||||||
|
go func() {
|
||||||
|
var isrc string
|
||||||
|
parts := strings.Split(spotifyURL, "/")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
sID := strings.Split(parts[len(parts)-1], "?")[0]
|
||||||
|
if sID != "" {
|
||||||
|
client := NewSongLinkClient()
|
||||||
|
if val, err := client.GetISRC(sID); err == nil {
|
||||||
|
isrc = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isrcChan <- isrc
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
close(isrcChan)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("Using Amazon URL: %s\n", amazonURL)
|
fmt.Printf("Using Amazon URL: %s\n", amazonURL)
|
||||||
|
|
||||||
filePath, err := a.DownloadFromService(amazonURL, outputDir, quality)
|
filePath, err := a.DownloadFromService(amazonURL, outputDir, quality)
|
||||||
@@ -286,14 +312,25 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isrc string
|
||||||
|
if spotifyURL != "" {
|
||||||
|
isrc = <-isrcChan
|
||||||
|
}
|
||||||
|
|
||||||
originalFileDir := filepath.Dir(filePath)
|
originalFileDir := filepath.Dir(filePath)
|
||||||
originalFileBase := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
|
originalFileBase := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
|
||||||
|
|
||||||
if spotifyTrackName != "" && spotifyArtistName != "" {
|
if spotifyTrackName != "" && spotifyArtistName != "" {
|
||||||
safeArtist := sanitizeFilename(spotifyArtistName)
|
safeArtist := sanitizeFilename(spotifyArtistName)
|
||||||
|
safeAlbumArtist := sanitizeFilename(spotifyAlbumArtist)
|
||||||
|
|
||||||
|
if useFirstArtistOnly {
|
||||||
|
safeArtist = sanitizeFilename(GetFirstArtist(spotifyArtistName))
|
||||||
|
safeAlbumArtist = sanitizeFilename(GetFirstArtist(spotifyAlbumArtist))
|
||||||
|
}
|
||||||
|
|
||||||
safeTitle := sanitizeFilename(spotifyTrackName)
|
safeTitle := sanitizeFilename(spotifyTrackName)
|
||||||
safeAlbum := sanitizeFilename(spotifyAlbumName)
|
safeAlbum := sanitizeFilename(spotifyAlbumName)
|
||||||
safeAlbumArtist := sanitizeFilename(spotifyAlbumArtist)
|
|
||||||
|
|
||||||
year := ""
|
year := ""
|
||||||
if len(spotifyReleaseDate) >= 4 {
|
if len(spotifyReleaseDate) >= 4 {
|
||||||
@@ -390,6 +427,7 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
|
|||||||
Copyright: spotifyCopyright,
|
Copyright: spotifyCopyright,
|
||||||
Publisher: spotifyPublisher,
|
Publisher: spotifyPublisher,
|
||||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||||
|
ISRC: isrc,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := EmbedMetadataToConvertedFile(filePath, metadata, coverPath); err != nil {
|
if err := EmbedMetadataToConvertedFile(filePath, metadata, coverPath); err != nil {
|
||||||
@@ -415,12 +453,14 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
|
|||||||
return filePath, nil
|
return filePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, quality, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string) (string, error) {
|
func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, quality, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string,
|
||||||
|
useFirstArtistOnly bool,
|
||||||
|
) (string, error) {
|
||||||
|
|
||||||
amazonURL, err := a.GetAmazonURLFromSpotify(spotifyTrackID)
|
amazonURL, err := a.GetAmazonURLFromSpotify(spotifyTrackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.DownloadByURL(amazonURL, outputDir, quality, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, embedMaxQualityCover, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL)
|
return a.DownloadByURL(amazonURL, outputDir, quality, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, embedMaxQualityCover, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL, useFirstArtistOnly)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,19 @@ func SanitizeFilename(name string) string {
|
|||||||
return sanitized
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetFirstArtist(artistString string) string {
|
||||||
|
if artistString == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
delimiters := []string{", ", " & ", " feat. ", " ft. ", " featuring "}
|
||||||
|
for _, d := range delimiters {
|
||||||
|
if idx := strings.Index(strings.ToLower(artistString), d); idx != -1 {
|
||||||
|
return strings.TrimSpace(artistString[:idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return artistString
|
||||||
|
}
|
||||||
|
|
||||||
func NormalizePath(folderPath string) string {
|
func NormalizePath(folderPath string) string {
|
||||||
|
|
||||||
return strings.ReplaceAll(folderPath, "/", string(filepath.Separator))
|
return strings.ReplaceAll(folderPath, "/", string(filepath.Separator))
|
||||||
|
|||||||
+24
-9
@@ -31,6 +31,7 @@ type Metadata struct {
|
|||||||
Publisher string
|
Publisher string
|
||||||
Lyrics string
|
Lyrics string
|
||||||
Description string
|
Description string
|
||||||
|
ISRC string
|
||||||
}
|
}
|
||||||
|
|
||||||
func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
||||||
@@ -86,6 +87,10 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
|||||||
_ = cmt.Add("DESCRIPTION", metadata.Description)
|
_ = cmt.Add("DESCRIPTION", metadata.Description)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if metadata.ISRC != "" {
|
||||||
|
_ = cmt.Add("ISRC", metadata.ISRC)
|
||||||
|
}
|
||||||
|
|
||||||
if metadata.Lyrics != "" {
|
if metadata.Lyrics != "" {
|
||||||
_ = cmt.Add("LYRICS", metadata.Lyrics)
|
_ = cmt.Add("LYRICS", metadata.Lyrics)
|
||||||
}
|
}
|
||||||
@@ -504,6 +509,13 @@ func EmbedLyricsOnlyUniversal(filepath string, lyrics string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validatedLyrics, err := validateLyricsDuration(lyrics, filepath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[EmbedLyricsOnlyUniversal] Warning: Failed to validate lyrics duration: %v, using original lyrics\n", err)
|
||||||
|
validatedLyrics = lyrics
|
||||||
|
}
|
||||||
|
lyrics = validatedLyrics
|
||||||
|
|
||||||
ext := strings.ToLower(pathfilepath.Ext(filepath))
|
ext := strings.ToLower(pathfilepath.Ext(filepath))
|
||||||
switch ext {
|
switch ext {
|
||||||
case ".mp3":
|
case ".mp3":
|
||||||
@@ -635,28 +647,23 @@ func validateLyricsDuration(lyrics string, filepath string) (string, error) {
|
|||||||
|
|
||||||
if strings.HasPrefix(trimmedLine, "[") {
|
if strings.HasPrefix(trimmedLine, "[") {
|
||||||
|
|
||||||
if strings.Index(trimmedLine, ":") > 0 {
|
|
||||||
|
|
||||||
validLines = append(validLines, line)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
closeBracket := strings.Index(trimmedLine, "]")
|
closeBracket := strings.Index(trimmedLine, "]")
|
||||||
if closeBracket > 0 {
|
if closeBracket > 0 {
|
||||||
timestampStr := trimmedLine[1:closeBracket]
|
timestampStr := trimmedLine[1:closeBracket]
|
||||||
|
|
||||||
ms := parseLRCTimestamp(timestampStr)
|
ms := parseLRCTimestamp(timestampStr)
|
||||||
if ms >= 0 && ms <= durationMs {
|
if ms >= 0 {
|
||||||
|
if ms <= durationMs {
|
||||||
validLines = append(validLines, line)
|
validLines = append(validLines, line)
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
fmt.Printf("[ValidateLyrics] Filtered out line with timestamp %s (exceeds duration %d ms): %s\n", timestampStr, durationMs, trimmedLine)
|
fmt.Printf("[ValidateLyrics] Filtered out line with timestamp %s (exceeds duration %d ms): %s\n", timestampStr, durationMs, trimmedLine)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
validLines = append(validLines, line)
|
validLines = append(validLines, line)
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
validLines = append(validLines, line)
|
validLines = append(validLines, line)
|
||||||
@@ -858,6 +865,11 @@ func embedMetadataToMP3(filePath string, metadata Metadata, coverPath string) er
|
|||||||
tag.AddTextFrame("TPUB", id3v2.EncodingUTF8, metadata.Publisher)
|
tag.AddTextFrame("TPUB", id3v2.EncodingUTF8, metadata.Publisher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if metadata.ISRC != "" {
|
||||||
|
tag.DeleteFrames("TSRC")
|
||||||
|
tag.AddTextFrame("TSRC", id3v2.EncodingUTF8, metadata.ISRC)
|
||||||
|
}
|
||||||
|
|
||||||
if coverPath != "" && fileExists(coverPath) {
|
if coverPath != "" && fileExists(coverPath) {
|
||||||
|
|
||||||
tag.DeleteFrames(tag.CommonID("Attached picture"))
|
tag.DeleteFrames(tag.CommonID("Attached picture"))
|
||||||
@@ -941,6 +953,9 @@ func embedMetadataToM4A(filePath string, metadata Metadata, coverPath string) er
|
|||||||
if metadata.Publisher != "" {
|
if metadata.Publisher != "" {
|
||||||
args = append(args, "-metadata", "publisher="+metadata.Publisher)
|
args = append(args, "-metadata", "publisher="+metadata.Publisher)
|
||||||
}
|
}
|
||||||
|
if metadata.ISRC != "" {
|
||||||
|
args = append(args, "-metadata", "isrc="+metadata.ISRC)
|
||||||
|
}
|
||||||
|
|
||||||
tmpOutputFile := strings.TrimSuffix(filePath, pathfilepath.Ext(filePath)) + ".tmp" + pathfilepath.Ext(filePath)
|
tmpOutputFile := strings.TrimSuffix(filePath, pathfilepath.Ext(filePath)) + ".tmp" + pathfilepath.Ext(filePath)
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
+3
-3
@@ -22,7 +22,7 @@ type DownloadItem struct {
|
|||||||
TrackName string `json:"track_name"`
|
TrackName string `json:"track_name"`
|
||||||
ArtistName string `json:"artist_name"`
|
ArtistName string `json:"artist_name"`
|
||||||
AlbumName string `json:"album_name"`
|
AlbumName string `json:"album_name"`
|
||||||
ISRC string `json:"isrc"`
|
SpotifyID string `json:"spotify_id"`
|
||||||
Status DownloadStatus `json:"status"`
|
Status DownloadStatus `json:"status"`
|
||||||
Progress float64 `json:"progress"`
|
Progress float64 `json:"progress"`
|
||||||
TotalSize float64 `json:"total_size"`
|
TotalSize float64 `json:"total_size"`
|
||||||
@@ -184,7 +184,7 @@ func (pw *ProgressWriter) GetTotal() int64 {
|
|||||||
return pw.total
|
return pw.total
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddToQueue(id, trackName, artistName, albumName, isrc string) {
|
func AddToQueue(id, trackName, artistName, albumName, spotifyID string) {
|
||||||
downloadQueueLock.Lock()
|
downloadQueueLock.Lock()
|
||||||
defer downloadQueueLock.Unlock()
|
defer downloadQueueLock.Unlock()
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ func AddToQueue(id, trackName, artistName, albumName, isrc string) {
|
|||||||
TrackName: trackName,
|
TrackName: trackName,
|
||||||
ArtistName: artistName,
|
ArtistName: artistName,
|
||||||
AlbumName: albumName,
|
AlbumName: albumName,
|
||||||
ISRC: isrc,
|
SpotifyID: spotifyID,
|
||||||
Status: StatusQueued,
|
Status: StatusQueued,
|
||||||
Progress: 0,
|
Progress: 0,
|
||||||
TotalSize: 0,
|
TotalSize: 0,
|
||||||
|
|||||||
+27
-4
@@ -77,7 +77,7 @@ func NewQobuzDownloader() *QobuzDownloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QobuzDownloader) SearchByISRC(isrc string) (*QobuzTrack, error) {
|
func (q *QobuzDownloader) searchByISRC(isrc string) (*QobuzTrack, error) {
|
||||||
apiBase := "https://www.qobuz.com/api.json/0.2/track/search?query="
|
apiBase := "https://www.qobuz.com/api.json/0.2/track/search?query="
|
||||||
url := fmt.Sprintf("%s%s&limit=1&app_id=%s", apiBase, isrc, q.appID)
|
url := fmt.Sprintf("%s%s&limit=1&app_id=%s", apiBase, isrc, q.appID)
|
||||||
|
|
||||||
@@ -433,7 +433,23 @@ func buildQobuzFilename(title, artist, album, albumArtist, releaseDate string, t
|
|||||||
return filename + ".flac"
|
return filename + ".flac"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QobuzDownloader) DownloadByISRC(deezerISRC, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, allowFallback bool) (string, error) {
|
func (q *QobuzDownloader) DownloadTrack(spotifyID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, allowFallback bool, useFirstArtistOnly bool) (string, error) {
|
||||||
|
var deezerISRC string
|
||||||
|
if spotifyID != "" {
|
||||||
|
songlinkClient := NewSongLinkClient()
|
||||||
|
isrc, err := songlinkClient.GetISRC(spotifyID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get ISRC: %v", err)
|
||||||
|
}
|
||||||
|
deezerISRC = isrc
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("spotify ID is required for Qobuz download")
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.DownloadTrackWithISRC(deezerISRC, spotifyID, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL, allowFallback, useFirstArtistOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QobuzDownloader) DownloadTrackWithISRC(deezerISRC, spotifyID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, allowFallback bool, useFirstArtistOnly bool) (string, error) {
|
||||||
fmt.Printf("Fetching track info for ISRC: %s\n", deezerISRC)
|
fmt.Printf("Fetching track info for ISRC: %s\n", deezerISRC)
|
||||||
|
|
||||||
if outputDir != "." {
|
if outputDir != "." {
|
||||||
@@ -442,7 +458,7 @@ func (q *QobuzDownloader) DownloadByISRC(deezerISRC, outputDir, quality, filenam
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
track, err := q.SearchByISRC(deezerISRC)
|
track, err := q.searchByISRC(deezerISRC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -477,9 +493,15 @@ func (q *QobuzDownloader) DownloadByISRC(deezerISRC, outputDir, quality, filenam
|
|||||||
fmt.Printf("Download URL obtained: %s\n", urlPreview)
|
fmt.Printf("Download URL obtained: %s\n", urlPreview)
|
||||||
|
|
||||||
safeArtist := sanitizeFilename(artists)
|
safeArtist := sanitizeFilename(artists)
|
||||||
|
safeAlbumArtist := sanitizeFilename(spotifyAlbumArtist)
|
||||||
|
|
||||||
|
if useFirstArtistOnly {
|
||||||
|
safeArtist = sanitizeFilename(GetFirstArtist(artists))
|
||||||
|
safeAlbumArtist = sanitizeFilename(GetFirstArtist(spotifyAlbumArtist))
|
||||||
|
}
|
||||||
|
|
||||||
safeTitle := sanitizeFilename(trackTitle)
|
safeTitle := sanitizeFilename(trackTitle)
|
||||||
safeAlbum := sanitizeFilename(albumTitle)
|
safeAlbum := sanitizeFilename(albumTitle)
|
||||||
safeAlbumArtist := sanitizeFilename(spotifyAlbumArtist)
|
|
||||||
|
|
||||||
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)
|
||||||
filepath := filepath.Join(outputDir, filename)
|
filepath := filepath.Join(outputDir, filename)
|
||||||
@@ -531,6 +553,7 @@ func (q *QobuzDownloader) DownloadByISRC(deezerISRC, outputDir, quality, filenam
|
|||||||
Copyright: spotifyCopyright,
|
Copyright: spotifyCopyright,
|
||||||
Publisher: spotifyPublisher,
|
Publisher: spotifyPublisher,
|
||||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||||
|
ISRC: deezerISRC,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := EmbedMetadata(filepath, metadata, coverPath); err != nil {
|
if err := EmbedMetadata(filepath, metadata, coverPath); err != nil {
|
||||||
|
|||||||
+18
-3
@@ -21,6 +21,7 @@ type SongLinkClient struct {
|
|||||||
type SongLinkURLs struct {
|
type SongLinkURLs struct {
|
||||||
TidalURL string `json:"tidal_url"`
|
TidalURL string `json:"tidal_url"`
|
||||||
AmazonURL string `json:"amazon_url"`
|
AmazonURL string `json:"amazon_url"`
|
||||||
|
ISRC string `json:"isrc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrackAvailability struct {
|
type TrackAvailability struct {
|
||||||
@@ -158,6 +159,12 @@ func (s *SongLinkClient) GetAllURLsFromSpotify(spotifyTrackID string, region str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" {
|
||||||
|
if isrc, err := getDeezerISRC(deezerLink.URL); err == nil && isrc != "" {
|
||||||
|
urls.ISRC = isrc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if urls.TidalURL == "" && urls.AmazonURL == "" {
|
if urls.TidalURL == "" && urls.AmazonURL == "" {
|
||||||
return nil, fmt.Errorf("no streaming URLs found")
|
return nil, fmt.Errorf("no streaming URLs found")
|
||||||
}
|
}
|
||||||
@@ -165,7 +172,7 @@ func (s *SongLinkClient) GetAllURLsFromSpotify(spotifyTrackID string, region str
|
|||||||
return urls, nil
|
return urls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc string) (*TrackAvailability, error) {
|
func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string) (*TrackAvailability, error) {
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if now.Sub(s.apiCallResetTime) >= time.Minute {
|
if now.Sub(s.apiCallResetTime) >= time.Minute {
|
||||||
@@ -278,7 +285,7 @@ func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc stri
|
|||||||
if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" {
|
if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" {
|
||||||
deezerURL := deezerLink.URL
|
deezerURL := deezerLink.URL
|
||||||
|
|
||||||
deezerISRC, err := GetDeezerISRC(deezerURL)
|
deezerISRC, err := getDeezerISRC(deezerURL)
|
||||||
if err == nil && deezerISRC != "" {
|
if err == nil && deezerISRC != "" {
|
||||||
qobuzAvailable := checkQobuzAvailability(deezerISRC)
|
qobuzAvailable := checkQobuzAvailability(deezerISRC)
|
||||||
availability.Qobuz = qobuzAvailable
|
availability.Qobuz = qobuzAvailable
|
||||||
@@ -408,7 +415,7 @@ func (s *SongLinkClient) GetDeezerURLFromSpotify(spotifyTrackID string) (string,
|
|||||||
return deezerURL, nil
|
return deezerURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDeezerISRC(deezerURL string) (string, error) {
|
func getDeezerISRC(deezerURL string) (string, error) {
|
||||||
|
|
||||||
var trackID string
|
var trackID string
|
||||||
if strings.Contains(deezerURL, "/track/") {
|
if strings.Contains(deezerURL, "/track/") {
|
||||||
@@ -452,3 +459,11 @@ func GetDeezerISRC(deezerURL string) (string, error) {
|
|||||||
fmt.Printf("Found ISRC from Deezer: %s (track: %s)\n", deezerTrack.ISRC, deezerTrack.Title)
|
fmt.Printf("Found ISRC from Deezer: %s (track: %s)\n", deezerTrack.ISRC, deezerTrack.Title)
|
||||||
return deezerTrack.ISRC, nil
|
return deezerTrack.ISRC, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SongLinkClient) GetISRC(spotifyID string) (string, error) {
|
||||||
|
deezerURL, err := s.GetDeezerURLFromSpotify(spotifyID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return getDeezerISRC(deezerURL)
|
||||||
|
}
|
||||||
|
|||||||
+7
-10
@@ -364,9 +364,6 @@ func getBool(m map[string]interface{}, key string) bool {
|
|||||||
|
|
||||||
func extractArtists(artistsData map[string]interface{}) []map[string]interface{} {
|
func extractArtists(artistsData map[string]interface{}) []map[string]interface{} {
|
||||||
items := getSlice(artistsData, "items")
|
items := getSlice(artistsData, "items")
|
||||||
if items == nil {
|
|
||||||
return []map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
artists := []map[string]interface{}{}
|
artists := []map[string]interface{}{}
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
@@ -384,7 +381,7 @@ func extractArtists(artistsData map[string]interface{}) []map[string]interface{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
func extractCoverImage(coverData map[string]interface{}) map[string]interface{} {
|
func extractCoverImage(coverData map[string]interface{}) map[string]interface{} {
|
||||||
if coverData == nil || len(coverData) == 0 {
|
if len(coverData) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +398,7 @@ func extractCoverImage(coverData map[string]interface{}) map[string]interface{}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sources == nil || len(sources) == 0 {
|
if len(sources) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,7 +529,7 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
|
|||||||
}
|
}
|
||||||
|
|
||||||
var albumFetchDataMap map[string]interface{}
|
var albumFetchDataMap map[string]interface{}
|
||||||
if len(albumFetchData) > 0 && albumFetchData[0] != nil {
|
if len(albumFetchData) > 0 {
|
||||||
albumFetchDataMap = albumFetchData[0]
|
albumFetchDataMap = albumFetchData[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,7 +538,6 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
|
|||||||
if len(artists) == 0 {
|
if len(artists) == 0 {
|
||||||
artists = []map[string]interface{}{}
|
artists = []map[string]interface{}{}
|
||||||
firstArtistItems := getSlice(getMap(trackData, "firstArtist"), "items")
|
firstArtistItems := getSlice(getMap(trackData, "firstArtist"), "items")
|
||||||
if firstArtistItems != nil {
|
|
||||||
for _, item := range firstArtistItems {
|
for _, item := range firstArtistItems {
|
||||||
itemMap, ok := item.(map[string]interface{})
|
itemMap, ok := item.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -557,10 +553,8 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
otherArtistItems := getSlice(getMap(trackData, "otherArtists"), "items")
|
otherArtistItems := getSlice(getMap(trackData, "otherArtists"), "items")
|
||||||
if otherArtistItems != nil {
|
|
||||||
for _, item := range otherArtistItems {
|
for _, item := range otherArtistItems {
|
||||||
itemMap, ok := item.(map[string]interface{})
|
itemMap, ok := item.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -577,7 +571,6 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(artists) == 0 {
|
if len(artists) == 0 {
|
||||||
albumData := getMap(trackData, "albumOfTrack")
|
albumData := getMap(trackData, "albumOfTrack")
|
||||||
@@ -710,6 +703,9 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
|
|||||||
}
|
}
|
||||||
albumArtistsString = strings.Join(albumArtistNames, ", ")
|
albumArtistsString = strings.Join(albumArtistNames, ", ")
|
||||||
}
|
}
|
||||||
|
if albumArtistsString == "" {
|
||||||
|
albumArtistsString = getString(albumUnionData, "artists")
|
||||||
|
}
|
||||||
albumLabel = getString(albumUnionData, "label")
|
albumLabel = getString(albumUnionData, "label")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -977,6 +973,7 @@ func FilterAlbum(data map[string]interface{}) map[string]interface{} {
|
|||||||
"discs": map[string]interface{}{
|
"discs": map[string]interface{}{
|
||||||
"totalCount": totalDiscs,
|
"totalCount": totalDiscs,
|
||||||
},
|
},
|
||||||
|
"label": getString(albumData, "label"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ type TrackMetadata struct {
|
|||||||
DiscNumber int `json:"disc_number,omitempty"`
|
DiscNumber int `json:"disc_number,omitempty"`
|
||||||
TotalDiscs int `json:"total_discs,omitempty"`
|
TotalDiscs int `json:"total_discs,omitempty"`
|
||||||
ExternalURL string `json:"external_urls"`
|
ExternalURL string `json:"external_urls"`
|
||||||
ISRC string `json:"isrc"`
|
|
||||||
Copyright string `json:"copyright,omitempty"`
|
Copyright string `json:"copyright,omitempty"`
|
||||||
Publisher string `json:"publisher,omitempty"`
|
Publisher string `json:"publisher,omitempty"`
|
||||||
Plays string `json:"plays,omitempty"`
|
Plays string `json:"plays,omitempty"`
|
||||||
@@ -70,7 +69,6 @@ type AlbumTrackMetadata struct {
|
|||||||
DiscNumber int `json:"disc_number,omitempty"`
|
DiscNumber int `json:"disc_number,omitempty"`
|
||||||
TotalDiscs int `json:"total_discs,omitempty"`
|
TotalDiscs int `json:"total_discs,omitempty"`
|
||||||
ExternalURL string `json:"external_urls"`
|
ExternalURL string `json:"external_urls"`
|
||||||
ISRC string `json:"isrc"`
|
|
||||||
AlbumType string `json:"album_type,omitempty"`
|
AlbumType string `json:"album_type,omitempty"`
|
||||||
AlbumID string `json:"album_id,omitempty"`
|
AlbumID string `json:"album_id,omitempty"`
|
||||||
AlbumURL string `json:"album_url,omitempty"`
|
AlbumURL string `json:"album_url,omitempty"`
|
||||||
@@ -210,6 +208,7 @@ type apiAlbumResponse struct {
|
|||||||
Cover string `json:"cover"`
|
Cover string `json:"cover"`
|
||||||
ReleaseDate string `json:"releaseDate"`
|
ReleaseDate string `json:"releaseDate"`
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
|
Label string `json:"label"`
|
||||||
Discs struct {
|
Discs struct {
|
||||||
TotalCount int `json:"totalCount"`
|
TotalCount int `json:"totalCount"`
|
||||||
} `json:"discs"`
|
} `json:"discs"`
|
||||||
@@ -472,6 +471,8 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string)
|
|||||||
"items": tracksItems,
|
"items": tracksItems,
|
||||||
"totalCount": albumResponse.Count,
|
"totalCount": albumResponse.Count,
|
||||||
},
|
},
|
||||||
|
"artists": albumResponse.Artists,
|
||||||
|
"label": albumResponse.Label,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -886,7 +887,6 @@ func (c *SpotifyMetadataClient) formatTrackData(raw *apiTrackResponse) TrackResp
|
|||||||
DiscNumber: raw.Disc,
|
DiscNumber: raw.Disc,
|
||||||
TotalDiscs: raw.Discs,
|
TotalDiscs: raw.Discs,
|
||||||
ExternalURL: externalURL,
|
ExternalURL: externalURL,
|
||||||
ISRC: raw.ID,
|
|
||||||
Copyright: raw.Copyright,
|
Copyright: raw.Copyright,
|
||||||
Publisher: raw.Album.Label,
|
Publisher: raw.Album.Label,
|
||||||
Plays: raw.Plays,
|
Plays: raw.Plays,
|
||||||
@@ -945,7 +945,6 @@ func (c *SpotifyMetadataClient) formatAlbumData(raw *apiAlbumResponse) (*AlbumRe
|
|||||||
DiscNumber: item.DiscNumber,
|
DiscNumber: item.DiscNumber,
|
||||||
TotalDiscs: raw.Discs.TotalCount,
|
TotalDiscs: raw.Discs.TotalCount,
|
||||||
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", item.ID),
|
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", item.ID),
|
||||||
ISRC: item.ID,
|
|
||||||
AlbumID: raw.ID,
|
AlbumID: raw.ID,
|
||||||
AlbumURL: fmt.Sprintf("https://open.spotify.com/album/%s", raw.ID),
|
AlbumURL: fmt.Sprintf("https://open.spotify.com/album/%s", raw.ID),
|
||||||
ArtistID: artistID,
|
ArtistID: artistID,
|
||||||
@@ -1005,7 +1004,6 @@ func (c *SpotifyMetadataClient) formatPlaylistData(raw *apiPlaylistResponse) Pla
|
|||||||
DiscNumber: item.DiscNumber,
|
DiscNumber: item.DiscNumber,
|
||||||
TotalDiscs: 0,
|
TotalDiscs: 0,
|
||||||
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", item.ID),
|
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", item.ID),
|
||||||
ISRC: item.ID,
|
|
||||||
AlbumID: item.AlbumID,
|
AlbumID: item.AlbumID,
|
||||||
AlbumURL: fmt.Sprintf("https://open.spotify.com/album/%s", item.AlbumID),
|
AlbumURL: fmt.Sprintf("https://open.spotify.com/album/%s", item.AlbumID),
|
||||||
ArtistID: artistID,
|
ArtistID: artistID,
|
||||||
@@ -1124,7 +1122,6 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context,
|
|||||||
TotalTracks: albumData.Count,
|
TotalTracks: albumData.Count,
|
||||||
DiscNumber: tr.DiscNumber,
|
DiscNumber: tr.DiscNumber,
|
||||||
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", tr.ID),
|
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", tr.ID),
|
||||||
ISRC: tr.ID,
|
|
||||||
AlbumID: albumID,
|
AlbumID: albumID,
|
||||||
AlbumURL: fmt.Sprintf("https://open.spotify.com/album/%s", albumID),
|
AlbumURL: fmt.Sprintf("https://open.spotify.com/album/%s", albumID),
|
||||||
ArtistID: artistID,
|
ArtistID: artistID,
|
||||||
|
|||||||
+70
-6
@@ -446,7 +446,7 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, allowFallback bool) (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, spotifyURL string, allowFallback bool, useFirstArtistOnly bool) (string, error) {
|
||||||
if outputDir != "." {
|
if outputDir != "." {
|
||||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
return "", fmt.Errorf("directory error: %w", err)
|
return "", fmt.Errorf("directory error: %w", err)
|
||||||
@@ -469,9 +469,15 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
|
|||||||
albumTitle := spotifyAlbumName
|
albumTitle := spotifyAlbumName
|
||||||
|
|
||||||
artistNameForFile := sanitizeFilename(artistName)
|
artistNameForFile := sanitizeFilename(artistName)
|
||||||
|
albumArtistForFile := sanitizeFilename(spotifyAlbumArtist)
|
||||||
|
|
||||||
|
if useFirstArtistOnly {
|
||||||
|
artistNameForFile = sanitizeFilename(GetFirstArtist(artistName))
|
||||||
|
albumArtistForFile = sanitizeFilename(GetFirstArtist(spotifyAlbumArtist))
|
||||||
|
}
|
||||||
|
|
||||||
trackTitleForFile := sanitizeFilename(trackTitle)
|
trackTitleForFile := sanitizeFilename(trackTitle)
|
||||||
albumTitleForFile := sanitizeFilename(albumTitle)
|
albumTitleForFile := sanitizeFilename(albumTitle)
|
||||||
albumArtistForFile := sanitizeFilename(spotifyAlbumArtist)
|
|
||||||
|
|
||||||
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)
|
||||||
outputFilename := filepath.Join(outputDir, filename)
|
outputFilename := filepath.Join(outputDir, filename)
|
||||||
@@ -494,11 +500,36 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isrcChan := make(chan string, 1)
|
||||||
|
if spotifyURL != "" {
|
||||||
|
go func() {
|
||||||
|
var isrc string
|
||||||
|
parts := strings.Split(spotifyURL, "/")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
sID := strings.Split(parts[len(parts)-1], "?")[0]
|
||||||
|
if sID != "" {
|
||||||
|
client := NewSongLinkClient()
|
||||||
|
if val, err := client.GetISRC(sID); err == nil {
|
||||||
|
isrc = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isrcChan <- isrc
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
close(isrcChan)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("Downloading to: %s\n", outputFilename)
|
fmt.Printf("Downloading to: %s\n", outputFilename)
|
||||||
if err := t.DownloadFile(downloadURL, outputFilename); err != nil {
|
if err := t.DownloadFile(downloadURL, outputFilename); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isrc string
|
||||||
|
if spotifyURL != "" {
|
||||||
|
isrc = <-isrcChan
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("Adding metadata...")
|
fmt.Println("Adding metadata...")
|
||||||
|
|
||||||
coverPath := ""
|
coverPath := ""
|
||||||
@@ -534,6 +565,7 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
|
|||||||
Copyright: spotifyCopyright,
|
Copyright: spotifyCopyright,
|
||||||
Publisher: spotifyPublisher,
|
Publisher: spotifyPublisher,
|
||||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||||
|
ISRC: isrc,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil {
|
if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil {
|
||||||
@@ -547,7 +579,7 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
|
|||||||
return outputFilename, nil
|
return outputFilename, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, allowFallback bool) (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, spotifyURL string, allowFallback bool, useFirstArtistOnly bool) (string, error) {
|
||||||
apis, err := t.GetAvailableAPIs()
|
apis, err := t.GetAvailableAPIs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("no APIs available for fallback: %w", err)
|
return "", fmt.Errorf("no APIs available for fallback: %w", err)
|
||||||
@@ -575,9 +607,15 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
|
|||||||
albumTitle := spotifyAlbumName
|
albumTitle := spotifyAlbumName
|
||||||
|
|
||||||
artistNameForFile := sanitizeFilename(artistName)
|
artistNameForFile := sanitizeFilename(artistName)
|
||||||
|
albumArtistForFile := sanitizeFilename(spotifyAlbumArtist)
|
||||||
|
|
||||||
|
if useFirstArtistOnly {
|
||||||
|
artistNameForFile = sanitizeFilename(GetFirstArtist(artistName))
|
||||||
|
albumArtistForFile = sanitizeFilename(GetFirstArtist(spotifyAlbumArtist))
|
||||||
|
}
|
||||||
|
|
||||||
trackTitleForFile := sanitizeFilename(trackTitle)
|
trackTitleForFile := sanitizeFilename(trackTitle)
|
||||||
albumTitleForFile := sanitizeFilename(albumTitle)
|
albumTitleForFile := sanitizeFilename(albumTitle)
|
||||||
albumArtistForFile := sanitizeFilename(spotifyAlbumArtist)
|
|
||||||
|
|
||||||
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)
|
||||||
outputFilename := filepath.Join(outputDir, filename)
|
outputFilename := filepath.Join(outputDir, filename)
|
||||||
@@ -600,12 +638,37 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isrcChan := make(chan string, 1)
|
||||||
|
if spotifyURL != "" {
|
||||||
|
go func() {
|
||||||
|
var isrc string
|
||||||
|
parts := strings.Split(spotifyURL, "/")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
sID := strings.Split(parts[len(parts)-1], "?")[0]
|
||||||
|
if sID != "" {
|
||||||
|
client := NewSongLinkClient()
|
||||||
|
if val, err := client.GetISRC(sID); err == nil {
|
||||||
|
isrc = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isrcChan <- isrc
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
close(isrcChan)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("Downloading to: %s\n", outputFilename)
|
fmt.Printf("Downloading to: %s\n", outputFilename)
|
||||||
downloader := NewTidalDownloader(successAPI)
|
downloader := NewTidalDownloader(successAPI)
|
||||||
if err := downloader.DownloadFile(downloadURL, outputFilename); err != nil {
|
if err := downloader.DownloadFile(downloadURL, outputFilename); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isrc string
|
||||||
|
if spotifyURL != "" {
|
||||||
|
isrc = <-isrcChan
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("Adding metadata...")
|
fmt.Println("Adding metadata...")
|
||||||
|
|
||||||
coverPath := ""
|
coverPath := ""
|
||||||
@@ -641,6 +704,7 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
|
|||||||
Copyright: spotifyCopyright,
|
Copyright: spotifyCopyright,
|
||||||
Publisher: spotifyPublisher,
|
Publisher: spotifyPublisher,
|
||||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||||
|
ISRC: isrc,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil {
|
if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil {
|
||||||
@@ -654,14 +718,14 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
|
|||||||
return outputFilename, nil
|
return outputFilename, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, allowFallback bool) (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, spotifyURL string, allowFallback bool, useFirstArtistOnly bool) (string, error) {
|
||||||
|
|
||||||
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
|
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("songlink couldn't find Tidal URL: %w", err)
|
return "", fmt.Errorf("songlink couldn't find Tidal URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL, allowFallback)
|
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL, allowFallback, useFirstArtistOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SegmentTemplate struct {
|
type SegmentTemplate struct {
|
||||||
|
|||||||
+40
-9
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
|
||||||
import { Search, X, ArrowUp } from "lucide-react";
|
import { Search, X, ArrowUp } from "lucide-react";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { getSettings, getSettingsWithDefaults, loadSettings, saveSettings, applyThemeMode, applyFont } from "@/lib/settings";
|
import { getSettings, getSettingsWithDefaults, loadSettings, saveSettings, applyThemeMode, applyFont, updateSettings } from "@/lib/settings";
|
||||||
import { applyTheme } from "@/lib/themes";
|
import { applyTheme } from "@/lib/themes";
|
||||||
import { OpenFolder, CheckFFmpegInstalled, DownloadFFmpeg } from "../wailsjs/go/main/App";
|
import { OpenFolder, CheckFFmpegInstalled, DownloadFFmpeg } from "../wailsjs/go/main/App";
|
||||||
import { EventsOn, EventsOff, Quit } from "../wailsjs/runtime/runtime";
|
import { EventsOn, EventsOff, Quit } from "../wailsjs/runtime/runtime";
|
||||||
@@ -119,6 +119,17 @@ function App() {
|
|||||||
window.removeEventListener("scroll", handleScroll);
|
window.removeEventListener("scroll", handleScroll);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
const handleEnableSpotFetchApi = async () => {
|
||||||
|
try {
|
||||||
|
await updateSettings({ useSpotFetchAPI: true });
|
||||||
|
metadata.setShowApiModal(false);
|
||||||
|
toast.success("SpotFetch API enabled! You can now try fetching again.");
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error("Failed to enable SpotFetch API:", err);
|
||||||
|
toast.error("Failed to update settings");
|
||||||
|
}
|
||||||
|
};
|
||||||
const scrollToTop = useCallback(() => {
|
const scrollToTop = useCallback(() => {
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
}, []);
|
}, []);
|
||||||
@@ -290,19 +301,19 @@ function App() {
|
|||||||
setSearchQuery(value);
|
setSearchQuery(value);
|
||||||
setCurrentListPage(1);
|
setCurrentListPage(1);
|
||||||
};
|
};
|
||||||
const toggleTrackSelection = (isrc: string) => {
|
const toggleTrackSelection = (id: string) => {
|
||||||
setSelectedTracks((prev) => prev.includes(isrc) ? prev.filter((id) => id !== isrc) : [...prev, isrc]);
|
setSelectedTracks((prev) => prev.includes(id) ? prev.filter((prevId) => prevId !== id) : [...prev, id]);
|
||||||
};
|
};
|
||||||
const toggleSelectAll = (tracks: any[]) => {
|
const toggleSelectAll = (tracks: any[]) => {
|
||||||
const tracksWithIsrc = tracks.filter((track) => track.isrc).map((track) => track.isrc);
|
const tracksWithId = tracks.filter((track) => track.spotify_id).map((track) => track.spotify_id || "");
|
||||||
if (tracksWithIsrc.length === 0)
|
if (tracksWithId.length === 0)
|
||||||
return;
|
return;
|
||||||
const allSelected = tracksWithIsrc.every(isrc => selectedTracks.includes(isrc));
|
const allSelected = tracksWithId.every(id => selectedTracks.includes(id));
|
||||||
if (allSelected) {
|
if (allSelected) {
|
||||||
setSelectedTracks(prev => prev.filter(isrc => !tracksWithIsrc.includes(isrc)));
|
setSelectedTracks(prev => prev.filter(id => !tracksWithId.includes(id)));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
setSelectedTracks(prev => Array.from(new Set([...prev, ...tracksWithIsrc])));
|
setSelectedTracks(prev => Array.from(new Set([...prev, ...tracksWithId])));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleOpenFolder = async () => {
|
const handleOpenFolder = async () => {
|
||||||
@@ -324,7 +335,8 @@ function App() {
|
|||||||
return null;
|
return null;
|
||||||
if ("track" in metadata.metadata) {
|
if ("track" in metadata.metadata) {
|
||||||
const { track } = metadata.metadata;
|
const { track } = metadata.metadata;
|
||||||
return (<TrackInfo track={track} isDownloading={download.isDownloading} downloadingTrack={download.downloadingTrack} isDownloaded={download.downloadedTracks.has(track.isrc)} isFailed={download.failedTracks.has(track.isrc)} isSkipped={download.skippedTracks.has(track.isrc)} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} downloadedLyrics={lyrics.downloadedLyrics.has(track.spotify_id || "")} failedLyrics={lyrics.failedLyrics.has(track.spotify_id || "")} skippedLyrics={lyrics.skippedLyrics.has(track.spotify_id || "")} checkingAvailability={availability.checkingTrackId === track.spotify_id} availability={availability.availabilityMap.get(track.spotify_id || "")} downloadingCover={cover.downloadingCoverTrack === (track.spotify_id || `${track.name}-${track.artists}`)} downloadedCover={cover.downloadedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} failedCover={cover.failedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} skippedCover={cover.skippedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} onDownload={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, albumArtist, releaseDate, discNumber) => lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, track.album_name, undefined, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _playlistName, _position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, track.album_name, undefined, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onOpenFolder={handleOpenFolder} onBack={metadata.resetMetadata}/>);
|
const trackId = track.spotify_id || "";
|
||||||
|
return (<TrackInfo track={track} isDownloading={download.isDownloading} downloadingTrack={download.downloadingTrack} isDownloaded={download.downloadedTracks.has(trackId)} isFailed={download.failedTracks.has(trackId)} isSkipped={download.skippedTracks.has(trackId)} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} downloadedLyrics={lyrics.downloadedLyrics.has(track.spotify_id || "")} failedLyrics={lyrics.failedLyrics.has(track.spotify_id || "")} skippedLyrics={lyrics.skippedLyrics.has(track.spotify_id || "")} checkingAvailability={availability.checkingTrackId === track.spotify_id} availability={availability.availabilityMap.get(track.spotify_id || "")} downloadingCover={cover.downloadingCoverTrack === (track.spotify_id || `${track.name}-${track.artists}`)} downloadedCover={cover.downloadedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} failedCover={cover.failedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} skippedCover={cover.skippedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} onDownload={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, albumArtist, releaseDate, discNumber) => lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, track.album_name, undefined, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _playlistName, _position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, track.album_name, undefined, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onOpenFolder={handleOpenFolder} onBack={metadata.resetMetadata}/>);
|
||||||
}
|
}
|
||||||
if ("album_info" in metadata.metadata) {
|
if ("album_info" in metadata.metadata) {
|
||||||
const { album_info, track_list } = metadata.metadata;
|
const { album_info, track_list } = metadata.metadata;
|
||||||
@@ -555,6 +567,25 @@ function App() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={metadata.showApiModal} onOpenChange={metadata.setShowApiModal}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>SpotFetch API Recommended</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Direct fetch failed. This usually happens when your <span className="text-foreground font-bold">country is blocked</span> by Spotify or your IP is restricted. Would you like to enable the <span className="text-foreground font-bold">SpotFetch API</span> to bypass this?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => metadata.setShowApiModal(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleEnableSpotFetchApi}>
|
||||||
|
Enable SpotFetch API
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>);
|
</TooltipProvider>);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ interface AlbumInfoProps {
|
|||||||
isBulkDownloadingLyrics?: boolean;
|
isBulkDownloadingLyrics?: boolean;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
onSortChange: (value: string) => void;
|
onSortChange: (value: string) => void;
|
||||||
onToggleTrack: (isrc: string) => void;
|
onToggleTrack: (id: string) => void;
|
||||||
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
||||||
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||||
onCheckAvailability?: (spotifyId: string) => void;
|
onCheckAvailability?: (spotifyId: string) => void;
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ interface ArtistInfoProps {
|
|||||||
isBulkDownloadingLyrics?: boolean;
|
isBulkDownloadingLyrics?: boolean;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
onSortChange: (value: string) => void;
|
onSortChange: (value: string) => void;
|
||||||
onToggleTrack: (isrc: string) => void;
|
onToggleTrack: (id: string) => void;
|
||||||
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
||||||
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||||
onCheckAvailability?: (spotifyId: string) => void;
|
onCheckAvailability?: (spotifyId: string) => void;
|
||||||
@@ -491,8 +491,8 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
|
|||||||
<ScrollArea className="flex-1 pr-4">
|
<ScrollArea className="flex-1 pr-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{filteredAlbumGroups.map(([albumName, data]) => {
|
{filteredAlbumGroups.map(([albumName, data]) => {
|
||||||
const tracksWithIsrc = data.tracks.filter(t => t.isrc);
|
const tracksWithId = data.tracks.filter(t => t.spotify_id);
|
||||||
const isSelected = tracksWithIsrc.length > 0 && tracksWithIsrc.every(t => selectedTracks.includes(t.isrc!));
|
const isSelected = tracksWithId.length > 0 && tracksWithId.every(t => selectedTracks.includes(t.spotify_id!));
|
||||||
return (<div key={albumName} className="flex items-start space-x-3 p-2 hover:bg-muted/50 rounded-md transition-colors">
|
return (<div key={albumName} className="flex items-start space-x-3 p-2 hover:bg-muted/50 rounded-md transition-colors">
|
||||||
<Checkbox id={`album-select-${albumName}`} checked={isSelected} onCheckedChange={() => onToggleSelectAll(data.tracks)} className="mt-1"/>
|
<Checkbox id={`album-select-${albumName}`} checked={isSelected} onCheckedChange={() => onToggleSelectAll(data.tracks)} className="mt-1"/>
|
||||||
<div className="grid gap-1.5 leading-none flex-1">
|
<div className="grid gap-1.5 leading-none flex-1">
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ interface PlaylistInfoProps {
|
|||||||
isBulkDownloadingLyrics?: boolean;
|
isBulkDownloadingLyrics?: boolean;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
onSortChange: (value: string) => void;
|
onSortChange: (value: string) => void;
|
||||||
onToggleTrack: (isrc: string) => void;
|
onToggleTrack: (id: string) => void;
|
||||||
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
||||||
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||||
onCheckAvailability?: (spotifyId: string) => void;
|
onCheckAvailability?: (spotifyId: string) => void;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { InputWithContext } from "@/components/ui/input-with-context";
|
import { InputWithContext } from "@/components/ui/input-with-context";
|
||||||
import { CloudDownload, XCircle, Link, Search, X, ChevronDown } from "lucide-react";
|
import { CloudDownload, XCircle, Link, Search, X, ChevronDown, } from "lucide-react";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip";
|
||||||
import { FetchHistory } from "@/components/FetchHistory";
|
import { FetchHistory } from "@/components/FetchHistory";
|
||||||
@@ -10,12 +10,13 @@ import { SearchSpotify, SearchSpotifyByType } from "../../wailsjs/go/main/App";
|
|||||||
import { backend } from "../../wailsjs/go/models";
|
import { backend } from "../../wailsjs/go/models";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useTypingEffect } from "@/hooks/useTypingEffect";
|
import { useTypingEffect } from "@/hooks/useTypingEffect";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
|
||||||
const FETCH_PLACEHOLDERS = [
|
const FETCH_PLACEHOLDERS = [
|
||||||
"https://open.spotify.com/track/...",
|
"https://open.spotify.com/track/...",
|
||||||
"https://open.spotify.com/album/...",
|
"https://open.spotify.com/album/...",
|
||||||
"https://open.spotify.com/playlist/...",
|
"https://open.spotify.com/playlist/...",
|
||||||
"https://open.spotify.com/artist/..."
|
"https://open.spotify.com/artist/...",
|
||||||
];
|
];
|
||||||
const SEARCH_PLACEHOLDERS = [
|
const SEARCH_PLACEHOLDERS = [
|
||||||
"Golden",
|
"Golden",
|
||||||
@@ -23,10 +24,194 @@ const SEARCH_PLACEHOLDERS = [
|
|||||||
"The Weeknd",
|
"The Weeknd",
|
||||||
"Starboy",
|
"Starboy",
|
||||||
"Joji",
|
"Joji",
|
||||||
"Die For You"
|
"Die For You",
|
||||||
];
|
];
|
||||||
const REGIONS = ["AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AU", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "ET", "FI", "FJ", "FM", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HK", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MG", "MH", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NR", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RS", "RW", "SA", "SB", "SC", "SE", "SG", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "VU", "WS", "XK", "ZA", "ZM", "ZW"];
|
const REGIONS = [
|
||||||
const regionNames = new Intl.DisplayNames(['en'], { type: 'region' });
|
"AD",
|
||||||
|
"AE",
|
||||||
|
"AG",
|
||||||
|
"AL",
|
||||||
|
"AM",
|
||||||
|
"AO",
|
||||||
|
"AR",
|
||||||
|
"AT",
|
||||||
|
"AU",
|
||||||
|
"AZ",
|
||||||
|
"BA",
|
||||||
|
"BB",
|
||||||
|
"BD",
|
||||||
|
"BE",
|
||||||
|
"BF",
|
||||||
|
"BG",
|
||||||
|
"BH",
|
||||||
|
"BI",
|
||||||
|
"BJ",
|
||||||
|
"BN",
|
||||||
|
"BO",
|
||||||
|
"BR",
|
||||||
|
"BS",
|
||||||
|
"BT",
|
||||||
|
"BW",
|
||||||
|
"BZ",
|
||||||
|
"CA",
|
||||||
|
"CD",
|
||||||
|
"CG",
|
||||||
|
"CH",
|
||||||
|
"CI",
|
||||||
|
"CL",
|
||||||
|
"CM",
|
||||||
|
"CO",
|
||||||
|
"CR",
|
||||||
|
"CV",
|
||||||
|
"CW",
|
||||||
|
"CY",
|
||||||
|
"CZ",
|
||||||
|
"DE",
|
||||||
|
"DJ",
|
||||||
|
"DK",
|
||||||
|
"DM",
|
||||||
|
"DO",
|
||||||
|
"DZ",
|
||||||
|
"EC",
|
||||||
|
"EE",
|
||||||
|
"EG",
|
||||||
|
"ES",
|
||||||
|
"ET",
|
||||||
|
"FI",
|
||||||
|
"FJ",
|
||||||
|
"FM",
|
||||||
|
"FR",
|
||||||
|
"GA",
|
||||||
|
"GB",
|
||||||
|
"GD",
|
||||||
|
"GE",
|
||||||
|
"GH",
|
||||||
|
"GM",
|
||||||
|
"GN",
|
||||||
|
"GQ",
|
||||||
|
"GR",
|
||||||
|
"GT",
|
||||||
|
"GW",
|
||||||
|
"GY",
|
||||||
|
"HK",
|
||||||
|
"HN",
|
||||||
|
"HR",
|
||||||
|
"HT",
|
||||||
|
"HU",
|
||||||
|
"ID",
|
||||||
|
"IE",
|
||||||
|
"IL",
|
||||||
|
"IN",
|
||||||
|
"IQ",
|
||||||
|
"IS",
|
||||||
|
"IT",
|
||||||
|
"JM",
|
||||||
|
"JO",
|
||||||
|
"JP",
|
||||||
|
"KE",
|
||||||
|
"KG",
|
||||||
|
"KH",
|
||||||
|
"KI",
|
||||||
|
"KM",
|
||||||
|
"KN",
|
||||||
|
"KR",
|
||||||
|
"KW",
|
||||||
|
"KZ",
|
||||||
|
"LA",
|
||||||
|
"LB",
|
||||||
|
"LC",
|
||||||
|
"LI",
|
||||||
|
"LK",
|
||||||
|
"LR",
|
||||||
|
"LS",
|
||||||
|
"LT",
|
||||||
|
"LU",
|
||||||
|
"LV",
|
||||||
|
"LY",
|
||||||
|
"MA",
|
||||||
|
"MC",
|
||||||
|
"MD",
|
||||||
|
"ME",
|
||||||
|
"MG",
|
||||||
|
"MH",
|
||||||
|
"MK",
|
||||||
|
"ML",
|
||||||
|
"MN",
|
||||||
|
"MO",
|
||||||
|
"MR",
|
||||||
|
"MT",
|
||||||
|
"MU",
|
||||||
|
"MV",
|
||||||
|
"MW",
|
||||||
|
"MX",
|
||||||
|
"MY",
|
||||||
|
"MZ",
|
||||||
|
"NA",
|
||||||
|
"NE",
|
||||||
|
"NG",
|
||||||
|
"NI",
|
||||||
|
"NL",
|
||||||
|
"NO",
|
||||||
|
"NP",
|
||||||
|
"NR",
|
||||||
|
"NZ",
|
||||||
|
"OM",
|
||||||
|
"PA",
|
||||||
|
"PE",
|
||||||
|
"PG",
|
||||||
|
"PH",
|
||||||
|
"PK",
|
||||||
|
"PL",
|
||||||
|
"PS",
|
||||||
|
"PT",
|
||||||
|
"PW",
|
||||||
|
"PY",
|
||||||
|
"QA",
|
||||||
|
"RO",
|
||||||
|
"RS",
|
||||||
|
"RW",
|
||||||
|
"SA",
|
||||||
|
"SB",
|
||||||
|
"SC",
|
||||||
|
"SE",
|
||||||
|
"SG",
|
||||||
|
"SI",
|
||||||
|
"SK",
|
||||||
|
"SL",
|
||||||
|
"SM",
|
||||||
|
"SN",
|
||||||
|
"SR",
|
||||||
|
"ST",
|
||||||
|
"SV",
|
||||||
|
"SZ",
|
||||||
|
"TD",
|
||||||
|
"TG",
|
||||||
|
"TH",
|
||||||
|
"TJ",
|
||||||
|
"TL",
|
||||||
|
"TN",
|
||||||
|
"TO",
|
||||||
|
"TR",
|
||||||
|
"TT",
|
||||||
|
"TV",
|
||||||
|
"TW",
|
||||||
|
"TZ",
|
||||||
|
"UA",
|
||||||
|
"UG",
|
||||||
|
"US",
|
||||||
|
"UY",
|
||||||
|
"UZ",
|
||||||
|
"VC",
|
||||||
|
"VE",
|
||||||
|
"VN",
|
||||||
|
"VU",
|
||||||
|
"WS",
|
||||||
|
"XK",
|
||||||
|
"ZA",
|
||||||
|
"ZM",
|
||||||
|
"ZW",
|
||||||
|
];
|
||||||
|
const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
|
||||||
const getRegionName = (code: string) => {
|
const getRegionName = (code: string) => {
|
||||||
try {
|
try {
|
||||||
if (code === "XK")
|
if (code === "XK")
|
||||||
@@ -56,7 +241,7 @@ interface SearchBarProps {
|
|||||||
region: string;
|
region: string;
|
||||||
onRegionChange: (region: string) => void;
|
onRegionChange: (region: string) => void;
|
||||||
}
|
}
|
||||||
export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, history, onHistorySelect, onHistoryRemove, hasResult, searchMode, onSearchModeChange, region, onRegionChange }: SearchBarProps) {
|
export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, history, onHistorySelect, onHistoryRemove, hasResult, searchMode, onSearchModeChange, region, onRegionChange, }: SearchBarProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [searchResults, setSearchResults] = useState<backend.SearchResponse | null>(null);
|
const [searchResults, setSearchResults] = useState<backend.SearchResponse | null>(null);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
@@ -70,6 +255,8 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
|||||||
artists: false,
|
artists: false,
|
||||||
playlists: false,
|
playlists: false,
|
||||||
});
|
});
|
||||||
|
const [showInvalidUrlDialog, setShowInvalidUrlDialog] = useState(false);
|
||||||
|
const [invalidUrl, setInvalidUrl] = useState("");
|
||||||
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const placeholders = searchMode ? SEARCH_PLACEHOLDERS : FETCH_PLACEHOLDERS;
|
const placeholders = searchMode ? SEARCH_PLACEHOLDERS : FETCH_PLACEHOLDERS;
|
||||||
const placeholderText = useTypingEffect(placeholders);
|
const placeholderText = useTypingEffect(placeholders);
|
||||||
@@ -125,7 +312,10 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
|||||||
searchTimeoutRef.current = setTimeout(async () => {
|
searchTimeoutRef.current = setTimeout(async () => {
|
||||||
setIsSearching(true);
|
setIsSearching(true);
|
||||||
try {
|
try {
|
||||||
const results = await SearchSpotify({ query: searchQuery, limit: SEARCH_LIMIT });
|
const results = await SearchSpotify({
|
||||||
|
query: searchQuery,
|
||||||
|
limit: SEARCH_LIMIT,
|
||||||
|
});
|
||||||
setSearchResults(results);
|
setSearchResults(results);
|
||||||
setLastSearchedQuery(searchQuery.trim());
|
setLastSearchedQuery(searchQuery.trim());
|
||||||
saveRecentSearch(searchQuery.trim());
|
saveRecentSearch(searchQuery.trim());
|
||||||
@@ -181,10 +371,18 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
|||||||
if (!prev)
|
if (!prev)
|
||||||
return prev;
|
return prev;
|
||||||
const updated = new backend.SearchResponse({
|
const updated = new backend.SearchResponse({
|
||||||
tracks: activeTab === "tracks" ? [...prev.tracks, ...moreResults] : prev.tracks,
|
tracks: activeTab === "tracks"
|
||||||
albums: activeTab === "albums" ? [...prev.albums, ...moreResults] : prev.albums,
|
? [...prev.tracks, ...moreResults]
|
||||||
artists: activeTab === "artists" ? [...prev.artists, ...moreResults] : prev.artists,
|
: prev.tracks,
|
||||||
playlists: activeTab === "playlists" ? [...prev.playlists, ...moreResults] : prev.playlists,
|
albums: activeTab === "albums"
|
||||||
|
? [...prev.albums, ...moreResults]
|
||||||
|
: prev.albums,
|
||||||
|
artists: activeTab === "artists"
|
||||||
|
? [...prev.artists, ...moreResults]
|
||||||
|
: prev.artists,
|
||||||
|
playlists: activeTab === "playlists"
|
||||||
|
? [...prev.playlists, ...moreResults]
|
||||||
|
: prev.playlists,
|
||||||
});
|
});
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
@@ -201,6 +399,35 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
|||||||
setIsLoadingMore(false);
|
setIsLoadingMore(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const isSpotifyUrl = (text: string) => {
|
||||||
|
const trimmed = text.trim();
|
||||||
|
if (!trimmed)
|
||||||
|
return true;
|
||||||
|
const isUrl = /^(https?:\/\/|www\.)/i.test(trimmed) || /^spotify:/i.test(trimmed);
|
||||||
|
if (!isUrl)
|
||||||
|
return true;
|
||||||
|
return (trimmed.includes("spotify.com") ||
|
||||||
|
trimmed.includes("spotify.link") ||
|
||||||
|
trimmed.startsWith("spotify:"));
|
||||||
|
};
|
||||||
|
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||||
|
if (searchMode)
|
||||||
|
return;
|
||||||
|
const pastedText = e.clipboardData.getData("text");
|
||||||
|
if (pastedText && !isSpotifyUrl(pastedText)) {
|
||||||
|
e.preventDefault();
|
||||||
|
setInvalidUrl(pastedText);
|
||||||
|
setShowInvalidUrlDialog(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleFetchWithValidation = () => {
|
||||||
|
if (!isSpotifyUrl(url)) {
|
||||||
|
setInvalidUrl(url);
|
||||||
|
setShowInvalidUrlDialog(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onFetch();
|
||||||
|
};
|
||||||
const handleResultClick = (externalUrl: string) => {
|
const handleResultClick = (externalUrl: string) => {
|
||||||
onSearchModeChange(false);
|
onSearchModeChange(false);
|
||||||
onFetchUrl(externalUrl);
|
onFetchUrl(externalUrl);
|
||||||
@@ -210,7 +437,8 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
|||||||
const seconds = Math.floor((ms % 60000) / 1000);
|
const seconds = Math.floor((ms % 60000) / 1000);
|
||||||
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||||
};
|
};
|
||||||
const hasAnyResults = searchResults && (searchResults.tracks.length > 0 ||
|
const hasAnyResults = searchResults &&
|
||||||
|
(searchResults.tracks.length > 0 ||
|
||||||
searchResults.albums.length > 0 ||
|
searchResults.albums.length > 0 ||
|
||||||
searchResults.artists.length > 0 ||
|
searchResults.artists.length > 0 ||
|
||||||
searchResults.playlists.length > 0);
|
searchResults.playlists.length > 0);
|
||||||
@@ -218,10 +446,14 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
|||||||
if (!searchResults)
|
if (!searchResults)
|
||||||
return 0;
|
return 0;
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case "tracks": return searchResults.tracks.length;
|
case "tracks":
|
||||||
case "albums": return searchResults.albums.length;
|
return searchResults.tracks.length;
|
||||||
case "artists": return searchResults.artists.length;
|
case "albums":
|
||||||
case "playlists": return searchResults.playlists.length;
|
return searchResults.albums.length;
|
||||||
|
case "artists":
|
||||||
|
return searchResults.artists.length;
|
||||||
|
case "playlists":
|
||||||
|
return searchResults.playlists.length;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const tabs: {
|
const tabs: {
|
||||||
@@ -238,7 +470,7 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="outline" size="icon" className="shrink-0" onClick={() => onSearchModeChange(!searchMode)}>
|
<Button variant="outline" size="icon" className="shrink-0" onClick={() => onSearchModeChange(!searchMode)}>
|
||||||
{searchMode ? <Link className="h-4 w-4"/> : <Search className="h-4 w-4"/>}
|
{searchMode ? (<Link className="h-4 w-4"/>) : (<Search className="h-4 w-4"/>)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -248,7 +480,7 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
|||||||
|
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
{!searchMode ? (<>
|
{!searchMode ? (<>
|
||||||
<InputWithContext id="spotify-url" placeholder={placeholderText} value={url} onChange={(e) => onUrlChange(e.target.value)} onKeyDown={(e) => e.key === "Enter" && onFetch()} className="pr-8"/>
|
<InputWithContext id="spotify-url" placeholder={placeholderText} value={url} onChange={(e) => onUrlChange(e.target.value)} onPaste={handlePaste} onKeyDown={(e) => e.key === "Enter" && handleFetchWithValidation()} className="pr-8"/>
|
||||||
{url && (<button type="button" className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" onClick={() => onUrlChange("")}>
|
{url && (<button type="button" className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" onClick={() => onUrlChange("")}>
|
||||||
<XCircle className="h-4 w-4"/>
|
<XCircle className="h-4 w-4"/>
|
||||||
</button>)}
|
</button>)}
|
||||||
@@ -271,11 +503,14 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-[300px]">
|
<SelectContent className="max-h-[300px]">
|
||||||
{REGIONS.map((r) => (<SelectItem key={r} value={r} textValue={r}>
|
{REGIONS.map((r) => (<SelectItem key={r} value={r} textValue={r}>
|
||||||
{r} <span className="text-muted-foreground">({getRegionName(r)})</span>
|
{r}{" "}
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
({getRegionName(r)})
|
||||||
|
</span>
|
||||||
</SelectItem>))}
|
</SelectItem>))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button onClick={onFetch} disabled={loading}>
|
<Button onClick={handleFetchWithValidation} disabled={loading}>
|
||||||
{loading ? (<>
|
{loading ? (<>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
Fetching...
|
Fetching...
|
||||||
@@ -396,5 +631,36 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
|||||||
</div>)}
|
</div>)}
|
||||||
</>)}
|
</>)}
|
||||||
</div>)}
|
</div>)}
|
||||||
|
|
||||||
|
<Dialog open={showInvalidUrlDialog} onOpenChange={setShowInvalidUrlDialog}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Invalid URL</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Only Spotify links are allowed in Fetch mode.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{invalidUrl && (<div className="p-3 bg-muted rounded-md border text-xs font-mono break-all opacity-70">
|
||||||
|
{invalidUrl}
|
||||||
|
</div>)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => {
|
||||||
|
setShowInvalidUrlDialog(false);
|
||||||
|
setInvalidUrl("");
|
||||||
|
}}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => {
|
||||||
|
onSearchModeChange(true);
|
||||||
|
setShowInvalidUrlDialog(false);
|
||||||
|
setInvalidUrl("");
|
||||||
|
}}>
|
||||||
|
Switch to Search
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ export function TitleBar() {
|
|||||||
if (settings) {
|
if (settings) {
|
||||||
setUseSpotFetchAPI(settings.useSpotFetchAPI || false);
|
setUseSpotFetchAPI(settings.useSpotFetchAPI || false);
|
||||||
}
|
}
|
||||||
|
const handleSettingsUpdate = (event: any) => {
|
||||||
|
const updatedSettings = event.detail;
|
||||||
|
if (updatedSettings && typeof updatedSettings.useSpotFetchAPI !== 'undefined') {
|
||||||
|
setUseSpotFetchAPI(updatedSettings.useSpotFetchAPI);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('settingsUpdated', handleSettingsUpdate);
|
||||||
|
return () => window.removeEventListener('settingsUpdated', handleSettingsUpdate);
|
||||||
}, []);
|
}, []);
|
||||||
const handleSpotFetchAPIToggle = () => {
|
const handleSpotFetchAPIToggle = () => {
|
||||||
const newValue = !useSpotFetchAPI;
|
const newValue = !useSpotFetchAPI;
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ interface TrackInfoProps {
|
|||||||
downloadedCover?: boolean;
|
downloadedCover?: boolean;
|
||||||
failedCover?: boolean;
|
failedCover?: boolean;
|
||||||
skippedCover?: boolean;
|
skippedCover?: boolean;
|
||||||
onDownload: (isrc: string, name: string, artists: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
onDownload: (id: string, name: string, artists: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||||
onCheckAvailability?: (spotifyId: string, isrc?: string) => void;
|
onCheckAvailability?: (spotifyId: string) => void;
|
||||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName?: string, playlistName?: string, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName?: string, playlistName?: string, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||||
onOpenFolder: () => void;
|
onOpenFolder: () => void;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
@@ -95,9 +95,9 @@ export function TrackInfo({ track, isDownloading, downloadingTrack, isDownloaded
|
|||||||
</div>)}
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{track.isrc && (<div className="flex gap-2 flex-wrap">
|
{track.spotify_id && (<div className="flex gap-2 flex-wrap">
|
||||||
<Button onClick={() => onDownload(track.isrc, track.name, track.artists, track.album_name, track.spotify_id, undefined, track.duration_ms, track.track_number, track.album_artist, track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher)} disabled={isDownloading || downloadingTrack === track.isrc}>
|
<Button onClick={() => onDownload(track.spotify_id || "", track.name, track.artists, track.album_name, track.spotify_id, undefined, track.duration_ms, track.track_number, track.album_artist, track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher)} disabled={isDownloading || downloadingTrack === track.spotify_id}>
|
||||||
{downloadingTrack === track.isrc ? (<Spinner />) : (<>
|
{downloadingTrack === track.spotify_id ? (<Spinner />) : (<>
|
||||||
<Download className="h-4 w-4"/>
|
<Download className="h-4 w-4"/>
|
||||||
Download
|
Download
|
||||||
</>)}
|
</>)}
|
||||||
@@ -134,7 +134,7 @@ export function TrackInfo({ track, isDownloading, downloadingTrack, isDownloaded
|
|||||||
</Tooltip>)}
|
</Tooltip>)}
|
||||||
{track.spotify_id && onCheckAvailability && (<Tooltip>
|
{track.spotify_id && onCheckAvailability && (<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button onClick={() => onCheckAvailability(track.spotify_id!, track.isrc)} variant="outline" size="icon" disabled={checkingAvailability}>
|
<Button onClick={() => onCheckAvailability(track.spotify_id!)} variant="outline" size="icon" disabled={checkingAvailability}>
|
||||||
{checkingAvailability ? (<Spinner />) : availability ? (<CheckCircle className="h-4 w-4 text-green-500"/>) : (<Globe className="h-4 w-4"/>)}
|
{checkingAvailability ? (<Spinner />) : availability ? (<CheckCircle className="h-4 w-4 text-green-500"/>) : (<Globe className="h-4 w-4"/>)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ interface TrackListProps {
|
|||||||
failedCovers?: Set<string>;
|
failedCovers?: Set<string>;
|
||||||
skippedCovers?: Set<string>;
|
skippedCovers?: Set<string>;
|
||||||
downloadingCoverTrack?: string | null;
|
downloadingCoverTrack?: string | null;
|
||||||
onToggleTrack: (isrc: string) => void;
|
onToggleTrack: (id: string) => void;
|
||||||
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
||||||
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||||
onCheckAvailability?: (spotifyId: string, isrc?: string) => void;
|
onCheckAvailability?: (spotifyId: string) => void;
|
||||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||||
onPageChange: (page: number) => void;
|
onPageChange: (page: number) => void;
|
||||||
onAlbumClick?: (album: {
|
onAlbumClick?: (album: {
|
||||||
@@ -104,15 +104,15 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
|||||||
}
|
}
|
||||||
else if (sortBy === "downloaded") {
|
else if (sortBy === "downloaded") {
|
||||||
filteredTracks = [...filteredTracks].sort((a, b) => {
|
filteredTracks = [...filteredTracks].sort((a, b) => {
|
||||||
const aDownloaded = downloadedTracks.has(a.isrc);
|
const aDownloaded = a.spotify_id ? downloadedTracks.has(a.spotify_id) : false;
|
||||||
const bDownloaded = downloadedTracks.has(b.isrc);
|
const bDownloaded = b.spotify_id ? downloadedTracks.has(b.spotify_id) : false;
|
||||||
return (bDownloaded ? 1 : 0) - (aDownloaded ? 1 : 0);
|
return (bDownloaded ? 1 : 0) - (aDownloaded ? 1 : 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (sortBy === "not-downloaded") {
|
else if (sortBy === "not-downloaded") {
|
||||||
filteredTracks = [...filteredTracks].sort((a, b) => {
|
filteredTracks = [...filteredTracks].sort((a, b) => {
|
||||||
const aDownloaded = downloadedTracks.has(a.isrc);
|
const aDownloaded = a.spotify_id ? downloadedTracks.has(a.spotify_id) : false;
|
||||||
const bDownloaded = downloadedTracks.has(b.isrc);
|
const bDownloaded = b.spotify_id ? downloadedTracks.has(b.spotify_id) : false;
|
||||||
return (aDownloaded ? 1 : 0) - (bDownloaded ? 1 : 0);
|
return (aDownloaded ? 1 : 0) - (bDownloaded ? 1 : 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -149,9 +149,9 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
|||||||
}
|
}
|
||||||
return pages;
|
return pages;
|
||||||
};
|
};
|
||||||
const tracksWithIsrc = filteredTracks.filter((track) => track.isrc);
|
const tracksWithId = filteredTracks.filter((track) => track.spotify_id);
|
||||||
const allSelected = tracksWithIsrc.length > 0 &&
|
const allSelected = tracksWithId.length > 0 &&
|
||||||
tracksWithIsrc.every((track) => selectedTracks.includes(track.isrc));
|
tracksWithId.every((track) => selectedTracks.includes(track.spotify_id!));
|
||||||
const formatDuration = (ms: number) => {
|
const formatDuration = (ms: number) => {
|
||||||
const minutes = Math.floor(ms / 60000);
|
const minutes = Math.floor(ms / 60000);
|
||||||
const seconds = Math.floor((ms % 60000) / 1000);
|
const seconds = Math.floor((ms % 60000) / 1000);
|
||||||
@@ -197,7 +197,7 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
|||||||
<tbody>
|
<tbody>
|
||||||
{paginatedTracks.map((track, index) => (<tr key={index} className="border-b transition-colors hover:bg-muted/50">
|
{paginatedTracks.map((track, index) => (<tr key={index} className="border-b transition-colors hover:bg-muted/50">
|
||||||
{showCheckboxes && (<td className="p-4 align-middle">
|
{showCheckboxes && (<td className="p-4 align-middle">
|
||||||
{track.isrc && (<Checkbox checked={selectedTracks.includes(track.isrc)} onCheckedChange={() => onToggleTrack(track.isrc)}/>)}
|
{track.spotify_id && (<Checkbox checked={selectedTracks.includes(track.spotify_id)} onCheckedChange={() => onToggleTrack(track.spotify_id!)}/>)}
|
||||||
</td>)}
|
</td>)}
|
||||||
<td className="p-4 align-middle text-sm text-muted-foreground">
|
<td className="p-4 align-middle text-sm text-muted-foreground">
|
||||||
<div className="flex flex-col items-center gap-0.5">
|
<div className="flex flex-col items-center gap-0.5">
|
||||||
@@ -223,7 +223,7 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
|||||||
</span>) : (<span className="font-medium">{track.name}</span>)}
|
</span>) : (<span className="font-medium">{track.name}</span>)}
|
||||||
{track.is_explicit && (<span className="inline-flex items-center justify-center bg-red-600 text-white text-[10px] h-4 w-4 rounded shrink-0" title="Explicit">E</span>)}
|
{track.is_explicit && (<span className="inline-flex items-center justify-center bg-red-600 text-white text-[10px] h-4 w-4 rounded shrink-0" title="Explicit">E</span>)}
|
||||||
|
|
||||||
{skippedTracks.has(track.isrc) ? (<FileCheck className="h-4 w-4 text-yellow-500 shrink-0"/>) : downloadedTracks.has(track.isrc) ? (<CheckCircle className="h-4 w-4 text-green-500 shrink-0"/>) : failedTracks.has(track.isrc) ? (<XCircle className="h-4 w-4 text-red-500 shrink-0"/>) : null}
|
{track.spotify_id && skippedTracks.has(track.spotify_id) ? (<FileCheck className="h-4 w-4 text-yellow-500 shrink-0"/>) : track.spotify_id && downloadedTracks.has(track.spotify_id) ? (<CheckCircle className="h-4 w-4 text-green-500 shrink-0"/>) : track.spotify_id && failedTracks.has(track.spotify_id) ? (<XCircle className="h-4 w-4 text-red-500 shrink-0"/>) : null}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{track.artists_data && track.artists_data.length > 0 ? ((() => {
|
{track.artists_data && track.artists_data.length > 0 ? ((() => {
|
||||||
@@ -270,14 +270,14 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
|||||||
</td>
|
</td>
|
||||||
<td className="p-4 align-middle text-center">
|
<td className="p-4 align-middle text-center">
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1">
|
||||||
{track.isrc && (<Tooltip>
|
{track.spotify_id && (<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button onClick={() => onDownloadTrack(track.isrc, track.name, track.artists, track.album_name, track.spotify_id, folderName, track.duration_ms, startIndex + index + 1, track.album_artist, track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher)} size="icon" disabled={isDownloading || downloadingTrack === track.isrc}>
|
<Button onClick={() => onDownloadTrack(track.spotify_id!, track.name, track.artists, track.album_name, track.spotify_id, folderName, track.duration_ms, startIndex + index + 1, track.album_artist, track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher)} size="icon" disabled={isDownloading || downloadingTrack === track.spotify_id}>
|
||||||
{downloadingTrack === track.isrc ? (<Spinner />) : skippedTracks.has(track.isrc) ? (<FileCheck className="h-4 w-4"/>) : downloadedTracks.has(track.isrc) ? (<CheckCircle className="h-4 w-4"/>) : failedTracks.has(track.isrc) ? (<XCircle className="h-4 w-4"/>) : (<Download className="h-4 w-4"/>)}
|
{downloadingTrack === track.spotify_id ? (<Spinner />) : skippedTracks.has(track.spotify_id) ? (<FileCheck className="h-4 w-4"/>) : downloadedTracks.has(track.spotify_id) ? (<CheckCircle className="h-4 w-4"/>) : failedTracks.has(track.spotify_id) ? (<XCircle className="h-4 w-4"/>) : (<Download className="h-4 w-4"/>)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{downloadingTrack === track.isrc ? (<p>Downloading...</p>) : skippedTracks.has(track.isrc) ? (<p>Already exists</p>) : downloadedTracks.has(track.isrc) ? (<p>Downloaded</p>) : failedTracks.has(track.isrc) ? (<p>Failed</p>) : (<p>Download Track</p>)}
|
{downloadingTrack === track.spotify_id ? (<p>Downloading...</p>) : skippedTracks.has(track.spotify_id) ? (<p>Already exists</p>) : downloadedTracks.has(track.spotify_id) ? (<p>Downloaded</p>) : failedTracks.has(track.spotify_id) ? (<p>Failed</p>) : (<p>Download Track</p>)}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>)}
|
</Tooltip>)}
|
||||||
{track.spotify_id && (<Tooltip>
|
{track.spotify_id && (<Tooltip>
|
||||||
@@ -315,7 +315,7 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
|||||||
</Tooltip>)}
|
</Tooltip>)}
|
||||||
{track.spotify_id && onCheckAvailability && (<Tooltip>
|
{track.spotify_id && onCheckAvailability && (<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button onClick={() => onCheckAvailability(track.spotify_id!, track.isrc)} size="icon" variant="outline" disabled={checkingAvailabilityTrack === track.spotify_id}>
|
<Button onClick={() => onCheckAvailability(track.spotify_id!)} size="icon" variant="outline" disabled={checkingAvailabilityTrack === track.spotify_id}>
|
||||||
{checkingAvailabilityTrack === track.spotify_id ? (<Spinner />) : availabilityMap?.has(track.spotify_id) ? (<CheckCircle className="h-4 w-4 text-green-500"/>) : (<Globe className="h-4 w-4"/>)}
|
{checkingAvailabilityTrack === track.spotify_id ? (<Spinner />) : availabilityMap?.has(track.spotify_id) ? (<CheckCircle className="h-4 w-4 text-green-500"/>) : (<Globe className="h-4 w-4"/>)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function useAvailability() {
|
|||||||
const [checkingTrackId, setCheckingTrackId] = useState<string | null>(null);
|
const [checkingTrackId, setCheckingTrackId] = useState<string | null>(null);
|
||||||
const [availabilityMap, setAvailabilityMap] = useState<Map<string, TrackAvailability>>(new Map());
|
const [availabilityMap, setAvailabilityMap] = useState<Map<string, TrackAvailability>>(new Map());
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const checkAvailability = useCallback(async (spotifyId: string, isrc?: string) => {
|
const checkAvailability = useCallback(async (spotifyId: string) => {
|
||||||
if (!spotifyId) {
|
if (!spotifyId) {
|
||||||
setError("No Spotify ID provided");
|
setError("No Spotify ID provided");
|
||||||
return null;
|
return null;
|
||||||
@@ -20,7 +20,7 @@ export function useAvailability() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
logger.info(`Checking availability for track: ${spotifyId}`);
|
logger.info(`Checking availability for track: ${spotifyId}`);
|
||||||
const response = await CheckTrackAvailability(spotifyId, isrc || "");
|
const response = await CheckTrackAvailability(spotifyId);
|
||||||
const availability: TrackAvailability = JSON.parse(response);
|
const availability: TrackAvailability = JSON.parse(response);
|
||||||
setAvailabilityMap((prev) => {
|
setAvailabilityMap((prev) => {
|
||||||
const newMap = new Map(prev);
|
const newMap = new Map(prev);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function useDownload(region: string) {
|
|||||||
artists: string;
|
artists: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const shouldStopDownloadRef = useRef(false);
|
const shouldStopDownloadRef = useRef(false);
|
||||||
const downloadWithAutoFallback = async (isrc: string, settings: any, trackName?: string, artistName?: string, albumName?: string, playlistName?: string, position?: number, spotifyId?: string, durationMs?: number, releaseYear?: string, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => {
|
const downloadWithAutoFallback = async (id: string, settings: any, trackName?: string, artistName?: string, albumName?: string, playlistName?: string, position?: number, spotifyId?: string, durationMs?: number, releaseYear?: string, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => {
|
||||||
const service = settings.downloader;
|
const service = settings.downloader;
|
||||||
const query = trackName && artistName ? `${trackName} ${artistName} ` : undefined;
|
const query = trackName && artistName ? `${trackName} ${artistName} ` : undefined;
|
||||||
const os = settings.operatingSystem;
|
const os = settings.operatingSystem;
|
||||||
@@ -117,7 +117,7 @@ export function useDownload(region: string) {
|
|||||||
if (trackName && artistName) {
|
if (trackName && artistName) {
|
||||||
try {
|
try {
|
||||||
const checkRequest: CheckFileExistenceRequest = {
|
const checkRequest: CheckFileExistenceRequest = {
|
||||||
spotify_id: spotifyId || isrc,
|
spotify_id: spotifyId || id,
|
||||||
track_name: trackName,
|
track_name: trackName,
|
||||||
artist_name: displayArtist || "",
|
artist_name: displayArtist || "",
|
||||||
album_name: albumName,
|
album_name: albumName,
|
||||||
@@ -149,7 +149,7 @@ export function useDownload(region: string) {
|
|||||||
const { AddToDownloadQueue } = await import("../../wailsjs/go/main/App");
|
const { AddToDownloadQueue } = await import("../../wailsjs/go/main/App");
|
||||||
let itemID: string | undefined;
|
let itemID: string | undefined;
|
||||||
if (!fileExists) {
|
if (!fileExists) {
|
||||||
itemID = await AddToDownloadQueue(isrc, trackName || "", displayArtist || "", albumName || "");
|
itemID = await AddToDownloadQueue(id, trackName || "", displayArtist || "", albumName || "");
|
||||||
}
|
}
|
||||||
if (service === "auto") {
|
if (service === "auto") {
|
||||||
let streamingURLs: any = null;
|
let streamingURLs: any = null;
|
||||||
@@ -174,13 +174,12 @@ export function useDownload(region: string) {
|
|||||||
try {
|
try {
|
||||||
logger.debug(`trying tidal for: ${trackName} - ${artistName}`);
|
logger.debug(`trying tidal for: ${trackName} - ${artistName}`);
|
||||||
const response = await downloadTrack({
|
const response = await downloadTrack({
|
||||||
isrc,
|
|
||||||
service: "tidal",
|
service: "tidal",
|
||||||
query,
|
query,
|
||||||
track_name: trackName,
|
track_name: trackName,
|
||||||
artist_name: artistName,
|
artist_name: displayArtist,
|
||||||
album_name: albumName,
|
album_name: albumName,
|
||||||
album_artist: albumArtist,
|
album_artist: displayAlbumArtist,
|
||||||
release_date: finalReleaseDate || releaseDate,
|
release_date: finalReleaseDate || releaseDate,
|
||||||
cover_url: coverUrl,
|
cover_url: coverUrl,
|
||||||
output_dir: outputDir,
|
output_dir: outputDir,
|
||||||
@@ -201,6 +200,7 @@ export function useDownload(region: string) {
|
|||||||
spotify_total_discs: spotifyTotalDiscs,
|
spotify_total_discs: spotifyTotalDiscs,
|
||||||
copyright: copyright,
|
copyright: copyright,
|
||||||
publisher: publisher,
|
publisher: publisher,
|
||||||
|
use_first_artist_only: settings.useFirstArtistOnly,
|
||||||
});
|
});
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
logger.success(`tidal: ${trackName} - ${artistName}`);
|
logger.success(`tidal: ${trackName} - ${artistName}`);
|
||||||
@@ -218,13 +218,12 @@ export function useDownload(region: string) {
|
|||||||
try {
|
try {
|
||||||
logger.debug(`trying amazon for: ${trackName} - ${artistName}`);
|
logger.debug(`trying amazon for: ${trackName} - ${artistName}`);
|
||||||
const response = await downloadTrack({
|
const response = await downloadTrack({
|
||||||
isrc,
|
|
||||||
service: "amazon",
|
service: "amazon",
|
||||||
query,
|
query,
|
||||||
track_name: trackName,
|
track_name: trackName,
|
||||||
artist_name: artistName,
|
artist_name: displayArtist,
|
||||||
album_name: albumName,
|
album_name: albumName,
|
||||||
album_artist: albumArtist,
|
album_artist: displayAlbumArtist,
|
||||||
release_date: finalReleaseDate || releaseDate,
|
release_date: finalReleaseDate || releaseDate,
|
||||||
cover_url: coverUrl,
|
cover_url: coverUrl,
|
||||||
output_dir: outputDir,
|
output_dir: outputDir,
|
||||||
@@ -260,13 +259,12 @@ export function useDownload(region: string) {
|
|||||||
try {
|
try {
|
||||||
logger.debug(`trying qobuz for: ${trackName} - ${artistName}`);
|
logger.debug(`trying qobuz for: ${trackName} - ${artistName}`);
|
||||||
const response = await downloadTrack({
|
const response = await downloadTrack({
|
||||||
isrc,
|
|
||||||
service: "qobuz",
|
service: "qobuz",
|
||||||
query,
|
query,
|
||||||
track_name: trackName,
|
track_name: trackName,
|
||||||
artist_name: artistName,
|
artist_name: displayArtist,
|
||||||
album_name: albumName,
|
album_name: albumName,
|
||||||
album_artist: albumArtist,
|
album_artist: displayAlbumArtist,
|
||||||
release_date: finalReleaseDate || releaseDate,
|
release_date: finalReleaseDate || releaseDate,
|
||||||
cover_url: coverUrl,
|
cover_url: coverUrl,
|
||||||
output_dir: outputDir,
|
output_dir: outputDir,
|
||||||
@@ -314,13 +312,12 @@ export function useDownload(region: string) {
|
|||||||
audioFormat = settings.qobuzQuality || "6";
|
audioFormat = settings.qobuzQuality || "6";
|
||||||
}
|
}
|
||||||
const singleServiceResponse = await downloadTrack({
|
const singleServiceResponse = await downloadTrack({
|
||||||
isrc,
|
|
||||||
service: service as "tidal" | "qobuz" | "amazon",
|
service: service as "tidal" | "qobuz" | "amazon",
|
||||||
query,
|
query,
|
||||||
track_name: trackName,
|
track_name: trackName,
|
||||||
artist_name: artistName,
|
artist_name: displayArtist,
|
||||||
album_name: albumName,
|
album_name: albumName,
|
||||||
album_artist: albumArtist,
|
album_artist: displayAlbumArtist,
|
||||||
release_date: finalReleaseDate || releaseDate,
|
release_date: finalReleaseDate || releaseDate,
|
||||||
cover_url: coverUrl,
|
cover_url: coverUrl,
|
||||||
output_dir: outputDir,
|
output_dir: outputDir,
|
||||||
@@ -347,7 +344,7 @@ export function useDownload(region: string) {
|
|||||||
}
|
}
|
||||||
return singleServiceResponse;
|
return singleServiceResponse;
|
||||||
};
|
};
|
||||||
const downloadWithItemID = async (isrc: string, settings: any, itemID: string, trackName?: string, artistName?: string, albumName?: string, folderName?: string, position?: number, spotifyId?: string, durationMs?: number, isAlbum?: boolean, releaseYear?: string, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => {
|
const downloadWithItemID = async (settings: any, itemID: string, trackName?: string, artistName?: string, albumName?: string, folderName?: string, position?: number, spotifyId?: string, durationMs?: number, isAlbum?: boolean, releaseYear?: string, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => {
|
||||||
const service = settings.downloader;
|
const service = settings.downloader;
|
||||||
const query = trackName && artistName ? `${trackName} ${artistName}` : undefined;
|
const query = trackName && artistName ? `${trackName} ${artistName}` : undefined;
|
||||||
const os = settings.operatingSystem;
|
const os = settings.operatingSystem;
|
||||||
@@ -375,13 +372,16 @@ export function useDownload(region: string) {
|
|||||||
const yearValue = releaseYear || finalReleaseDate?.substring(0, 4);
|
const yearValue = releaseYear || finalReleaseDate?.substring(0, 4);
|
||||||
const hasSubfolder = settings.folderTemplate && settings.folderTemplate.trim() !== "";
|
const hasSubfolder = settings.folderTemplate && settings.folderTemplate.trim() !== "";
|
||||||
const trackNumberForTemplate = (hasSubfolder && finalTrackNumber > 0) ? finalTrackNumber : (position || 0);
|
const trackNumberForTemplate = (hasSubfolder && finalTrackNumber > 0) ? finalTrackNumber : (position || 0);
|
||||||
if (hasSubfolder) {
|
const displayArtist = settings.useFirstArtistOnly && artistName
|
||||||
useAlbumTrackNumber = true;
|
? getFirstArtist(artistName)
|
||||||
}
|
: artistName;
|
||||||
|
const displayAlbumArtist = settings.useFirstArtistOnly && albumArtist
|
||||||
|
? getFirstArtist(albumArtist)
|
||||||
|
: albumArtist;
|
||||||
const templateData: TemplateData = {
|
const templateData: TemplateData = {
|
||||||
artist: artistName?.replace(/\//g, placeholder),
|
artist: displayArtist?.replace(/\//g, placeholder),
|
||||||
album: albumName?.replace(/\//g, placeholder),
|
album: albumName?.replace(/\//g, placeholder),
|
||||||
album_artist: albumArtist?.replace(/\//g, placeholder) || artistName?.replace(/\//g, placeholder),
|
album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
|
||||||
title: trackName?.replace(/\//g, placeholder),
|
title: trackName?.replace(/\//g, placeholder),
|
||||||
track: trackNumberForTemplate,
|
track: trackNumberForTemplate,
|
||||||
year: yearValue,
|
year: yearValue,
|
||||||
@@ -424,13 +424,12 @@ export function useDownload(region: string) {
|
|||||||
if (s === "tidal" && streamingURLs?.tidal_url) {
|
if (s === "tidal" && streamingURLs?.tidal_url) {
|
||||||
try {
|
try {
|
||||||
const response = await downloadTrack({
|
const response = await downloadTrack({
|
||||||
isrc,
|
|
||||||
service: "tidal",
|
service: "tidal",
|
||||||
query,
|
query,
|
||||||
track_name: trackName,
|
track_name: trackName,
|
||||||
artist_name: artistName,
|
artist_name: displayArtist,
|
||||||
album_name: albumName,
|
album_name: albumName,
|
||||||
album_artist: albumArtist,
|
album_artist: displayAlbumArtist,
|
||||||
release_date: finalReleaseDate || releaseDate,
|
release_date: finalReleaseDate || releaseDate,
|
||||||
cover_url: coverUrl,
|
cover_url: coverUrl,
|
||||||
output_dir: outputDir,
|
output_dir: outputDir,
|
||||||
@@ -451,6 +450,7 @@ export function useDownload(region: string) {
|
|||||||
spotify_total_discs: spotifyTotalDiscs,
|
spotify_total_discs: spotifyTotalDiscs,
|
||||||
copyright: copyright,
|
copyright: copyright,
|
||||||
publisher: publisher,
|
publisher: publisher,
|
||||||
|
use_first_artist_only: settings.useFirstArtistOnly,
|
||||||
});
|
});
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
return response;
|
return response;
|
||||||
@@ -465,13 +465,12 @@ export function useDownload(region: string) {
|
|||||||
else if (s === "amazon" && streamingURLs?.amazon_url) {
|
else if (s === "amazon" && streamingURLs?.amazon_url) {
|
||||||
try {
|
try {
|
||||||
const response = await downloadTrack({
|
const response = await downloadTrack({
|
||||||
isrc,
|
|
||||||
service: "amazon",
|
service: "amazon",
|
||||||
query,
|
query,
|
||||||
track_name: trackName,
|
track_name: trackName,
|
||||||
artist_name: artistName,
|
artist_name: displayArtist,
|
||||||
album_name: albumName,
|
album_name: albumName,
|
||||||
album_artist: albumArtist,
|
album_artist: displayAlbumArtist,
|
||||||
release_date: finalReleaseDate || releaseDate,
|
release_date: finalReleaseDate || releaseDate,
|
||||||
cover_url: coverUrl,
|
cover_url: coverUrl,
|
||||||
output_dir: outputDir,
|
output_dir: outputDir,
|
||||||
@@ -490,6 +489,7 @@ export function useDownload(region: string) {
|
|||||||
spotify_total_discs: spotifyTotalDiscs,
|
spotify_total_discs: spotifyTotalDiscs,
|
||||||
copyright: copyright,
|
copyright: copyright,
|
||||||
publisher: publisher,
|
publisher: publisher,
|
||||||
|
use_first_artist_only: settings.useFirstArtistOnly,
|
||||||
});
|
});
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
return response;
|
return response;
|
||||||
@@ -504,13 +504,12 @@ export function useDownload(region: string) {
|
|||||||
else if (s === "qobuz") {
|
else if (s === "qobuz") {
|
||||||
try {
|
try {
|
||||||
const response = await downloadTrack({
|
const response = await downloadTrack({
|
||||||
isrc,
|
|
||||||
service: "qobuz",
|
service: "qobuz",
|
||||||
query,
|
query,
|
||||||
track_name: trackName,
|
track_name: trackName,
|
||||||
artist_name: artistName,
|
artist_name: displayArtist,
|
||||||
album_name: albumName,
|
album_name: albumName,
|
||||||
album_artist: albumArtist,
|
album_artist: displayAlbumArtist,
|
||||||
release_date: finalReleaseDate || releaseDate,
|
release_date: finalReleaseDate || releaseDate,
|
||||||
cover_url: coverUrl,
|
cover_url: coverUrl,
|
||||||
output_dir: outputDir,
|
output_dir: outputDir,
|
||||||
@@ -530,6 +529,7 @@ export function useDownload(region: string) {
|
|||||||
spotify_total_discs: spotifyTotalDiscs,
|
spotify_total_discs: spotifyTotalDiscs,
|
||||||
copyright: copyright,
|
copyright: copyright,
|
||||||
publisher: publisher,
|
publisher: publisher,
|
||||||
|
use_first_artist_only: settings.useFirstArtistOnly,
|
||||||
});
|
});
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
return response;
|
return response;
|
||||||
@@ -557,13 +557,12 @@ export function useDownload(region: string) {
|
|||||||
audioFormat = settings.qobuzQuality || "6";
|
audioFormat = settings.qobuzQuality || "6";
|
||||||
}
|
}
|
||||||
const singleServiceResponse = await downloadTrack({
|
const singleServiceResponse = await downloadTrack({
|
||||||
isrc,
|
|
||||||
service: service as "tidal" | "qobuz" | "amazon",
|
service: service as "tidal" | "qobuz" | "amazon",
|
||||||
query,
|
query,
|
||||||
track_name: trackName,
|
track_name: trackName,
|
||||||
artist_name: artistName,
|
artist_name: displayArtist,
|
||||||
album_name: albumName,
|
album_name: albumName,
|
||||||
album_artist: albumArtist,
|
album_artist: displayAlbumArtist,
|
||||||
release_date: finalReleaseDate || releaseDate,
|
release_date: finalReleaseDate || releaseDate,
|
||||||
cover_url: coverUrl,
|
cover_url: coverUrl,
|
||||||
output_dir: outputDir,
|
output_dir: outputDir,
|
||||||
@@ -590,40 +589,41 @@ export function useDownload(region: string) {
|
|||||||
}
|
}
|
||||||
return singleServiceResponse;
|
return singleServiceResponse;
|
||||||
};
|
};
|
||||||
const handleDownloadTrack = async (isrc: string, trackName?: string, artistName?: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => {
|
const handleDownloadTrack = async (id: string, trackName?: string, artistName?: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => {
|
||||||
if (!isrc) {
|
if (!id) {
|
||||||
toast.error("No ISRC found for this track");
|
toast.error("No ID found for this track");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.info(`starting download: ${trackName} - ${artistName}`);
|
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
setDownloadingTrack(isrc);
|
const displayArtist = settings.useFirstArtistOnly && artistName ? getFirstArtist(artistName) : artistName;
|
||||||
|
logger.info(`starting download: ${trackName} - ${displayArtist}`);
|
||||||
|
setDownloadingTrack(id);
|
||||||
try {
|
try {
|
||||||
const releaseYear = releaseDate?.substring(0, 4);
|
const releaseYear = releaseDate?.substring(0, 4);
|
||||||
const response = await downloadWithAutoFallback(isrc, settings, trackName, artistName, albumName, playlistName, position, spotifyId, durationMs, releaseYear, albumArtist || "", releaseDate, coverUrl, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, copyright, publisher);
|
const response = await downloadWithAutoFallback(id, settings, trackName, artistName, albumName, playlistName, position, spotifyId, durationMs, releaseYear, albumArtist || "", releaseDate, coverUrl, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, copyright, publisher);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
if (response.already_exists) {
|
if (response.already_exists) {
|
||||||
toast.info(response.message);
|
toast.info(response.message);
|
||||||
setSkippedTracks((prev) => new Set(prev).add(isrc));
|
setSkippedTracks((prev) => new Set(prev).add(id));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
toast.success(response.message);
|
toast.success(response.message);
|
||||||
}
|
}
|
||||||
setDownloadedTracks((prev) => new Set(prev).add(isrc));
|
setDownloadedTracks((prev) => new Set(prev).add(id));
|
||||||
setFailedTracks((prev) => {
|
setFailedTracks((prev) => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
newSet.delete(isrc);
|
newSet.delete(id);
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
toast.error(response.error || "Download failed");
|
toast.error(response.error || "Download failed");
|
||||||
setFailedTracks((prev) => new Set(prev).add(isrc));
|
setFailedTracks((prev) => new Set(prev).add(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : "Download failed");
|
toast.error(err instanceof Error ? err.message : "Download failed");
|
||||||
setFailedTracks((prev) => new Set(prev).add(isrc));
|
setFailedTracks((prev) => new Set(prev).add(id));
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setDownloadingTrack(null);
|
setDownloadingTrack(null);
|
||||||
@@ -646,18 +646,20 @@ export function useDownload(region: string) {
|
|||||||
outputDir = joinPath(os, outputDir, sanitizePath(folderName.replace(/\//g, " "), os));
|
outputDir = joinPath(os, outputDir, sanitizePath(folderName.replace(/\//g, " "), os));
|
||||||
}
|
}
|
||||||
const selectedTrackObjects = selectedTracks
|
const selectedTrackObjects = selectedTracks
|
||||||
.map((isrc) => allTracks.find((t) => t.isrc === isrc))
|
.map((id) => allTracks.find((t) => t.spotify_id === id))
|
||||||
.filter((t): t is TrackMetadata => t !== undefined);
|
.filter((t): t is TrackMetadata => t !== undefined);
|
||||||
logger.info(`checking existing files in parallel...`);
|
logger.info(`checking existing files in parallel...`);
|
||||||
const useAlbumTrackNumber = settings.folderTemplate?.includes("{album}") || false;
|
const useAlbumTrackNumber = settings.folderTemplate?.includes("{album}") || false;
|
||||||
const audioFormat = "flac";
|
const audioFormat = "flac";
|
||||||
const existenceChecks = selectedTrackObjects.map((track, index) => {
|
const existenceChecks = selectedTrackObjects.map((track, index) => {
|
||||||
|
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;
|
||||||
return {
|
return {
|
||||||
spotify_id: track.spotify_id || track.isrc,
|
spotify_id: track.spotify_id || "",
|
||||||
track_name: track.name || "",
|
track_name: track.name || "",
|
||||||
artist_name: track.artists || "",
|
artist_name: displayArtist || "",
|
||||||
album_name: track.album_name || "",
|
album_name: track.album_name || "",
|
||||||
album_artist: track.album_artist || "",
|
album_artist: displayAlbumArtist || "",
|
||||||
release_date: track.release_date || "",
|
release_date: track.release_date || "",
|
||||||
track_number: track.track_number || 0,
|
track_number: track.track_number || 0,
|
||||||
disc_number: track.disc_number || 0,
|
disc_number: track.disc_number || 0,
|
||||||
@@ -682,20 +684,23 @@ export function useDownload(region: string) {
|
|||||||
logger.info(`found ${existingSpotifyIDs.size} existing files`);
|
logger.info(`found ${existingSpotifyIDs.size} existing files`);
|
||||||
const { AddToDownloadQueue } = await import("../../wailsjs/go/main/App");
|
const { AddToDownloadQueue } = await import("../../wailsjs/go/main/App");
|
||||||
const itemIDs: string[] = [];
|
const itemIDs: string[] = [];
|
||||||
for (const isrc of selectedTracks) {
|
for (const id of selectedTracks) {
|
||||||
const track = allTracks.find((t) => t.isrc === isrc);
|
const track = allTracks.find((t) => t.spotify_id === id);
|
||||||
const trackID = track?.spotify_id || isrc;
|
if (!track)
|
||||||
const itemID = await AddToDownloadQueue(trackID, track?.name || "", track?.artists || "", track?.album_name || "");
|
continue;
|
||||||
|
const trackID = track.spotify_id || id;
|
||||||
|
const displayArtist = settings.useFirstArtistOnly && track.artists ? getFirstArtist(track.artists) : track.artists;
|
||||||
|
const itemID = await AddToDownloadQueue(trackID, track.name || "", displayArtist || "", track.album_name || "");
|
||||||
itemIDs.push(itemID);
|
itemIDs.push(itemID);
|
||||||
if (existingSpotifyIDs.has(trackID)) {
|
if (existingSpotifyIDs.has(trackID)) {
|
||||||
const filePath = existingFilePaths.get(trackID) || "";
|
const filePath = existingFilePaths.get(trackID) || "";
|
||||||
setTimeout(() => SkipDownloadItem(itemID, filePath), 10);
|
setTimeout(() => SkipDownloadItem(itemID, filePath), 10);
|
||||||
setSkippedTracks((prev) => new Set(prev).add(isrc));
|
setSkippedTracks((prev) => new Set(prev).add(id));
|
||||||
setDownloadedTracks((prev) => new Set(prev).add(isrc));
|
setDownloadedTracks((prev) => new Set(prev).add(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tracksToDownload = selectedTrackObjects.filter((track) => {
|
const tracksToDownload = selectedTrackObjects.filter((track) => {
|
||||||
const trackID = track.spotify_id || track.isrc;
|
const trackID = track.spotify_id || "";
|
||||||
return !existingSpotifyIDs.has(trackID);
|
return !existingSpotifyIDs.has(trackID);
|
||||||
});
|
});
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
@@ -709,45 +714,46 @@ export function useDownload(region: string) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const track = tracksToDownload[i];
|
const track = tracksToDownload[i];
|
||||||
const isrc = track.isrc;
|
const id = track.spotify_id || "";
|
||||||
const originalIndex = selectedTracks.indexOf(isrc);
|
const originalIndex = selectedTracks.indexOf(id);
|
||||||
const itemID = itemIDs[originalIndex];
|
const itemID = itemIDs[originalIndex];
|
||||||
setDownloadingTrack(isrc);
|
setDownloadingTrack(id);
|
||||||
setCurrentDownloadInfo({ name: track.name, artists: track.artists });
|
const displayArtist = settings.useFirstArtistOnly && track.artists ? getFirstArtist(track.artists) : track.artists;
|
||||||
|
setCurrentDownloadInfo({ name: track.name, artists: displayArtist || "" });
|
||||||
try {
|
try {
|
||||||
const releaseYear = track.release_date?.substring(0, 4);
|
const releaseYear = track.release_date?.substring(0, 4);
|
||||||
const response = await downloadWithItemID(isrc, settings, itemID, track.name, track.artists, track.album_name, folderName, originalIndex + 1, track.spotify_id, track.duration_ms, isAlbum, releaseYear, track.album_artist || "", track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher);
|
const response = await downloadWithItemID(settings, itemID, track.name, track.artists, track.album_name, folderName, originalIndex + 1, track.spotify_id, track.duration_ms, isAlbum, releaseYear, track.album_artist || "", track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
if (response.already_exists) {
|
if (response.already_exists) {
|
||||||
skippedCount++;
|
skippedCount++;
|
||||||
logger.info(`skipped: ${track.name} - ${track.artists} (already exists)`);
|
logger.info(`skipped: ${track.name} - ${displayArtist} (already exists)`);
|
||||||
setSkippedTracks((prev) => new Set(prev).add(isrc));
|
setSkippedTracks((prev) => new Set(prev).add(id));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
successCount++;
|
successCount++;
|
||||||
logger.success(`downloaded: ${track.name} - ${track.artists}`);
|
logger.success(`downloaded: ${track.name} - ${displayArtist}`);
|
||||||
}
|
}
|
||||||
if (response.file) {
|
if (response.file) {
|
||||||
finalFilePaths.set(isrc, response.file);
|
finalFilePaths.set(id, response.file);
|
||||||
finalFilePaths.set(track.spotify_id || isrc, response.file);
|
finalFilePaths.set(track.spotify_id || id, response.file);
|
||||||
}
|
}
|
||||||
setDownloadedTracks((prev) => new Set(prev).add(isrc));
|
setDownloadedTracks((prev) => new Set(prev).add(id));
|
||||||
setFailedTracks((prev) => {
|
setFailedTracks((prev) => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
newSet.delete(isrc);
|
newSet.delete(id);
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
errorCount++;
|
errorCount++;
|
||||||
logger.error(`failed: ${track.name} - ${track.artists}`);
|
logger.error(`failed: ${track.name} - ${displayArtist}`);
|
||||||
setFailedTracks((prev) => new Set(prev).add(isrc));
|
setFailedTracks((prev) => new Set(prev).add(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
errorCount++;
|
errorCount++;
|
||||||
logger.error(`error: ${track.name} - ${err}`);
|
logger.error(`error: ${track.name} - ${err}`);
|
||||||
setFailedTracks((prev) => new Set(prev).add(isrc));
|
setFailedTracks((prev) => new Set(prev).add(id));
|
||||||
if (itemID) {
|
if (itemID) {
|
||||||
const { MarkDownloadItemFailed } = await import("../../wailsjs/go/main/App");
|
const { MarkDownloadItemFailed } = await import("../../wailsjs/go/main/App");
|
||||||
await MarkDownloadItemFailed(itemID, err instanceof Error ? err.message : String(err));
|
await MarkDownloadItemFailed(itemID, err instanceof Error ? err.message : String(err));
|
||||||
@@ -764,7 +770,7 @@ export function useDownload(region: string) {
|
|||||||
const { CancelAllQueuedItems } = await import("../../wailsjs/go/main/App");
|
const { CancelAllQueuedItems } = await import("../../wailsjs/go/main/App");
|
||||||
await CancelAllQueuedItems();
|
await CancelAllQueuedItems();
|
||||||
if (settings.createM3u8File && folderName) {
|
if (settings.createM3u8File && folderName) {
|
||||||
const paths = selectedTrackObjects.map((t) => finalFilePaths.get(t.spotify_id || t.isrc) || "").filter((p) => p !== "");
|
const paths = selectedTrackObjects.map((t) => finalFilePaths.get(t.spotify_id || "") || "").filter((p) => p !== "");
|
||||||
if (paths.length > 0) {
|
if (paths.length > 0) {
|
||||||
try {
|
try {
|
||||||
logger.info(`creating m3u8 playlist: ${folderName}`);
|
logger.info(`creating m3u8 playlist: ${folderName}`);
|
||||||
@@ -798,12 +804,12 @@ export function useDownload(region: string) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleDownloadAll = async (tracks: TrackMetadata[], folderName?: string, isAlbum?: boolean) => {
|
const handleDownloadAll = async (tracks: TrackMetadata[], folderName?: string, isAlbum?: boolean) => {
|
||||||
const tracksWithIsrc = tracks.filter((track) => track.isrc);
|
const tracksWithId = tracks.filter((track) => track.spotify_id);
|
||||||
if (tracksWithIsrc.length === 0) {
|
if (tracksWithId.length === 0) {
|
||||||
toast.error("No tracks available for download");
|
toast.error("No tracks available for download");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.info(`starting batch download: ${tracksWithIsrc.length} tracks`);
|
logger.info(`starting batch download: ${tracksWithId.length} tracks`);
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
setIsDownloading(true);
|
setIsDownloading(true);
|
||||||
setBulkDownloadType("all");
|
setBulkDownloadType("all");
|
||||||
@@ -817,13 +823,15 @@ export function useDownload(region: string) {
|
|||||||
logger.info(`checking existing files in parallel...`);
|
logger.info(`checking existing files in parallel...`);
|
||||||
const useAlbumTrackNumber = settings.folderTemplate?.includes("{album}") || false;
|
const useAlbumTrackNumber = settings.folderTemplate?.includes("{album}") || false;
|
||||||
const audioFormat = "flac";
|
const audioFormat = "flac";
|
||||||
const existenceChecks = tracksWithIsrc.map((track, index) => {
|
const existenceChecks = tracksWithId.map((track, index) => {
|
||||||
|
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;
|
||||||
return {
|
return {
|
||||||
spotify_id: track.spotify_id || track.isrc,
|
spotify_id: track.spotify_id || "",
|
||||||
track_name: track.name || "",
|
track_name: track.name || "",
|
||||||
artist_name: track.artists || "",
|
artist_name: displayArtist || "",
|
||||||
album_name: track.album_name || "",
|
album_name: track.album_name || "",
|
||||||
album_artist: track.album_artist || "",
|
album_artist: displayAlbumArtist || "",
|
||||||
release_date: track.release_date || "",
|
release_date: track.release_date || "",
|
||||||
track_number: track.track_number || 0,
|
track_number: track.track_number || 0,
|
||||||
disc_number: track.disc_number || 0,
|
disc_number: track.disc_number || 0,
|
||||||
@@ -835,7 +843,7 @@ export function useDownload(region: string) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
const existenceResults = await CheckFilesExistence(outputDir, settings.downloadPath, existenceChecks);
|
const existenceResults = await CheckFilesExistence(outputDir, settings.downloadPath, existenceChecks);
|
||||||
const finalFilePaths: string[] = new Array(tracksWithIsrc.length).fill("");
|
const finalFilePaths: string[] = new Array(tracksWithId.length).fill("");
|
||||||
const existingSpotifyIDs = new Set<string>();
|
const existingSpotifyIDs = new Set<string>();
|
||||||
const existingFilePaths = new Map<string, string>();
|
const existingFilePaths = new Map<string, string>();
|
||||||
for (let i = 0; i < existenceResults.length; i++) {
|
for (let i = 0; i < existenceResults.length; i++) {
|
||||||
@@ -849,25 +857,26 @@ export function useDownload(region: string) {
|
|||||||
logger.info(`found ${existingSpotifyIDs.size} existing files`);
|
logger.info(`found ${existingSpotifyIDs.size} existing files`);
|
||||||
const { AddToDownloadQueue } = await import("../../wailsjs/go/main/App");
|
const { AddToDownloadQueue } = await import("../../wailsjs/go/main/App");
|
||||||
const itemIDs: string[] = [];
|
const itemIDs: string[] = [];
|
||||||
for (const track of tracksWithIsrc) {
|
for (const track of tracksWithId) {
|
||||||
const itemID = await AddToDownloadQueue(track.isrc, track.name, track.artists, track.album_name || "");
|
const displayArtist = settings.useFirstArtistOnly && track.artists ? getFirstArtist(track.artists) : track.artists;
|
||||||
|
const itemID = await AddToDownloadQueue(track.spotify_id || "", track.name || "", displayArtist || "", track.album_name || "");
|
||||||
itemIDs.push(itemID);
|
itemIDs.push(itemID);
|
||||||
const trackID = track.spotify_id || track.isrc;
|
const trackID = track.spotify_id || "";
|
||||||
if (existingSpotifyIDs.has(trackID)) {
|
if (existingSpotifyIDs.has(trackID)) {
|
||||||
const filePath = existingFilePaths.get(trackID) || "";
|
const filePath = existingFilePaths.get(trackID) || "";
|
||||||
setTimeout(() => SkipDownloadItem(itemID, filePath), 10);
|
setTimeout(() => SkipDownloadItem(itemID, filePath), 10);
|
||||||
setSkippedTracks((prev) => new Set(prev).add(track.isrc));
|
setSkippedTracks((prev: Set<string>) => new Set(prev).add(trackID));
|
||||||
setDownloadedTracks((prev) => new Set(prev).add(track.isrc));
|
setDownloadedTracks((prev: Set<string>) => new Set(prev).add(trackID));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tracksToDownload = tracksWithIsrc.filter((track) => {
|
const tracksToDownload = tracksWithId.filter((track) => {
|
||||||
const trackID = track.spotify_id || track.isrc;
|
const trackID = track.spotify_id || "";
|
||||||
return !existingSpotifyIDs.has(trackID);
|
return !existingSpotifyIDs.has(trackID);
|
||||||
});
|
});
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
let skippedCount = existingSpotifyIDs.size;
|
let skippedCount = existingSpotifyIDs.size;
|
||||||
const total = tracksWithIsrc.length;
|
const total = tracksWithId.length;
|
||||||
setDownloadProgress(Math.round((skippedCount / total) * 100));
|
setDownloadProgress(Math.round((skippedCount / total) * 100));
|
||||||
for (let i = 0; i < tracksToDownload.length; i++) {
|
for (let i = 0; i < tracksToDownload.length; i++) {
|
||||||
if (shouldStopDownloadRef.current) {
|
if (shouldStopDownloadRef.current) {
|
||||||
@@ -875,27 +884,29 @@ export function useDownload(region: string) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const track = tracksToDownload[i];
|
const track = tracksToDownload[i];
|
||||||
const originalIndex = tracksWithIsrc.findIndex((t) => t.isrc === track.isrc);
|
const originalIndex = tracksWithId.findIndex((t) => t.spotify_id === track.spotify_id);
|
||||||
const itemID = itemIDs[originalIndex];
|
const itemID = itemIDs[originalIndex];
|
||||||
setDownloadingTrack(track.isrc);
|
const trackId = track.spotify_id || "";
|
||||||
setCurrentDownloadInfo({ name: track.name, artists: track.artists });
|
setDownloadingTrack(trackId);
|
||||||
|
const displayArtist = settings.useFirstArtistOnly && track.artists ? getFirstArtist(track.artists) : track.artists;
|
||||||
|
setCurrentDownloadInfo({ name: track.name || "", artists: displayArtist || "" });
|
||||||
try {
|
try {
|
||||||
const releaseYear = track.release_date?.substring(0, 4);
|
const releaseYear = track.release_date?.substring(0, 4);
|
||||||
const response = await downloadWithItemID(track.isrc, settings, itemID, track.name, track.artists, track.album_name, folderName, originalIndex + 1, track.spotify_id, track.duration_ms, isAlbum, releaseYear, track.album_artist || "", track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher);
|
const response = await downloadWithItemID(settings, itemID, track.name, track.artists, track.album_name, folderName, originalIndex + 1, track.spotify_id, track.duration_ms, isAlbum, releaseYear, track.album_artist || "", track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
if (response.already_exists) {
|
if (response.already_exists) {
|
||||||
skippedCount++;
|
skippedCount++;
|
||||||
logger.info(`skipped: ${track.name} - ${track.artists} (already exists)`);
|
logger.info(`skipped: ${track.name} - ${displayArtist} (already exists)`);
|
||||||
setSkippedTracks((prev) => new Set(prev).add(track.isrc));
|
setSkippedTracks((prev) => new Set(prev).add(trackId));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
successCount++;
|
successCount++;
|
||||||
logger.success(`downloaded: ${track.name} - ${track.artists}`);
|
logger.success(`downloaded: ${track.name} - ${displayArtist}`);
|
||||||
}
|
}
|
||||||
setDownloadedTracks((prev) => new Set(prev).add(track.isrc));
|
setDownloadedTracks((prev) => new Set(prev).add(trackId));
|
||||||
setFailedTracks((prev) => {
|
setFailedTracks((prev) => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
newSet.delete(track.isrc);
|
newSet.delete(trackId);
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
if (response.file) {
|
if (response.file) {
|
||||||
@@ -904,14 +915,14 @@ export function useDownload(region: string) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
errorCount++;
|
errorCount++;
|
||||||
logger.error(`failed: ${track.name} - ${track.artists}`);
|
logger.error(`failed: ${track.name} - ${displayArtist}`);
|
||||||
setFailedTracks((prev) => new Set(prev).add(track.isrc));
|
setFailedTracks((prev) => new Set(prev).add(trackId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
errorCount++;
|
errorCount++;
|
||||||
logger.error(`error: ${track.name} - ${err}`);
|
logger.error(`error: ${track.name} - ${err}`);
|
||||||
setFailedTracks((prev) => new Set(prev).add(track.isrc));
|
setFailedTracks((prev) => new Set(prev).add(trackId));
|
||||||
const { MarkDownloadItemFailed } = await import("../../wailsjs/go/main/App");
|
const { MarkDownloadItemFailed } = await import("../../wailsjs/go/main/App");
|
||||||
await MarkDownloadItemFailed(itemID, err instanceof Error ? err.message : String(err));
|
await MarkDownloadItemFailed(itemID, err instanceof Error ? err.message : String(err));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { getSettings } from "@/lib/settings";
|
||||||
import { fetchSpotifyMetadata } from "@/lib/api";
|
import { fetchSpotifyMetadata } from "@/lib/api";
|
||||||
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
@@ -7,6 +8,7 @@ import type { SpotifyMetadataResponse } from "@/types/api";
|
|||||||
export function useMetadata() {
|
export function useMetadata() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [metadata, setMetadata] = useState<SpotifyMetadataResponse | null>(null);
|
const [metadata, setMetadata] = useState<SpotifyMetadataResponse | null>(null);
|
||||||
|
const [showApiModal, setShowApiModal] = useState(false);
|
||||||
const [showAlbumDialog, setShowAlbumDialog] = useState(false);
|
const [showAlbumDialog, setShowAlbumDialog] = useState(false);
|
||||||
const [selectedAlbum, setSelectedAlbum] = useState<{
|
const [selectedAlbum, setSelectedAlbum] = useState<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -109,7 +111,7 @@ export function useMetadata() {
|
|||||||
saveToHistory(url, data);
|
saveToHistory(url, data);
|
||||||
if ("track" in data) {
|
if ("track" in data) {
|
||||||
logger.success(`fetched track: ${data.track.name} - ${data.track.artists}`);
|
logger.success(`fetched track: ${data.track.name} - ${data.track.artists}`);
|
||||||
logger.debug(`isrc: ${data.track.isrc}, duration: ${data.track.duration_ms}ms`);
|
logger.debug(`duration: ${data.track.duration_ms}ms`);
|
||||||
}
|
}
|
||||||
else if ("album_info" in data) {
|
else if ("album_info" in data) {
|
||||||
logger.success(`fetched album: ${data.album_info.name}`);
|
logger.success(`fetched album: ${data.album_info.name}`);
|
||||||
@@ -129,8 +131,14 @@ export function useMetadata() {
|
|||||||
catch (err) {
|
catch (err) {
|
||||||
const errorMsg = err instanceof Error ? err.message : "Failed to fetch metadata";
|
const errorMsg = err instanceof Error ? err.message : "Failed to fetch metadata";
|
||||||
logger.error(`fetch failed: ${errorMsg}`);
|
logger.error(`fetch failed: ${errorMsg}`);
|
||||||
|
const settings = getSettings();
|
||||||
|
if (!settings.useSpotFetchAPI) {
|
||||||
|
setShowApiModal(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
toast.error(errorMsg);
|
toast.error(errorMsg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
finally {
|
finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -224,8 +232,14 @@ export function useMetadata() {
|
|||||||
catch (err) {
|
catch (err) {
|
||||||
const errorMsg = err instanceof Error ? err.message : "Failed to fetch album metadata";
|
const errorMsg = err instanceof Error ? err.message : "Failed to fetch album metadata";
|
||||||
logger.error(`fetch failed: ${errorMsg}`);
|
logger.error(`fetch failed: ${errorMsg}`);
|
||||||
|
const settings = getSettings();
|
||||||
|
if (!settings.useSpotFetchAPI) {
|
||||||
|
setShowApiModal(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
toast.error(errorMsg);
|
toast.error(errorMsg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
finally {
|
finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setSelectedAlbum(null);
|
setSelectedAlbum(null);
|
||||||
@@ -243,6 +257,8 @@ export function useMetadata() {
|
|||||||
handleConfirmAlbumFetch,
|
handleConfirmAlbumFetch,
|
||||||
handleArtistClick,
|
handleArtistClick,
|
||||||
loadFromCache,
|
loadFromCache,
|
||||||
|
showApiModal,
|
||||||
|
setShowApiModal,
|
||||||
resetMetadata: () => setMetadata(null),
|
resetMetadata: () => setMetadata(null),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -363,6 +363,7 @@ export async function saveSettings(settings: Settings): Promise<void> {
|
|||||||
cachedSettings = settings;
|
cachedSettings = settings;
|
||||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
||||||
await SaveToBackend(settings as any);
|
await SaveToBackend(settings as any);
|
||||||
|
window.dispatchEvent(new CustomEvent('settingsUpdated', { detail: settings }));
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error("Failed to save settings:", error);
|
console.error("Failed to save settings:", error);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export interface TrackMetadata {
|
|||||||
total_discs?: number;
|
total_discs?: number;
|
||||||
disc_number?: number;
|
disc_number?: number;
|
||||||
external_urls: string;
|
external_urls: string;
|
||||||
isrc: string;
|
|
||||||
album_type?: string;
|
album_type?: string;
|
||||||
spotify_id?: string;
|
spotify_id?: string;
|
||||||
album_id?: string;
|
album_id?: string;
|
||||||
@@ -109,7 +108,6 @@ export interface ArtistResponse {
|
|||||||
}
|
}
|
||||||
export type SpotifyMetadataResponse = TrackResponse | AlbumResponse | PlaylistResponse | ArtistDiscographyResponse | ArtistResponse;
|
export type SpotifyMetadataResponse = TrackResponse | AlbumResponse | PlaylistResponse | ArtistDiscographyResponse | ArtistResponse;
|
||||||
export interface DownloadRequest {
|
export interface DownloadRequest {
|
||||||
isrc: string;
|
|
||||||
service: "tidal" | "qobuz" | "amazon";
|
service: "tidal" | "qobuz" | "amazon";
|
||||||
query?: string;
|
query?: string;
|
||||||
track_name?: string;
|
track_name?: string;
|
||||||
@@ -139,6 +137,7 @@ export interface DownloadRequest {
|
|||||||
copyright?: string;
|
copyright?: string;
|
||||||
publisher?: string;
|
publisher?: string;
|
||||||
spotify_url?: string;
|
spotify_url?: string;
|
||||||
|
use_first_artist_only?: boolean;
|
||||||
}
|
}
|
||||||
export interface DownloadResponse {
|
export interface DownloadResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"productName": "SpotiFLAC",
|
"productName": "SpotiFLAC",
|
||||||
"productVersion": "7.0.8",
|
"productVersion": "7.0.9",
|
||||||
"copyright": "© 2026 afkarxyz"
|
"copyright": "© 2026 afkarxyz"
|
||||||
},
|
},
|
||||||
"wailsjsdir": "./frontend",
|
"wailsjsdir": "./frontend",
|
||||||
|
|||||||
Reference in New Issue
Block a user