implemente pmoparadise
This commit is contained in:
183
pmoparadise/src/stream.rs
Normal file
183
pmoparadise/src/stream.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
//! Block streaming functionality
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::models::Block;
|
||||
use crate::RadioParadiseClient;
|
||||
use bytes::Bytes;
|
||||
use futures::stream::Stream;
|
||||
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<Item = Result<Bytes>>`
|
||||
/// that can be consumed by audio players or written to a file.
|
||||
pub struct BlockStream {
|
||||
inner: Pin<Box<dyn Stream<Item = Result<Bytes>> + Send>>,
|
||||
}
|
||||
|
||||
impl BlockStream {
|
||||
/// Create a new block stream from a reqwest response
|
||||
pub(crate) fn new(stream: impl Stream<Item = Result<Bytes>> + Send + 'static) -> Self {
|
||||
Self {
|
||||
inner: Box::pin(stream),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for BlockStream {
|
||||
type Item = Result<Bytes>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
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<dyn std::error::Error>> {
|
||||
/// 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<BlockStream> {
|
||||
#[cfg(feature = "logging")]
|
||||
tracing::debug!("Starting block stream: {}", block_url);
|
||||
|
||||
let response = self.client
|
||||
.get(block_url.clone())
|
||||
.timeout(self.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<dyn std::error::Error>> {
|
||||
/// 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<BlockStream> {
|
||||
let url = Url::parse(&block.url)?;
|
||||
self.stream_block(&url).await
|
||||
}
|
||||
|
||||
/// Download a complete block to memory
|
||||
///
|
||||
/// **Warning**: Blocks can be large (50-100MB for FLAC). Use streaming
|
||||
/// for playback instead of downloading the entire block to memory.
|
||||
///
|
||||
/// This is useful for the per-track feature which needs random access.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoparadise::RadioParadiseClient;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = RadioParadiseClient::new().await?;
|
||||
/// let block = client.get_block(None).await?;
|
||||
///
|
||||
/// let data = client.download_block(&block.url.parse()?).await?;
|
||||
/// println!("Downloaded {} bytes", data.len());
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn download_block(&self, block_url: &Url) -> Result<Bytes> {
|
||||
#[cfg(feature = "logging")]
|
||||
tracing::debug!("Downloading complete block: {}", block_url);
|
||||
|
||||
let response = self.client
|
||||
.get(block_url.clone())
|
||||
.timeout(self.timeout)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(Error::other(format!(
|
||||
"Failed to download block: HTTP {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
#[cfg(feature = "logging")]
|
||||
tracing::debug!("Downloaded {} bytes", bytes.len());
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user