diff --git a/src/audio/mod.rs b/src/audio/mod.rs index e7cb2ed..8ec48ea 100644 --- a/src/audio/mod.rs +++ b/src/audio/mod.rs @@ -12,7 +12,9 @@ use pipewire::PipewireContext; use crate::{config::Settings, player::PlayerEvent, utils::Error}; -pub enum AudioEvent {} +pub enum AudioEvent { + DecodeChunk(Vec), +} pub struct SoundManager { settings: Arc, diff --git a/src/player/current.rs b/src/player/current.rs index dc7ffe4..5e0e193 100644 --- a/src/player/current.rs +++ b/src/player/current.rs @@ -29,6 +29,10 @@ impl Metadata { } } + pub fn bytes_pending(&self) -> bool { + self.bytes_received < self.size + } + pub fn set_cover(&mut self, cover: DynamicImage) { self.cover = cover.thumbnail(200, 200); } diff --git a/src/player/mod.rs b/src/player/mod.rs index fc570ca..f5d1eee 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -21,7 +21,7 @@ pub enum PlayerEvent { UpdateCover(DynamicImage), UserQuit, PlayNext, - AddAudioChunks(Vec), + AddAudioChunk(Vec), } pub struct Player { diff --git a/src/player/player.rs b/src/player/player.rs index 8cd083e..07862c6 100644 --- a/src/player/player.rs +++ b/src/player/player.rs @@ -45,6 +45,7 @@ impl Player { PlayerEvent::AddSongList(list) => self.playlist.append(list), PlayerEvent::PlayNext => self.play_next()?, PlayerEvent::UpdateCover(cover) => self.tui_root.update_cover(cover), + PlayerEvent::AddAudioChunk(chunk) => self.recv_chunk(chunk)?, _ => unimplemented!(), } } @@ -81,18 +82,28 @@ impl Player { .send(PlayerEvent::UpdateCover(default_cover()))?; } self.tui_root.metadata.update_metadata(song); - self.fetch_audio_chunk(self.tui_root.metadata.id.clone())?; + self.fetch_audio_chunk()?; Ok(()) } - fn fetch_audio_chunk(&mut self, id: String) -> Result<(), Error> { - let (mut start, mut end) = (0, 0); - if self.tui_root.metadata.bytes_received < self.tui_root.metadata.size { - start = self.tui_root.metadata.bytes_received; - let diff = self.tui_root.metadata.size - self.tui_root.metadata.bytes_received; - end = std::cmp::min(diff, MAX_CHUNK_SIZE); - self.api_chan.send(APIEvent::StreamSong(id, start, end))?; + fn fetch_audio_chunk(&mut self) -> Result<(), Error> { + let start = self.tui_root.metadata.bytes_received; + let diff = self.tui_root.metadata.size - self.tui_root.metadata.bytes_received; + let end = std::cmp::min(diff, MAX_CHUNK_SIZE) + start - 1; + self.api_chan.send(APIEvent::StreamSong( + self.tui_root.metadata.id.clone(), + start, + end, + ))?; + Ok(()) + } + + fn recv_chunk(&mut self, chunk: Vec) -> Result<(), Error> { + self.tui_root.metadata.bytes_received += chunk.len() as u32; + self.audio_chan.send(AudioEvent::DecodeChunk(chunk))?; + if self.tui_root.metadata.bytes_pending() { + self.fetch_audio_chunk()?; } Ok(()) } diff --git a/src/ssonic/client.rs b/src/ssonic/client.rs index cc79603..be51ec8 100644 --- a/src/ssonic/client.rs +++ b/src/ssonic/client.rs @@ -42,45 +42,41 @@ impl APIClient { header.insert(RANGE, range_header.parse()?); let mut response = self.request("/stream", Some(&[("id", id)]), Some(header))?; - if !response.status().is_success() { - return Err(Box::new(APIError::StatusError( - response.status(), - "/stream", - ))); - }; + response = self.validate_media(response, "/stream", "audio")?; - if response.headers()[CONTENT_TYPE].to_str()? == "application/json" { - self.validate(response, "/stream")?; // will never error if not image - } else { - eprintln!("Fetched chunk"); - let mut audio_chunk = Vec::new(); - response.read_to_end(&mut audio_chunk)?; - return Ok(PlayerEvent::AddAudioChunks(audio_chunk)); + let mut audio_chunk = Vec::new(); + response.read_to_end(&mut audio_chunk)?; + return Ok(PlayerEvent::AddAudioChunk(audio_chunk)); + } + + fn validate_media( + &mut self, + response: Response, + path: &'static str, + expected_mime_class: &'static str, + ) -> Result { + if !response.status().is_success() { + return Err(Box::new(APIError::StatusError(response.status(), path))); } - unreachable!(); + let mime = response.headers()[CONTENT_TYPE].to_str()?; + match mime { + "application/json" => drop(self.validate(response, path)?), // must error on binary endpoints + _ if mime.starts_with(expected_mime_class) => return Ok(response), + _ => return Err(Box::new(APIError::InvalidResponseType(String::from(mime)))), + } + unreachable!() } fn get_cover_art(&mut self, cover_id: String) -> Result { let mut response = self.request("/getCoverArt", Some(&[("id", cover_id)]), None)?; - if !response.status().is_success() { - return Err(Box::new(APIError::StatusError( - response.status(), - "/getCoverArt", - ))); - }; - if response.headers()[CONTENT_TYPE].to_str()? == "application/json" { - self.validate(response, "/getCoverArt")?; // will never error if not image - } else { - let mut image = Vec::new(); - response.read_to_end(&mut image)?; - let cover = image::load_from_memory_with_format( - &mut image, - image::ImageFormat::from_mime_type(response.headers()[CONTENT_TYPE].to_str()?) - .unwrap(), - )?; - return Ok(PlayerEvent::UpdateCover(cover)); - } - unreachable!(); + response = self.validate_media(response, "/getCoverArt", "image")?; + let mut image = Vec::new(); + response.read_to_end(&mut image)?; + let cover = image::load_from_memory_with_format( + &mut image, + image::ImageFormat::from_mime_type(response.headers()[CONTENT_TYPE].to_str()?).unwrap(), + )?; + return Ok(PlayerEvent::UpdateCover(cover)); } fn get_random(&mut self, n: i32) -> Result { diff --git a/src/ssonic/errors.rs b/src/ssonic/errors.rs index 0403f4a..79d75a5 100644 --- a/src/ssonic/errors.rs +++ b/src/ssonic/errors.rs @@ -3,6 +3,7 @@ use reqwest::StatusCode; #[derive(Debug)] pub enum APIError { StatusError(StatusCode, &'static str), + InvalidResponseType(String), SubsonicError(String), } @@ -17,6 +18,9 @@ impl std::fmt::Display for APIError { code, path ), Self::SubsonicError(message) => write!(f, "Subsonic API error: {}", message), + Self::InvalidResponseType(mime) => { + write!(f, "Expected media mime-type, found: {}", mime) + } } } }