From 96c8e1e5ac561e450b78912f65b2415d41fc6a58 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 23 Dec 2018 02:49:04 -0500 Subject: rust 2018 --- src/lastfm.rs | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/lastfm.rs (limited to 'src/lastfm.rs') diff --git a/src/lastfm.rs b/src/lastfm.rs new file mode 100644 index 0000000..2666e9a --- /dev/null +++ b/src/lastfm.rs @@ -0,0 +1,187 @@ +use crate::util; + +use failure::Fail; +use std::io::{Read, Write}; + +mod api_types; + +const API_ROOT: &str = "https://ws.audioscrobbler.com/2.0/"; + +pub struct LastFMClient { + client: reqwest::Client, + api_key: String, + user: String, +} + +pub struct Track { + pub artist: String, + pub album: String, + pub name: String, + pub timestamp: i64, +} + +pub struct Tracks<'a> { + client: &'a LastFMClient, + page: Option, + buf: Vec, + from: Option, +} + +impl<'a> Tracks<'a> { + fn new(client: &LastFMClient, from: Option) -> Tracks { + Tracks { + client, + page: None, + buf: vec![], + from, + } + } + + fn get_next_page(&mut self) -> failure::Fallible<()> { + if self.page.is_none() { + self.page = Some(self.client.get_total_pages(self.from)?); + } + let page = self.page.unwrap(); + if page < 1 { + return Ok(()); + } + + let req = self.client.client.get(API_ROOT).query(&[ + ("method", "user.getrecenttracks"), + ("api_key", &self.client.api_key), + ("user", &self.client.user), + ("format", "json"), + ("page", &format!("{}", page)), + ("limit", "200"), + ]); + let req = if let Some(from) = self.from { + req.query(&[("from", &format!("{}", from))]) + } else { + req + }; + + let mut res = req.send()?; + + if res.status().is_success() { + let data: api_types::recent_tracks = res.json()?; + self.buf = data + .recenttracks + .track + .iter() + .filter(|t| t.date.is_some()) + .map(|t| { + Ok(Track { + artist: t.artist.text.clone(), + album: t.album.text.clone(), + name: t.name.clone(), + timestamp: t.date.as_ref().unwrap().uts.parse()?, + }) + }) + .collect::>>()?; + self.page = Some(page - 1); + Ok(()) + } else { + Err(failure::err_msg(res.status().as_str().to_string())) + } + } +} + +impl<'a> Iterator for Tracks<'a> { + type Item = Track; + + fn next(&mut self) -> Option { + if self.buf.is_empty() { + let result = self.get_next_page(); + if result.is_err() { + return None; + } + } + self.buf.pop() + } +} + +impl LastFMClient { + pub fn new(user: &str) -> failure::Fallible { + Ok(LastFMClient { + client: reqwest::Client::new(), + api_key: find_api_key()?, + user: user.to_string(), + }) + } + + pub fn track_count(&self, from: Option) -> failure::Fallible { + let data = self.recent_tracks(from)?; + Ok(data.recenttracks.attr.total.parse()?) + } + + pub fn tracks(&self, from: Option) -> Tracks { + Tracks::new(&self, from) + } + + fn get_total_pages(&self, from: Option) -> failure::Fallible { + let data = self.recent_tracks(from)?; + Ok(data.recenttracks.attr.totalPages.parse()?) + } + + fn recent_tracks( + &self, + from: Option, + ) -> failure::Fallible { + let req = self.client.get(API_ROOT).query(&[ + ("method", "user.getrecenttracks"), + ("api_key", &self.api_key), + ("user", &self.user), + ("format", "json"), + ("limit", "200"), + ]); + let req = if let Some(from) = from { + req.query(&[("from", &format!("{}", from))]) + } else { + req + }; + + let mut res = req.send()?; + + if res.status().is_success() { + let data: api_types::recent_tracks = res.json()?; + Ok(data) + } else { + Err(failure::err_msg(res.status().as_str().to_string())) + } + } +} + +fn find_api_key() -> failure::Fallible { + let api_key_path = util::api_key_path() + .map_err(|e| e.context("failed to determine api key path"))?; + let api_key = if api_key_path.exists() { + let mut api_key = String::new(); + let mut f = std::fs::File::open(&api_key_path).map_err(|e| { + e.context(format!("failed to open {}", api_key_path.display())) + })?; + f.read_to_string(&mut api_key).map_err(|e| { + e.context(format!( + "failed to read from {}", + api_key_path.display() + )) + })?; + api_key + } else { + let api_key = rpassword::prompt_password_stderr(&format!( + "last.fm api key (will be stored in {}): ", + api_key_path.display() + ))?; + std::fs::create_dir_all(api_key_path.parent().unwrap())?; + let mut f = std::fs::File::create(&api_key_path).map_err(|e| { + e.context(format!("failed to open {}", api_key_path.display())) + })?; + f.write_all(api_key.as_bytes()).map_err(|e| { + e.context(format!( + "failed to write to {}", + api_key_path.display() + )) + })?; + api_key + }; + Ok(api_key.trim_end().to_string()) +} -- cgit v1.2.3-54-g00ecf