diff --git a/src/error.rs b/src/error.rs index 5f1d4ee..90325c8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -26,6 +26,10 @@ pub enum Error { /// There was some other HTTP error while interacting with the API. #[error("HTTP error")] Http(#[source] Box), + + /// Tried to access a service that requires authentication. + #[error("not authenticated")] + NotAuthenticated, } #[derive(Debug, Deserialize)] diff --git a/src/lib.rs b/src/lib.rs index 3d1a5f9..6a3d75d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,5 +3,7 @@ mod endpoint; mod error; pub mod raw; +mod wrapper; pub use crate::error::Error; +pub use crate::wrapper::ListenBrainz; diff --git a/src/wrapper.rs b/src/wrapper.rs new file mode 100644 index 0000000..79d5332 --- /dev/null +++ b/src/wrapper.rs @@ -0,0 +1,125 @@ +use std::convert::TryInto; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::error::Error; +use crate::raw::Client; +use crate::raw::request::{ListenType, Payload, SubmitListens, TrackMetadata}; + +/// Contains a ListenBrainz token and the associated username +/// for authentication purposes. +struct Auth { + token: String, + user: String, +} + +/// An ergonomic ListenBrainz client. +/// +/// As opposed to [`Client`](crate::raw::Client), this aims to be a convenient and high-level +/// wrapper of the ListenBrainz API. +pub struct ListenBrainz { + client: Client, + auth: Option, +} + +impl ListenBrainz { + /// Construct a new ListenBrainz client. + pub fn new() -> Self { + Self { + client: Client::new(), + auth: None, + } + } + + /// Check if this client is authenticated. + pub fn is_authenticated(&self) -> bool { + self.auth.is_some() + } + + /// Return the token if authenticated or [`None`] if not. + pub fn authenticated_token(&self) -> Option<&str> { + self.auth.as_ref().map(|auth| auth.token.as_str()) + } + + /// Return the user if authenticated or [`None`] if not. + pub fn authenticated_user(&self) -> Option<&str> { + self.auth.as_ref().map(|auth| auth.user.as_str()) + } + + /// Authenticate this client with the given token. + /// If the token is valid, authenticates the client and returns true, otherwise returns false. + /// + /// # Errors + /// + /// If there was an error while validating the token, that error is returned. + /// See the Errors section of [`Client`] for more info on what errors might occur. + pub fn authenticate(&mut self, token: &str) -> Result { + let result = self.client.validate_token(token)?; + if result.valid && result.user_name.is_some() { + self.auth.replace(Auth { + token: token.to_string(), + user: result.user_name.unwrap(), + }); + return Ok(true); + } + Ok(false) + } + + /// Helper method to submit a listen (either "single" or "playing now"). + fn submit_listen( + &self, + listen_type: ListenType, + timestamp: Option, + artist: &str, + track: &str, + release: &str, + ) -> Result<(), Error> { + if !self.is_authenticated() { + return Err(Error::NotAuthenticated); + } + + let payload = Payload { + listened_at: timestamp, + track_metadata: TrackMetadata { + artist_name: artist, + track_name: track, + release_name: Some(release), + additional_info: None, + }, + }; + + let token = self.authenticated_token().unwrap(); + self.client.submit_listens( + token, + SubmitListens { + listen_type, + payload: &[payload], + }, + )?; + + Ok(()) + } + + /// Submit a listened track with the current time as the listen time. + /// This requires authentication. + /// + /// # Errors + /// + /// If not authenticated, returns [`Error::NotAuthenticated`]. + /// Otherwise, see the Errors section of [`Client`] for more info on + /// what errors might occur. + pub fn listen(&self, artist: &str, track: &str, release: &str) -> Result<(), Error> { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + .try_into() + .unwrap(); + + self.submit_listen(ListenType::Single, Some(now), artist, track, release) + } + + /// Submit a currently playing track. + pub fn playing_now(&self, artist: &str, track: &str, release: &str) -> Result<(), Error> { + self.submit_listen(ListenType::PlayingNow, None, artist, track, release) + } +}