diff --git a/pmoparadise/examples/show_source_image.rs b/pmoparadise/examples/show_source_image.rs deleted file mode 100644 index 14c9ff9b..00000000 --- a/pmoparadise/examples/show_source_image.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Example showing how to access and save the Radio Paradise source image -//! -//! This example demonstrates: -//! - Getting source information via the MusicSource trait -//! - Accessing the embedded WebP image -//! - Optionally saving it to a file - -use pmoaudiocache::cache as audio_cache; -use pmocovers::cache as covers_cache; -use pmoparadise::{RadioParadiseClient, RadioParadiseSource}; -use pmosource::MusicSource; -use std::fs; -use std::io::Write; -use std::sync::Arc; - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Create the client and source - let client = RadioParadiseClient::new().await?; - - // Build lightweight caches under the system temp dir for this example - let base_dir = std::env::temp_dir().join(format!( - "pmoparadise_show_source_image_{}", - std::process::id() - )); - let covers_dir = base_dir.join("covers"); - let audio_dir = base_dir.join("audio"); - std::fs::create_dir_all(&covers_dir)?; - std::fs::create_dir_all(&audio_dir)?; - - let cover_cache = Arc::new(covers_cache::new_cache( - covers_dir.to_string_lossy().as_ref(), - 32, - )?); - let audio_cache = Arc::new(audio_cache::new_cache( - audio_dir.to_string_lossy().as_ref(), - 32, - )?); - - let source = RadioParadiseSource::new_default(client, cover_cache, audio_cache); - - // Display source information - println!("Music Source Information"); - println!("========================"); - println!("Name: {}", source.name()); - println!("ID: {}", source.id()); - println!("Image MIME type: {}", source.default_image_mime_type()); - - // Get the embedded image - let image_data = source.default_image(); - println!("Embedded image size: {} bytes", image_data.len()); - - // Verify WebP format - if image_data.len() >= 12 { - let is_webp = &image_data[0..4] == b"RIFF" && &image_data[8..12] == b"WEBP"; - println!("Valid WebP format: {}", is_webp); - } - - // Optional: save to file - if std::env::args().any(|arg| arg == "--save") { - let filename = format!("{}_default.webp", source.id()); - let mut file = fs::File::create(&filename)?; - file.write_all(image_data)?; - println!("\nImage saved to: {}", filename); - println!("You can view it with: open {}", filename); - } else { - println!("\nTo save the image to disk, run with: --save"); - } - - Ok(()) -} diff --git a/pmoparadise/examples/test_streaming.rs b/pmoparadise/examples/test_streaming.rs deleted file mode 100644 index 78a1279d..00000000 --- a/pmoparadise/examples/test_streaming.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! Test progressive streaming implementation -//! -//! This example tests the streaming implementation and measures performance -//! -//! Run with: -//! ```bash -//! RUST_LOG=info cargo run --example test_streaming -//! ``` - -use pmoparadise::RadioParadiseClient; -use std::time::Instant; - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Initialize tracing with timestamps - tracing_subscriber::fmt() - .with_target(false) - .with_thread_ids(false) - .with_level(true) - .init(); - - println!("🎵 Testing Progressive FLAC Streaming"); - println!("=====================================\n"); - - // Create the Radio Paradise client - println!("📡 Connecting to Radio Paradise..."); - let client = RadioParadiseClient::new().await?; - println!("✅ Connected!\n"); - - // Get current block - println!("🎧 Fetching current block metadata..."); - let block = client.get_block(None).await?; - - println!("\n📊 Block Information:"); - println!(" Event ID: {}", block.event); - println!(" Songs: {}", block.song_count()); - println!(" Duration: ~{} seconds\n", block.length / 1000); - - // List songs - println!("🎵 Songs in this block:"); - for (idx, song) in block.songs_ordered() { - println!( - " {}. {} - {} ({}s at {}s)", - idx + 1, - song.artist, - song.title, - song.duration / 1000, - song.elapsed / 1000 - ); - } - println!(); - - // Now test the streaming decoder - println!("⚡ Starting progressive streaming test..."); - println!(" (This will download and decode the block progressively)"); - println!(); - - let start_time = Instant::now(); - let block_url = block.url.parse()?; - let http_stream = client.stream_block(&block_url).await?; - - use pmoparadise::streaming::StreamingPCMDecoder; - - // Decode in a blocking task - let decode_task = tokio::task::spawn_blocking(move || -> anyhow::Result> { - let mut decoder = StreamingPCMDecoder::new(http_stream)?; - - println!( - " 🎼 Stream info: {}Hz, {} channels, {} bits", - decoder.sample_rate(), - decoder.channels(), - decoder.bits_per_sample() - ); - - let mut chunk_times = Vec::new(); - let mut chunk_count = 0; - - while let Some(chunk) = decoder.decode_chunk()? { - chunk_count += 1; - chunk_times.push((chunk.position_ms, chunk.samples.len())); - - if chunk_count % 50 == 0 { - println!( - " 📦 Chunk {} at {}ms ({} samples)", - chunk_count, - chunk.position_ms, - chunk.samples.len() - ); - } - } - - Ok(chunk_times) - }); - - let chunk_times = decode_task - .await - .map_err(|e| anyhow::anyhow!("Join error: {}", e))??; - let total_time = start_time.elapsed(); - - println!("\n✅ Streaming Complete!"); - println!("\n📈 Performance Metrics:"); - println!(" Total chunks decoded: {}", chunk_times.len()); - println!(" Total time: {:.2}s", total_time.as_secs_f64()); - - if let Some((first_pos, _)) = chunk_times.first() { - println!(" First chunk at: {}ms", first_pos); - } - - if let Some((last_pos, _)) = chunk_times.last() { - println!( - " Last chunk at: {}ms (~{:.1}s)", - last_pos, - last_pos / 1000 - ); - } - - println!("\n💡 Analysis:"); - println!(" With the old approach (download all first):"); - println!(" - Would need to wait for full download (~12-16s)"); - println!(" - Then decode all samples"); - println!(" - Total: ~15-20s before first track"); - println!(); - println!(" With progressive streaming:"); - println!(" - First chunks arrive in ~2-3s"); - println!(" - First track (3min) ready in ~6-8s"); - println!(" - Improvement: ~2x faster! ⚡"); - - Ok(()) -} diff --git a/pmoparadise/examples/with_cache.rs b/pmoparadise/examples/with_cache.rs deleted file mode 100644 index 563ed755..00000000 --- a/pmoparadise/examples/with_cache.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! Example demonstrating Radio Paradise with cache support -//! -//! This example shows how to use the RadioParadiseSource with pmocovers -//! and pmoaudiocache to cache both cover images and audio tracks. -//! -//! Run with: -//! ```bash -//! cargo run --example with_cache --features cache -//! ``` - -use pmoaudiocache::AudioCache; -use pmocovers::Cache as CoverCache; -use pmoparadise::{RadioParadiseClient, RadioParadiseSource}; -use pmosource::MusicSource; -use std::sync::Arc; -use tokio::time::{sleep, Duration}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Initialize tracing - tracing_subscriber::fmt::init(); - - println!("🎵 Radio Paradise with Cache Support"); - println!("=====================================\n"); - - // Create the Radio Paradise client - println!("📡 Connecting to Radio Paradise..."); - let client = RadioParadiseClient::new().await?; - println!("✅ Connected!\n"); - - // Initialize caches - println!("💾 Initializing caches..."); - let cover_cache = Arc::new(CoverCache::new("./cache/covers", 500)?); - let audio_cache = Arc::new(AudioCache::new("./cache/audio", 100)?); - println!("✅ Caches initialized!\n"); - - // Create the source with caching enabled - let source = RadioParadiseSource::new_with_cache( - client.clone(), - "http://localhost:8080", - 50, - Some(cover_cache.clone()), - Some(audio_cache.clone()), - ); - - println!("📻 Source: {}", source.name()); - println!("🆔 ID: {}", source.id()); - println!("📝 Supports FIFO: {}\n", source.supports_fifo()); - - // Fetch current playing information - println!("🎧 Fetching current track information..."); - let now_playing = client.now_playing().await?; - let block = Arc::new(now_playing.block.clone()); - - println!("\n🎵 Now Playing:"); - println!(" Event: {}", block.event); - if let Some(song) = &now_playing.current_song { - println!(" Title: {}", song.title); - println!(" Artist: {}", song.artist); - println!(" Album: {}", song.album); - } - println!(); - - // Add current song to the source - println!("➕ Adding current track to FIFO with caching..."); - if let Some(song) = &now_playing.current_song { - source - .add_song( - block.clone(), - song, - now_playing.current_song_index.unwrap_or(0), - ) - .await?; - println!("✅ Track added and caching started!"); - println!(" - Cover image will be cached to: ./cache/covers/"); - println!(" - Audio will be cached to: ./cache/audio/\n"); - } - - // Wait a bit for caching to start - println!("⏳ Waiting for cache operations to complete..."); - sleep(Duration::from_secs(5)).await; - - // Get items from FIFO - println!("\n📋 Items in FIFO:"); - let items = source.get_items(0, 10).await?; - for (i, item) in items.iter().enumerate() { - println!( - " {}. {} - {}", - i + 1, - item.artist.as_deref().unwrap_or("Unknown"), - item.title - ); - - // Show resolved URI (will use cached version if available) - if let Ok(uri) = source.resolve_uri(&item.id).await { - println!(" URI: {}", uri); - } - } - - println!("\n✨ Example complete!"); - println!("\n💡 Tips:"); - println!(" - Run the example again to see faster loading from cache"); - println!(" - Check ./cache/covers/ for cached cover images"); - println!(" - Check ./cache/audio/ for cached FLAC files"); - - Ok(()) -} diff --git a/pmoparadise/src/lib.rs b/pmoparadise/src/lib.rs index 512230a1..83e5f70f 100644 --- a/pmoparadise/src/lib.rs +++ b/pmoparadise/src/lib.rs @@ -216,7 +216,6 @@ pub mod error; pub mod models; pub mod source; pub mod stream; -pub mod streaming; #[cfg(feature = "per-track")] pub mod track; diff --git a/pmoparadise/src/streaming.rs b/pmoparadise/src/streaming.rs deleted file mode 100644 index b38e9be5..00000000 --- a/pmoparadise/src/streaming.rs +++ /dev/null @@ -1,217 +0,0 @@ -use anyhow::Result; -use bytes::Bytes; -use futures::stream::Stream; -use std::io::{self, Read}; -use std::pin::Pin; -use std::sync::mpsc::{sync_channel, Receiver, RecvError, SyncSender}; -use std::time::{Duration, Instant}; - -const CHANNEL_BUFFER_SIZE: usize = 64; // Augmenté de 16 à 64 pour réduire les warnings "buffer plein" -pub const CHUNK_SIZE_FRAMES: usize = 4096; - -pub struct ChannelReader { - receiver: Receiver>, - current_chunk: Option, - position: usize, -} - -impl ChannelReader { - pub fn new( - stream: Pin> + Send>>, - ) -> Self { - let (tx, rx) = sync_channel(CHANNEL_BUFFER_SIZE); - tokio::spawn(Self::stream_feeder(stream, tx)); - Self { - receiver: rx, - current_chunk: None, - position: 0, - } - } - - async fn stream_feeder( - mut stream: Pin> + Send>>, - tx: SyncSender>, - ) { - use futures::StreamExt; - while let Some(result) = stream.next().await { - let start = Instant::now(); - - let to_send = result.map_err(|e| e.to_string()); - match tx.try_send(to_send) { - Ok(_) => { /* message envoyé sans attente */ } - Err(std::sync::mpsc::TrySendError::Full(value)) => { - tracing::warn!("stream_feeder: buffer plein"); - // Revenir à l’envoi bloquant pour ne pas perdre le message - if tx.send(value).is_err() { - break; - } - } - Err(std::sync::mpsc::TrySendError::Disconnected(_)) => break, - } - let waited = start.elapsed(); - tracing::trace!("stream_feeder send {:?}", waited); - if waited > Duration::from_millis(200) { - tracing::warn!("stream_feeder wait {:?}", waited); - } - } - } -} - -impl Read for ChannelReader { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - let start = Instant::now(); - loop { - if let Some(chunk) = &self.current_chunk { - if self.position < chunk.len() { - let available = chunk.len() - self.position; - let to_copy = available.min(buf.len()); - buf[..to_copy].copy_from_slice(&chunk[self.position..self.position + to_copy]); - self.position += to_copy; - tracing::trace!( - "ChannelReader copied {} bytes (elapsed {:?})", - to_copy, - start.elapsed() - ); - return Ok(to_copy); - } - } - - match self.receiver.recv() { - Ok(Ok(bytes)) => { - tracing::trace!( - "ChannelReader received chunk of {} bytes after {:?}", - bytes.len(), - start.elapsed() - ); - self.current_chunk = Some(bytes); - self.position = 0; - } - Ok(Err(e)) => { - tracing::warn!("ChannelReader received error chunk: {}", e); - return Err(io::Error::new(io::ErrorKind::Other, e)); - } - Err(RecvError) => { - tracing::trace!("ChannelReader stream closed after {:?}", start.elapsed()); - return Ok(0); - } - } - } - } -} - -#[derive(Debug, Clone)] -pub struct PCMChunk { - pub samples: Vec, - pub position_ms: u64, - pub sample_rate: u32, - pub channels: u32, -} - -pub struct StreamingPCMDecoder { - reader: claxon::FlacReader>, - sample_rate: u32, - channels: u32, - bits_per_sample: u32, - total_samples_decoded: u64, - done: bool, -} - -impl StreamingPCMDecoder { - /// Create a new decoder from an HTTP stream with default chunk size - pub fn new(http_stream: crate::stream::BlockStream) -> anyhow::Result { - Self::with_chunk_size(http_stream, CHUNK_SIZE_FRAMES) - } - - pub fn with_chunk_size( - http_stream: crate::stream::BlockStream, - _chunk_size: usize, - ) -> anyhow::Result { - let channel_reader = ChannelReader::new(http_stream.into_inner()); - let buffered = std::io::BufReader::new(channel_reader); - let reader = claxon::FlacReader::new(buffered) - .map_err(|e| anyhow::anyhow!("FLAC reader error: {}", e))?; - let info = reader.streaminfo(); - - Ok(Self { - reader, - sample_rate: info.sample_rate, - channels: info.channels, - bits_per_sample: info.bits_per_sample, - total_samples_decoded: 0, - done: false, - }) - } - - /// Get the sample rate (e.g., 44100 Hz) - pub fn sample_rate(&self) -> u32 { - self.sample_rate - } - - /// Get the number of channels (e.g., 2 for stereo) - pub fn channels(&self) -> u32 { - self.channels - } - - /// Get bits per sample (e.g., 16) - pub fn bits_per_sample(&self) -> u32 { - self.bits_per_sample - } - - pub fn decode_chunk(&mut self) -> anyhow::Result> { - if self.done { - return Ok(None); - } - - // Crée le FrameReader à la volée (emprunt de self.reader) - let mut frames = self.reader.blocks(); - - // API claxon 0.6.x : il FAUT fournir un Vec par valeur - let buf: Vec = Vec::new(); - let frame = match frames.read_next_or_eof(buf) { - Ok(None) => { - self.done = true; - return Ok(None); - } - Ok(Some(f)) => f, - Err(e) => return Err(anyhow::anyhow!("FLAC decode error: {}", e)), - }; - - let planar_samples: Vec = frame.into_buffer(); - if planar_samples.is_empty() { - self.done = true; - return Ok(None); - } - - // IMPORTANT: Claxon retourne les samples en format PLANAR (tous les L, puis tous les R) - // Mais nous avons besoin du format INTERLEAVED (L, R, L, R, ...) pour l'encodage - let block_size = planar_samples.len() / self.channels as usize; - let mut samples = Vec::with_capacity(planar_samples.len()); - - for i in 0..block_size { - for ch in 0..self.channels as usize { - samples.push(planar_samples[ch * block_size + i]); - } - } - - let position_ms = { - let frames = self.total_samples_decoded / self.channels as u64; - (frames * 1000) / self.sample_rate as u64 - }; - self.total_samples_decoded += samples.len() as u64; - - Ok(Some(PCMChunk { - samples, - position_ms, - sample_rate: self.sample_rate, - channels: self.channels, - })) - } -} - -pub fn ms_to_frames(ms: u64, sample_rate: u32) -> usize { - ((ms as u128 * sample_rate as u128) / 1000) as usize -} - -pub fn frames_to_ms(frames: usize, sample_rate: u32) -> u64 { - ((frames as u128 * 1000) / sample_rate as u128) as u64 -}