1248 lines
36 KiB
Go
1248 lines
36 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
|
|
"path/filepath"
|
|
"regexp"
|
|
|
|
"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"`
|
|
PlaylistName string `json:"playlist_name,omitempty"`
|
|
PlaylistOwner string `json:"playlist_owner,omitempty"`
|
|
AllowFallback bool `json:"allow_fallback"`
|
|
}
|
|
|
|
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, region string) (string, error) {
|
|
if spotifyTrackID == "" {
|
|
return "", fmt.Errorf("spotify track ID is required")
|
|
}
|
|
|
|
fmt.Printf("[GetStreamingURLs] Called for track ID: %s, Region: %s\n", spotifyTrackID, region)
|
|
client := backend.NewSongLinkClient()
|
|
urls, err := client.GetAllURLsFromSpotify(spotifyTrackID, region)
|
|
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 {
|
|
|
|
if req.PlaylistName != "" {
|
|
sanitizedPlaylist := backend.SanitizeFilename(req.PlaylistName)
|
|
req.OutputDir = filepath.Join(req.OutputDir, sanitizedPlaylist)
|
|
}
|
|
|
|
req.OutputDir = backend.SanitizeFolderPath(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.PlaylistName, req.PlaylistOwner, 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.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)
|
|
} 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.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":
|
|
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, req.AllowFallback)
|
|
} 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, req.AllowFallback)
|
|
}
|
|
} 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, req.AllowFallback)
|
|
} 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, req.AllowFallback)
|
|
}
|
|
}
|
|
|
|
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, req.AllowFallback)
|
|
|
|
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:])
|
|
}
|
|
}
|
|
|
|
switch item.Format {
|
|
case "6", "7", "27":
|
|
item.Format = "FLAC"
|
|
}
|
|
|
|
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) DeleteDownloadHistoryItem(id string) error {
|
|
return backend.DeleteHistoryItem(id, "SpotiFLAC")
|
|
}
|
|
|
|
func (a *App) GetFetchHistory() ([]backend.FetchHistoryItem, error) {
|
|
return backend.GetFetchHistoryItems("SpotiFLAC")
|
|
}
|
|
|
|
func (a *App) AddFetchHistory(item backend.FetchHistoryItem) error {
|
|
return backend.AddFetchHistoryItem(item, "SpotiFLAC")
|
|
}
|
|
|
|
func (a *App) ClearFetchHistory() error {
|
|
return backend.ClearFetchHistory("SpotiFLAC")
|
|
}
|
|
|
|
func (a *App) DeleteFetchHistoryItem(id string) error {
|
|
return backend.DeleteFetchHistoryItem(id, "SpotiFLAC")
|
|
}
|
|
|
|
func (a *App) ClearFetchHistoryByType(itemType string) error {
|
|
return backend.ClearFetchHistoryByType(itemType, "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) UploadImage(filePath string) (string, error) {
|
|
return backend.UploadToSendNow(filePath)
|
|
}
|
|
|
|
func (a *App) UploadImageBytes(filename string, base64Data string) (string, error) {
|
|
|
|
if idx := strings.Index(base64Data, ","); idx != -1 {
|
|
base64Data = base64Data[idx+1:]
|
|
}
|
|
|
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to decode base64: %v", err)
|
|
}
|
|
return backend.UploadBytesToSendNow(filename, data)
|
|
}
|
|
|
|
func (a *App) SelectImageVideo() ([]string, error) {
|
|
return backend.SelectImageVideoDialog(a.ctx)
|
|
}
|
|
|
|
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"`
|
|
RelativePath string `json:"relative_path,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
|
|
|
|
targetDir := outputDir
|
|
if t.RelativePath != "" {
|
|
targetDir = filepath.Join(outputDir, t.RelativePath)
|
|
}
|
|
|
|
expectedPath := filepath.Join(targetDir, 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) {
|
|
return backend.GetOSInfo()
|
|
}
|