diff --git a/app.go b/app.go index 4609349..58c0fc9 100644 --- a/app.go +++ b/app.go @@ -357,10 +357,14 @@ func (a *App) AnalyzeMultipleTracks(filePaths []string) (string, error) { // LyricsDownloadRequest represents the request structure for downloading lyrics type LyricsDownloadRequest struct { - SpotifyID string `json:"spotify_id"` - TrackName string `json:"track_name"` - ArtistName string `json:"artist_name"` - OutputDir string `json:"output_dir"` + SpotifyID string `json:"spotify_id"` + TrackName string `json:"track_name"` + ArtistName string `json:"artist_name"` + 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"` } // DownloadLyrics downloads lyrics for a single track @@ -374,10 +378,14 @@ func (a *App) DownloadLyrics(req LyricsDownloadRequest) (backend.LyricsDownloadR client := backend.NewLyricsClient() backendReq := backend.LyricsDownloadRequest{ - SpotifyID: req.SpotifyID, - TrackName: req.TrackName, - ArtistName: req.ArtistName, - OutputDir: req.OutputDir, + SpotifyID: req.SpotifyID, + TrackName: req.TrackName, + ArtistName: req.ArtistName, + OutputDir: req.OutputDir, + FilenameFormat: req.FilenameFormat, + TrackNumber: req.TrackNumber, + Position: req.Position, + UseAlbumTrackNumber: req.UseAlbumTrackNumber, } resp, err := client.DownloadLyrics(backendReq) @@ -390,3 +398,23 @@ func (a *App) DownloadLyrics(req LyricsDownloadRequest) (backend.LyricsDownloadR return *resp, nil } + +// CheckTrackAvailability checks the availability of a track on different streaming platforms +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 +} diff --git a/backend/lyrics.go b/backend/lyrics.go index 56c6b39..42727c3 100644 --- a/backend/lyrics.go +++ b/backend/lyrics.go @@ -28,10 +28,14 @@ type LyricsResponse struct { // LyricsDownloadRequest represents a request to download lyrics type LyricsDownloadRequest struct { - SpotifyID string `json:"spotify_id"` - TrackName string `json:"track_name"` - ArtistName string `json:"artist_name"` - OutputDir string `json:"output_dir"` + SpotifyID string `json:"spotify_id"` + TrackName string `json:"track_name"` + ArtistName string `json:"artist_name"` + 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"` } // LyricsDownloadResponse represents the response from lyrics download @@ -121,6 +125,31 @@ func msToLRCTimestamp(msStr string) string { return fmt.Sprintf("[%02d:%02d.%02d]", minutes, seconds, centiseconds) } +// buildLyricsFilename builds the lyrics filename based on settings (same as track filename) +func buildLyricsFilename(trackName, artistName, filenameFormat string, includeTrackNumber bool, position int) string { + safeTitle := sanitizeFilename(trackName) + safeArtist := sanitizeFilename(artistName) + + var filename string + + // Build base filename based on format + switch filenameFormat { + case "artist-title": + filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle) + case "title": + filename = safeTitle + default: // "title-artist" + filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist) + } + + // Add track number prefix if enabled + if includeTrackNumber && position > 0 { + filename = fmt.Sprintf("%02d. %s", position, filename) + } + + return filename + ".lrc" +} + // DownloadLyrics downloads lyrics for a single track func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloadResponse, error) { if req.SpotifyID == "" { @@ -143,8 +172,12 @@ func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloa }, err } - // Generate filename - filename := sanitizeFilename(fmt.Sprintf("%s - %s.lrc", req.TrackName, req.ArtistName)) + // Generate filename using same format as track + filenameFormat := req.FilenameFormat + if filenameFormat == "" { + filenameFormat = "title-artist" // default + } + filename := buildLyricsFilename(req.TrackName, req.ArtistName, filenameFormat, req.TrackNumber, req.Position) filePath := filepath.Join(outputDir, filename) // Check if file already exists diff --git a/backend/songlink.go b/backend/songlink.go index 735866e..2e3008e 100644 --- a/backend/songlink.go +++ b/backend/songlink.go @@ -22,6 +22,19 @@ type SongLinkURLs struct { AmazonURL string `json:"amazon_url"` } +// TrackAvailability represents the availability of a track on different platforms +type TrackAvailability struct { + SpotifyID string `json:"spotify_id"` + Tidal bool `json:"tidal"` + Deezer bool `json:"deezer"` + Amazon bool `json:"amazon"` + Qobuz bool `json:"qobuz"` + TidalURL string `json:"tidal_url,omitempty"` + DeezerURL string `json:"deezer_url,omitempty"` + AmazonURL string `json:"amazon_url,omitempty"` + QobuzURL string `json:"qobuz_url,omitempty"` +} + func NewSongLinkClient() *SongLinkClient { return &SongLinkClient{ client: &http.Client{ @@ -148,3 +161,152 @@ func (s *SongLinkClient) GetAllURLsFromSpotify(spotifyTrackID string) (*SongLink return urls, nil } + +// CheckTrackAvailability checks the availability of a track on different platforms +func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc string) (*TrackAvailability, error) { + // Rate limiting: max 10 requests per minute (song.link API limit) + now := time.Now() + if now.Sub(s.apiCallResetTime) >= time.Minute { + s.apiCallCount = 0 + s.apiCallResetTime = now + } + + // If we've hit the limit, wait until the next minute + if s.apiCallCount >= 9 { + waitTime := time.Minute - now.Sub(s.apiCallResetTime) + if waitTime > 0 { + fmt.Printf("Rate limit reached, waiting %v...\n", waitTime.Round(time.Second)) + time.Sleep(waitTime) + s.apiCallCount = 0 + s.apiCallResetTime = time.Now() + } + } + + // Add delay between requests (7 seconds to be safe) + if !s.lastAPICallTime.IsZero() { + timeSinceLastCall := now.Sub(s.lastAPICallTime) + minDelay := 7 * time.Second + if timeSinceLastCall < minDelay { + waitTime := minDelay - timeSinceLastCall + fmt.Printf("Rate limiting: waiting %v...\n", waitTime.Round(time.Second)) + time.Sleep(waitTime) + } + } + + // Decode base64 API URL + spotifyBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9vcGVuLnNwb3RpZnkuY29tL3RyYWNrLw==") + spotifyURL := fmt.Sprintf("%s%s", string(spotifyBase), spotifyTrackID) + + apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuc29uZy5saW5rL3YxLWFscGhhLjEvbGlua3M/dXJsPQ==") + apiURL := fmt.Sprintf("%s%s", string(apiBase), url.QueryEscape(spotifyURL)) + + req, err := http.NewRequest("GET", apiURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + fmt.Printf("Checking availability for track: %s\n", spotifyTrackID) + + // Retry logic for rate limit errors + maxRetries := 3 + var resp *http.Response + for i := 0; i < maxRetries; i++ { + resp, err = s.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to check availability: %w", err) + } + + // Update rate limit tracking + s.lastAPICallTime = time.Now() + s.apiCallCount++ + + if resp.StatusCode == 429 { + resp.Body.Close() + if i < maxRetries-1 { + waitTime := 15 * time.Second + fmt.Printf("Rate limited by API, waiting %v before retry...\n", waitTime) + time.Sleep(waitTime) + continue + } + return nil, fmt.Errorf("API rate limit exceeded after %d retries", maxRetries) + } + + if resp.StatusCode != 200 { + resp.Body.Close() + return nil, fmt.Errorf("API returned status %d", resp.StatusCode) + } + + break + } + defer resp.Body.Close() + + var songLinkResp struct { + LinksByPlatform map[string]struct { + URL string `json:"url"` + } `json:"linksByPlatform"` + } + if err := json.NewDecoder(resp.Body).Decode(&songLinkResp); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + availability := &TrackAvailability{ + SpotifyID: spotifyTrackID, + } + + // Check Tidal + if tidalLink, ok := songLinkResp.LinksByPlatform["tidal"]; ok && tidalLink.URL != "" { + availability.Tidal = true + availability.TidalURL = tidalLink.URL + } + + // Check Deezer + if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" { + availability.Deezer = true + availability.DeezerURL = deezerLink.URL + } + + // Check Amazon + if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" { + availability.Amazon = true + availability.AmazonURL = amazonLink.URL + } + + // Check Qobuz using ISRC (song.link doesn't support Qobuz) + if isrc != "" { + qobuzAvailable := checkQobuzAvailability(isrc) + availability.Qobuz = qobuzAvailable + } + + return availability, nil +} + +// checkQobuzAvailability checks if a track is available on Qobuz using ISRC +func checkQobuzAvailability(isrc string) bool { + client := &http.Client{Timeout: 10 * time.Second} + appID := "798273057" + + // Decode base64 API URL + apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly93d3cucW9idXouY29tL2FwaS5qc29uLzAuMi90cmFjay9zZWFyY2g/cXVlcnk9") + searchURL := fmt.Sprintf("%s%s&limit=1&app_id=%s", string(apiBase), isrc, appID) + + resp, err := client.Get(searchURL) + if err != nil { + return false + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return false + } + + var searchResp struct { + Tracks struct { + Total int `json:"total"` + } `json:"tracks"` + } + if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { + return false + } + + return searchResp.Tracks.Total > 0 +} diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 5df5779..680b5df 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -2d92c35b92c8ea713ea561773c5b7b7b \ No newline at end of file +e92e100705a0bb90f6783cd4074df1a7 \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2a99562..2ce6cc6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -31,6 +31,7 @@ import type { HistoryItem } from "@/components/FetchHistory"; import { useDownload } from "@/hooks/useDownload"; import { useMetadata } from "@/hooks/useMetadata"; import { useLyrics } from "@/hooks/useLyrics"; +import { useAvailability } from "@/hooks/useAvailability"; const HISTORY_KEY = "spotiflac_fetch_history"; const MAX_HISTORY = 5; @@ -45,11 +46,12 @@ function App() { const [fetchHistory, setFetchHistory] = useState([]); const ITEMS_PER_PAGE = 50; - const CURRENT_VERSION = "6.3"; + const CURRENT_VERSION = "6.4"; const download = useDownload(); const metadata = useMetadata(); const lyrics = useLyrics(); + const availability = useAvailability(); useEffect(() => { const settings = getSettings(); @@ -79,6 +81,7 @@ function App() { setSearchQuery(""); download.resetDownloadedTracks(); lyrics.resetLyricsState(); + availability.clearAvailability(); setSortBy("default"); setCurrentPage(1); }, [metadata.metadata]); @@ -256,8 +259,11 @@ function App() { downloadedLyrics={lyrics.downloadedLyrics.has(track.spotify_id || "")} failedLyrics={lyrics.failedLyrics.has(track.spotify_id || "")} skippedLyrics={lyrics.skippedLyrics.has(track.spotify_id || "")} + checkingAvailability={availability.checkingTrackId === track.spotify_id} + availability={availability.getAvailability(track.spotify_id || "")} onDownload={download.handleDownloadTrack} onDownloadLyrics={lyrics.handleDownloadLyrics} + onCheckAvailability={availability.checkAvailability} onOpenFolder={handleOpenFolder} /> ); @@ -286,14 +292,17 @@ function App() { failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} + checkingAvailabilityTrack={availability.checkingTrackId} + availabilityMap={availability.availabilityMap} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} - onDownloadLyrics={(spotifyId, name, artists, albumName) => - lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, album_info.name) + onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position) => + lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, album_info.name, false, position) } + onCheckAvailability={availability.checkAvailability} onDownloadAll={() => download.handleDownloadAll(track_list, album_info.name)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, album_info.name) @@ -340,14 +349,17 @@ function App() { failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} + checkingAvailabilityTrack={availability.checkingTrackId} + availabilityMap={availability.availabilityMap} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} - onDownloadLyrics={(spotifyId, name, artists, albumName) => - lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, playlist_info.owner.name) + onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position) => + lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, playlist_info.owner.name, false, position) } + onCheckAvailability={availability.checkAvailability} onDownloadAll={() => download.handleDownloadAll(track_list, playlist_info.owner.name)} onDownloadSelected={() => download.handleDownloadSelected( @@ -400,14 +412,17 @@ function App() { failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} + checkingAvailabilityTrack={availability.checkingTrackId} + availabilityMap={availability.availabilityMap} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} - onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, isArtistDiscography) => - lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, artist_info.name, isArtistDiscography) + onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, isArtistDiscography, position) => + lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, artist_info.name, isArtistDiscography, position) } + onCheckAvailability={availability.checkAvailability} onDownloadAll={() => download.handleDownloadAll(track_list, artist_info.name, true)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, artist_info.name, true) diff --git a/frontend/src/components/AlbumInfo.tsx b/frontend/src/components/AlbumInfo.tsx index 633fbd8..64d08c6 100644 --- a/frontend/src/components/AlbumInfo.tsx +++ b/frontend/src/components/AlbumInfo.tsx @@ -5,7 +5,7 @@ import { Spinner } from "@/components/ui/spinner"; import { SearchAndSort } from "./SearchAndSort"; import { TrackList } from "./TrackList"; import { DownloadProgress } from "./DownloadProgress"; -import type { TrackMetadata } from "@/types/api"; +import type { TrackMetadata, TrackAvailability } from "@/types/api"; interface AlbumInfoProps { albumInfo: { @@ -36,12 +36,16 @@ interface AlbumInfoProps { failedLyrics?: Set; skippedLyrics?: Set; downloadingLyricsTrack?: string | null; + // Availability props + checkingAvailabilityTrack?: string | null; + availabilityMap?: Map; onSearchChange: (value: string) => void; onSortChange: (value: string) => void; onToggleTrack: (isrc: string) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void; onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string) => void; - onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean) => void; + onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void; + onCheckAvailability?: (spotifyId: string) => void; onDownloadAll: () => void; onDownloadSelected: () => void; onStopDownload: () => void; @@ -71,12 +75,15 @@ export function AlbumInfo({ failedLyrics, skippedLyrics, downloadingLyricsTrack, + checkingAvailabilityTrack, + availabilityMap, onSearchChange, onSortChange, onToggleTrack, onToggleSelectAll, onDownloadTrack, onDownloadLyrics, + onCheckAvailability, onDownloadAll, onDownloadSelected, onStopDownload, @@ -191,10 +198,13 @@ export function AlbumInfo({ failedLyrics={failedLyrics} skippedLyrics={skippedLyrics} downloadingLyricsTrack={downloadingLyricsTrack} + checkingAvailabilityTrack={checkingAvailabilityTrack} + availabilityMap={availabilityMap} onToggleTrack={onToggleTrack} onToggleSelectAll={onToggleSelectAll} onDownloadTrack={onDownloadTrack} onDownloadLyrics={onDownloadLyrics} + onCheckAvailability={onCheckAvailability} onPageChange={onPageChange} onTrackClick={onTrackClick} /> diff --git a/frontend/src/components/ArtistInfo.tsx b/frontend/src/components/ArtistInfo.tsx index bc984e3..4fda137 100644 --- a/frontend/src/components/ArtistInfo.tsx +++ b/frontend/src/components/ArtistInfo.tsx @@ -5,7 +5,7 @@ import { Spinner } from "@/components/ui/spinner"; import { SearchAndSort } from "./SearchAndSort"; import { TrackList } from "./TrackList"; import { DownloadProgress } from "./DownloadProgress"; -import type { TrackMetadata } from "@/types/api"; +import type { TrackMetadata, TrackAvailability } from "@/types/api"; interface ArtistInfoProps { artistInfo: { @@ -41,12 +41,16 @@ interface ArtistInfoProps { failedLyrics?: Set; skippedLyrics?: Set; downloadingLyricsTrack?: string | null; + // Availability props + checkingAvailabilityTrack?: string | null; + availabilityMap?: Map; onSearchChange: (value: string) => void; onSortChange: (value: string) => void; onToggleTrack: (isrc: string) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void; onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string) => void; - onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean) => void; + onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void; + onCheckAvailability?: (spotifyId: string) => void; onDownloadAll: () => void; onDownloadSelected: () => void; onStopDownload: () => void; @@ -78,12 +82,15 @@ export function ArtistInfo({ failedLyrics, skippedLyrics, downloadingLyricsTrack, + checkingAvailabilityTrack, + availabilityMap, onSearchChange, onSortChange, onToggleTrack, onToggleSelectAll, onDownloadTrack, onDownloadLyrics, + onCheckAvailability, onDownloadAll, onDownloadSelected, onStopDownload, @@ -230,10 +237,13 @@ export function ArtistInfo({ failedLyrics={failedLyrics} skippedLyrics={skippedLyrics} downloadingLyricsTrack={downloadingLyricsTrack} + checkingAvailabilityTrack={checkingAvailabilityTrack} + availabilityMap={availabilityMap} onToggleTrack={onToggleTrack} onToggleSelectAll={onToggleSelectAll} onDownloadTrack={onDownloadTrack} onDownloadLyrics={onDownloadLyrics} + onCheckAvailability={onCheckAvailability} onPageChange={onPageChange} onAlbumClick={onAlbumClick} onArtistClick={onArtistClick} diff --git a/frontend/src/components/AudioAnalysisDialog.tsx b/frontend/src/components/AudioAnalysisDialog.tsx index 56eda97..c94795e 100644 --- a/frontend/src/components/AudioAnalysisDialog.tsx +++ b/frontend/src/components/AudioAnalysisDialog.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -17,11 +17,14 @@ import { SpectrumVisualization } from "@/components/SpectrumVisualization"; import { useAudioAnalysis } from "@/hooks/useAudioAnalysis"; import { SelectFile } from "../../wailsjs/go/main/App"; import { toastWithSound as toast } from "@/lib/toast-with-sound"; +import { OnFileDrop, OnFileDropOff } from "../../wailsjs/runtime/runtime"; +import { useEffect } from "react"; export function AudioAnalysisDialog() { const [open, setOpen] = useState(false); const { analyzing, result, analyzeFile, clearResult } = useAudioAnalysis(); const [selectedFilePath, setSelectedFilePath] = useState(""); + const [isDragging, setIsDragging] = useState(false); const handleSelectFile = async () => { try { @@ -37,6 +40,38 @@ export function AudioAnalysisDialog() { } }; + const handleFileDrop = useCallback(async (_x: number, _y: number, paths: string[]) => { + setIsDragging(false); + + if (paths.length === 0) return; + + const filePath = paths[0]; + + // Check if it's a FLAC file + if (!filePath.toLowerCase().endsWith('.flac')) { + toast.error("Invalid File Type", { + description: "Please drop a FLAC file for analysis", + }); + return; + } + + setSelectedFilePath(filePath); + await analyzeFile(filePath); + }, [analyzeFile]); + + // Register drag and drop handlers when dialog is open + useEffect(() => { + if (open) { + OnFileDrop((x, y, paths) => { + handleFileDrop(x, y, paths); + }, true); + + return () => { + OnFileDropOff(); + }; + } + }, [open, handleFileDrop]); + const handleClose = () => { setOpen(false); setTimeout(() => { @@ -82,11 +117,32 @@ export function AudioAnalysisDialog() {
{/* File Selection */} {!result && !analyzing && ( -
- +
{ + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={(e) => { + e.preventDefault(); + setIsDragging(false); + }} + onDrop={(e) => { + e.preventDefault(); + setIsDragging(false); + }} + style={{ "--wails-drop-target": "drop" } as React.CSSProperties} + > +

Analyze FLAC Audio Quality

- Upload a FLAC file to verify true lossless quality, view detailed technical specifications, and see the frequency spectrum + {isDragging + ? "Drop your FLAC file here" + : "Drag and drop a FLAC file here, or click the button below to select"}

+ {track.spotify_id && onCheckAvailability && ( + + + + + +

Check Availability

+
+
+ )} {track.spotify_id && onDownloadLyrics && ( - + + + +

Download Lyric

+
+ )} {isDownloaded && ( )} + {track.spotify_id && onCheckAvailability && ( + + + + + + {availabilityMap?.has(track.spotify_id) ? ( +
+ + + + +
+ ) : ( +

Check Availability

+ )} +
+
+ )} {track.spotify_id && onDownloadLyrics && (