perf(suggestions): limit to top 5 artists and reduce rate limiting

Major optimizations:
- Extract only top 5 artists by track frequency instead of all artists
- Use faster rate limiter (200ms) when using MusicBrainz proxy
- Remove skip_vector_build since we now have manageable artist count

Expected load time: ~5-8 seconds for first load with 5 artists
vs previous: 50+ seconds with 50 artists
This commit is contained in:
vicrodh committed 2026-01-27 13:20:16 -06:00
1 parent 7506d69e53
commit b551df71c0
4 files changed
+66 -22

No files matched your search

+21 -3
View File
@@ -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<Instant>,
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)),
}
}
@@ -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:`, {
@@ -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(
+41 -15
View File
@@ -157,26 +157,52 @@ export async function cleanupVectorStore(maxAgeDays?: number): Promise<number> {
// ============ 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<string, { count: number; qobuzId?: number }>();
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<string>();
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);
}
/**