icy-metadata 0.6.0

A library for parsing Icecast metadata
Documentation
use std::error::Error;
use std::num::NonZeroUsize;
use std::time::Duration;

use icy_metadata::{IcyHeaders, IcyMetadataReader, RequestIcyMetadata};
use rodio::source::SeekError;
use stream_download::http::HttpStream;
use stream_download::http::reqwest::Client;
use stream_download::source::DecodeError;
use stream_download::storage::bounded::BoundedStorageProvider;
use stream_download::storage::memory::MemoryStorageProvider;
use stream_download::{Settings, StreamDownload};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?;
    let sink = rodio::Sink::connect_new(stream_handle.mixer());

    // We need to add a header to tell the Icecast server that we can parse the metadata embedded
    // within the stream itself.
    let client = Client::builder().request_icy_metadata().build()?;

    let stream =
        HttpStream::new(client, "https://ice6.somafm.com/insound-128-mp3".parse()?).await?;

    let icy_headers = IcyHeaders::parse_from_headers(stream.headers());
    println!("Icecast headers: {icy_headers:#?}\n");
    println!("content type={:?}\n", stream.content_type());

    // buffer 5 seconds of audio
    // bitrate (in kilobits) / bits per byte * bytes per kilobyte * 5 seconds
    let prefetch_bytes = icy_headers.bitrate().unwrap() / 8 * 1024 * 5;

    let reader = match StreamDownload::from_stream(
        stream,
        // use bounded storage to keep the underlying size from growing indefinitely
        BoundedStorageProvider::new(
            MemoryStorageProvider,
            // be liberal with the buffer size, you need to make sure it holds enough space to
            // prevent any out-of-bounds reads
            NonZeroUsize::new(512 * 1024).unwrap(),
        ),
        Settings::default().prefetch_bytes(prefetch_bytes as u64),
    )
    .await
    {
        Ok(reader) => reader,
        Err(e) => return Err(e.decode_error().await)?,
    };
    sink.append(
        rodio::Decoder::builder()
            .with_seekable(true)
            .with_data(IcyMetadataReader::new(
                reader,
                // Since we requested icy metadata, the metadata interval header should be present
                // in the response. This will allow us to parse the metadata within
                // the stream
                icy_headers.metadata_interval(),
                // Print the stream metadata whenever we receive new values
                |metadata| println!("{metadata:#?}\n"),
            ))
            .build()?,
    );

    let handle = tokio::task::spawn_blocking(move || {
        println!("seeking in 5 seconds\n");
        std::thread::sleep(Duration::from_secs(5));
        println!("seeking to beginning\n");
        sink.try_seek(Duration::from_secs(0))?;
        std::thread::sleep(Duration::from_secs(5));
        println!("seeking to 10 seconds\n");
        sink.try_seek(Duration::from_secs(10))?;
        sink.sleep_until_end();
        Ok::<_, SeekError>(())
    });
    handle.await??;
    Ok(())
}