somafm 0.5.0

A Rust-based command line player for SomaFM radio
use icy_metadata::{IcyHeaders, IcyMetadataReader, RequestIcyMetadata};
use rodio::{Decoder, OutputStream, Sink};
use std::num::NonZeroUsize;
use stream_download::http::reqwest::Client;
use stream_download::http::HttpStream;
use stream_download::storage::bounded::BoundedStorageProvider;
use stream_download::storage::memory::MemoryStorageProvider;
use stream_download::{Settings, StreamDownload};

const AUDIO_BUFFER_SECONDS: u32 = 5;

pub struct Rodio {}

impl Rodio {
    pub async fn play(&self, url: &str) {
        let (_stream, handle) = OutputStream::try_default().unwrap();
        let sink = Sink::try_new(&handle).unwrap();

        // request metadata header
        let client = Client::builder().request_icy_metadata().build().unwrap();

        let stream = HttpStream::new(client, url.parse().unwrap()).await.unwrap();

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

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

        let reader = 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
        .unwrap();

        sink.append(
            Decoder::new(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(),
                |metadata| {
                    if let Ok(md) = metadata {
                        if let Some(tr) = md.stream_title() {
                            println!(" {tr}");
                        }
                    }
                },
            ))
            .unwrap(),
        );

        let handle = tokio::task::spawn_blocking(move || {
            sink.sleep_until_end();
        });
        handle.await.unwrap();
    }
}