diff --git a/src/scrobbler/last_fm.rs b/src/scrobbler/last_fm.rs index 9084555..7cfdc6d 100644 --- a/src/scrobbler/last_fm.rs +++ b/src/scrobbler/last_fm.rs @@ -28,10 +28,11 @@ struct LastfmSessionData { } impl LastfmScrobbler { - pub fn new(username: String, password: String, api_key: String, api_secret: String) -> Self { + /// create a new LastfmScrobbler instance. + pub fn new(username: String, password: String, api_key: String, api_secret: String) -> Result { let client = Client::new(); - let session_key = match client + let session_key = client .post(API_BASE_URL) .header("content-length", "0") .query( @@ -43,21 +44,29 @@ impl LastfmScrobbler { .sign(&api_secret), ) .send() - .unwrap() + .map_err(|err| format!("Failed to authenticate with Last.fm: {}", err))? .json::() - { - Ok(session) => { - println!("{} Successfully authenticated with username {}.", "[Last.fm]".bright_green(), session.session.name.bright_blue()); + .map_err(|err| format!("Failed to parse Last.fm session: {}", err)) + .map(|session| { + println!( + "{} Successfully authenticated with username {}.", + "[Last.fm]".bright_green(), + session.session.name.bright_blue() + ); session.session.key - }, - Err(_) => panic!("{} Invalid credentials provided.", "[Last.fm]".bright_red()), - }; + })?; - Self { client, api_key, api_secret, session_key } + Ok(Self { client, api_key, api_secret, session_key }) } + /// scrobble a track to Last.fm. pub fn scrobble(&self, title: &str, artist: &str, total_length: u32) -> Result<(), String> { - match self + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|err| format!("Failed to get timestamp: {}", err))? + .as_secs(); + + let response = self .client .post(API_BASE_URL) .header("content-length", "0") @@ -68,17 +77,16 @@ impl LastfmScrobbler { .insert("duration[0]", total_length) .insert("method", "track.scrobble") .insert("sk", &self.session_key) - .insert("timestamp[0]", SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()) + .insert("timestamp[0]", timestamp) .insert("track[0]", title) .sign(&self.api_secret), ) .send() - { - Ok(response) => match response.status() { - StatusCode::OK => Ok(()), - status_code => Err(format!("Received status code {status_code}.")), - }, - Err(error) => Err(error.to_string()), + .map_err(|err| format!("Failed to send scrobble request: {}", err))?; + + match response.status() { + StatusCode::OK => Ok(()), + status_code => Err(format!("Received status code {}.", status_code)), } } } @@ -88,20 +96,26 @@ struct LastfmQuery { } impl LastfmQuery { + /// create a new LastfmQuery instance. pub fn new() -> Self { Self { query: BTreeMap::new() } } + /// insert a key-value pair into the query parameters. pub fn insert(mut self, key: T, value: U) -> Self { self.query.insert(key.to_string(), value.to_string()); self } + /// sign the query parameters using the API secret. pub fn sign(self, api_secret: T) -> BTreeMap { - let api_sig = format!( - "{:x}", - compute(self.query.iter().map(|(key, value)| format!("{key}{value}")).collect::() + &api_secret.to_string()), - ); + let api_sig = format!("{:x}", compute( + self.query + .iter() + .map(|(key, value)| format!("{}{}", key, value)) + .collect::() + + &api_secret.to_string(), + )); self.insert("api_sig", api_sig).insert("format", "json").query } diff --git a/src/scrobbler/listenbrainz.rs b/src/scrobbler/listenbrainz.rs index 1738fad..c814519 100644 --- a/src/scrobbler/listenbrainz.rs +++ b/src/scrobbler/listenbrainz.rs @@ -17,53 +17,61 @@ struct ListenBrainzToken { } impl ListenBrainzScrobbler { - pub fn new(user_token: String) -> Self { + pub fn new(user_token: String) -> Result { let client = Client::new(); - - match client - .get(format!("{API_BASE_URL}/validate-token")) - .header("authorization", format!("Token {user_token}")) + let response = client + .get(format!("{}/validate-token", API_BASE_URL)) + .header("authorization", format!("Token {}", user_token)) .send() - .unwrap() - .json::() - { - Ok(token) => { - println!("{} Successfully authenticated with username {}.", "[ListenBrainz]".bright_green(), token.user_name.bright_blue()) - }, - Err(_) => panic!("{} Invalid user token provided.", "[ListenBrainz]".bright_red()), - }; + .map_err(|error| format!("Error validating user token: {}", error))?; - Self { client, user_token } + if response.status().is_success() { + let token = response + .json::() + .map_err(|error| format!("Error parsing token response: {}", error))?; + println!( + "{} Successfully authenticated with username {}.", + "[ListenBrainz]".bright_green(), + token.user_name.bright_blue() + ); + Ok(Self { client, user_token }) + } else { + Err(format!("Invalid user token provided.")) + } } pub fn scrobble(&self, title: &str, artist: &str, total_length: u32) -> Result<(), String> { - match self + let listen_data = json!({ + "listen_type": "single", + "payload": [{ + "listened_at": SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| format!("Error getting current time: {}", error))? + .as_secs(), + "track_metadata": { + "artist_name": artist, + "track_name": title, + "additional_info": { + "media_player": "osu!", + "submission_client": "osu-scrobbler (github.com/flazepe/osu-scrobbler)", + "submission_client_version": env!("CARGO_PKG_VERSION"), + "duration_ms": total_length * 1000, + }, + }, + }], + }); + + let response = self .client - .post(format!("{API_BASE_URL}/submit-listens")) + .post(format!("{}/submit-listens", API_BASE_URL)) .header("authorization", format!("Token {}", self.user_token)) - .json(&json!({ - "listen_type": "single", - "payload": [{ - "listened_at": SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), - "track_metadata": { - "artist_name": artist, - "track_name": title, - "additional_info": { - "media_player": "osu!", - "submission_client": "osu-scrobbler (github.com/flazepe/osu-scrobbler)", - "submission_client_version": env!("CARGO_PKG_VERSION"), - "duration_ms": total_length * 1000, - }, - }, - }], - })) + .json(&listen_data) .send() - { - Ok(response) => match response.status() { - StatusCode::OK => Ok(()), - status_code => Err(format!("Received status code {status_code}.")), - }, - Err(error) => Err(error.to_string()), + .map_err(|error| format!("Error sending scrobble request: {}", error))?; + + match response.status() { + StatusCode::OK => Ok(()), + status_code => Err(format!("Received status code {}.", status_code)), } } } diff --git a/src/scrobbler/mod.rs b/src/scrobbler/mod.rs index 1eb76d5..a9f5ff5 100644 --- a/src/scrobbler/mod.rs +++ b/src/scrobbler/mod.rs @@ -64,10 +64,10 @@ impl Scrobbler { } fn poll(&mut self) { - let Some(score) = self.get_recent_score() else { return; }; - - if self.recent_score.as_ref().map_or(true, |recent_score| recent_score.ended_at != score.ended_at) { - self.scrobble(score); + if let Some(score) = self.get_recent_score() { + if self.recent_score.as_ref().map_or(true, |recent_score| recent_score.ended_at != score.ended_at) { + self.scrobble(score); + } } } @@ -76,14 +76,16 @@ impl Scrobbler { return; } - let title = match self.config.use_original_metadata { - true => &score.beatmapset.title_unicode, - false => &score.beatmapset.title, + let title = if self.config.use_original_metadata { + &score.beatmapset.title_unicode + } else { + &score.beatmapset.title }; - let artist = match self.config.use_original_metadata { - true => &score.beatmapset.artist_unicode, - false => &score.beatmapset.artist, + let artist = if self.config.use_original_metadata { + &score.beatmapset.artist_unicode + } else { + &score.beatmapset.artist }; println!("{} New score found: {}", "[Scrobbler]".bright_green(), format!("{artist} - {title}").bright_blue()); @@ -91,15 +93,15 @@ impl Scrobbler { if let Some(last_fm) = self.last_fm.as_ref() { match last_fm.scrobble(title, artist, score.beatmap.total_length) { Ok(_) => println!("\t{} Successfully scrobbled score.", "[Last.fm]".bright_green()), - Err(error) => println!("\t{} {error}", "[Last.fm]".bright_red()), - }; + Err(error) => println!("\t{} Error while scrobbling score: {}", "[Last.fm]".bright_red(), error), + } } if let Some(listenbrainz) = self.listenbrainz.as_ref() { match listenbrainz.scrobble(title, artist, score.beatmap.total_length) { Ok(_) => println!("\t{} Successfully scrobbled score.", "[ListenBrainz]".bright_green()), - Err(error) => println!("\t{} {error}", "[ListenBrainz]".bright_red()), - }; + Err(error) => println!("\t{} Error while scrobbling score: {}", "[ListenBrainz]".bright_red(), error), + } } self.recent_score = Some(score); @@ -115,9 +117,9 @@ impl Scrobbler { let response = match request.send() { Ok(response) => response, Err(error) => { - println!("{} Could not get user's recent score: {error}", "[Scrobbler]".bright_red()); + println!("{} Error while getting user's recent score: {}", "[Scrobbler]".bright_red(), error); return None; - }, + } }; let status_code = response.status(); @@ -126,17 +128,20 @@ impl Scrobbler { match status_code { StatusCode::NOT_FOUND => panic!("{} Invalid osu! user ID given.", "[Scrobbler]".bright_red()), _ => { - println!("{} Could not get user's recent score: Received status code {status_code}.", "[Scrobbler]".bright_red()); + println!("{} Error while getting user's recent score: Received status code {}.", "[Scrobbler]".bright_red(), status_code); return None; - }, + } } } - let Ok(mut scores) = response.json::>() else { return None; }; + let scores = match response.json::>() { + Ok(scores) => scores, + Err(error) => { + println!("{} Error while parsing scores: {}", "[Scrobbler]".bright_red(), error); + return None; + } + }; - match scores.is_empty() { - true => None, - false => Some(scores.remove(0)), - } + scores.into_iter().next() } }