diff --git a/src-tauri/src/musicbrainz/client.rs b/src-tauri/src/musicbrainz/client.rs index baf8b7b50..ea569e48a 100644 --- a/src-tauri/src/musicbrainz/client.rs +++ b/src-tauri/src/musicbrainz/client.rs @@ -16,18 +16,29 @@ const MUSICBRAINZ_PROXY_URL: &str = "https://qbz-api-proxy.blitzkriegfc.workers. /// Direct MusicBrainz API URL (fallback) const MUSICBRAINZ_API_URL: &str = "https://musicbrainz.org/ws/2"; -/// Rate limiter for MusicBrainz API (1 request per second) +/// Rate limiter for MusicBrainz API pub struct RateLimiter { last_request: Mutex, min_interval: Duration, } impl RateLimiter { + /// Create rate limiter for direct MusicBrainz API (1 req/sec) pub fn new() -> Self { + Self::with_interval(Duration::from_millis(1100)) + } + + /// Create rate limiter for proxy (faster, proxy handles actual rate limiting) + pub fn for_proxy() -> Self { + Self::with_interval(Duration::from_millis(200)) + } + + /// Create rate limiter with custom interval + pub fn with_interval(min_interval: Duration) -> Self { Self { // Start in the past so first request doesn't wait last_request: Mutex::new(Instant::now() - Duration::from_secs(2)), - min_interval: Duration::from_millis(1100), // Slightly over 1 second for safety + min_interval, } } @@ -96,9 +107,16 @@ impl MusicBrainzClient { .build() .unwrap_or_else(|_| Client::new()); + // Use faster rate limiter when using proxy (proxy handles actual rate limiting) + let rate_limiter = if config.use_proxy { + RateLimiter::for_proxy() + } else { + RateLimiter::new() + }; + Self { client, - rate_limiter: Arc::new(RateLimiter::new()), + rate_limiter: Arc::new(rate_limiter), config: Arc::new(Mutex::new(config)), } } diff --git a/src/lib/components/PlaylistSuggestions.svelte b/src/lib/components/PlaylistSuggestions.svelte index ccb5ae162..bb232d376 100644 --- a/src/lib/components/PlaylistSuggestions.svelte +++ b/src/lib/components/PlaylistSuggestions.svelte @@ -97,7 +97,7 @@ artists, excludeTrackIds, showReasons, - { max_pool_size: poolSize, skip_vector_build: true } + { max_pool_size: poolSize } ); const elapsed = ((performance.now() - startTime) / 1000).toFixed(2); log(`Backend returned in ${elapsed}s:`, { diff --git a/src/lib/components/views/PlaylistDetailView.svelte b/src/lib/components/views/PlaylistDetailView.svelte index fd48b5191..7cff4e4b1 100644 --- a/src/lib/components/views/PlaylistDetailView.svelte +++ b/src/lib/components/views/PlaylistDetailView.svelte @@ -8,7 +8,7 @@ import { open } from '@tauri-apps/plugin-dialog'; import TrackRow from '../TrackRow.svelte'; import PlaylistSuggestions from '../PlaylistSuggestions.svelte'; - import { extractUniqueArtists } from '$lib/services/playlistSuggestionsService'; + import { extractTopArtists } from '$lib/services/playlistSuggestionsService'; import { type OfflineCacheStatus } from '$lib/stores/offlineCacheState'; import { subscribe as subscribeOffline, @@ -194,9 +194,9 @@ let localTracksDuration = $derived(localTracks.reduce((sum, t) => sum + t.duration_secs, 0)); let totalDuration = $derived((playlist?.duration ?? 0) + localTracksDuration); - // Playlist suggestions: extract unique artists from Qobuz tracks (not local) + // Playlist suggestions: extract top 5 artists by track count (not local tracks) const playlistArtists = $derived( - extractUniqueArtists(tracks.filter(t => !t.isLocal)) + extractTopArtists(tracks.filter(t => !t.isLocal), 5) ); // Track IDs to exclude from suggestions (already in playlist) const excludeTrackIds = $derived( diff --git a/src/lib/services/playlistSuggestionsService.ts b/src/lib/services/playlistSuggestionsService.ts index 1d4543e41..159ab821e 100644 --- a/src/lib/services/playlistSuggestionsService.ts +++ b/src/lib/services/playlistSuggestionsService.ts @@ -157,26 +157,52 @@ export async function cleanupVectorStore(maxAgeDays?: number): Promise { // ============ Helpers ============ +/** Default number of top artists to use for suggestions */ +const DEFAULT_TOP_ARTISTS = 5; + /** - * Extract unique artists from playlist tracks + * Extract top artists from playlist tracks, sorted by frequency (track count) + * + * @param tracks - Playlist tracks + * @param limit - Maximum number of artists to return (default: 5) + */ +export function extractTopArtists( + tracks: Array<{ artist?: string; artistId?: number }>, + limit: number = DEFAULT_TOP_ARTISTS +): PlaylistArtist[] { + // Count tracks per artist + const artistCounts = new Map(); + + for (const track of tracks) { + if (track.artist) { + const existing = artistCounts.get(track.artist); + if (existing) { + existing.count++; + } else { + artistCounts.set(track.artist, { count: 1, qobuzId: track.artistId }); + } + } + } + + // Sort by count descending, take top N + const sorted = [...artistCounts.entries()] + .sort((a, b) => b[1].count - a[1].count) + .slice(0, limit); + + return sorted.map(([name, data]) => ({ + name, + qobuzId: data.qobuzId + })); +} + +/** + * Extract unique artists from playlist tracks (legacy, use extractTopArtists instead) + * @deprecated Use extractTopArtists for better performance */ export function extractUniqueArtists( tracks: Array<{ artist?: string; artistId?: number }> ): PlaylistArtist[] { - const seen = new Set(); - const artists: PlaylistArtist[] = []; - - for (const track of tracks) { - if (track.artist && !seen.has(track.artist)) { - seen.add(track.artist); - artists.push({ - name: track.artist, - qobuzId: track.artistId - }); - } - } - - return artists; + return extractTopArtists(tracks, Infinity); } /**