v7.1.4
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/afkarxyz/SpotiFLAC/backend"
|
||||
@@ -25,7 +26,22 @@ type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
type CurrentIPInfo struct {
|
||||
IP string `json:"ip"`
|
||||
Country string `json:"country"`
|
||||
CountryCode string `json:"country_code,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
const checkOperationTimeout = 10 * time.Second
|
||||
const unifiedStatusAPIURL = "https://api-status.afkarxyz.qzz.io/api/status/spotiflac/"
|
||||
const unifiedStatusCacheTTL = 5 * time.Second
|
||||
|
||||
var (
|
||||
unifiedStatusCacheMu sync.Mutex
|
||||
unifiedStatusCacheBody string
|
||||
unifiedStatusCacheExpiry time.Time
|
||||
)
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
@@ -78,6 +94,42 @@ func containsStreamingURL(body []byte) bool {
|
||||
return isStreamingURL(trimmedBody)
|
||||
}
|
||||
|
||||
func containsLRCLIBResults(body []byte) bool {
|
||||
trimmedBody := strings.TrimSpace(string(body))
|
||||
if trimmedBody == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
var searchResults []map[string]interface{}
|
||||
if err := json.Unmarshal(body, &searchResults); err == nil {
|
||||
return len(searchResults) > 0
|
||||
}
|
||||
|
||||
var exactResult map[string]interface{}
|
||||
if err := json.Unmarshal(body, &exactResult); err == nil {
|
||||
return len(exactResult) > 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func containsMusicBrainzResults(body []byte) bool {
|
||||
trimmedBody := strings.TrimSpace(string(body))
|
||||
if trimmedBody == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Count int `json:"count"`
|
||||
Recordings []json.RawMessage `json:"recordings"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return payload.Count > 0 || len(payload.Recordings) > 0
|
||||
}
|
||||
|
||||
func isStreamingURL(raw string) bool {
|
||||
candidate := strings.TrimSpace(raw)
|
||||
if candidate == "" {
|
||||
@@ -92,6 +144,179 @@ func isStreamingURL(raw string) bool {
|
||||
return (parsed.Scheme == "http" || parsed.Scheme == "https") && parsed.Host != ""
|
||||
}
|
||||
|
||||
func previewResponseBody(body []byte, maxLen int) string {
|
||||
preview := strings.TrimSpace(string(body))
|
||||
if maxLen > 0 && len(preview) > maxLen {
|
||||
return preview[:maxLen] + "..."
|
||||
}
|
||||
return preview
|
||||
}
|
||||
|
||||
func fetchUnifiedStatusPayload(forceRefresh bool, endpoint string) (string, error) {
|
||||
unifiedStatusCacheMu.Lock()
|
||||
defer unifiedStatusCacheMu.Unlock()
|
||||
|
||||
if !forceRefresh && unifiedStatusCacheBody != "" && time.Now().Before(unifiedStatusCacheExpiry) {
|
||||
return unifiedStatusCacheBody, nil
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
maxRetries := 3
|
||||
var lastErr error
|
||||
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create unified status request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if readErr != nil {
|
||||
lastErr = fmt.Errorf("attempt %d: failed reading response: %w", i+1, readErr)
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
lastErr = fmt.Errorf("attempt %d: returned status %d (%s)", i+1, resp.StatusCode, previewResponseBody(body, 200))
|
||||
} else {
|
||||
payload := strings.TrimSpace(string(body))
|
||||
if payload == "" {
|
||||
lastErr = fmt.Errorf("attempt %d: empty response body", i+1)
|
||||
} else {
|
||||
unifiedStatusCacheBody = payload
|
||||
unifiedStatusCacheExpiry = time.Now().Add(unifiedStatusCacheTTL)
|
||||
return payload, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lastErr = fmt.Errorf("attempt %d: connection failed: %w", i+1, err)
|
||||
}
|
||||
|
||||
if i < maxRetries-1 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
if lastErr == nil {
|
||||
lastErr = fmt.Errorf("unknown error")
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unified status API failed after %d retries: %w", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
func fetchCurrentIPInfo() (CurrentIPInfo, error) {
|
||||
type ipwhoisResponse struct {
|
||||
Success bool `json:"success"`
|
||||
IP string `json:"ip"`
|
||||
Country string `json:"country"`
|
||||
CountryCode string `json:"country_code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
type ipapiResponse struct {
|
||||
IP string `json:"ip"`
|
||||
Country string `json:"country_name"`
|
||||
CountryCode string `json:"country_code"`
|
||||
Error bool `json:"error"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 8 * time.Second}
|
||||
tryFetch := func(source, reqURL string, parse func(body []byte) (CurrentIPInfo, error)) (CurrentIPInfo, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
return CurrentIPInfo{}, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return CurrentIPInfo{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return CurrentIPInfo{}, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return CurrentIPInfo{}, fmt.Errorf("%s returned status %d: %s", source, resp.StatusCode, previewResponseBody(body, 200))
|
||||
}
|
||||
|
||||
info, err := parse(body)
|
||||
if err != nil {
|
||||
return CurrentIPInfo{}, err
|
||||
}
|
||||
info.Source = source
|
||||
return info, nil
|
||||
}
|
||||
|
||||
info, err := tryFetch("ipwho.is", "https://ipwho.is/", func(body []byte) (CurrentIPInfo, error) {
|
||||
var payload ipwhoisResponse
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return CurrentIPInfo{}, err
|
||||
}
|
||||
if !payload.Success {
|
||||
return CurrentIPInfo{}, fmt.Errorf("ipwho.is lookup failed: %s", strings.TrimSpace(payload.Message))
|
||||
}
|
||||
if strings.TrimSpace(payload.IP) == "" || strings.TrimSpace(payload.Country) == "" {
|
||||
return CurrentIPInfo{}, fmt.Errorf("ipwho.is returned incomplete IP data")
|
||||
}
|
||||
return CurrentIPInfo{
|
||||
IP: strings.TrimSpace(payload.IP),
|
||||
Country: strings.TrimSpace(payload.Country),
|
||||
CountryCode: strings.TrimSpace(payload.CountryCode),
|
||||
}, nil
|
||||
})
|
||||
if err == nil {
|
||||
return info, nil
|
||||
}
|
||||
firstErr := err
|
||||
|
||||
info, err = tryFetch("ipapi.co", "https://ipapi.co/json/", func(body []byte) (CurrentIPInfo, error) {
|
||||
var payload ipapiResponse
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return CurrentIPInfo{}, err
|
||||
}
|
||||
if payload.Error {
|
||||
return CurrentIPInfo{}, fmt.Errorf("ipapi.co lookup failed: %s", strings.TrimSpace(payload.Reason))
|
||||
}
|
||||
if strings.TrimSpace(payload.IP) == "" || strings.TrimSpace(payload.Country) == "" {
|
||||
return CurrentIPInfo{}, fmt.Errorf("ipapi.co returned incomplete IP data")
|
||||
}
|
||||
return CurrentIPInfo{
|
||||
IP: strings.TrimSpace(payload.IP),
|
||||
Country: strings.TrimSpace(payload.Country),
|
||||
CountryCode: strings.TrimSpace(payload.CountryCode),
|
||||
}, nil
|
||||
})
|
||||
if err == nil {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
return CurrentIPInfo{}, fmt.Errorf("failed to detect public IP: %v; fallback failed: %v", firstErr, err)
|
||||
}
|
||||
|
||||
func (a *App) GetCurrentIPInfo() (string, error) {
|
||||
info, err := fetchCurrentIPInfo()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(payload), nil
|
||||
}
|
||||
|
||||
func (a *App) FetchUnifiedAPIStatus(forceRefresh bool) (string, error) {
|
||||
return fetchUnifiedStatusPayload(forceRefresh, unifiedStatusAPIURL)
|
||||
}
|
||||
|
||||
func (a *App) getFirstArtist(artistString string) string {
|
||||
if artistString == "" {
|
||||
return ""
|
||||
@@ -142,7 +367,7 @@ type DownloadRequest struct {
|
||||
AlbumArtist string `json:"album_artist,omitempty"`
|
||||
ReleaseDate string `json:"release_date,omitempty"`
|
||||
CoverURL string `json:"cover_url,omitempty"`
|
||||
ApiURL string `json:"api_url,omitempty"`
|
||||
TidalAPIURL string `json:"tidal_api_url,omitempty"`
|
||||
OutputDir string `json:"output_dir,omitempty"`
|
||||
AudioFormat string `json:"audio_format,omitempty"`
|
||||
FilenameFormat string `json:"filename_format,omitempty"`
|
||||
@@ -159,8 +384,10 @@ type DownloadRequest struct {
|
||||
SpotifyDiscNumber int `json:"spotify_disc_number,omitempty"`
|
||||
SpotifyTotalTracks int `json:"spotify_total_tracks,omitempty"`
|
||||
SpotifyTotalDiscs int `json:"spotify_total_discs,omitempty"`
|
||||
ISRC string `json:"isrc,omitempty"`
|
||||
Copyright string `json:"copyright,omitempty"`
|
||||
Publisher string `json:"publisher,omitempty"`
|
||||
Composer string `json:"composer,omitempty"`
|
||||
PlaylistName string `json:"playlist_name,omitempty"`
|
||||
PlaylistOwner string `json:"playlist_owner,omitempty"`
|
||||
AllowFallback bool `json:"allow_fallback"`
|
||||
@@ -245,27 +472,6 @@ func (a *App) GetSpotifyMetadata(req SpotifyMetadataRequest) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil && settings != nil {
|
||||
if useAPI, ok := settings["useSpotFetchAPI"].(bool); ok && useAPI {
|
||||
if apiURL, ok := settings["spotFetchAPIUrl"].(string); ok && apiURL != "" {
|
||||
|
||||
data, err := backend.GetSpotifyDataWithAPI(ctx, req.URL, true, apiURL, req.Batch, time.Duration(req.Delay*float64(time.Second)), separator, func(tracks interface{}) {
|
||||
runtime.EventsEmit(a.ctx, "metadata-stream", tracks)
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch metadata from API: %v", err)
|
||||
}
|
||||
|
||||
jsonData, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encode response: %v", err)
|
||||
}
|
||||
|
||||
return string(jsonData), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data, err := backend.GetFilteredSpotifyData(ctx, req.URL, req.Batch, time.Duration(req.Delay*float64(time.Second)), separator, func(tracks interface{}) {
|
||||
runtime.EventsEmit(a.ctx, "metadata-stream", tracks)
|
||||
})
|
||||
@@ -362,6 +568,9 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
if req.FilenameFormat == "" {
|
||||
req.FilenameFormat = "title-artist"
|
||||
}
|
||||
if req.ISRC == "" && strings.Contains(req.FilenameFormat, "{isrc}") && req.SpotifyID != "" {
|
||||
req.ISRC = backend.ResolveTrackISRC(req.SpotifyID)
|
||||
}
|
||||
|
||||
itemID := req.ItemID
|
||||
if itemID == "" {
|
||||
@@ -384,25 +593,26 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
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) {
|
||||
metadataSeparator := req.Separator
|
||||
if metadataSeparator == "" {
|
||||
metadataSeparator = ", "
|
||||
metadataSettings, _ := a.LoadSettings()
|
||||
if metadataSettings != nil {
|
||||
if sep, ok := metadataSettings["separator"].(string); ok {
|
||||
if sep == "semicolon" {
|
||||
metadataSeparator = "; "
|
||||
} else if sep == "comma" {
|
||||
metadataSeparator = ", "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.SpotifyID != "" && (req.Copyright == "" || req.Publisher == "" || req.Composer == "" || 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)
|
||||
metadataSeparator := req.Separator
|
||||
if metadataSeparator == "" {
|
||||
metadataSeparator = ", "
|
||||
metadataSettings, _ := a.LoadSettings()
|
||||
if metadataSettings != nil {
|
||||
if sep, ok := metadataSettings["separator"].(string); ok {
|
||||
if sep == "semicolon" {
|
||||
metadataSeparator = "; "
|
||||
} else if sep == "comma" {
|
||||
metadataSeparator = ", "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
trackData, err := backend.GetFilteredSpotifyData(ctx, trackURL, false, 0, metadataSeparator, nil)
|
||||
if err == nil {
|
||||
|
||||
@@ -410,6 +620,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
Track struct {
|
||||
Copyright string `json:"copyright"`
|
||||
Publisher string `json:"publisher"`
|
||||
Composer string `json:"composer"`
|
||||
TotalDiscs int `json:"total_discs"`
|
||||
TotalTracks int `json:"total_tracks"`
|
||||
TrackNumber int `json:"track_number"`
|
||||
@@ -425,6 +636,9 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
if req.Publisher == "" && trackResp.Track.Publisher != "" {
|
||||
req.Publisher = trackResp.Track.Publisher
|
||||
}
|
||||
if req.Composer == "" && trackResp.Track.Composer != "" {
|
||||
req.Composer = trackResp.Track.Composer
|
||||
}
|
||||
if req.SpotifyTotalDiscs == 0 && trackResp.Track.TotalDiscs > 0 {
|
||||
req.SpotifyTotalDiscs = trackResp.Track.TotalDiscs
|
||||
}
|
||||
@@ -443,19 +657,21 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
}
|
||||
|
||||
if req.TrackName != "" && req.ArtistName != "" {
|
||||
expectedFilename := backend.BuildExpectedFilename(req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.SpotifyDiscNumber, req.UseAlbumTrackNumber)
|
||||
expectedFilename := backend.BuildExpectedFilename(req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.SpotifyDiscNumber, req.UseAlbumTrackNumber, req.ISRC)
|
||||
expectedPath := filepath.Join(req.OutputDir, expectedFilename)
|
||||
|
||||
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 {
|
||||
if !backend.GetRedownloadWithSuffixSetting() {
|
||||
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 {
|
||||
|
||||
backend.SkipDownloadItem(itemID, expectedPath)
|
||||
return DownloadResponse{
|
||||
Success: true,
|
||||
Message: "File already exists",
|
||||
File: expectedPath,
|
||||
AlreadyExists: true,
|
||||
ItemID: itemID,
|
||||
}, nil
|
||||
backend.SkipDownloadItem(itemID, expectedPath)
|
||||
return DownloadResponse{
|
||||
Success: true,
|
||||
Message: "File already exists",
|
||||
File: expectedPath,
|
||||
AlreadyExists: true,
|
||||
ItemID: itemID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,38 +716,41 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
||||
|
||||
downloader := backend.NewAmazonDownloader()
|
||||
if req.ServiceURL != "" {
|
||||
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
} else {
|
||||
filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
}
|
||||
|
||||
case "tidal":
|
||||
if req.ApiURL == "" || req.ApiURL == "auto" {
|
||||
if req.TidalAPIURL == "" || req.TidalAPIURL == "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, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
filename, err = downloader.DownloadByURLWithFallback(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
} else {
|
||||
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
}
|
||||
} else {
|
||||
downloader := backend.NewTidalDownloader(req.ApiURL)
|
||||
downloader := backend.NewTidalDownloader(req.TidalAPIURL)
|
||||
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, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
} else {
|
||||
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
}
|
||||
}
|
||||
|
||||
case "qobuz":
|
||||
|
||||
fmt.Println("Waiting for ISRC (Qobuz dependency)...")
|
||||
isrc := <-isrcChan
|
||||
isrc := strings.TrimSpace(req.ISRC)
|
||||
if isrc == "" {
|
||||
fmt.Println("Waiting for ISRC (Qobuz dependency)...")
|
||||
isrc = <-isrcChan
|
||||
}
|
||||
downloader := backend.NewQobuzDownloader()
|
||||
quality := req.AudioFormat
|
||||
if quality == "" {
|
||||
quality = "6"
|
||||
}
|
||||
filename, err = downloader.DownloadTrackWithISRC(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, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
filename, err = downloader.DownloadTrackWithISRC(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, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||
|
||||
default:
|
||||
return DownloadResponse{
|
||||
@@ -832,6 +1051,10 @@ func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
||||
checkURL = fmt.Sprintf("%s/api/track/360735657?quality=27", apiURL)
|
||||
} else if apiType == "amazon" {
|
||||
checkURL = fmt.Sprintf("%s/status", apiURL)
|
||||
} else if apiType == "lrclib" {
|
||||
checkURL = fmt.Sprintf("%s/api/search?artist_name=Adele&track_name=Hello", strings.TrimRight(apiURL, "/"))
|
||||
} else if apiType == "musicbrainz" {
|
||||
checkURL = fmt.Sprintf("%s/ws/2/recording?query=%s&fmt=json&limit=1", strings.TrimRight(apiURL, "/"), url.QueryEscape(`recording:"Hello" AND artist:"Adele"`))
|
||||
} else {
|
||||
checkURL = apiURL
|
||||
}
|
||||
@@ -842,6 +1065,7 @@ func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
maxRetries := 3
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
@@ -865,7 +1089,15 @@ func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if apiType != "amazon" && apiType != "qobuz" && apiType != "qbz" && statusCode == 200 {
|
||||
if apiType == "lrclib" && statusCode == 200 && containsLRCLIBResults(body) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if apiType == "musicbrainz" && statusCode == 200 && containsMusicBrainzResults(body) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if apiType != "amazon" && apiType != "qobuz" && apiType != "qbz" && apiType != "lrclib" && apiType != "musicbrainz" && statusCode == 200 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
@@ -876,10 +1108,17 @@ func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
if apiType == "musicbrainz" {
|
||||
backend.SetMusicBrainzStatusCheckResult(false)
|
||||
}
|
||||
fmt.Printf("CheckAPIStatus timeout/error for %s (%s): %v\n", apiType, apiURL, err)
|
||||
return false
|
||||
}
|
||||
|
||||
if apiType == "musicbrainz" {
|
||||
backend.SetMusicBrainzStatusCheckResult(isOnline)
|
||||
}
|
||||
|
||||
return isOnline
|
||||
}
|
||||
|
||||
@@ -920,6 +1159,34 @@ func (a *App) ClearFetchHistoryByType(itemType string) error {
|
||||
return backend.ClearFetchHistoryByType(itemType, "SpotiFLAC")
|
||||
}
|
||||
|
||||
func (a *App) GetRecentFetches() (string, error) {
|
||||
items, err := backend.LoadRecentFetches()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(items)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (a *App) SaveRecentFetches(payload string) error {
|
||||
payload = strings.TrimSpace(payload)
|
||||
if payload == "" {
|
||||
payload = "[]"
|
||||
}
|
||||
|
||||
var items []backend.RecentFetchItem
|
||||
if err := json.Unmarshal([]byte(payload), &items); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return backend.SaveRecentFetches(items)
|
||||
}
|
||||
|
||||
func (a *App) SaveSpectrumImage(audioFilePath string, base64Data string) (string, error) {
|
||||
if audioFilePath == "" || base64Data == "" {
|
||||
return "", fmt.Errorf("file path and image data are required")
|
||||
@@ -951,6 +1218,7 @@ type LyricsDownloadRequest struct {
|
||||
AlbumName string `json:"album_name"`
|
||||
AlbumArtist string `json:"album_artist"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
ISRC string `json:"isrc,omitempty"`
|
||||
OutputDir string `json:"output_dir"`
|
||||
FilenameFormat string `json:"filename_format"`
|
||||
TrackNumber bool `json:"track_number"`
|
||||
@@ -975,6 +1243,7 @@ func (a *App) DownloadLyrics(req LyricsDownloadRequest) (backend.LyricsDownloadR
|
||||
AlbumName: req.AlbumName,
|
||||
AlbumArtist: req.AlbumArtist,
|
||||
ReleaseDate: req.ReleaseDate,
|
||||
ISRC: req.ISRC,
|
||||
OutputDir: req.OutputDir,
|
||||
FilenameFormat: req.FilenameFormat,
|
||||
TrackNumber: req.TrackNumber,
|
||||
@@ -1398,6 +1667,7 @@ type CheckFileExistenceRequest struct {
|
||||
AlbumName string `json:"album_name,omitempty"`
|
||||
AlbumArtist string `json:"album_artist,omitempty"`
|
||||
ReleaseDate string `json:"release_date,omitempty"`
|
||||
ISRC string `json:"isrc,omitempty"`
|
||||
TrackNumber int `json:"track_number,omitempty"`
|
||||
DiscNumber int `json:"disc_number,omitempty"`
|
||||
Position int `json:"position,omitempty"`
|
||||
@@ -1427,6 +1697,7 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
|
||||
}
|
||||
|
||||
defaultFilenameFormat := "title-artist"
|
||||
redownloadWithSuffix := backend.GetRedownloadWithSuffixSetting()
|
||||
|
||||
type result struct {
|
||||
index int
|
||||
@@ -1477,6 +1748,10 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
|
||||
if filenameFormat == "" {
|
||||
filenameFormat = defaultFilenameFormat
|
||||
}
|
||||
isrc := strings.TrimSpace(t.ISRC)
|
||||
if isrc == "" && strings.Contains(filenameFormat, "{isrc}") && t.SpotifyID != "" {
|
||||
isrc = backend.ResolveTrackISRC(t.SpotifyID)
|
||||
}
|
||||
|
||||
trackNumber := t.Position
|
||||
if t.UseAlbumTrackNumber && t.TrackNumber > 0 {
|
||||
@@ -1501,6 +1776,7 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
|
||||
trackNumber,
|
||||
t.DiscNumber,
|
||||
t.UseAlbumTrackNumber,
|
||||
isrc,
|
||||
)
|
||||
|
||||
expectedFilename := strings.TrimSuffix(expectedFilenameBase, ".flac") + fileExt
|
||||
@@ -1511,13 +1787,17 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
|
||||
}
|
||||
|
||||
expectedPath := filepath.Join(targetDir, expectedFilename)
|
||||
|
||||
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 {
|
||||
res.Exists = true
|
||||
res.FilePath = expectedPath
|
||||
if redownloadWithSuffix {
|
||||
expectedPath, _ = backend.ResolveOutputPathForDownload(expectedPath, true)
|
||||
res.FilePath = filepath.Base(expectedPath)
|
||||
} else {
|
||||
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 100*1024 {
|
||||
res.Exists = true
|
||||
res.FilePath = expectedPath
|
||||
} else {
|
||||
|
||||
res.FilePath = expectedFilename
|
||||
res.FilePath = expectedFilename
|
||||
}
|
||||
}
|
||||
|
||||
resultsChan <- result{index: idx, result: res}
|
||||
@@ -1567,6 +1847,10 @@ func (a *App) SkipDownloadItem(itemID, filePath string) {
|
||||
backend.SkipDownloadItem(itemID, filePath)
|
||||
}
|
||||
|
||||
func (a *App) GetTrackISRC(spotifyTrackID string) string {
|
||||
return backend.ResolveTrackISRC(spotifyTrackID)
|
||||
}
|
||||
|
||||
func (a *App) GetPreviewURL(trackID string) (string, error) {
|
||||
return backend.GetPreviewURL(trackID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user