summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.rustfmt.toml1
-rw-r--r--src/cmd/mod.rs17
-rw-r--r--src/cmd/recommend.rs53
-rw-r--r--src/cmd/sql.rs9
-rw-r--r--src/cmd/sync.rs7
-rw-r--r--src/db.rs144
-rw-r--r--src/lastfm/api_types.rs1
-rw-r--r--src/lastfm/mod.rs101
-rw-r--r--src/main.rs11
-rw-r--r--src/util.rs7
10 files changed, 187 insertions, 164 deletions
diff --git a/.rustfmt.toml b/.rustfmt.toml
new file mode 100644
index 0000000..bcad605
--- /dev/null
+++ b/.rustfmt.toml
@@ -0,0 +1 @@
+max_width = 78
diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs
index 021bc48..68ae2fb 100644
--- a/src/cmd/mod.rs
+++ b/src/cmd/mod.rs
@@ -1,6 +1,6 @@
+mod recommend;
mod sql;
mod sync;
-mod recommend;
const _DUMMY_DEPENDENCY: &'static str = include_str!("../../Cargo.toml");
@@ -18,16 +18,19 @@ fn get_command() -> failure::Fallible<Box<Command>> {
sql::subcommand(),
recommend::subcommand(),
];
- let mut app = app_from_crate!()
- .subcommands(subcommands.into_iter().map(|s| {
- s.setting(clap::AppSettings::DisableVersion)
- }));
+ 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<Command> = 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)?),
+ ("recommend", Some(matches)) => {
+ Box::new(recommend::Command::new(matches)?)
+ }
(name, Some(_)) => bail!("unknown subcommand: {}", name),
(_, None) => {
@@ -35,7 +38,7 @@ fn get_command() -> failure::Fallible<Box<Command>> {
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 2f613fa..1d8eecc 100644
--- a/src/cmd/recommend.rs
+++ b/src/cmd/recommend.rs
@@ -19,40 +19,48 @@ pub fn subcommand<'a, 'b>() -> clap::App<'a, 'b> {
.arg(
clap::Arg::with_name("count")
.default_value("20")
- .help("number of results to return")
+ .help("number of results to return"),
)
.arg(
clap::Arg::with_name("random")
.long("random")
- .help("picks randomly instead of by weight")
+ .help("picks randomly instead of by weight"),
)
.arg(
clap::Arg::with_name("album")
.long("album")
- .help("also choose a random album by the chosen artists")
+ .help("also choose a random album by the chosen artists"),
)
.arg(
clap::Arg::with_name("include")
.long("include")
.default_value("yearly")
- .possible_values(&["all", "yearly", "monthly", "weekly"])
+ .possible_values(&["all", "yearly", "monthly", "weekly"]),
)
.arg(
clap::Arg::with_name("exclude")
.long("exclude")
.default_value("weekly")
- .possible_values(&["all", "yearly", "monthly", "weekly", "none"])
+ .possible_values(&[
+ "all", "yearly", "monthly", "weekly", "none",
+ ]),
)
}
impl Command {
- pub fn new<'a>(matches: &clap::ArgMatches<'a>) -> failure::Fallible<Command> {
+ pub fn new<'a>(
+ matches: &clap::ArgMatches<'a>,
+ ) -> failure::Fallible<Command> {
Ok(Command {
count: matches.value_of("count").unwrap().parse()?,
random: matches.is_present("random"),
album: matches.is_present("album"),
- include: db::parse_timewindow(matches.value_of("include").unwrap()),
- exclude: db::parse_timewindow(matches.value_of("exclude").unwrap()),
+ include: db::parse_timewindow(
+ matches.value_of("include").unwrap(),
+ ),
+ exclude: db::parse_timewindow(
+ matches.value_of("exclude").unwrap(),
+ ),
db: db::DB::new(&util::db_path()?)?,
})
@@ -65,21 +73,24 @@ impl super::Command for Command {
self.count,
self.random,
self.include,
- self.exclude
+ self.exclude,
)?;
if self.album {
- artists = artists.iter().map(|artist| {
- Ok(format!(
- "{} - {}",
- artist,
- self.db.recommend_album(
- &artist,
- self.random,
- self.include,
- self.exclude
- )?
- ))
- }).collect::<failure::Fallible<Vec<String>>>()?;
+ artists = artists
+ .iter()
+ .map(|artist| {
+ Ok(format!(
+ "{} - {}",
+ artist,
+ self.db.recommend_album(
+ &artist,
+ self.random,
+ self.include,
+ self.exclude
+ )?
+ ))
+ })
+ .collect::<failure::Fallible<Vec<String>>>()?;
}
for line in artists {
println!("{}", line);
diff --git a/src/cmd/sql.rs b/src/cmd/sql.rs
index 81fdaef..404c97e 100644
--- a/src/cmd/sql.rs
+++ b/src/cmd/sql.rs
@@ -14,12 +14,12 @@ pub fn subcommand<'a, 'b>() -> clap::App<'a, 'b> {
.arg(
clap::Arg::with_name("query")
.required(true)
- .help("query to run")
+ .help("query to run"),
)
.arg(
clap::Arg::with_name("tsv")
.long("tsv")
- .help("format output as tsv")
+ .help("format output as tsv"),
)
}
@@ -47,8 +47,7 @@ impl super::Command for Command {
if self.tsv {
print_tsv(&rows);
- }
- else {
+ } else {
print_table(&cols, &rows);
}
@@ -100,7 +99,7 @@ fn print_row(widths: &[usize], row: &[String]) {
let fixed_width_row: Vec<String> = row
.iter()
.zip(widths.iter())
- .map(|(s, width)| format!("{:width$}", s, width=width))
+ .map(|(s, width)| format!("{:width$}", s, width = width))
.collect();
println!("{}", &fixed_width_row.join(" | "));
}
diff --git a/src/cmd/sync.rs b/src/cmd/sync.rs
index 99e39f7..b6019c4 100644
--- a/src/cmd/sync.rs
+++ b/src/cmd/sync.rs
@@ -14,7 +14,7 @@ pub fn subcommand<'a, 'b>() -> clap::App<'a, 'b> {
.arg(
clap::Arg::with_name("username")
.required(true)
- .help("last.fm username to fetch tracks for")
+ .help("last.fm username to fetch tracks for"),
)
}
@@ -39,7 +39,10 @@ impl super::Command for Command {
bar.set_style(
indicatif::ProgressStyle::default_bar()
.progress_chars("=> ")
- .template("Downloading {pos}/{len} tracks...\n{percent:>3}% [{wide_bar}] {eta:5}")
+ .template(
+ "Downloading {pos}/{len} tracks...\n\
+ {percent:>3}% [{wide_bar}] {eta:5}",
+ ),
);
db.insert_tracks(bar.wrap_iter(lastfm.tracks(from)))?;
diff --git a/src/db.rs b/src/db.rs
index c428da8..27737cb 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -39,24 +39,22 @@ pub struct DB {
impl DB {
pub fn new<P: AsRef<std::path::Path>>(path: &P) -> failure::Fallible<DB> {
let conn = if path.as_ref().exists() {
- rusqlite::Connection::open(path)
- .map_err(|e| {
- let msg = format!(
- "couldn't open db at {}",
- path.as_ref().display()
- );
- e.context(msg)
- })?
- }
- else {
+ rusqlite::Connection::open(path).map_err(|e| {
+ let msg = format!(
+ "couldn't open db at {}",
+ path.as_ref().display()
+ );
+ e.context(msg)
+ })?
+ } else {
Self::create(path)?
};
- return Ok(DB { conn })
+ return Ok(DB { conn });
}
fn create<P: AsRef<std::path::Path>>(
- path: &P
+ path: &P,
) -> failure::Fallible<rusqlite::Connection> {
eprintln!(
"Initializing database at {}",
@@ -65,49 +63,50 @@ impl DB {
if let Some(parent) = path.as_ref().parent() {
std::fs::create_dir_all(parent)?;
- let conn = rusqlite::Connection::open(path)
- .map_err(|e| {
- let msg = format!(
- "couldn't create db at {}",
- path.as_ref().display()
- );
- e.context(msg)
- })?;
+ let conn = rusqlite::Connection::open(path).map_err(|e| {
+ let msg = format!(
+ "couldn't create db at {}",
+ path.as_ref().display()
+ );
+ e.context(msg)
+ })?;
conn.execute(SCHEMA, rusqlite::NO_PARAMS)
.map_err(|e| e.context("failed to execute schema"))?;
Ok(conn)
- }
- else {
+ } else {
unimplemented!();
}
}
pub fn most_recent_timestamp(&self) -> failure::Fallible<Option<i64>> {
- Ok(self.conn.query_row(
- "SELECT timestamp FROM tracks ORDER BY timestamp DESC LIMIT 1",
- rusqlite::NO_PARAMS,
- |row| Some(row.get(0))
- ).or_else(|e| {
- match e {
+ Ok(self
+ .conn
+ .query_row(
+ "SELECT timestamp FROM tracks ORDER BY timestamp DESC LIMIT 1",
+ rusqlite::NO_PARAMS,
+ |row| Some(row.get(0)),
+ )
+ .or_else(|e| match e {
rusqlite::Error::QueryReturnedNoRows => Ok(None),
_ => Err(e),
- }
- })?)
+ })?)
}
pub fn insert_tracks(
&self,
- tracks: impl Iterator<Item=lastfm::Track>,
+ tracks: impl Iterator<Item = lastfm::Track>,
) -> failure::Fallible<()> {
- let mut sth = self.conn.prepare("INSERT INTO tracks VALUES (?, ?, ?, ?)")?;
+ let mut sth = self
+ .conn
+ .prepare("INSERT INTO tracks VALUES (?, ?, ?, ?)")?;
for track in tracks {
- sth.execute(
- &[
+ sth.execute(&[
&track.artist as &rusqlite::types::ToSql,
&track.album,
&track.name,
&track.timestamp,
- ]).map(|_| ())?;
+ ])
+ .map(|_| ())?;
}
Ok(())
}
@@ -115,19 +114,16 @@ impl DB {
pub fn query<F: FnMut(&rusqlite::Row)>(
&self,
query: &str,
- mut f: F
+ mut f: F,
) -> failure::Fallible<Vec<String>> {
let mut sth = self.conn.prepare(query)?;
- let cols = sth.column_names()
- .iter()
- .map(|s| s.to_string())
- .collect();
+ let cols = sth.column_names().iter().map(|s| s.to_string()).collect();
- let rows = sth.query_and_then(
- rusqlite::NO_PARAMS,
- |row| { f(row); Ok(()) },
- )?;
+ let rows = sth.query_and_then(rusqlite::NO_PARAMS, |row| {
+ f(row);
+ Ok(())
+ })?;
// this call to collect() forces it to actually consume the iterator
// (and therefore call the callbacks). what i really want here is for
// there to be a query_for_each or something like that, but the weird
@@ -146,35 +142,44 @@ impl DB {
exclude: TimeWindow,
) -> failure::Fallible<Vec<String>> {
let exclude = if exclude != TimeWindow::None {
- format!("
+ format!(
+ "
WHERE artist NOT IN (
SELECT DISTINCT(artist)
FROM {}
)
- ", timewindow_table(&exclude))
- }
- else {
+ ",
+ timewindow_table(&exclude)
+ )
+ } else {
"".to_string()
};
let order = if random {
"ORDER BY random()"
- }
- else {
+ } else {
"ORDER BY count(artist) * (strftime('%s') - max(timestamp)) DESC"
};
- let sql = format!("
+ let sql = format!(
+ "
SELECT artist
FROM {}
{}
GROUP BY artist
{}
LIMIT {}
- ", timewindow_table(&include), exclude, order, count);
+ ",
+ timewindow_table(&include),
+ exclude,
+ order,
+ count
+ );
let mut sth = self.conn.prepare(&sql)?;
- let artists = sth.query_and_then(rusqlite::NO_PARAMS, |row| {
- Ok(row.get_checked(0)?)
- })?.collect::<failure::Fallible<Vec<String>>>()?;
+ let artists = sth
+ .query_and_then(rusqlite::NO_PARAMS, |row| {
+ Ok(row.get_checked(0)?)
+ })?
+ .collect::<failure::Fallible<Vec<String>>>()?;
Ok(artists)
}
@@ -189,25 +194,27 @@ impl DB {
let mut params = vec![artist];
let exclude = if exclude != TimeWindow::None {
params.push(artist);
- format!("
+ format!(
+ "
AND album NOT IN (
SELECT DISTINCT(album)
FROM {}
WHERE artist = ?
)
- ", timewindow_table(&exclude))
- }
- else {
+ ",
+ timewindow_table(&exclude)
+ )
+ } else {
"".to_string()
};
let order = if random {
"ORDER BY random()"
- }
- else {
+ } else {
"ORDER BY count(album) * (strftime('%s') - max(timestamp)) DESC"
};
- let sql = format!("
+ let sql = format!(
+ "
SELECT album
FROM {}
WHERE artist = ?
@@ -215,11 +222,16 @@ impl DB {
GROUP BY album
{}
LIMIT 1
- ", timewindow_table(&include), exclude, order);
+ ",
+ timewindow_table(&include),
+ exclude,
+ order
+ );
let mut sth = self.conn.prepare(&sql)?;
- let artists = sth.query_row::<failure::Fallible<String>, _, _>(&params, |row| {
- Ok(row.get_checked(0)?)
- })??;
+ let artists = sth.query_row::<failure::Fallible<String>, _, _>(
+ &params,
+ |row| Ok(row.get_checked(0)?),
+ )??;
Ok(artists)
}
diff --git a/src/lastfm/api_types.rs b/src/lastfm/api_types.rs
index a7c3712..148b17f 100644
--- a/src/lastfm/api_types.rs
+++ b/src/lastfm/api_types.rs
@@ -43,4 +43,3 @@ pub struct recent_tracks_recenttracks {
pub struct recent_tracks {
pub recenttracks: recent_tracks_recenttracks,
}
-
diff --git a/src/lastfm/mod.rs b/src/lastfm/mod.rs
index 081038a..ed6e81b 100644
--- a/src/lastfm/mod.rs
+++ b/src/lastfm/mod.rs
@@ -43,23 +43,20 @@ impl<'a> Tracks<'a> {
}
let page = self.page.unwrap();
if page < 1 {
- return Ok(())
+ 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 = 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 {
+ } else {
req
};
@@ -67,9 +64,11 @@ impl<'a> Tracks<'a> {
if res.status().is_success() {
let data: api_types::recent_tracks = res.json()?;
- self.buf = data.recenttracks.track
+ self.buf = data
+ .recenttracks
+ .track
.iter()
- .filter(|t| { t.date.is_some() })
+ .filter(|t| t.date.is_some())
.map(|t| {
Ok(Track {
artist: t.artist.text.clone(),
@@ -81,8 +80,7 @@ impl<'a> Tracks<'a> {
.collect::<failure::Fallible<Vec<Track>>>()?;
self.page = Some(page - 1);
Ok(())
- }
- else {
+ } else {
Err(failure::err_msg(res.status().as_str().to_string()))
}
}
@@ -129,19 +127,16 @@ impl LastFMClient {
&self,
from: Option<i64>,
) -> failure::Fallible<api_types::recent_tracks> {
- 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 = 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 {
+ } else {
req
};
@@ -150,8 +145,7 @@ impl LastFMClient {
if res.status().is_success() {
let data: api_types::recent_tracks = res.json()?;
Ok(data)
- }
- else {
+ } else {
Err(failure::err_msg(res.status().as_str().to_string()))
}
}
@@ -162,32 +156,31 @@ fn find_api_key() -> failure::Fallible<String> {
.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 {}): ",
+ 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()))
- })?;
+ 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 4f9ce07..8432184 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -19,13 +19,12 @@ mod util;
fn main() {
match cmd::run() {
- Ok(_) => {},
+ Ok(_) => {}
Err(e) => {
- let name = util::program_name()
- .unwrap_or_else(|e| {
- eprintln!("{}", e);
- "?".to_string()
- });
+ let name = util::program_name().unwrap_or_else(|e| {
+ eprintln!("{}", e);
+ "?".to_string()
+ });
let cause = e
.iter_chain()
.fold(String::new(), |acc, x| acc + ": " + &format!("{}", x));
diff --git a/src/util.rs b/src/util.rs
index 3e95981..0e99fc8 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -3,7 +3,8 @@ pub fn program_name() -> failure::Fallible<String> {
.next()
.ok_or_else(|| format_err!("no program name found"))?;
let path = std::path::Path::new(&program);
- let filename = path.file_name()
+ let filename = path
+ .file_name()
.ok_or_else(|| format_err!("invalid filename found"))?
.to_string_lossy()
.to_string();
@@ -19,7 +20,9 @@ pub fn db_path() -> failure::Fallible<std::path::PathBuf> {
pub fn api_key_path() -> failure::Fallible<std::path::PathBuf> {
Ok(directories::ProjectDirs::from("", "", "lastfm-query")
- .ok_or_else(|| failure::err_msg("couldn't determine config directory"))?
+ .ok_or_else(|| {
+ failure::err_msg("couldn't determine config directory")
+ })?
.config_dir()
.join("lastfm-api-key"))
}