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/cmd.rs | 51 +++++++++++++ src/cmd/mod.rs | 45 ------------ src/cmd/recommend.rs | 4 +- src/cmd/sql.rs | 4 +- src/cmd/sync.rs | 6 +- src/db.rs | 2 +- src/lastfm.rs | 187 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lastfm/api_types.rs | 2 + src/lastfm/mod.rs | 187 ------------------------------------------------ src/main.rs | 14 ---- src/util.rs | 2 + 11 files changed, 250 insertions(+), 254 deletions(-) create mode 100644 src/cmd.rs delete mode 100644 src/cmd/mod.rs create mode 100644 src/lastfm.rs delete mode 100644 src/lastfm/mod.rs (limited to 'src') diff --git a/src/cmd.rs b/src/cmd.rs new file mode 100644 index 0000000..172f6a6 --- /dev/null +++ b/src/cmd.rs @@ -0,0 +1,51 @@ +mod recommend; +mod sql; +mod sync; + +use clap::{ + app_from_crate, crate_authors, crate_description, crate_name, + crate_version, +}; +use failure::bail; + +const _DUMMY_DEPENDENCY: &str = include_str!("../Cargo.toml"); + +trait Command { + fn run(&self) -> failure::Fallible<()>; +} + +pub fn run() -> failure::Fallible<()> { + get_command()?.run() +} + +fn get_command() -> failure::Fallible> { + let subcommands = vec![ + sync::subcommand(), + sql::subcommand(), + recommend::subcommand(), + ]; + let mut app = app_from_crate!().subcommands( + subcommands + .into_iter() + .map(|s| s.setting(clap::AppSettings::DisableVersion)), + ); + let matches = app.clone().get_matches(); + + let command: Box = match matches.subcommand() { + ("sync", Some(matches)) => Box::new(sync::Command::new(matches)), + ("sql", Some(matches)) => Box::new(sql::Command::new(matches)), + ("recommend", Some(matches)) => { + Box::new(recommend::Command::new(matches)?) + } + + (name, Some(_)) => bail!("unknown subcommand: {}", name), + (_, None) => { + let mut stderr = std::io::stderr(); + app.write_long_help(&mut stderr)?; + eprintln!(""); + bail!("no subcommand given") + } + }; + + Ok(command) +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs deleted file mode 100644 index e793b55..0000000 --- a/src/cmd/mod.rs +++ /dev/null @@ -1,45 +0,0 @@ -mod recommend; -mod sql; -mod sync; - -const _DUMMY_DEPENDENCY: &str = include_str!("../../Cargo.toml"); - -trait Command { - fn run(&self) -> failure::Fallible<()>; -} - -pub fn run() -> failure::Fallible<()> { - get_command()?.run() -} - -fn get_command() -> failure::Fallible> { - let subcommands = vec![ - sync::subcommand(), - sql::subcommand(), - recommend::subcommand(), - ]; - let mut app = app_from_crate!().subcommands( - subcommands - .into_iter() - .map(|s| s.setting(clap::AppSettings::DisableVersion)), - ); - let matches = app.clone().get_matches(); - - let command: Box = match matches.subcommand() { - ("sync", Some(matches)) => Box::new(sync::Command::new(matches)), - ("sql", Some(matches)) => Box::new(sql::Command::new(matches)), - ("recommend", Some(matches)) => { - Box::new(recommend::Command::new(matches)?) - } - - (name, Some(_)) => bail!("unknown subcommand: {}", name), - (_, None) => { - let mut stderr = std::io::stderr(); - app.write_long_help(&mut stderr)?; - eprintln!(""); - bail!("no subcommand given") - } - }; - - Ok(command) -} diff --git a/src/cmd/recommend.rs b/src/cmd/recommend.rs index 1d8eecc..413dc5f 100644 --- a/src/cmd/recommend.rs +++ b/src/cmd/recommend.rs @@ -1,5 +1,5 @@ -use db; -use util; +use crate::db; +use crate::util; use clap; diff --git a/src/cmd/sql.rs b/src/cmd/sql.rs index bf4b768..5abc5ef 100644 --- a/src/cmd/sql.rs +++ b/src/cmd/sql.rs @@ -1,5 +1,5 @@ -use db; -use util; +use crate::db; +use crate::util; use clap; diff --git a/src/cmd/sync.rs b/src/cmd/sync.rs index 2d5b25b..88fe95c 100644 --- a/src/cmd/sync.rs +++ b/src/cmd/sync.rs @@ -1,6 +1,6 @@ -use db; -use lastfm; -use util; +use crate::db; +use crate::lastfm; +use crate::util; use clap; diff --git a/src/db.rs b/src/db.rs index 4afa3df..15b15f5 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,4 +1,4 @@ -use lastfm; +use crate::lastfm; use failure::Fail; 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()) +} diff --git a/src/lastfm/api_types.rs b/src/lastfm/api_types.rs index 148b17f..5a87956 100644 --- a/src/lastfm/api_types.rs +++ b/src/lastfm/api_types.rs @@ -1,6 +1,8 @@ #![allow(non_camel_case_types)] #![allow(non_snake_case)] +use serde_derive::Deserialize; + #[derive(Deserialize)] pub struct track_artist { #[serde(rename = "#text")] diff --git a/src/lastfm/mod.rs b/src/lastfm/mod.rs deleted file mode 100644 index 0704e5a..0000000 --- a/src/lastfm/mod.rs +++ /dev/null @@ -1,187 +0,0 @@ -use 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()) -} diff --git a/src/main.rs b/src/main.rs index 8432184..d667eb1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,3 @@ -#[macro_use] -extern crate clap; -extern crate directories; -#[macro_use] -extern crate failure; -extern crate indicatif; -extern crate reqwest; -extern crate rpassword; -extern crate rusqlite; -extern crate serde; -extern crate serde_json; -#[macro_use] -extern crate serde_derive; - mod cmd; mod db; mod lastfm; diff --git a/src/util.rs b/src/util.rs index 0e99fc8..8e2444e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,5 @@ +use failure::format_err; + pub fn program_name() -> failure::Fallible { let program = std::env::args() .next() -- cgit v1.2.3-54-g00ecf