package gobackend import ( "fmt" "sync" "time" ) // ======================================== // ISRC to Track ID Cache // ======================================== // TrackIDCacheEntry holds cached track ID with metadata type TrackIDCacheEntry struct { TidalTrackID int64 QobuzTrackID int64 AmazonTrackID string ExpiresAt time.Time } // TrackIDCache caches ISRC to track ID mappings type TrackIDCache struct { cache map[string]*TrackIDCacheEntry mu sync.RWMutex ttl time.Duration } var ( globalTrackIDCache *TrackIDCache trackIDCacheOnce sync.Once ) // GetTrackIDCache returns the global track ID cache func GetTrackIDCache() *TrackIDCache { trackIDCacheOnce.Do(func() { globalTrackIDCache = &TrackIDCache{ cache: make(map[string]*TrackIDCacheEntry), ttl: 30 * time.Minute, } }) return globalTrackIDCache } // Get retrieves a cached entry by ISRC func (c *TrackIDCache) Get(isrc string) *TrackIDCacheEntry { c.mu.RLock() defer c.mu.RUnlock() entry, exists := c.cache[isrc] if !exists || time.Now().After(entry.ExpiresAt) { return nil } return entry } // SetTidal caches Tidal track ID for an ISRC func (c *TrackIDCache) SetTidal(isrc string, trackID int64) { c.mu.Lock() defer c.mu.Unlock() entry, exists := c.cache[isrc] if !exists { entry = &TrackIDCacheEntry{} c.cache[isrc] = entry } entry.TidalTrackID = trackID entry.ExpiresAt = time.Now().Add(c.ttl) } // SetQobuz caches Qobuz track ID for an ISRC func (c *TrackIDCache) SetQobuz(isrc string, trackID int64) { c.mu.Lock() defer c.mu.Unlock() entry, exists := c.cache[isrc] if !exists { entry = &TrackIDCacheEntry{} c.cache[isrc] = entry } entry.QobuzTrackID = trackID entry.ExpiresAt = time.Now().Add(c.ttl) } // SetAmazon caches Amazon track ID for an ISRC func (c *TrackIDCache) SetAmazon(isrc string, trackID string) { c.mu.Lock() defer c.mu.Unlock() entry, exists := c.cache[isrc] if !exists { entry = &TrackIDCacheEntry{} c.cache[isrc] = entry } entry.AmazonTrackID = trackID entry.ExpiresAt = time.Now().Add(c.ttl) } // Clear removes all cached entries func (c *TrackIDCache) Clear() { c.mu.Lock() defer c.mu.Unlock() c.cache = make(map[string]*TrackIDCacheEntry) } // Size returns the number of cached entries func (c *TrackIDCache) Size() int { c.mu.RLock() defer c.mu.RUnlock() return len(c.cache) } // ======================================== // Parallel Download Helper // ======================================== // ParallelDownloadResult holds results from parallel operations type ParallelDownloadResult struct { CoverData []byte LyricsData *LyricsResponse LyricsLRC string CoverErr error LyricsErr error } // FetchCoverAndLyricsParallel downloads cover and fetches lyrics in parallel // This runs while the main audio download is happening // durationMs: track duration in milliseconds for lyrics matching func FetchCoverAndLyricsParallel( coverURL string, maxQualityCover bool, spotifyID string, trackName string, artistName string, embedLyrics bool, durationMs int64, ) *ParallelDownloadResult { result := &ParallelDownloadResult{} var wg sync.WaitGroup if coverURL != "" { wg.Add(1) go func() { defer wg.Done() fmt.Println("[Parallel] Starting cover download...") data, err := downloadCoverToMemory(coverURL, maxQualityCover) if err != nil { result.CoverErr = err fmt.Printf("[Parallel] Cover download failed: %v\n", err) } else { result.CoverData = data fmt.Printf("[Parallel] Cover downloaded: %d bytes\n", len(data)) } }() } // Fetch lyrics in parallel if embedLyrics { wg.Add(1) go func() { defer wg.Done() fmt.Println("[Parallel] Starting lyrics fetch...") client := NewLyricsClient() durationSec := float64(durationMs) / 1000.0 lyrics, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName, durationSec) if err != nil { result.LyricsErr = err fmt.Printf("[Parallel] Lyrics fetch failed: %v\n", err) } else if lyrics != nil && len(lyrics.Lines) > 0 { result.LyricsData = lyrics result.LyricsLRC = convertToLRCWithMetadata(lyrics, trackName, artistName) fmt.Printf("[Parallel] Lyrics fetched: %d lines\n", len(lyrics.Lines)) } else { result.LyricsErr = fmt.Errorf("no lyrics found") fmt.Println("[Parallel] No lyrics found") } }() } wg.Wait() return result } // ======================================== // Pre-warm Cache for Album/Playlist // ======================================== // PreWarmCacheRequest represents a track to pre-warm cache for type PreWarmCacheRequest struct { ISRC string TrackName string ArtistName string SpotifyID string // Needed for Amazon (SongLink lookup) Service string // "tidal", "qobuz", "amazon" } // PreWarmTrackCache pre-fetches track IDs for multiple tracks (for album/playlist) // This runs in background while user is viewing the track list func PreWarmTrackCache(requests []PreWarmCacheRequest) { if len(requests) == 0 { return } fmt.Printf("[Cache] Pre-warming cache for %d tracks...\n", len(requests)) cache := GetTrackIDCache() semaphore := make(chan struct{}, 3) var wg sync.WaitGroup for _, req := range requests { if cached := cache.Get(req.ISRC); cached != nil { continue } wg.Add(1) go func(r PreWarmCacheRequest) { defer wg.Done() semaphore <- struct{}{} // Acquire defer func() { <-semaphore }() // Release switch r.Service { case "tidal": preWarmTidalCache(r.ISRC, r.TrackName, r.ArtistName) case "qobuz": preWarmQobuzCache(r.ISRC) case "amazon": preWarmAmazonCache(r.ISRC, r.SpotifyID) } }(req) } wg.Wait() fmt.Printf("[Cache] Pre-warm complete. Cache size: %d\n", cache.Size()) } func preWarmTidalCache(isrc, _, _ string) { downloader := NewTidalDownloader() track, err := downloader.SearchTrackByISRC(isrc) if err == nil && track != nil { GetTrackIDCache().SetTidal(isrc, track.ID) fmt.Printf("[Cache] Cached Tidal ID for ISRC %s: %d\n", isrc, track.ID) } } func preWarmQobuzCache(isrc string) { downloader := NewQobuzDownloader() track, err := downloader.SearchTrackByISRC(isrc) if err == nil && track != nil { GetTrackIDCache().SetQobuz(isrc, track.ID) fmt.Printf("[Cache] Cached Qobuz ID for ISRC %s: %d\n", isrc, track.ID) } } func preWarmAmazonCache(isrc, spotifyID string) { client := NewSongLinkClient() availability, err := client.CheckTrackAvailability(spotifyID, isrc) if err == nil && availability != nil && availability.Amazon { GetTrackIDCache().SetAmazon(isrc, availability.AmazonURL) fmt.Printf("[Cache] Cached Amazon URL for ISRC %s\n", isrc) } } // ======================================== // Exported Functions for Flutter // ======================================== // PreWarmCache is called from Flutter to pre-warm cache for album/playlist tracks // tracksJSON is a JSON array of {isrc, track_name, artist_name, service} func PreWarmCache(tracksJSON string) error { var requests []PreWarmCacheRequest go PreWarmTrackCache(requests) return nil } // ClearTrackCache clears the track ID cache func ClearTrackCache() { GetTrackIDCache().Clear() fmt.Println("[Cache] Track ID cache cleared") } // GetCacheSize returns the current cache size func GetCacheSize() int { return GetTrackIDCache().Size() }