package main import ( "context" "encoding/base64" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "regexp" goRuntime "runtime" "spotiflac/backend" "strings" "time" "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 { ctx context.Context } func NewApp() *App { return &App{} } func (a *App) startup(ctx context.Context) { a.ctx = ctx if err := backend.InitHistoryDB("SpotiFLAC"); err != nil { fmt.Printf("Failed to init history DB: %v\n", err) } } func (a *App) shutdown(ctx context.Context) { backend.CloseHistoryDB() } type SpotifyMetadataRequest struct { URL string `json:"url"` Batch bool `json:"batch"` Delay float64 `json:"delay"` Timeout float64 `json:"timeout"` } type DownloadRequest struct { ISRC string `json:"isrc"` Service string `json:"service"` Query string `json:"query,omitempty"` TrackName string `json:"track_name,omitempty"` ArtistName string `json:"artist_name,omitempty"` AlbumName string `json:"album_name,omitempty"` AlbumArtist string `json:"album_artist,omitempty"` ReleaseDate string `json:"release_date,omitempty"` 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"` 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"` } 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"` } func (a *App) GetStreamingURLs(spotifyTrackID string) (string, error) { if spotifyTrackID == "" { return "", fmt.Errorf("spotify track ID is required") } fmt.Printf("[GetStreamingURLs] Called for track ID: %s\n", spotifyTrackID) client := backend.NewSongLinkClient() urls, err := client.GetAllURLsFromSpotify(spotifyTrackID) if err != nil { return "", err } jsonData, err := json.Marshal(urls) if err != nil { return "", fmt.Errorf("failed to encode response: %v", err) } return string(jsonData), nil } func (a *App) GetSpotifyMetadata(req SpotifyMetadataRequest) (string, error) { if req.URL == "" { return "", fmt.Errorf("URL parameter is required") } if req.Delay == 0 { req.Delay = 1.0 } if req.Timeout == 0 { req.Timeout = 300.0 } ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Timeout*float64(time.Second))) defer cancel() data, err := backend.GetFilteredSpotifyData(ctx, req.URL, req.Batch, time.Duration(req.Delay*float64(time.Second))) if err != nil { return "", fmt.Errorf("failed to fetch metadata: %v", err) } jsonData, err := json.MarshalIndent(data, "", " ") if err != nil { return "", fmt.Errorf("failed to encode response: %v", err) } return string(jsonData), nil } type SpotifySearchRequest struct { Query string `json:"query"` Limit int `json:"limit"` } func (a *App) SearchSpotify(req SpotifySearchRequest) (*backend.SearchResponse, error) { if req.Query == "" { return nil, fmt.Errorf("search query is required") } if req.Limit <= 0 { req.Limit = 10 } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() return backend.SearchSpotify(ctx, req.Query, req.Limit) } type SpotifySearchByTypeRequest struct { Query string `json:"query"` SearchType string `json:"search_type"` Limit int `json:"limit"` Offset int `json:"offset"` } func (a *App) SearchSpotifyByType(req SpotifySearchByTypeRequest) ([]backend.SearchResult, error) { if req.Query == "" { return nil, fmt.Errorf("search query is required") } if req.SearchType == "" { return nil, fmt.Errorf("search type is required") } if req.Limit <= 0 { req.Limit = 50 } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() return backend.SearchSpotifyByType(ctx, req.Query, req.SearchType, req.Limit, req.Offset) } func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { if req.Service == "qobuz" && req.ISRC == "" && req.SpotifyID == "" { return DownloadResponse{ Success: false, Error: "Spotify ID is required for Qobuz", }, fmt.Errorf("spotify ID is required for Qobuz") } if req.Service == "" { req.Service = "tidal" } if req.OutputDir == "" { req.OutputDir = "." } else { req.OutputDir = backend.NormalizePath(req.OutputDir) } if req.AudioFormat == "" { req.AudioFormat = "LOSSLESS" } var err error var filename string if req.FilenameFormat == "" { req.FilenameFormat = "title-artist" } itemID := req.ItemID if itemID == "" { 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) } backend.SetDownloading(true) backend.StartDownloadItem(itemID) defer backend.SetDownloading(false) 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 } } } } } 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 { backend.SkipDownloadItem(itemID, expectedPath) return DownloadResponse{ Success: true, Message: "File already exists", File: expectedPath, AlreadyExists: true, ItemID: itemID, }, nil } } switch req.Service { case "amazon": downloader := backend.NewAmazonDownloader() 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.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL) } else { if req.SpotifyID == "" { 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.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 != "" { 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{ 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) } } else { downloader := backend.NewTidalDownloader(req.ApiURL) if req.ServiceURL != "" { filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL) } else { if req.SpotifyID == "" { 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) } } case "qobuz": downloader := backend.NewQobuzDownloader() quality := req.AudioFormat if quality == "" { quality = "6" } 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) default: return DownloadResponse{ Success: false, Error: fmt.Sprintf("Unknown service: %s", req.Service), }, fmt.Errorf("unknown service: %s", req.Service) } if err != nil { backend.FailDownloadItem(itemID, fmt.Sprintf("Download failed: %v", err)) if filename != "" && !strings.HasPrefix(filename, "EXISTS:") { 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 { fmt.Printf("Warning: Failed to remove corrupted file %s: %v\n", filename, removeErr) } } } return DownloadResponse{ Success: false, Error: fmt.Sprintf("Download failed: %v", err), ItemID: itemID, }, err } alreadyExists := false if strings.HasPrefix(filename, "EXISTS:") { alreadyExists = true filename = strings.TrimPrefix(filename, "EXISTS:") } if !alreadyExists && req.SpotifyID != "" && req.EmbedLyrics && strings.HasSuffix(filename, ".flac") { go func(filePath, spotifyID, trackName, artistName string) { fmt.Printf("\n========== LYRICS FETCH START ==========\n") fmt.Printf("Spotify ID: %s\n", spotifyID) 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.Println(lyrics) fmt.Printf("--- End LRC Content ---\n\n") fmt.Printf("Embedding into: %s\n", filePath) if err := backend.EmbedLyricsOnly(filePath, lyrics); err != nil { fmt.Printf("Failed to embed lyrics: %v\n", err) fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n") } else { fmt.Printf("Lyrics embedded successfully!\n") fmt.Printf("========== LYRICS FETCH END (SUCCESS) ==========\n\n") } }(filename, req.SpotifyID, req.TrackName, req.ArtistName) } message := "Download completed successfully" if alreadyExists { message = "File already exists" backend.SkipDownloadItem(itemID, filename) } else { if fileInfo, statErr := os.Stat(filename); statErr == nil { finalSize := float64(fileInfo.Size()) / (1024 * 1024) backend.CompleteDownloadItem(itemID, filename, finalSize) } else { backend.CompleteDownloadItem(itemID, filename, 0) } go func(fPath, track, artist, album, sID, cover, format string) { quality := "Unknown" durationStr := "--:--" meta, err := backend.GetTrackMetadata(fPath) if err == nil && meta != nil { quality = fmt.Sprintf("%d-bit/%.1fkHz", meta.BitsPerSample, float64(meta.SampleRate)/1000.0) d := int(meta.Duration) durationStr = fmt.Sprintf("%d:%02d", d/60, d%60) } else { } item := backend.HistoryItem{ SpotifyID: sID, Title: track, Artists: artist, Album: album, DurationStr: durationStr, CoverURL: cover, Quality: quality, Format: format, Path: fPath, } if item.Format == "" || item.Format == "LOSSLESS" { ext := filepath.Ext(fPath) if len(ext) > 1 { item.Format = strings.ToUpper(ext[1:]) } } backend.AddHistoryItem(item, "SpotiFLAC") }(filename, req.TrackName, req.ArtistName, req.AlbumName, req.SpotifyID, req.CoverURL, req.AudioFormat) } return DownloadResponse{ Success: true, Message: message, File: filename, AlreadyExists: alreadyExists, ItemID: itemID, }, nil } func (a *App) OpenFolder(path string) error { if path == "" { return fmt.Errorf("path is required") } err := backend.OpenFolderInExplorer(path) if err != nil { return fmt.Errorf("failed to open folder: %v", err) } return nil } func (a *App) SelectFolder(defaultPath string) (string, error) { return backend.SelectFolderDialog(a.ctx, defaultPath) } func (a *App) SelectFile() (string, error) { return backend.SelectFileDialog(a.ctx) } func (a *App) GetDefaults() map[string]string { return map[string]string{ "downloadPath": backend.GetDefaultMusicPath(), } } func (a *App) GetDownloadProgress() backend.ProgressInfo { return backend.GetDownloadProgress() } func (a *App) GetDownloadQueue() backend.DownloadQueueInfo { return backend.GetDownloadQueue() } func (a *App) ClearCompletedDownloads() { backend.ClearDownloadQueue() } func (a *App) ClearAllDownloads() { backend.ClearAllDownloads() } 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 } func (a *App) MarkDownloadItemFailed(itemID, errorMsg string) { backend.FailDownloadItem(itemID, errorMsg) } func (a *App) CancelAllQueuedItems() { backend.CancelAllQueuedItems() } func (a *App) Quit() { panic("quit") } func (a *App) GetDownloadHistory() ([]backend.HistoryItem, error) { return backend.GetHistoryItems("SpotiFLAC") } func (a *App) ClearDownloadHistory() error { return backend.ClearHistory("SpotiFLAC") } func (a *App) AnalyzeTrack(filePath string) (string, error) { if filePath == "" { return "", fmt.Errorf("file path is required") } result, err := backend.AnalyzeTrack(filePath) if err != nil { return "", fmt.Errorf("failed to analyze track: %v", err) } jsonData, err := json.Marshal(result) if err != nil { return "", fmt.Errorf("failed to encode response: %v", err) } return string(jsonData), nil } func (a *App) AnalyzeMultipleTracks(filePaths []string) (string, error) { if len(filePaths) == 0 { return "", fmt.Errorf("at least one file path is required") } results := make([]*backend.AnalysisResult, 0, len(filePaths)) for _, filePath := range filePaths { result, err := backend.AnalyzeTrack(filePath) if err != nil { continue } results = append(results, result) } jsonData, err := json.Marshal(results) if err != nil { return "", fmt.Errorf("failed to encode response: %v", err) } return string(jsonData), nil } type LyricsDownloadRequest struct { SpotifyID string `json:"spotify_id"` TrackName string `json:"track_name"` ArtistName string `json:"artist_name"` AlbumName string `json:"album_name"` AlbumArtist string `json:"album_artist"` ReleaseDate string `json:"release_date"` OutputDir string `json:"output_dir"` FilenameFormat string `json:"filename_format"` TrackNumber bool `json:"track_number"` Position int `json:"position"` UseAlbumTrackNumber bool `json:"use_album_track_number"` DiscNumber int `json:"disc_number"` } func (a *App) DownloadLyrics(req LyricsDownloadRequest) (backend.LyricsDownloadResponse, error) { if req.SpotifyID == "" { return backend.LyricsDownloadResponse{ Success: false, Error: "Spotify ID is required", }, fmt.Errorf("spotify ID is required") } client := backend.NewLyricsClient() backendReq := backend.LyricsDownloadRequest{ SpotifyID: req.SpotifyID, TrackName: req.TrackName, ArtistName: req.ArtistName, AlbumName: req.AlbumName, AlbumArtist: req.AlbumArtist, ReleaseDate: req.ReleaseDate, OutputDir: req.OutputDir, FilenameFormat: req.FilenameFormat, TrackNumber: req.TrackNumber, Position: req.Position, UseAlbumTrackNumber: req.UseAlbumTrackNumber, DiscNumber: req.DiscNumber, } resp, err := client.DownloadLyrics(backendReq) if err != nil { return backend.LyricsDownloadResponse{ Success: false, Error: err.Error(), }, err } return *resp, nil } type CoverDownloadRequest struct { CoverURL string `json:"cover_url"` TrackName string `json:"track_name"` ArtistName string `json:"artist_name"` AlbumName string `json:"album_name"` AlbumArtist string `json:"album_artist"` ReleaseDate string `json:"release_date"` OutputDir string `json:"output_dir"` FilenameFormat string `json:"filename_format"` TrackNumber bool `json:"track_number"` Position int `json:"position"` DiscNumber int `json:"disc_number"` } func (a *App) DownloadCover(req CoverDownloadRequest) (backend.CoverDownloadResponse, error) { if req.CoverURL == "" { return backend.CoverDownloadResponse{ Success: false, Error: "Cover URL is required", }, fmt.Errorf("cover URL is required") } client := backend.NewCoverClient() backendReq := backend.CoverDownloadRequest{ CoverURL: req.CoverURL, TrackName: req.TrackName, ArtistName: req.ArtistName, AlbumName: req.AlbumName, AlbumArtist: req.AlbumArtist, ReleaseDate: req.ReleaseDate, OutputDir: req.OutputDir, FilenameFormat: req.FilenameFormat, TrackNumber: req.TrackNumber, Position: req.Position, DiscNumber: req.DiscNumber, } resp, err := client.DownloadCover(backendReq) if err != nil { return backend.CoverDownloadResponse{ Success: false, Error: err.Error(), }, err } return *resp, nil } 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") } client := backend.NewSongLinkClient() availability, err := client.CheckTrackAvailability(spotifyTrackID, isrc) if err != nil { return "", err } jsonData, err := json.Marshal(availability) if err != nil { return "", fmt.Errorf("failed to encode response: %v", err) } return string(jsonData), nil } func (a *App) IsFFmpegInstalled() (bool, error) { return backend.IsFFmpegInstalled() } func (a *App) IsFFprobeInstalled() (bool, error) { return backend.IsFFprobeInstalled() } func (a *App) GetFFmpegPath() (string, error) { return backend.GetFFmpegPath() } type DownloadFFmpegRequest struct{} type DownloadFFmpegResponse struct { Success bool `json:"success"` Message string `json:"message"` Error string `json:"error,omitempty"` } func (a *App) DownloadFFmpeg() DownloadFFmpegResponse { runtime.EventsEmit(a.ctx, "ffmpeg:status", "starting") err := backend.DownloadFFmpeg(func(progress int) { runtime.EventsEmit(a.ctx, "ffmpeg:progress", progress) }) if err != nil { runtime.EventsEmit(a.ctx, "ffmpeg:status", "failed") return DownloadFFmpegResponse{ Success: false, Error: err.Error(), } } runtime.EventsEmit(a.ctx, "ffmpeg:status", "completed") return DownloadFFmpegResponse{ Success: true, Message: "FFmpeg installed successfully", } } type ConvertAudioRequest struct { InputFiles []string `json:"input_files"` OutputFormat string `json:"output_format"` Bitrate string `json:"bitrate"` Codec string `json:"codec"` } func (a *App) ConvertAudio(req ConvertAudioRequest) ([]backend.ConvertAudioResult, error) { backendReq := backend.ConvertAudioRequest{ InputFiles: req.InputFiles, OutputFormat: req.OutputFormat, Bitrate: req.Bitrate, Codec: req.Codec, } return backend.ConvertAudio(backendReq) } func (a *App) SelectAudioFiles() ([]string, error) { files, err := backend.SelectMultipleFiles(a.ctx) if err != nil { return nil, err } return files, nil } func (a *App) GetFileSizes(files []string) map[string]int64 { return backend.GetFileSizes(files) } func (a *App) ListDirectoryFiles(dirPath string) ([]backend.FileInfo, error) { if dirPath == "" { return nil, fmt.Errorf("directory path is required") } return backend.ListDirectory(dirPath) } func (a *App) ListAudioFilesInDir(dirPath string) ([]backend.FileInfo, error) { if dirPath == "" { return nil, fmt.Errorf("directory path is required") } return backend.ListAudioFiles(dirPath) } func (a *App) ReadFileMetadata(filePath string) (*backend.AudioMetadata, error) { if filePath == "" { return nil, fmt.Errorf("file path is required") } return backend.ReadAudioMetadata(filePath) } func (a *App) PreviewRenameFiles(files []string, format string) []backend.RenamePreview { return backend.PreviewRename(files, format) } func (a *App) RenameFilesByMetadata(files []string, format string) []backend.RenameResult { return backend.RenameFiles(files, format) } func (a *App) ReadTextFile(filePath string) (string, error) { content, err := os.ReadFile(filePath) if err != nil { return "", err } return string(content), nil } func (a *App) RenameFileTo(oldPath, newName string) error { dir := filepath.Dir(oldPath) ext := filepath.Ext(oldPath) newPath := filepath.Join(dir, newName+ext) return os.Rename(oldPath, newPath) } 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 { case ".jpg", ".jpeg": mimeType = "image/jpeg" case ".png": mimeType = "image/png" case ".gif": mimeType = "image/gif" case ".webp": mimeType = "image/webp" default: mimeType = "image/jpeg" } encoded := base64.StdEncoding.EncodeToString(content) return fmt.Sprintf("data:%s;base64,%s", mimeType, encoded), nil } type CheckFileExistenceRequest struct { 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"` } 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"` } func (a *App) CheckFilesExistence(outputDir string, tracks []CheckFileExistenceRequest) []CheckFileExistenceResult { if len(tracks) == 0 { return []CheckFileExistenceResult{} } 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 } func (a *App) SkipDownloadItem(itemID, filePath string) { backend.SkipDownloadItem(itemID, filePath) } func (a *App) GetPreviewURL(trackID string) (string, error) { return backend.GetPreviewURL(trackID) } func (a *App) GetConfigPath() (string, error) { dir, err := backend.GetFFmpegDir() if err != nil { return "", err } return filepath.Join(dir, "config.json"), nil } func (a *App) SaveSettings(settings map[string]interface{}) error { configPath, err := a.GetConfigPath() if err != nil { return err } dir := filepath.Dir(configPath) if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, 0755); err != nil { return err } } data, err := json.MarshalIndent(settings, "", " ") if err != nil { return err } return os.WriteFile(configPath, data, 0644) } func (a *App) LoadSettings() (map[string]interface{}, error) { configPath, err := a.GetConfigPath() if err != nil { return nil, err } if _, err := os.Stat(configPath); os.IsNotExist(err) { return nil, nil } data, err := os.ReadFile(configPath) if err != nil { return nil, err } var settings map[string]interface{} if err := json.Unmarshal(data, &settings); err != nil { return nil, err } return settings, nil } func (a *App) CheckFFmpegInstalled() (bool, error) { return backend.IsFFmpegInstalled() } func (a *App) GetOSInfo() (string, error) { osType := goRuntime.GOOS arch := goRuntime.GOARCH switch osType { case "windows": out, err := exec.Command("wmic", "os", "get", "Caption,Version", "/value").Output() if err != nil { outVer, errVer := exec.Command("cmd", "/c", "ver").Output() if errVer != nil { return fmt.Sprintf("Windows %s", arch), nil } return strings.TrimSpace(string(outVer)), nil } lines := strings.Split(string(out), "\n") var caption, version string for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "Caption=") { caption = strings.TrimPrefix(line, "Caption=") } else if strings.HasPrefix(line, "Version=") { version = strings.TrimPrefix(line, "Version=") } } if caption != "" && version != "" { return fmt.Sprintf("%s (%s, %s)", caption, version, arch), nil } return strings.TrimSpace(string(out)), nil case "darwin": out, err := exec.Command("sw_vers", "-productVersion").Output() if err != nil { return fmt.Sprintf("macOS %s", arch), nil } version := strings.TrimSpace(string(out)) return fmt.Sprintf("macOS %s (%s)", version, arch), nil case "linux": out, err := exec.Command("cat", "/etc/os-release").Output() if err == nil { lines := strings.Split(string(out), "\n") for _, line := range lines { if strings.HasPrefix(line, "PRETTY_NAME=") { name := strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"") return fmt.Sprintf("%s (%s)", name, arch), nil } } } return fmt.Sprintf("Linux %s", arch), nil default: return fmt.Sprintf("%s %s", osType, arch), nil } }