v7.0.1
This commit is contained in:
@@ -12,23 +12,18 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
// so we can call the runtime methods
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
}
|
||||
|
||||
// SpotifyMetadataRequest represents the request structure for fetching Spotify metadata
|
||||
type SpotifyMetadataRequest struct {
|
||||
URL string `json:"url"`
|
||||
Batch bool `json:"batch"`
|
||||
@@ -36,7 +31,6 @@ type SpotifyMetadataRequest struct {
|
||||
Timeout float64 `json:"timeout"`
|
||||
}
|
||||
|
||||
// DownloadRequest represents the request structure for downloading tracks
|
||||
type DownloadRequest struct {
|
||||
ISRC string `json:"isrc"`
|
||||
Service string `json:"service"`
|
||||
@@ -46,36 +40,37 @@ type DownloadRequest struct {
|
||||
AlbumName string `json:"album_name,omitempty"`
|
||||
AlbumArtist string `json:"album_artist,omitempty"`
|
||||
ReleaseDate string `json:"release_date,omitempty"`
|
||||
CoverURL string `json:"cover_url,omitempty"` // Spotify cover URL for embedding
|
||||
CoverURL string `json:"cover_url,omitempty"`
|
||||
ApiURL string `json:"api_url,omitempty"`
|
||||
OutputDir string `json:"output_dir,omitempty"`
|
||||
AudioFormat string `json:"audio_format,omitempty"`
|
||||
FilenameFormat string `json:"filename_format,omitempty"`
|
||||
TrackNumber bool `json:"track_number,omitempty"`
|
||||
Position int `json:"position,omitempty"` // Position in playlist/album (1-based)
|
||||
UseAlbumTrackNumber bool `json:"use_album_track_number,omitempty"` // Use album track number instead of playlist position
|
||||
SpotifyID string `json:"spotify_id,omitempty"` // Spotify track ID
|
||||
EmbedLyrics bool `json:"embed_lyrics,omitempty"` // Whether to embed lyrics into the audio file
|
||||
EmbedMaxQualityCover bool `json:"embed_max_quality_cover,omitempty"` // Whether to embed max quality cover art
|
||||
ServiceURL string `json:"service_url,omitempty"` // Direct service URL (Tidal/Deezer/Amazon) to skip song.link API call
|
||||
Duration int `json:"duration,omitempty"` // Track duration in seconds for better matching
|
||||
ItemID string `json:"item_id,omitempty"` // Optional queue item ID for multi-service fallback tracking
|
||||
SpotifyTrackNumber int `json:"spotify_track_number,omitempty"` // Track number from Spotify album
|
||||
SpotifyDiscNumber int `json:"spotify_disc_number,omitempty"` // Disc number from Spotify album
|
||||
SpotifyTotalTracks int `json:"spotify_total_tracks,omitempty"` // Total tracks in album from Spotify
|
||||
Position int `json:"position,omitempty"`
|
||||
UseAlbumTrackNumber bool `json:"use_album_track_number,omitempty"`
|
||||
SpotifyID string `json:"spotify_id,omitempty"`
|
||||
EmbedLyrics bool `json:"embed_lyrics,omitempty"`
|
||||
EmbedMaxQualityCover bool `json:"embed_max_quality_cover,omitempty"`
|
||||
ServiceURL string `json:"service_url,omitempty"`
|
||||
Duration int `json:"duration,omitempty"`
|
||||
ItemID string `json:"item_id,omitempty"`
|
||||
SpotifyTrackNumber int `json:"spotify_track_number,omitempty"`
|
||||
SpotifyDiscNumber int `json:"spotify_disc_number,omitempty"`
|
||||
SpotifyTotalTracks int `json:"spotify_total_tracks,omitempty"`
|
||||
SpotifyTotalDiscs int `json:"spotify_total_discs,omitempty"`
|
||||
Copyright string `json:"copyright,omitempty"`
|
||||
Publisher string `json:"publisher,omitempty"`
|
||||
}
|
||||
|
||||
// DownloadResponse represents the response structure for download operations
|
||||
type DownloadResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
File string `json:"file,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
AlreadyExists bool `json:"already_exists,omitempty"`
|
||||
ItemID string `json:"item_id,omitempty"` // Queue item ID for tracking
|
||||
ItemID string `json:"item_id,omitempty"`
|
||||
}
|
||||
|
||||
// GetStreamingURLs fetches all streaming URLs from song.link API
|
||||
func (a *App) GetStreamingURLs(spotifyTrackID string) (string, error) {
|
||||
if spotifyTrackID == "" {
|
||||
return "", fmt.Errorf("spotify track ID is required")
|
||||
@@ -96,7 +91,6 @@ func (a *App) GetStreamingURLs(spotifyTrackID string) (string, error) {
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// GetSpotifyMetadata fetches metadata from Spotify
|
||||
func (a *App) GetSpotifyMetadata(req SpotifyMetadataRequest) (string, error) {
|
||||
if req.URL == "" {
|
||||
return "", fmt.Errorf("URL parameter is required")
|
||||
@@ -125,13 +119,11 @@ func (a *App) GetSpotifyMetadata(req SpotifyMetadataRequest) (string, error) {
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// SpotifySearchRequest represents the request structure for searching Spotify
|
||||
type SpotifySearchRequest struct {
|
||||
Query string `json:"query"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// SearchSpotify searches for tracks, albums, artists, and playlists on Spotify
|
||||
func (a *App) SearchSpotify(req SpotifySearchRequest) (*backend.SearchResponse, error) {
|
||||
if req.Query == "" {
|
||||
return nil, fmt.Errorf("search query is required")
|
||||
@@ -147,15 +139,13 @@ func (a *App) SearchSpotify(req SpotifySearchRequest) (*backend.SearchResponse,
|
||||
return backend.SearchSpotify(ctx, req.Query, req.Limit)
|
||||
}
|
||||
|
||||
// SpotifySearchByTypeRequest represents the request for searching by specific type with offset
|
||||
type SpotifySearchByTypeRequest struct {
|
||||
Query string `json:"query"`
|
||||
SearchType string `json:"search_type"` // track, album, artist, playlist
|
||||
SearchType string `json:"search_type"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
|
||||
// SearchSpotifyByType searches for a specific type with offset support for pagination
|
||||
func (a *App) SearchSpotifyByType(req SpotifySearchByTypeRequest) ([]backend.SearchResult, error) {
|
||||
if req.Query == "" {
|
||||
return nil, fmt.Errorf("search query is required")
|
||||
@@ -175,13 +165,13 @@ func (a *App) SearchSpotifyByType(req SpotifySearchByTypeRequest) ([]backend.Sea
|
||||
return backend.SearchSpotifyByType(ctx, req.Query, req.SearchType, req.Limit, req.Offset)
|
||||
}
|
||||
|
||||
// DownloadTrack downloads a track by ISRC
|
||||
func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
if req.ISRC == "" {
|
||||
|
||||
if req.Service == "qobuz" && req.ISRC == "" && req.SpotifyID == "" {
|
||||
return DownloadResponse{
|
||||
Success: false,
|
||||
Error: "ISRC is required",
|
||||
}, fmt.Errorf("ISRC is required")
|
||||
Error: "Spotify ID is required for Qobuz",
|
||||
}, fmt.Errorf("spotify ID is required for Qobuz")
|
||||
}
|
||||
|
||||
if req.Service == "" {
|
||||
@@ -191,7 +181,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
if req.OutputDir == "" {
|
||||
req.OutputDir = "."
|
||||
} else {
|
||||
// Only normalize path separators, don't sanitize user's existing folder names
|
||||
|
||||
req.OutputDir = backend.NormalizePath(req.OutputDir)
|
||||
}
|
||||
|
||||
@@ -202,62 +192,89 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
var err error
|
||||
var filename string
|
||||
|
||||
// Set default filename format if not provided
|
||||
if req.FilenameFormat == "" {
|
||||
req.FilenameFormat = "title-artist"
|
||||
}
|
||||
|
||||
// ItemID should always be provided by frontend (created via AddToDownloadQueue)
|
||||
// If not provided, generate one for backwards compatibility
|
||||
itemID := req.ItemID
|
||||
if itemID == "" {
|
||||
itemID = fmt.Sprintf("%s-%d", req.ISRC, time.Now().UnixNano())
|
||||
// Add to queue if no ItemID was provided (legacy support)
|
||||
backend.AddToQueue(itemID, req.TrackName, req.ArtistName, req.AlbumName, req.ISRC)
|
||||
|
||||
if req.SpotifyID != "" {
|
||||
itemID = fmt.Sprintf("%s-%d", req.SpotifyID, time.Now().UnixNano())
|
||||
} else {
|
||||
itemID = fmt.Sprintf("%s-%s-%d", req.TrackName, req.ArtistName, time.Now().UnixNano())
|
||||
}
|
||||
|
||||
backend.AddToQueue(itemID, req.TrackName, req.ArtistName, req.AlbumName, req.SpotifyID)
|
||||
}
|
||||
|
||||
// Mark item as downloading immediately
|
||||
backend.SetDownloading(true)
|
||||
backend.StartDownloadItem(itemID)
|
||||
defer backend.SetDownloading(false)
|
||||
|
||||
// Early check: Check if file with same ISRC already exists
|
||||
if existingFile, exists := backend.CheckISRCExists(req.OutputDir, req.ISRC); exists {
|
||||
fmt.Printf("File with ISRC %s already exists: %s\n", req.ISRC, existingFile)
|
||||
backend.SkipDownloadItem(itemID, existingFile)
|
||||
return DownloadResponse{
|
||||
Success: true,
|
||||
Message: "File with same ISRC already exists",
|
||||
File: existingFile,
|
||||
AlreadyExists: true,
|
||||
ItemID: itemID,
|
||||
}, nil
|
||||
spotifyURL := ""
|
||||
if req.SpotifyID != "" {
|
||||
spotifyURL = fmt.Sprintf("https://open.spotify.com/track/%s", req.SpotifyID)
|
||||
}
|
||||
|
||||
if req.SpotifyID != "" && (req.Copyright == "" || req.Publisher == "" || req.SpotifyTotalDiscs == 0 || req.ReleaseDate == "" || req.SpotifyTotalTracks == 0 || req.SpotifyTrackNumber == 0) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
trackURL := fmt.Sprintf("https://open.spotify.com/track/%s", req.SpotifyID)
|
||||
trackData, err := backend.GetFilteredSpotifyData(ctx, trackURL, false, 0)
|
||||
if err == nil {
|
||||
|
||||
var trackResp struct {
|
||||
Track struct {
|
||||
Copyright string `json:"copyright"`
|
||||
Publisher string `json:"publisher"`
|
||||
TotalDiscs int `json:"total_discs"`
|
||||
TotalTracks int `json:"total_tracks"`
|
||||
TrackNumber int `json:"track_number"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
} `json:"track"`
|
||||
}
|
||||
if jsonData, jsonErr := json.Marshal(trackData); jsonErr == nil {
|
||||
if json.Unmarshal(jsonData, &trackResp) == nil {
|
||||
|
||||
if req.Copyright == "" && trackResp.Track.Copyright != "" {
|
||||
req.Copyright = trackResp.Track.Copyright
|
||||
}
|
||||
if req.Publisher == "" && trackResp.Track.Publisher != "" {
|
||||
req.Publisher = trackResp.Track.Publisher
|
||||
}
|
||||
if req.SpotifyTotalDiscs == 0 && trackResp.Track.TotalDiscs > 0 {
|
||||
req.SpotifyTotalDiscs = trackResp.Track.TotalDiscs
|
||||
}
|
||||
if req.SpotifyTotalTracks == 0 && trackResp.Track.TotalTracks > 0 {
|
||||
req.SpotifyTotalTracks = trackResp.Track.TotalTracks
|
||||
}
|
||||
if req.SpotifyTrackNumber == 0 && trackResp.Track.TrackNumber > 0 {
|
||||
req.SpotifyTrackNumber = trackResp.Track.TrackNumber
|
||||
}
|
||||
if req.ReleaseDate == "" && trackResp.Track.ReleaseDate != "" {
|
||||
req.ReleaseDate = trackResp.Track.ReleaseDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if we have track metadata, check if file already exists by filename
|
||||
if req.TrackName != "" && req.ArtistName != "" {
|
||||
expectedFilename := backend.BuildExpectedFilename(req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.FilenameFormat, req.TrackNumber, req.Position, req.SpotifyDiscNumber, req.UseAlbumTrackNumber)
|
||||
expectedPath := filepath.Join(req.OutputDir, expectedFilename)
|
||||
|
||||
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 {
|
||||
// Validate the file by checking if it has valid ISRC metadata
|
||||
if fileISRC, readErr := backend.ReadISRCFromFile(expectedPath); readErr == nil && fileISRC != "" {
|
||||
// File exists and has valid metadata - skip download
|
||||
backend.SkipDownloadItem(itemID, expectedPath)
|
||||
return DownloadResponse{
|
||||
Success: true,
|
||||
Message: "File already exists",
|
||||
File: expectedPath,
|
||||
AlreadyExists: true,
|
||||
ItemID: itemID,
|
||||
}, nil
|
||||
} else {
|
||||
// File exists but has no valid ISRC metadata - it's corrupted, delete it
|
||||
fmt.Printf("Removing corrupted file (no valid ISRC metadata): %s\n", expectedPath)
|
||||
if removeErr := os.Remove(expectedPath); removeErr != nil {
|
||||
fmt.Printf("Warning: Failed to remove corrupted file %s: %v\n", expectedPath, removeErr)
|
||||
}
|
||||
}
|
||||
|
||||
backend.SkipDownloadItem(itemID, expectedPath)
|
||||
return DownloadResponse{
|
||||
Success: true,
|
||||
Message: "File already exists",
|
||||
File: expectedPath,
|
||||
AlreadyExists: true,
|
||||
ItemID: itemID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,8 +282,8 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
case "amazon":
|
||||
downloader := backend.NewAmazonDownloader()
|
||||
if req.ServiceURL != "" {
|
||||
// Use provided URL directly
|
||||
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.ISRC, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover)
|
||||
|
||||
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.FilenameFormat, 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)
|
||||
} else {
|
||||
if req.SpotifyID == "" {
|
||||
return DownloadResponse{
|
||||
@@ -274,15 +291,15 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
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.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.ISRC, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover)
|
||||
filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.FilenameFormat, 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":
|
||||
if req.ApiURL == "" || req.ApiURL == "auto" {
|
||||
downloader := backend.NewTidalDownloader("")
|
||||
if req.ServiceURL != "" {
|
||||
// Use provided URL directly with fallback to multiple APIs
|
||||
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.ISRC)
|
||||
|
||||
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)
|
||||
} else {
|
||||
if req.SpotifyID == "" {
|
||||
return DownloadResponse{
|
||||
@@ -290,14 +307,14 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
Error: "Spotify ID is required for Tidal",
|
||||
}, fmt.Errorf("spotify ID is required for Tidal")
|
||||
}
|
||||
// Use ISRC matching for search fallback
|
||||
filename, err = downloader.DownloadWithFallbackAndISRC(req.SpotifyID, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.Duration, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks)
|
||||
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
downloader := backend.NewTidalDownloader(req.ApiURL)
|
||||
if req.ServiceURL != "" {
|
||||
// Use provided URL directly with specific API
|
||||
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.ISRC)
|
||||
|
||||
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)
|
||||
} else {
|
||||
if req.SpotifyID == "" {
|
||||
return DownloadResponse{
|
||||
@@ -305,19 +322,45 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
Error: "Spotify ID is required for Tidal",
|
||||
}, fmt.Errorf("spotify ID is required for Tidal")
|
||||
}
|
||||
// Use ISRC matching for search fallback
|
||||
filename, err = downloader.DownloadWithISRC(req.SpotifyID, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.Duration, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
case "qobuz":
|
||||
downloader := backend.NewQobuzDownloader()
|
||||
// Default to "6" (FLAC 16-bit) for Qobuz if not specified
|
||||
|
||||
quality := req.AudioFormat
|
||||
if quality == "" {
|
||||
quality = "6"
|
||||
}
|
||||
filename, err = downloader.DownloadByISRC(req.ISRC, 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)
|
||||
|
||||
deezerISRC := req.ISRC
|
||||
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)
|
||||
|
||||
default:
|
||||
return DownloadResponse{
|
||||
@@ -327,9 +370,9 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Clean up any partial/corrupted file that was created during failed download
|
||||
|
||||
if filename != "" && !strings.HasPrefix(filename, "EXISTS:") {
|
||||
// Check if file exists and delete it
|
||||
|
||||
if _, statErr := os.Stat(filename); statErr == nil {
|
||||
fmt.Printf("Removing corrupted/partial file after failed download: %s\n", filename)
|
||||
if removeErr := os.Remove(filename); removeErr != nil {
|
||||
@@ -338,8 +381,6 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Don't mark as failed in backend - let the frontend handle it
|
||||
// Frontend will call MarkDownloadItemFailed after all services are tried
|
||||
return DownloadResponse{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("Download failed: %v", err),
|
||||
@@ -347,14 +388,12 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
}, err
|
||||
}
|
||||
|
||||
// Check if file already existed
|
||||
alreadyExists := false
|
||||
if strings.HasPrefix(filename, "EXISTS:") {
|
||||
alreadyExists = true
|
||||
filename = strings.TrimPrefix(filename, "EXISTS:")
|
||||
}
|
||||
|
||||
// Embed lyrics after successful download (only for new downloads with Spotify ID and if embedLyrics is enabled)
|
||||
if !alreadyExists && req.SpotifyID != "" && req.EmbedLyrics && strings.HasSuffix(filename, ".flac") {
|
||||
go func(filePath, spotifyID, trackName, artistName string) {
|
||||
fmt.Printf("\n========== LYRICS FETCH START ==========\n")
|
||||
@@ -365,8 +404,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
|
||||
lyricsClient := backend.NewLyricsClient()
|
||||
|
||||
// Try all sources with fallbacks
|
||||
lyricsResp, source, err := lyricsClient.FetchLyricsAllSources(spotifyID, trackName, artistName)
|
||||
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")
|
||||
@@ -390,7 +428,6 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Show full lyrics in console for debugging
|
||||
fmt.Printf("\n--- Full LRC Content ---\n")
|
||||
fmt.Println(lyrics)
|
||||
fmt.Printf("--- End LRC Content ---\n\n")
|
||||
@@ -411,12 +448,12 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
message = "File already exists"
|
||||
backend.SkipDownloadItem(itemID, filename)
|
||||
} else {
|
||||
// Get file size for completed download
|
||||
|
||||
if fileInfo, statErr := os.Stat(filename); statErr == nil {
|
||||
finalSize := float64(fileInfo.Size()) / (1024 * 1024) // Convert to MB
|
||||
finalSize := float64(fileInfo.Size()) / (1024 * 1024)
|
||||
backend.CompleteDownloadItem(itemID, filename, finalSize)
|
||||
} else {
|
||||
// Fallback: mark as completed without size
|
||||
|
||||
backend.CompleteDownloadItem(itemID, filename, 0)
|
||||
}
|
||||
}
|
||||
@@ -430,7 +467,6 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OpenFolder opens a folder in the file explorer
|
||||
func (a *App) OpenFolder(path string) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("path is required")
|
||||
@@ -444,67 +480,55 @@ func (a *App) OpenFolder(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelectFolder opens a folder selection dialog and returns the selected path
|
||||
func (a *App) SelectFolder(defaultPath string) (string, error) {
|
||||
return backend.SelectFolderDialog(a.ctx, defaultPath)
|
||||
}
|
||||
|
||||
// SelectFile opens a file selection dialog and returns the selected file path
|
||||
func (a *App) SelectFile() (string, error) {
|
||||
return backend.SelectFileDialog(a.ctx)
|
||||
}
|
||||
|
||||
// GetDefaults returns the default configuration
|
||||
func (a *App) GetDefaults() map[string]string {
|
||||
return map[string]string{
|
||||
"downloadPath": backend.GetDefaultMusicPath(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetDownloadProgress returns current download progress
|
||||
func (a *App) GetDownloadProgress() backend.ProgressInfo {
|
||||
return backend.GetDownloadProgress()
|
||||
}
|
||||
|
||||
// GetDownloadQueue returns the complete download queue state
|
||||
func (a *App) GetDownloadQueue() backend.DownloadQueueInfo {
|
||||
return backend.GetDownloadQueue()
|
||||
}
|
||||
|
||||
// ClearCompletedDownloads clears completed, failed, and skipped items from the queue
|
||||
func (a *App) ClearCompletedDownloads() {
|
||||
backend.ClearDownloadQueue()
|
||||
}
|
||||
|
||||
// ClearAllDownloads clears the entire queue and resets session stats
|
||||
func (a *App) ClearAllDownloads() {
|
||||
backend.ClearAllDownloads()
|
||||
}
|
||||
|
||||
// AddToDownloadQueue adds a new item to the download queue and returns its ID
|
||||
func (a *App) AddToDownloadQueue(isrc, trackName, artistName, albumName string) string {
|
||||
itemID := fmt.Sprintf("%s-%d", isrc, time.Now().UnixNano())
|
||||
backend.AddToQueue(itemID, trackName, artistName, albumName, isrc)
|
||||
return itemID
|
||||
}
|
||||
|
||||
// MarkDownloadItemFailed marks a download item as failed
|
||||
func (a *App) MarkDownloadItemFailed(itemID, errorMsg string) {
|
||||
backend.FailDownloadItem(itemID, errorMsg)
|
||||
}
|
||||
|
||||
// CancelAllQueuedItems marks all queued items as cancelled/skipped
|
||||
func (a *App) CancelAllQueuedItems() {
|
||||
backend.CancelAllQueuedItems()
|
||||
}
|
||||
|
||||
// Quit closes the application
|
||||
func (a *App) Quit() {
|
||||
// You can add cleanup logic here if needed
|
||||
panic("quit") // This will trigger Wails to close the app
|
||||
|
||||
panic("quit")
|
||||
}
|
||||
|
||||
// AnalyzeTrack analyzes audio quality of a FLAC file
|
||||
func (a *App) AnalyzeTrack(filePath string) (string, error) {
|
||||
if filePath == "" {
|
||||
return "", fmt.Errorf("file path is required")
|
||||
@@ -523,7 +547,6 @@ func (a *App) AnalyzeTrack(filePath string) (string, error) {
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// AnalyzeMultipleTracks analyzes multiple FLAC files
|
||||
func (a *App) AnalyzeMultipleTracks(filePaths []string) (string, error) {
|
||||
if len(filePaths) == 0 {
|
||||
return "", fmt.Errorf("at least one file path is required")
|
||||
@@ -534,7 +557,7 @@ func (a *App) AnalyzeMultipleTracks(filePaths []string) (string, error) {
|
||||
for _, filePath := range filePaths {
|
||||
result, err := backend.AnalyzeTrack(filePath)
|
||||
if err != nil {
|
||||
// Skip failed analyses
|
||||
|
||||
continue
|
||||
}
|
||||
results = append(results, result)
|
||||
@@ -548,7 +571,6 @@ func (a *App) AnalyzeMultipleTracks(filePaths []string) (string, error) {
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// LyricsDownloadRequest represents the request structure for downloading lyrics
|
||||
type LyricsDownloadRequest struct {
|
||||
SpotifyID string `json:"spotify_id"`
|
||||
TrackName string `json:"track_name"`
|
||||
@@ -564,7 +586,6 @@ type LyricsDownloadRequest struct {
|
||||
DiscNumber int `json:"disc_number"`
|
||||
}
|
||||
|
||||
// DownloadLyrics downloads lyrics for a single track
|
||||
func (a *App) DownloadLyrics(req LyricsDownloadRequest) (backend.LyricsDownloadResponse, error) {
|
||||
if req.SpotifyID == "" {
|
||||
return backend.LyricsDownloadResponse{
|
||||
@@ -600,7 +621,6 @@ func (a *App) DownloadLyrics(req LyricsDownloadRequest) (backend.LyricsDownloadR
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
// CoverDownloadRequest represents the request structure for downloading cover art
|
||||
type CoverDownloadRequest struct {
|
||||
CoverURL string `json:"cover_url"`
|
||||
TrackName string `json:"track_name"`
|
||||
@@ -615,7 +635,6 @@ type CoverDownloadRequest struct {
|
||||
DiscNumber int `json:"disc_number"`
|
||||
}
|
||||
|
||||
// DownloadCover downloads cover art for a single track
|
||||
func (a *App) DownloadCover(req CoverDownloadRequest) (backend.CoverDownloadResponse, error) {
|
||||
if req.CoverURL == "" {
|
||||
return backend.CoverDownloadResponse{
|
||||
@@ -650,7 +669,125 @@ func (a *App) DownloadCover(req CoverDownloadRequest) (backend.CoverDownloadResp
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
// CheckTrackAvailability checks the availability of a track on different streaming platforms
|
||||
type HeaderDownloadRequest struct {
|
||||
HeaderURL string `json:"header_url"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
OutputDir string `json:"output_dir"`
|
||||
}
|
||||
|
||||
func (a *App) DownloadHeader(req HeaderDownloadRequest) (backend.HeaderDownloadResponse, error) {
|
||||
if req.HeaderURL == "" {
|
||||
return backend.HeaderDownloadResponse{
|
||||
Success: false,
|
||||
Error: "Header URL is required",
|
||||
}, fmt.Errorf("header URL is required")
|
||||
}
|
||||
|
||||
if req.ArtistName == "" {
|
||||
return backend.HeaderDownloadResponse{
|
||||
Success: false,
|
||||
Error: "Artist name is required",
|
||||
}, fmt.Errorf("artist name is required")
|
||||
}
|
||||
|
||||
client := backend.NewCoverClient()
|
||||
backendReq := backend.HeaderDownloadRequest{
|
||||
HeaderURL: req.HeaderURL,
|
||||
ArtistName: req.ArtistName,
|
||||
OutputDir: req.OutputDir,
|
||||
}
|
||||
|
||||
resp, err := client.DownloadHeader(backendReq)
|
||||
if err != nil {
|
||||
return backend.HeaderDownloadResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}, err
|
||||
}
|
||||
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
type GalleryImageDownloadRequest struct {
|
||||
ImageURL string `json:"image_url"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
ImageIndex int `json:"image_index"`
|
||||
OutputDir string `json:"output_dir"`
|
||||
}
|
||||
|
||||
func (a *App) DownloadGalleryImage(req GalleryImageDownloadRequest) (backend.GalleryImageDownloadResponse, error) {
|
||||
if req.ImageURL == "" {
|
||||
return backend.GalleryImageDownloadResponse{
|
||||
Success: false,
|
||||
Error: "Image URL is required",
|
||||
}, fmt.Errorf("image URL is required")
|
||||
}
|
||||
|
||||
if req.ArtistName == "" {
|
||||
return backend.GalleryImageDownloadResponse{
|
||||
Success: false,
|
||||
Error: "Artist name is required",
|
||||
}, fmt.Errorf("artist name is required")
|
||||
}
|
||||
|
||||
client := backend.NewCoverClient()
|
||||
backendReq := backend.GalleryImageDownloadRequest{
|
||||
ImageURL: req.ImageURL,
|
||||
ArtistName: req.ArtistName,
|
||||
ImageIndex: req.ImageIndex,
|
||||
OutputDir: req.OutputDir,
|
||||
}
|
||||
|
||||
resp, err := client.DownloadGalleryImage(backendReq)
|
||||
if err != nil {
|
||||
return backend.GalleryImageDownloadResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}, err
|
||||
}
|
||||
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
type AvatarDownloadRequest struct {
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
OutputDir string `json:"output_dir"`
|
||||
}
|
||||
|
||||
func (a *App) DownloadAvatar(req AvatarDownloadRequest) (backend.AvatarDownloadResponse, error) {
|
||||
if req.AvatarURL == "" {
|
||||
return backend.AvatarDownloadResponse{
|
||||
Success: false,
|
||||
Error: "Avatar URL is required",
|
||||
}, fmt.Errorf("avatar URL is required")
|
||||
}
|
||||
|
||||
if req.ArtistName == "" {
|
||||
return backend.AvatarDownloadResponse{
|
||||
Success: false,
|
||||
Error: "Artist name is required",
|
||||
}, fmt.Errorf("artist name is required")
|
||||
}
|
||||
|
||||
client := backend.NewCoverClient()
|
||||
backendReq := backend.AvatarDownloadRequest{
|
||||
AvatarURL: req.AvatarURL,
|
||||
ArtistName: req.ArtistName,
|
||||
OutputDir: req.OutputDir,
|
||||
}
|
||||
|
||||
resp, err := client.DownloadAvatar(backendReq)
|
||||
if err != nil {
|
||||
return backend.AvatarDownloadResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}, err
|
||||
}
|
||||
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (a *App) CheckTrackAvailability(spotifyTrackID string, isrc string) (string, error) {
|
||||
if spotifyTrackID == "" {
|
||||
return "", fmt.Errorf("spotify track ID is required")
|
||||
@@ -670,32 +807,26 @@ func (a *App) CheckTrackAvailability(spotifyTrackID string, isrc string) (string
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// IsFFmpegInstalled checks if ffmpeg is installed
|
||||
func (a *App) IsFFmpegInstalled() (bool, error) {
|
||||
return backend.IsFFmpegInstalled()
|
||||
}
|
||||
|
||||
// IsFFprobeInstalled checks if ffprobe is installed
|
||||
func (a *App) IsFFprobeInstalled() (bool, error) {
|
||||
return backend.IsFFprobeInstalled()
|
||||
}
|
||||
|
||||
// GetFFmpegPath returns the path to ffmpeg
|
||||
func (a *App) GetFFmpegPath() (string, error) {
|
||||
return backend.GetFFmpegPath()
|
||||
}
|
||||
|
||||
// DownloadFFmpegRequest represents a request to download ffmpeg
|
||||
type DownloadFFmpegRequest struct{}
|
||||
|
||||
// DownloadFFmpegResponse represents the response from downloading ffmpeg
|
||||
type DownloadFFmpegResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// DownloadFFmpeg downloads and installs ffmpeg
|
||||
func (a *App) DownloadFFmpeg() DownloadFFmpegResponse {
|
||||
err := backend.DownloadFFmpeg(func(progress int) {
|
||||
fmt.Printf("[FFmpeg] Download progress: %d%%\n", progress)
|
||||
@@ -713,15 +844,13 @@ func (a *App) DownloadFFmpeg() DownloadFFmpegResponse {
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertAudioRequest represents a request to convert audio files
|
||||
type ConvertAudioRequest struct {
|
||||
InputFiles []string `json:"input_files"`
|
||||
OutputFormat string `json:"output_format"`
|
||||
Bitrate string `json:"bitrate"`
|
||||
Codec string `json:"codec"` // For m4a: "aac" (lossy) or "alac" (lossless)
|
||||
Codec string `json:"codec"`
|
||||
}
|
||||
|
||||
// ConvertAudio converts audio files using ffmpeg
|
||||
func (a *App) ConvertAudio(req ConvertAudioRequest) ([]backend.ConvertAudioResult, error) {
|
||||
backendReq := backend.ConvertAudioRequest{
|
||||
InputFiles: req.InputFiles,
|
||||
@@ -732,7 +861,6 @@ func (a *App) ConvertAudio(req ConvertAudioRequest) ([]backend.ConvertAudioResul
|
||||
return backend.ConvertAudio(backendReq)
|
||||
}
|
||||
|
||||
// SelectAudioFiles opens a file dialog to select audio files for conversion
|
||||
func (a *App) SelectAudioFiles() ([]string, error) {
|
||||
files, err := backend.SelectMultipleFiles(a.ctx)
|
||||
if err != nil {
|
||||
@@ -741,12 +869,10 @@ func (a *App) SelectAudioFiles() ([]string, error) {
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// GetFileSizes returns file sizes for a list of file paths
|
||||
func (a *App) GetFileSizes(files []string) map[string]int64 {
|
||||
return backend.GetFileSizes(files)
|
||||
}
|
||||
|
||||
// ListDirectoryFiles lists files and folders in a directory
|
||||
func (a *App) ListDirectoryFiles(dirPath string) ([]backend.FileInfo, error) {
|
||||
if dirPath == "" {
|
||||
return nil, fmt.Errorf("directory path is required")
|
||||
@@ -754,7 +880,6 @@ func (a *App) ListDirectoryFiles(dirPath string) ([]backend.FileInfo, error) {
|
||||
return backend.ListDirectory(dirPath)
|
||||
}
|
||||
|
||||
// ListAudioFilesInDir lists only audio files in a directory recursively
|
||||
func (a *App) ListAudioFilesInDir(dirPath string) ([]backend.FileInfo, error) {
|
||||
if dirPath == "" {
|
||||
return nil, fmt.Errorf("directory path is required")
|
||||
@@ -762,7 +887,6 @@ func (a *App) ListAudioFilesInDir(dirPath string) ([]backend.FileInfo, error) {
|
||||
return backend.ListAudioFiles(dirPath)
|
||||
}
|
||||
|
||||
// ReadFileMetadata reads metadata from an audio file
|
||||
func (a *App) ReadFileMetadata(filePath string) (*backend.AudioMetadata, error) {
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("file path is required")
|
||||
@@ -770,17 +894,14 @@ func (a *App) ReadFileMetadata(filePath string) (*backend.AudioMetadata, error)
|
||||
return backend.ReadAudioMetadata(filePath)
|
||||
}
|
||||
|
||||
// PreviewRenameFiles generates a preview of rename operations
|
||||
func (a *App) PreviewRenameFiles(files []string, format string) []backend.RenamePreview {
|
||||
return backend.PreviewRename(files, format)
|
||||
}
|
||||
|
||||
// RenameFilesByMetadata renames files based on their metadata
|
||||
func (a *App) RenameFilesByMetadata(files []string, format string) []backend.RenameResult {
|
||||
return backend.RenameFiles(files, format)
|
||||
}
|
||||
|
||||
// ReadTextFile reads a text file and returns its content
|
||||
func (a *App) ReadTextFile(filePath string) (string, error) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
@@ -789,7 +910,6 @@ func (a *App) ReadTextFile(filePath string) (string, error) {
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
// RenameFileTo renames a file to a new name (keeping same directory)
|
||||
func (a *App) RenameFileTo(oldPath, newName string) error {
|
||||
dir := filepath.Dir(oldPath)
|
||||
ext := filepath.Ext(oldPath)
|
||||
@@ -797,13 +917,12 @@ func (a *App) RenameFileTo(oldPath, newName string) error {
|
||||
return os.Rename(oldPath, newPath)
|
||||
}
|
||||
|
||||
// ReadImageAsBase64 reads an image file and returns it as base64 data URL
|
||||
func (a *App) ReadImageAsBase64(filePath string) (string, error) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
var mimeType string
|
||||
switch ext {
|
||||
@@ -818,44 +937,115 @@ func (a *App) ReadImageAsBase64(filePath string) (string, error) {
|
||||
default:
|
||||
mimeType = "image/jpeg"
|
||||
}
|
||||
|
||||
|
||||
encoded := base64.StdEncoding.EncodeToString(content)
|
||||
return fmt.Sprintf("data:%s;base64,%s", mimeType, encoded), nil
|
||||
}
|
||||
|
||||
// CheckFileExistenceRequest represents a track to check for existence
|
||||
type CheckFileExistenceRequest struct {
|
||||
ISRC string `json:"isrc"`
|
||||
TrackName string `json:"track_name"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
SpotifyID string `json:"spotify_id"`
|
||||
TrackName string `json:"track_name"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
AlbumName string `json:"album_name,omitempty"`
|
||||
AlbumArtist string `json:"album_artist,omitempty"`
|
||||
ReleaseDate string `json:"release_date,omitempty"`
|
||||
TrackNumber int `json:"track_number,omitempty"`
|
||||
DiscNumber int `json:"disc_number,omitempty"`
|
||||
Position int `json:"position,omitempty"`
|
||||
UseAlbumTrackNumber bool `json:"use_album_track_number,omitempty"`
|
||||
FilenameFormat string `json:"filename_format,omitempty"`
|
||||
IncludeTrackNumber bool `json:"include_track_number,omitempty"`
|
||||
AudioFormat string `json:"audio_format,omitempty"`
|
||||
}
|
||||
|
||||
// CheckFilesExistence checks if multiple files already exist in the output directory
|
||||
// This is done in parallel for better performance
|
||||
func (a *App) CheckFilesExistence(outputDir string, tracks []CheckFileExistenceRequest) []backend.FileExistenceResult {
|
||||
// Convert to backend struct format
|
||||
backendTracks := make([]struct {
|
||||
ISRC string
|
||||
TrackName string
|
||||
ArtistName string
|
||||
}, len(tracks))
|
||||
type CheckFileExistenceResult struct {
|
||||
SpotifyID string `json:"spotify_id"`
|
||||
Exists bool `json:"exists"`
|
||||
FilePath string `json:"file_path,omitempty"`
|
||||
TrackName string `json:"track_name,omitempty"`
|
||||
ArtistName string `json:"artist_name,omitempty"`
|
||||
}
|
||||
|
||||
for i, t := range tracks {
|
||||
backendTracks[i] = struct {
|
||||
ISRC string
|
||||
TrackName string
|
||||
ArtistName string
|
||||
}{
|
||||
ISRC: t.ISRC,
|
||||
TrackName: t.TrackName,
|
||||
ArtistName: t.ArtistName,
|
||||
}
|
||||
func (a *App) CheckFilesExistence(outputDir string, tracks []CheckFileExistenceRequest) []CheckFileExistenceResult {
|
||||
if len(tracks) == 0 {
|
||||
return []CheckFileExistenceResult{}
|
||||
}
|
||||
|
||||
return backend.CheckFilesExistParallel(outputDir, backendTracks)
|
||||
outputDir = backend.NormalizePath(outputDir)
|
||||
|
||||
defaultFilenameFormat := "title-artist"
|
||||
|
||||
type result struct {
|
||||
index int
|
||||
result CheckFileExistenceResult
|
||||
}
|
||||
|
||||
resultsChan := make(chan result, len(tracks))
|
||||
|
||||
for i, track := range tracks {
|
||||
go func(idx int, t CheckFileExistenceRequest) {
|
||||
res := CheckFileExistenceResult{
|
||||
SpotifyID: t.SpotifyID,
|
||||
TrackName: t.TrackName,
|
||||
ArtistName: t.ArtistName,
|
||||
Exists: false,
|
||||
}
|
||||
|
||||
if t.TrackName == "" || t.ArtistName == "" {
|
||||
resultsChan <- result{index: idx, result: res}
|
||||
return
|
||||
}
|
||||
|
||||
filenameFormat := t.FilenameFormat
|
||||
if filenameFormat == "" {
|
||||
filenameFormat = defaultFilenameFormat
|
||||
}
|
||||
|
||||
trackNumber := t.Position
|
||||
if t.UseAlbumTrackNumber && t.TrackNumber > 0 {
|
||||
trackNumber = t.TrackNumber
|
||||
}
|
||||
|
||||
fileExt := ".flac"
|
||||
if t.AudioFormat == "mp3" {
|
||||
fileExt = ".mp3"
|
||||
}
|
||||
|
||||
expectedFilenameBase := backend.BuildExpectedFilename(
|
||||
t.TrackName,
|
||||
t.ArtistName,
|
||||
t.AlbumName,
|
||||
t.AlbumArtist,
|
||||
t.ReleaseDate,
|
||||
filenameFormat,
|
||||
t.IncludeTrackNumber,
|
||||
trackNumber,
|
||||
t.DiscNumber,
|
||||
t.UseAlbumTrackNumber,
|
||||
)
|
||||
|
||||
expectedFilename := strings.TrimSuffix(expectedFilenameBase, ".flac") + fileExt
|
||||
|
||||
expectedPath := filepath.Join(outputDir, expectedFilename)
|
||||
|
||||
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 {
|
||||
res.Exists = true
|
||||
res.FilePath = expectedPath
|
||||
}
|
||||
|
||||
resultsChan <- result{index: idx, result: res}
|
||||
}(i, track)
|
||||
}
|
||||
|
||||
results := make([]CheckFileExistenceResult, len(tracks))
|
||||
for i := 0; i < len(tracks); i++ {
|
||||
r := <-resultsChan
|
||||
results[r.index] = r.result
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// SkipDownloadItem marks a download item as skipped (file already exists)
|
||||
func (a *App) SkipDownloadItem(itemID, filePath string) {
|
||||
backend.SkipDownloadItem(itemID, filePath)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user