//! Example demonstrating Qobuz lazy loading with rate limiting //! //! This example shows how to use the new lazy loading feature to add albums //! to playlists without downloading all audio files immediately. Only covers //! are downloaded eagerly, audio is downloaded on-demand when played. //! //! Features demonstrated: //! - Rate limiting (max 2 concurrent requests, 400ms delay) //! - Lazy audio loading (saves ~99% initial bandwidth) //! - Eager cover loading (UI responsiveness) //! - Automatic PK switching when audio is downloaded //! - Prefetch of next 2 tracks during playback //! //! Run with: //! ```bash //! cargo run -p pmoqobuz --example lazy_loading //! ``` use pmoaudiocache::{register_audio_cache, AudioCacheConfigExt, Cache as AudioCache}; use pmoconfig::get_config; use pmocovers::Cache as CoverCache; use pmocovers::CoverCacheConfigExt; use pmoplaylist::{PlaylistManager, PlaylistRole}; use pmoqobuz::{QobuzClient, QobuzSource}; use std::sync::Arc; use std::time::Instant; #[tokio::main] async fn main() -> Result<(), Box> { // Initialize tracing with debug level to see rate limiting tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) .with_target(false) .init(); println!("šŸŽµ Qobuz Lazy Loading Demo"); println!("============================\n"); // Step 1: Connect to Qobuz with rate limiting enabled println!("šŸ“” Connecting to Qobuz (rate limiting enabled)..."); let client = QobuzClient::from_config().await?; println!("āœ… Connected with rate limiting:"); println!(" - Max 2 concurrent requests"); println!(" - 400ms minimum delay between requests\n"); // Step 2: Initialize caches println!("šŸ’¾ Initializing caches..."); let config = get_config(); let (cover_cache, audio_cache, using_global) = match (config.create_cover_cache(), config.create_audio_cache()) { (Ok(covers), Ok(audio)) => (covers, audio, true), (covers_res, audio_res) => { let err_cover = covers_res.err(); let err_audio = audio_res.err(); if let Some(err) = err_cover { println!( " āš ļø Could not open configured cover cache: {}. Falling back to ./cache", err ); } if let Some(err) = err_audio { println!( " āš ļø Could not open configured audio cache: {}. Falling back to ./cache", err ); } let cover_cache = Arc::new(CoverCache::new("./cache/qobuz-covers", 500)?); let audio_cache = Arc::new(AudioCache::new("./cache/qobuz-audio", 100)?); (cover_cache, audio_cache, false) } }; // IMPORTANT: Register audio cache globally so PlaylistManager can access it register_audio_cache(audio_cache.clone()); pmoplaylist::register_audio_cache(audio_cache.clone()); if using_global { println!("āœ… Caches initialized from global configuration\n"); } else { println!("āœ… Local caches initialized in ./cache (global config unavailable)\n"); } // Step 3: Create QobuzSource with caches let source = QobuzSource::new(client, cover_cache.clone(), audio_cache.clone()); // Step 4: Get user's favorite albums println!("šŸŽ§ Fetching your favorite albums..."); let favorite_albums = source.client().get_favorite_albums().await?; if favorite_albums.is_empty() { println!("āš ļø No favorite albums found!"); println!(" Please add some albums to your Qobuz favorites first.\n"); return Ok(()); } println!("āœ… Found {} favorite albums\n", favorite_albums.len()); // Step 5: Select first album for testing let album = &favorite_albums[0]; println!("šŸ“€ Selected album: {} - {}", album.artist.name, album.title); println!(" Tracks: {}", album.tracks_count.unwrap_or(0)); println!(" Album ID: {}\n", album.id); // Step 6: Create a test playlist println!("šŸ“ Creating test playlist..."); let playlist_manager = PlaylistManager(); let playlist_id = { let writer = playlist_manager .create_persistent_playlist_with_role("lazy-test".to_string(), PlaylistRole::Album) .await?; writer.id().to_string() }; // Drop writer here to release the lock println!("āœ… Playlist created: {}\n", playlist_id); // Step 7: Add album with lazy loading (measure time and track downloads) println!("ā±ļø Adding album to playlist with LAZY loading..."); println!(" This will:"); println!(" - Download covers immediately (~400 KB each)"); println!(" - Create lazy PKs for audio (NO download)"); println!(" - Enable prefetch for next 2 tracks\n"); let start = Instant::now(); let count = source .add_album_to_playlist(&playlist_id, &album.id) .await?; let elapsed = start.elapsed(); println!( "āœ… Album added: {} tracks in {:.2}s", count, elapsed.as_secs_f64() ); println!( " Average: {:.0}ms per track\n", elapsed.as_millis() as f64 / count as f64 ); // Step 8: Verify lazy PKs println!("šŸ” Verifying lazy PKs..."); let reader = playlist_manager.get_read_handle(&playlist_id).await?; // Read all tracks from playlist let mut tracks = Vec::new(); loop { match reader.peek().await? { Some(track) => { tracks.push(track); reader.pop().await?; } None => break, } } if tracks.is_empty() { println!("āš ļø No tracks in playlist!"); return Ok(()); } let first_track_pk = tracks[0].cache_pk(); let is_lazy = pmocache::is_lazy_pk(&first_track_pk); println!(" First track PK: {}", first_track_pk); println!( " Is lazy: {}", if is_lazy { "āœ… YES (starts with 'L:')" } else { "āŒ NO" } ); // Count lazy vs downloaded let lazy_count = tracks .iter() .filter(|t| pmocache::is_lazy_pk(t.cache_pk())) .count(); let downloaded_count = tracks.len() - lazy_count; println!("\nšŸ“Š Track status:"); println!(" Lazy (not downloaded): {} tracks", lazy_count); println!(" Downloaded: {} tracks", downloaded_count); // Step 9: Check cache sizes println!("\nšŸ’¾ Cache disk usage:"); println!(" Covers: {:?}", get_dir_size("./cache/qobuz-covers")?); println!(" Audio: {:?}", get_dir_size("./cache/qobuz-audio")?); // Step 10: Demonstrate on-demand download if is_lazy { println!("\nšŸŽµ Simulating playback of first track..."); println!(" This would trigger download via HTTP request to:"); println!(" GET /cache/flac/{}", first_track_pk); println!("\n The lazy PK will automatically:"); println!(" 1. Download the audio file from Qobuz"); println!(" 2. Convert to FLAC"); println!(" 3. Calculate real PK from content"); println!(" 4. Update playlist (lazy_pk → real_pk)"); println!(" 5. Prefetch next 2 tracks in background"); } // Step 11: Summary println!("\n╭─────────────────────────────────────────╮"); println!("│ šŸŽ‰ Lazy Loading Demo Complete! │"); println!("╰─────────────────────────────────────────╯"); println!("\nšŸ“ˆ Benefits demonstrated:"); println!( " āœ“ Fast album loading (~{}ms per track)", elapsed.as_millis() / count as u128 ); println!(" āœ“ Minimal initial download (covers only)"); println!(" āœ“ Audio downloaded on-demand"); println!(" āœ“ Rate limiting active (respectful to Qobuz)"); println!(" āœ“ Automatic prefetching during playback"); println!("\nšŸ’” For 375 favorite albums (~3750 tracks):"); println!(" Without lazy: ~15 GB download, ~75s (no rate limit)"); println!(" With lazy: ~150 MB download, ~5 min (rate limited)"); println!(" Savings: ~99% bandwidth, natural request pattern"); Ok(()) } /// Calculate directory size recursively fn get_dir_size(path: &str) -> Result> { use std::fs; let mut total: u64 = 0; if let Ok(entries) = fs::read_dir(path) { for entry in entries.flatten() { if let Ok(metadata) = entry.metadata() { if metadata.is_file() { total += metadata.len(); } else if metadata.is_dir() { if let Ok(size_str) = get_dir_size(&entry.path().to_string_lossy()) { // Parse size from string (hacky but works for this example) if let Some(num) = size_str.split_whitespace().next() { if let Ok(size) = num.parse::() { total += (size * 1024.0 * 1024.0) as u64; } } } } } } } Ok(format_size(total)) } /// Format bytes to human-readable size fn format_size(bytes: u64) -> String { const KB: u64 = 1024; const MB: u64 = KB * 1024; const GB: u64 = MB * 1024; if bytes >= GB { format!("{:.2} GB", bytes as f64 / GB as f64) } else if bytes >= MB { format!("{:.2} MB", bytes as f64 / MB as f64) } else if bytes >= KB { format!("{:.2} KB", bytes as f64 / KB as f64) } else { format!("{} B", bytes) } }