From 69831a17dff7b4f4156db08294f0c89d2296278b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 09:15:11 +0000 Subject: [PATCH] refactor: Remove obsolete streaming API (stream.rs, track.rs, per-track feature) The old streaming API has been completely replaced by RadioParadiseStreamSource which integrates directly with the pmoaudio pipeline. Removed: - src/stream.rs (179 lines) - BlockStream, stream_block(), download_block() - src/track.rs - Per-track extraction functionality - examples/stream_block.rs - Obsolete streaming example - examples/extract_track.rs - Per-track extraction example - Feature "per-track" and dependencies (hound, tempfile) Updated: - Cargo.toml: Removed per-track feature and obsolete examples - lib.rs: Removed module declarations and re-exports The new RadioParadiseStreamSource provides: - Direct integration with pmoaudio pipeline - FLAC decoding via pmoflac - Automatic TrackBoundary insertion - Better performance and lower latency --- Cargo.lock | 8 - pmoparadise/Cargo.toml | 20 -- pmoparadise/examples/extract_track.rs | 120 -------- pmoparadise/examples/stream_block.rs | 100 ------- pmoparadise/src/lib.rs | 8 - pmoparadise/src/stream.rs | 178 ------------ pmoparadise/src/track.rs | 397 -------------------------- 7 files changed, 831 deletions(-) delete mode 100644 pmoparadise/examples/extract_track.rs delete mode 100644 pmoparadise/examples/stream_block.rs delete mode 100644 pmoparadise/src/stream.rs delete mode 100644 pmoparadise/src/track.rs diff --git a/Cargo.lock b/Cargo.lock index 17fa254b..751c47c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1472,12 +1472,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - [[package]] name = "htmlescape" version = "0.3.1" @@ -2884,7 +2878,6 @@ dependencies = [ "futures", "futures-util", "hex", - "hound", "pmoaudio", "pmoaudiocache", "pmoconfig", @@ -2901,7 +2894,6 @@ dependencies = [ "serde_yaml", "sha2", "symphonia", - "tempfile", "thiserror 2.0.17", "tokio", "tokio-test", diff --git a/pmoparadise/Cargo.toml b/pmoparadise/Cargo.toml index 06b65022..fe7b770c 100644 --- a/pmoparadise/Cargo.toml +++ b/pmoparadise/Cargo.toml @@ -51,10 +51,6 @@ symphonia = { version = "0.5", features = ["all"] } # Audio decoding - claxon for FLAC streaming claxon = "0.4" -# Per-track feature dependencies -hound = { version = "3.5", optional = true } -tempfile = { version = "3.8", optional = true } - # Common music source traits pmosource = { path = "../pmosource" } @@ -81,8 +77,6 @@ futures-util = { version = "0.3", optional = true } default = ["metadata-only", "pmoconfig"] # Mode métadonnées seules (pas de décodage FLAC) metadata-only = [] -# Active l'extraction par-track (WAV export, etc.) -per-track = ["dep:hound", "dep:tempfile"] # Active l'API REST pmoserver pmoserver = ["dep:pmoserver", "dep:utoipa", "dep:axum", "server"] # Feature pour activer le support serveur (cache registry) @@ -107,17 +101,3 @@ pmoaudiocache = { path = "../pmoaudiocache" } [[example]] name = "now_playing" path = "examples/now_playing.rs" - -[[example]] -name = "stream_block" -path = "examples/stream_block.rs" - -[[example]] -name = "extract_track" -path = "examples/extract_track.rs" -required-features = ["per-track"] - -[[example]] -name = "with_cache" -path = "examples/with_cache.rs" -required-features = ["cache"] diff --git a/pmoparadise/examples/extract_track.rs b/pmoparadise/examples/extract_track.rs deleted file mode 100644 index 8d403893..00000000 --- a/pmoparadise/examples/extract_track.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! Example: Extract individual tracks from a FLAC block (requires `per-track` feature) -//! -//! This example demonstrates: -//! - Per-track extraction from FLAC blocks -//! - Exporting tracks to WAV files -//! - Alternative player-based seeking (recommended) -//! -//! **Warning**: This approach downloads and decodes entire blocks. -//! For most use cases, player-based seeking is more efficient. -//! -//! Run with: cargo run --example extract_track --features per-track - -#[cfg(feature = "per-track")] -use pmoparadise::{RadioParadiseClient, Result}; -#[cfg(feature = "per-track")] -use std::path::Path; - -#[cfg(feature = "per-track")] -#[tokio::main] -async fn main() -> Result<()> { - // Initialize logging - #[cfg(feature = "logging")] - tracing_subscriber::fmt::init(); - - println!("Radio Paradise - Per-Track Extraction Demo"); - println!("===========================================\n"); - - println!("WARNING: This feature downloads entire blocks (50-100MB)"); - println!(" and performs CPU-intensive FLAC decoding."); - println!(" For most use cases, player-based seeking is better.\n"); - - // Create client - let client = RadioParadiseClient::new().await?; - - // Get current block - let block = client.get_block(None).await?; - - println!("Block Information:"); - println!(" Event: {}", block.event); - println!(" Songs: {}", block.song_count()); - println!(" URL: {}\n", block.url); - - // Display all tracks - println!("Available Tracks:"); - for (index, song) in block.songs_ordered() { - println!( - " {}. {} - {} ({:.1}s)", - index, - song.artist, - song.title, - song.duration as f64 / 1000.0 - ); - } - println!(); - - // Extract first track - let track_index = 0; - if let Some((_, song)) = block.songs_ordered().first() { - println!("Extracting Track {}:", track_index); - println!(" Artist: {}", song.artist); - println!(" Title: {}", song.title); - println!(" Album: {}\n", song.album); - - println!("Downloading and decoding... (this may take a while)"); - - // Open track stream - let mut track_stream = client.open_track_stream(&block, track_index).await?; - - println!("Track Metadata:"); - println!(" Sample Rate: {} Hz", track_stream.metadata.sample_rate); - println!(" Channels: {}", track_stream.metadata.channels); - println!( - " Bits Per Sample: {}", - track_stream.metadata.bits_per_sample - ); - println!(" Total Samples: {}", track_stream.metadata.total_samples); - println!(); - - // Export to WAV - let output_path = Path::new("track.wav"); - println!("Exporting to {:?}...", output_path); - track_stream.export_wav(output_path)?; - println!("✓ Export complete!\n"); - } - - // Show alternative: player-based seeking - println!("RECOMMENDED ALTERNATIVE: Player-Based Seeking"); - println!("=============================================\n"); - - for (index, song) in block.songs_ordered().into_iter().take(3) { - let (start, duration) = client.track_position_seconds(&block, index)?; - println!("Track {}: {} - {}", index, song.artist, song.title); - println!(" mpv command:"); - println!( - " mpv --start={:.3} --length={:.3} '{}'", - start, duration, block.url - ); - println!(" ffmpeg command (extract to file):"); - println!( - " ffmpeg -ss {:.3} -t {:.3} -i '{}' -c copy track_{}.flac", - start, duration, block.url, index - ); - println!(); - } - - println!("These methods are much more efficient as they:"); - println!(" - Don't download the entire block"); - println!(" - Use the player's optimized seeking"); - println!(" - Start playback immediately"); - println!(" - Preserve original quality (with -c copy)"); - - Ok(()) -} - -#[cfg(not(feature = "per-track"))] -fn main() { - eprintln!("ERROR: This example requires the 'per-track' feature."); - eprintln!("Run with: cargo run --example extract_track --features per-track"); - std::process::exit(1); -} diff --git a/pmoparadise/examples/stream_block.rs b/pmoparadise/examples/stream_block.rs deleted file mode 100644 index 23f3327e..00000000 --- a/pmoparadise/examples/stream_block.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! Example: Stream a Radio Paradise block with prefetching -//! -//! This example demonstrates: -//! - Streaming block audio data -//! - Writing to a file or piping to a player -//! - Prefetching the next block for gapless playback -//! - Continuous playback loop -//! -//! Run with: cargo run --example stream_block -//! -//! To play directly with mpv: -//! cargo run --example stream_block | mpv --no-cache --demuxer=+lavf - - -use futures::StreamExt; -use pmoparadise::{RadioParadiseClient, Result}; -use std::io::Write; - -#[tokio::main] -async fn main() -> Result<()> { - // Initialize logging (optional) - #[cfg(feature = "logging")] - tracing_subscriber::fmt::init(); - - eprintln!("Radio Paradise - Block Streaming Demo"); - eprintln!("======================================\n"); - - // Create client - let mut client = RadioParadiseClient::builder().build().await?; - - eprintln!("Client configured for FLAC streaming\n"); - - // Get current block - let current_block = client.get_block(None).await?; - - eprintln!("Current Block:"); - eprintln!(" Event: {}", current_block.event); - eprintln!(" Songs: {}", current_block.song_count()); - eprintln!( - " Duration: {:.1} minutes", - current_block.length as f64 / 60000.0 - ); - eprintln!(" URL: {}\n", current_block.url); - - // Display tracklist - eprintln!("Tracklist:"); - for (index, song) in current_block.songs_ordered() { - eprintln!(" {}. {} - {}", index + 1, song.artist, song.title); - } - eprintln!(); - - // Prefetch next block in advance - eprintln!("Prefetching next block..."); - client.prefetch_next(¤t_block).await?; - eprintln!( - "Next block prefetched: {}\n", - client.next_block_url().unwrap() - ); - - // Stream the block - eprintln!("Streaming block... (writing to stdout)"); - eprintln!("Tip: Pipe to a player like: cargo run --example stream_block | mpv -\n"); - - let mut stream = client.stream_block_from_metadata(¤t_block).await?; - let mut total_bytes = 0u64; - let mut stdout = std::io::stdout(); - - while let Some(chunk_result) = stream.next().await { - let chunk = chunk_result?; - total_bytes += chunk.len() as u64; - - // Write to stdout (can be piped to a player) - stdout.write_all(&chunk)?; - stdout.flush()?; - - // Progress indicator (to stderr so it doesn't interfere with piped audio) - if total_bytes % (1024 * 1024) == 0 { - eprintln!( - " Downloaded: {:.1} MB", - total_bytes as f64 / 1024.0 / 1024.0 - ); - } - } - - eprintln!("\nBlock streaming complete!"); - eprintln!( - "Total downloaded: {:.2} MB", - total_bytes as f64 / 1024.0 / 1024.0 - ); - - // In a real application, you would now: - // 1. Get the next block using prefetched metadata - // 2. Stream it seamlessly - // 3. Prefetch the following block - // 4. Repeat for continuous playback - - eprintln!("\nFor continuous playback, you would now stream the next block:"); - eprintln!(" Event: {}", current_block.end_event); - - Ok(()) -} diff --git a/pmoparadise/src/lib.rs b/pmoparadise/src/lib.rs index 83e5f70f..960b3d94 100644 --- a/pmoparadise/src/lib.rs +++ b/pmoparadise/src/lib.rs @@ -215,10 +215,6 @@ pub mod client; pub mod error; pub mod models; pub mod source; -pub mod stream; - -#[cfg(feature = "per-track")] -pub mod track; #[cfg(feature = "pmoserver")] pub mod pmoserver_ext; @@ -234,14 +230,10 @@ pub use client::{ClientBuilder, RadioParadiseClient}; pub use error::{Error, Result}; pub use models::{Block, DurationMs, EventId, NowPlaying, Song}; pub use source::RadioParadiseSource; -pub use stream::BlockStream; #[cfg(feature = "pmoaudio")] pub use radio_paradise_stream_source::RadioParadiseStreamSource; -#[cfg(feature = "per-track")] -pub use track::{TrackMetadata, TrackStream}; - #[cfg(feature = "pmoserver")] pub use pmoserver_ext::{ create_api_router, RadioParadiseApiDoc, RadioParadiseExt, RadioParadiseState, diff --git a/pmoparadise/src/stream.rs b/pmoparadise/src/stream.rs deleted file mode 100644 index 6c9f8dba..00000000 --- a/pmoparadise/src/stream.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Block streaming functionality - -use crate::error::{Error, Result}; -use crate::models::Block; -use crate::RadioParadiseClient; -use bytes::Bytes; -use futures::stream::{Stream, StreamExt}; -use std::pin::Pin; -use std::task::{Context, Poll}; -use url::Url; - -/// A stream of audio data from a Radio Paradise block -/// -/// This wraps the HTTP response body and provides a `Stream>` -/// that can be consumed by audio players or written to a file. -pub struct BlockStream { - inner: Pin> + Send>>, -} - -impl BlockStream { - /// Create a new block stream from a reqwest response - pub(crate) fn new(stream: impl Stream> + Send + 'static) -> Self { - Self { - inner: Box::pin(stream), - } - } - - /// Extract the inner stream - /// - /// Consumes the BlockStream and returns the underlying pinned stream. - /// Useful for advanced streaming scenarios like progressive decoding. - pub fn into_inner(self) -> Pin> + Send>> { - self.inner - } -} - -impl Stream for BlockStream { - type Item = Result; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.inner.as_mut().poll_next(cx) - } -} - -impl RadioParadiseClient { - /// Stream a block from its URL - /// - /// Returns a `Stream` of audio bytes that can be consumed by an audio player. - /// The stream will continue until the entire block is downloaded or an error occurs. - /// - /// # Arguments - /// - /// * `block_url` - The URL of the block to stream - /// - /// # Example - /// - /// ```no_run - /// use pmoparadise::RadioParadiseClient; - /// use futures::StreamExt; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), Box> { - /// let client = RadioParadiseClient::new().await?; - /// let block = client.get_block(None).await?; - /// - /// let mut stream = client.stream_block(&block.url.parse()?).await?; - /// - /// while let Some(chunk) = stream.next().await { - /// let bytes = chunk?; - /// // Write bytes to audio player or file - /// println!("Received {} bytes", bytes.len()); - /// } - /// - /// Ok(()) - /// } - /// ``` - pub async fn stream_block(&self, block_url: &Url) -> Result { - #[cfg(feature = "logging")] - tracing::debug!("Starting block stream: {}", block_url); - - let response = self - .client - .get(block_url.clone()) - .timeout(self.block_timeout) - .send() - .await?; - - if !response.status().is_success() { - return Err(Error::other(format!( - "Failed to stream block: HTTP {}", - response.status() - ))); - } - - // Convert reqwest's byte stream to our Result type - let stream = response.bytes_stream(); - let mapped = futures::stream::StreamExt::map(stream, |result| result.map_err(Error::from)); - - Ok(BlockStream::new(mapped)) - } - - /// Stream a block directly from a Block struct - /// - /// Convenience method that parses the URL from the block. - /// - /// # Example - /// - /// ```no_run - /// use pmoparadise::RadioParadiseClient; - /// use futures::StreamExt; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), Box> { - /// let client = RadioParadiseClient::new().await?; - /// let block = client.get_block(None).await?; - /// - /// let mut stream = client.stream_block_from_metadata(&block).await?; - /// - /// while let Some(chunk) = stream.next().await { - /// let bytes = chunk?; - /// // Process bytes... - /// } - /// - /// Ok(()) - /// } - /// ``` - pub async fn stream_block_from_metadata(&self, block: &Block) -> Result { - let url = Url::parse(&block.url)?; - self.stream_block(&url).await - } - - /// Download an entire block as Bytes - /// - /// This downloads the complete block file into memory. For streaming playback, - /// use `stream_block()` instead which is more memory efficient. - /// - /// # Arguments - /// - /// * `block_url` - The URL of the block to download - /// - /// # Example - /// - /// ```no_run - /// use pmoparadise::RadioParadiseClient; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), Box> { - /// let client = RadioParadiseClient::new().await?; - /// let block = client.get_block(None).await?; - /// let url = block.url.parse()?; - /// let bytes = client.download_block(&url).await?; - /// println!("Downloaded {} bytes", bytes.len()); - /// Ok(()) - /// } - /// ``` - pub async fn download_block(&self, block_url: &Url) -> Result { - let mut stream = self.stream_block(block_url).await?; - let mut data = Vec::new(); - - while let Some(chunk_result) = stream.next().await { - let chunk = chunk_result?; - data.extend_from_slice(&chunk); - } - - Ok(Bytes::from(data)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_block_stream_creation() { - let stream = futures::stream::once(async { Ok(Bytes::from("test")) }); - let _block_stream = BlockStream::new(stream); - } -} diff --git a/pmoparadise/src/track.rs b/pmoparadise/src/track.rs deleted file mode 100644 index 8cab4c44..00000000 --- a/pmoparadise/src/track.rs +++ /dev/null @@ -1,397 +0,0 @@ -//! Per-track extraction from FLAC blocks (optional feature) -//! -//! **Important Notes:** -//! -//! Radio Paradise publishes *blocks* containing multiple songs, not individual -//! per-track files. This module provides experimental functionality to extract -//! individual tracks from FLAC blocks, but comes with significant tradeoffs: -//! -//! - **Storage**: Requires downloading the entire block (50-100MB) to disk -//! - **Latency**: Must download and decode before playback can start -//! - **CPU**: FLAC decoding is CPU-intensive -//! - **Complexity**: Seeking in FLAC requires decoding from the beginning -//! -//! ## Recommended Alternative -//! -//! For most use cases, it's better to: -//! 1. Stream the entire block to your audio player -//! 2. Use the `song[i].elapsed` metadata to seek within the player -//! 3. Let the player handle gapless transitions between tracks -//! -//! Modern players (mpv, VLC, ffmpeg) can seek in FLAC streams efficiently. -//! -//! ## When to Use This Module -//! -//! Only use per-track extraction when you need: -//! - Individual WAV files for further processing -//! - PCM data for custom audio analysis -//! - Separate files for non-streaming scenarios -//! -//! ## Block URL Pattern -//! -//! Blocks follow this URL pattern: -//! ```text -//! https://apps.radioparadise.com/blocks/chan/0/4/-.flac -//! ``` -//! -//! The `song[i].elapsed` field (in milliseconds) indicates when each track -//! starts within the block. - -#[cfg(feature = "per-track")] -use crate::error::{Error, Result}; -#[cfg(feature = "per-track")] -use crate::models::Block; -#[cfg(feature = "per-track")] -use crate::RadioParadiseClient; -#[cfg(feature = "per-track")] -use std::io::Write; -#[cfg(feature = "per-track")] -use std::path::PathBuf; - -/// Metadata for a decoded track stream -#[cfg(feature = "per-track")] -#[derive(Debug, Clone)] -pub struct TrackMetadata { - /// Sample rate in Hz (e.g., 44100) - pub sample_rate: u32, - /// Number of audio channels (1 = mono, 2 = stereo) - pub channels: u16, - /// Bits per sample (typically 16 or 24) - pub bits_per_sample: u16, - /// Total number of samples in this track - pub total_samples: u64, -} - -/// A stream of decoded PCM audio for a single track -/// -/// Provides access to decoded FLAC audio data for one track within a block. -/// The audio is decoded to 16-bit PCM format. -#[cfg(feature = "per-track")] -pub struct TrackStream { - /// Audio format metadata - pub metadata: TrackMetadata, - /// Path to the temporary FLAC file - temp_path: PathBuf, - /// FLAC reader - reader: Option>>, - /// Current sample position - current_sample: u64, - /// End sample position (where this track ends) - end_sample: u64, -} - -#[cfg(feature = "per-track")] -impl TrackStream { - /// Create a new track stream from a block - /// - /// This will: - /// 1. Download the entire block to a temporary file - /// 2. Open it with a FLAC decoder - /// 3. Seek to the track's start position - /// 4. Prepare to decode samples - /// - /// **Warning**: This is an expensive operation. Consider caching blocks. - async fn from_block_internal( - client: &RadioParadiseClient, - block: &Block, - track_index: usize, - ) -> Result { - // Validate track index - let song = block - .get_song(track_index) - .ok_or(Error::InvalidIndex(track_index, block.song_count()))?; - - // Download block to temporary file - let url = block - .url - .parse() - .map_err(|e| Error::other(format!("Invalid block URL: {}", e)))?; - - let block_data = client.download_block(&url).await?; - - // Write to temp file - let mut temp_file = tempfile::NamedTempFile::new()?; - temp_file.write_all(&block_data)?; - temp_file.flush()?; - - let temp_path = temp_file.into_temp_path(); - let path_buf = temp_path.to_path_buf(); - - #[cfg(feature = "logging")] - tracing::debug!("Wrote block to temp file: {:?}", path_buf); - - // Open FLAC reader - let file = std::fs::File::open(&path_buf)?; - let buffered = std::io::BufReader::new(file); - let mut reader = claxon::FlacReader::new(buffered)?; - - let streaminfo = reader.streaminfo(); - let sample_rate = streaminfo.sample_rate; - let channels = streaminfo.channels as u16; - let bits_per_sample = streaminfo.bits_per_sample as u16; - - // Calculate start and end sample positions - let start_sample = Self::ms_to_samples(song.elapsed, sample_rate); - let duration_samples = Self::ms_to_samples(song.duration, sample_rate); - let end_sample = start_sample + duration_samples; - - #[cfg(feature = "logging")] - tracing::debug!( - "Track {} spans samples {} to {} ({} ms to {} ms)", - track_index, - start_sample, - end_sample, - song.elapsed, - song.elapsed + song.duration - ); - - // Seek to start position by reading and discarding samples - // Note: FLAC doesn't support random access, so we must decode from beginning - if start_sample > 0 { - #[cfg(feature = "logging")] - tracing::debug!("Seeking to sample {}", start_sample); - - Self::skip_samples(&mut reader, start_sample)?; - } - - let metadata = TrackMetadata { - sample_rate, - channels, - bits_per_sample, - total_samples: duration_samples, - }; - - Ok(Self { - metadata, - temp_path: path_buf, - reader: Some(reader), - current_sample: start_sample, - end_sample, - }) - } - - /// Convert milliseconds to sample count - fn ms_to_samples(ms: u64, sample_rate: u32) -> u64 { - (ms * sample_rate as u64) / 1000 - } - - /// Skip samples by reading and discarding - fn skip_samples( - reader: &mut claxon::FlacReader>, - count: u64, - ) -> Result<()> { - let mut samples = reader.samples(); - for _ in 0..count { - if samples.next().is_none() { - return Err(Error::other("Unexpected end of FLAC stream while seeking")); - } - } - Ok(()) - } - - /// Read decoded PCM samples - /// - /// Returns samples as 16-bit signed integers (i16), interleaved by channel. - /// For stereo: [L, R, L, R, ...]. Returns None when track ends. - pub fn read_samples(&mut self, buffer: &mut [i16]) -> Result> { - let reader = self - .reader - .as_mut() - .ok_or(Error::other("TrackStream already consumed"))?; - - let mut samples_iter = reader.samples(); - let mut count = 0; - - for chunk in buffer.chunks_mut(self.metadata.channels as usize) { - if self.current_sample >= self.end_sample { - break; - } - - // Read one sample per channel - for sample_slot in chunk.iter_mut() { - match samples_iter.next() { - Some(Ok(sample)) => { - // Claxon returns i32, convert to i16 - *sample_slot = (sample >> (self.metadata.bits_per_sample - 16)) as i16; - count += 1; - } - Some(Err(e)) => { - return Err(Error::FlacDecode(e.to_string())); - } - None => { - return Ok(if count > 0 { Some(count) } else { None }); - } - } - } - - self.current_sample += 1; - } - - Ok(if count > 0 { Some(count) } else { None }) - } - - /// Export track to a WAV file - /// - /// Decodes the entire track and writes it as a WAV file. - /// - /// # Example - /// - /// ```no_run - /// # #[cfg(feature = "per-track")] - /// # { - /// use pmoparadise::RadioParadiseClient; - /// use std::path::Path; - /// - /// # #[tokio::main] - /// # async fn main() -> Result<(), Box> { - /// let client = RadioParadiseClient::new().await?; - /// let block = client.get_block(None).await?; - /// - /// let mut track_stream = client.open_track_stream(&block, 0).await?; - /// track_stream.export_wav(Path::new("track.wav"))?; - /// # Ok(()) - /// # } - /// # } - /// ``` - pub fn export_wav(&mut self, output_path: &std::path::Path) -> Result<()> { - let spec = hound::WavSpec { - channels: self.metadata.channels, - sample_rate: self.metadata.sample_rate, - bits_per_sample: 16, - sample_format: hound::SampleFormat::Int, - }; - - let mut writer = hound::WavWriter::create(output_path, spec)?; - let mut buffer = vec![0i16; 8192 * self.metadata.channels as usize]; - - #[cfg(feature = "logging")] - tracing::info!("Exporting track to WAV: {:?}", output_path); - - loop { - match self.read_samples(&mut buffer)? { - Some(count) => { - for &sample in &buffer[..count] { - writer.write_sample(sample)?; - } - } - None => break, - } - } - - writer.finalize()?; - - #[cfg(feature = "logging")] - tracing::info!("Successfully exported WAV file"); - - Ok(()) - } -} - -#[cfg(feature = "per-track")] -impl Drop for TrackStream { - fn drop(&mut self) { - // Close reader before removing temp file - self.reader.take(); - - // Clean up temporary file - if let Err(_e) = std::fs::remove_file(&self.temp_path) { - #[cfg(feature = "logging")] - tracing::warn!("Failed to remove temp file {:?}: {}", self.temp_path, _e); - } - } -} - -#[cfg(feature = "per-track")] -impl RadioParadiseClient { - /// Open a stream for a specific track within a block - /// - /// **Warning**: This downloads the entire block to a temporary file - /// and performs FLAC decoding. See module documentation for alternatives. - /// - /// # Arguments - /// - /// * `block` - The block containing the track - /// * `track_index` - Index of the track (0-based) - /// - /// # Example - /// - /// ```no_run - /// # #[cfg(feature = "per-track")] - /// # { - /// use pmoparadise::RadioParadiseClient; - /// - /// # #[tokio::main] - /// # async fn main() -> Result<(), Box> { - /// let client = RadioParadiseClient::new().await?; - /// let block = client.get_block(None).await?; - /// - /// // Extract first track - /// let mut track = client.open_track_stream(&block, 0).await?; - /// println!("Track: {} Hz, {} channels", - /// track.metadata.sample_rate, - /// track.metadata.channels); - /// - /// // Read some samples - /// let mut buffer = vec![0i16; 4096]; - /// if let Some(count) = track.read_samples(&mut buffer)? { - /// println!("Read {} samples", count); - /// } - /// # Ok(()) - /// # } - /// # } - /// ``` - pub async fn open_track_stream( - &self, - block: &Block, - track_index: usize, - ) -> Result { - TrackStream::from_block_internal(self, block, track_index).await - } - - /// Helper: Get track position in seconds for player-based seeking - /// - /// Instead of downloading and decoding, you can pass this information - /// to your audio player for efficient seeking. - /// - /// Returns (start_seconds, duration_seconds) - /// - /// # Example - /// - /// ```no_run - /// use pmoparadise::RadioParadiseClient; - /// - /// # #[tokio::main] - /// # async fn main() -> Result<(), Box> { - /// let client = RadioParadiseClient::new().await?; - /// let block = client.get_block(None).await?; - /// - /// let (start, duration) = client.track_position_seconds(&block, 1)?; - /// println!("Track 1 starts at {}s, duration {}s", start, duration); - /// println!("Play with: mpv --start={} --length={} {}", start, duration, block.url); - /// # Ok(()) - /// # } - /// ``` - pub fn track_position_seconds(&self, block: &Block, track_index: usize) -> Result<(f64, f64)> { - let song = block - .get_song(track_index) - .ok_or(Error::InvalidIndex(track_index, block.song_count()))?; - - let start_secs = song.elapsed as f64 / 1000.0; - let duration_secs = song.duration as f64 / 1000.0; - - Ok((start_secs, duration_secs)) - } -} - -#[cfg(test)] -#[cfg(feature = "per-track")] -mod tests { - use super::*; - - #[test] - fn test_ms_to_samples() { - assert_eq!(TrackStream::ms_to_samples(1000, 44100), 44100); - assert_eq!(TrackStream::ms_to_samples(500, 44100), 22050); - assert_eq!(TrackStream::ms_to_samples(0, 44100), 0); - } -}