diff --git a/Cargo.lock b/Cargo.lock index 570a2f1..4611187 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.8.0" @@ -951,6 +957,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.7.4" @@ -1169,6 +1181,15 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -1197,6 +1218,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "ratatui" version = "0.29.0" @@ -1265,6 +1316,7 @@ dependencies = [ "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", @@ -1593,10 +1645,13 @@ name = "subtails" version = "0.1.0" dependencies = [ "crossterm", + "md5", "pipewire", + "rand", "ratatui", "reqwest", "serde", + "serde_json", "toml", ] @@ -2196,6 +2251,27 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index beb060e..e6c7561 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,11 @@ edition = "2021" [dependencies] crossterm = "0.28.1" +md5 = "0.7.0" pipewire = "0.8.0" +rand = "0.8.5" ratatui = "0.29.0" -reqwest = "0.12.9" +reqwest = { version = "0.12.9", features = ["blocking", "json"] } serde = { version = "1.0.215", features = ["derive"] } +serde_json = "1.0.133" toml = "0.8.19" diff --git a/src/config/errors.rs b/src/config/errors.rs index c2f1f59..974de80 100644 --- a/src/config/errors.rs +++ b/src/config/errors.rs @@ -4,6 +4,7 @@ use std::fmt::Display; pub enum ConfigError { ConfigNotFound, MissingConfigValue(&'static str), + InvalidServerAddress, } impl std::error::Error for ConfigError {} @@ -15,7 +16,8 @@ impl Display for ConfigError { f, "Config File was not found in either the current directory or ~/.config." ), - Self::MissingConfigValue(var_name) => write!(f, "Neither the Env variable \"{}\" was set, nor the equivalent field in the config file was found.", var_name) + Self::MissingConfigValue(var_name) => write!(f, "Neither the Env variable \"{}\" was set, nor the equivalent field in the config file was found.", var_name), + Self::InvalidServerAddress => write!(f, "The configured server address is not a valid url"), } } } diff --git a/src/config/validate.rs b/src/config/validate.rs index 23d3f3e..a0b37c2 100644 --- a/src/config/validate.rs +++ b/src/config/validate.rs @@ -1,5 +1,7 @@ use std::env::var; +use reqwest::Url; + use crate::{config::errors::ConfigError, utils::Error}; use super::Settings; @@ -14,6 +16,17 @@ pub fn validate_config(settings: &mut Settings) -> Result<(), Error> { ))) } } - } + }; + let url = Url::parse(settings.subsonic.server_address.as_str()); + match url { + Err(_) => return Err(Box::new(ConfigError::InvalidServerAddress)), + Ok(u) => { + if u.scheme() != "http" && u.scheme() != "https" { + return Err(Box::new(ConfigError::InvalidServerAddress)); + } + settings.subsonic.server_address = + format!("{}{}", settings.subsonic.server_address, "/rest") + } + }; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 747a585..e8c9f4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ fn init() -> Result, utils::Error> { audio::init(settings.clone(), error_in.clone(), player_events_in.clone())?; let api_event_chan = ssonic::init(settings.clone(), error_in.clone(), player_events_in.clone())?; + std::thread::sleep(std::time::Duration::from_secs(5)); let mut player = player::init( settings.clone(), audio_event_chan, diff --git a/src/ssonic/client.rs b/src/ssonic/client.rs new file mode 100644 index 0000000..e358e24 --- /dev/null +++ b/src/ssonic/client.rs @@ -0,0 +1,71 @@ +use std::io::Read; + +use reqwest::{blocking::Response, header::ACCEPT}; +use serde::Serialize; + +use crate::{ + ssonic::{errors::APIError, response}, + utils::{generate_random_salt, Error}, +}; + +const SSONIC_CLIENT: &str = "subtails"; +const SSONIC_VERSION: &str = "1.16.1"; + +use super::APIClient; + +impl APIClient { + pub fn begin(&mut self) -> Result<(), Error> { + self.validate()?; + loop { + unimplemented!(); + } + } + + fn validate(&mut self) -> Result<(), Error> { + let response = self.request::<[(&str, String); 0]>("/ping.view", None)?; + + if !response.status().is_success() { + return Err(Box::new(APIError::StatusError( + response.status(), + "/ping.view", + ))); + } + let ssonic_response: response::Response = response.json()?; + if ssonic_response.subsonic_response.status != "ok" { + return Err(Box::new(APIError::SubsonicError( + ssonic_response.subsonic_response.error.unwrap().message, + ))); + } + Ok(()) + } + + fn generate_random_token(&self) -> (String, String) { + let salt = generate_random_salt(); + let hash = md5::compute(format!("{}{}", self.settings.subsonic.password, salt)); + (format!("{:x}", hash), salt) + } + + fn request( + &mut self, + uri: &str, + extra_params: Option<&T>, + ) -> Result { + let (token, salt) = self.generate_random_token(); + let mut req = self + .ssonic_client + .get(format!("{}{}", self.settings.subsonic.server_address, uri)); + req = req.query(&[ + ("u", self.settings.subsonic.username.clone()), + ("t", token), + ("s", salt), + ("c", String::from(SSONIC_CLIENT)), + ("v", String::from(SSONIC_VERSION)), + ("f", String::from("json")), + ]); + if let Some(params) = extra_params { + req = req.query(params); + } + req = req.header(ACCEPT, "application/json"); + Ok(req.send()?) + } +} diff --git a/src/ssonic/errors.rs b/src/ssonic/errors.rs new file mode 100644 index 0000000..0403f4a --- /dev/null +++ b/src/ssonic/errors.rs @@ -0,0 +1,22 @@ +use reqwest::StatusCode; + +#[derive(Debug)] +pub enum APIError { + StatusError(StatusCode, &'static str), + SubsonicError(String), +} + +impl std::error::Error for APIError {} + +impl std::fmt::Display for APIError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::StatusError(code, path) => write!( + f, + "Encountered an error code of {} while executing request on: {}", + code, path + ), + Self::SubsonicError(message) => write!(f, "Subsonic API error: {}", message), + } + } +} diff --git a/src/ssonic/mod.rs b/src/ssonic/mod.rs index 15ccf39..f1d7f4e 100644 --- a/src/ssonic/mod.rs +++ b/src/ssonic/mod.rs @@ -6,6 +6,8 @@ use std::{ thread, }; +use reqwest::blocking::Client; + use crate::{config::Settings, player::PlayerEvent, utils::Error}; pub enum APIEvent {} @@ -15,6 +17,7 @@ pub struct APIClient { error_chan: Sender, api_requests: Receiver, player_chan: Sender, + ssonic_client: Client, } pub fn init( @@ -23,11 +26,26 @@ pub fn init( player_chan: Sender, ) -> Result, Error> { let (api_requests_in, api_requests_out) = channel(); - thread::spawn(|| APIClient { - settings, - error_chan, - api_requests: api_requests_out, - player_chan, + thread::spawn(move || { + let mut client = APIClient { + settings, + error_chan: error_chan.clone(), + api_requests: api_requests_out, + player_chan, + ssonic_client: Client::new(), + }; + match client.begin() { + Ok(_) => (), + Err(err) => { + error_chan.send(err).unwrap(); + thread::park(); + return; + } + } }); Ok(api_requests_in) } + +mod client; +mod errors; +mod response; diff --git a/src/ssonic/response.rs b/src/ssonic/response.rs new file mode 100644 index 0000000..aa8ccb7 --- /dev/null +++ b/src/ssonic/response.rs @@ -0,0 +1,34 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct Response { + #[serde(alias = "subsonic-response")] + pub subsonic_response: SSonicResponse, +} + +#[derive(Deserialize)] +pub struct SSonicResponse { + pub status: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub random_songs: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub play_queue: Option, +} + +#[derive(Deserialize)] +pub struct Error { + pub message: String, +} + +#[derive(Deserialize)] +pub struct Song { + pub id: String, + pub title: String, + pub artist: String, + pub cover_art: String, +} + +#[derive(Deserialize)] +pub struct PlayQueue {} diff --git a/src/utils.rs b/src/utils.rs index f34e60e..6d8094a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1 +1,11 @@ +use rand::{distributions::Alphanumeric, thread_rng, Rng}; + pub type Error = Box; + +pub fn generate_random_salt() -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(8) + .map(|c| c as char) + .collect() +}