#![deny(warnings, clippy::all)]
use apodclient::*;
use chrono::{Local, NaiveDate};
use clap::{
app_from_crate, crate_authors, crate_description, crate_name, crate_version, value_t_or_exit,
AppSettings, Arg, SubCommand,
};
use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::str::FromStr;
#[derive(Debug)]
struct Settings<'a> {
directory: &'a Path,
}
fn apod_file_for_date(directory: &Path, date: NaiveDate) -> std::io::Result<Option<PathBuf>> {
let date_marker = format!("{}-", date.format("%Y-%m-%d"));
match std::fs::read_dir(directory) {
Ok(entries) => {
for entry_result in entries {
let entry = entry_result?;
let filename = entry.file_name();
if filename
.as_os_str()
.to_string_lossy()
.starts_with(&date_marker)
{
return Ok(Some(entry.path()));
}
}
Ok(None)
}
Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn download_filename(apod: &APOD) -> String {
let basename = apod
.download_url()
.path_segments()
.and_then(Iterator::last)
.unwrap();
format!("{}-{}", apod.date.format("%Y-%m-%d"), basename)
}
fn cmd_download(settings: Settings, date: NaiveDate) -> Result<(), Box<Error>> {
if let Some(file) = apod_file_for_date(settings.directory, date)? {
println!("Already downloaded at {}", file.to_string_lossy());
Ok(())
} else {
let client = make_client();
let apod = client.get(date)?;
let client = make_client();
if apod.media_type == "image" {
std::fs::create_dir_all(settings.directory)?;
let target = settings.directory.join(download_filename(&apod));
let mut sink = File::create(&target)?;
client.copy_to(&apod, &mut sink)?;
let meta_target = target.with_extension("md");
let mut meta_sink = File::create(meta_target)?;
writeln!(
&mut meta_sink,
"# {}\n\nCopyright {}\n\n{}",
apod.title,
apod.copyright
.as_ref()
.map_or("public domain", String::as_str),
apod.explanation
)?;
println!(
"APOD {} downloaded to {}",
apod.date,
target.to_string_lossy()
);
Ok(())
} else {
use std::io::{Error, ErrorKind};
Err(Error::new(
ErrorKind::InvalidData,
format!(
"Media type {} of APOD at {} not supported",
apod.media_type, apod.date
),
)
.into())
}
}
}
fn cmd_title(_settings: Settings, date: NaiveDate) -> Result<(), Box<Error>> {
let apod = make_client().get(date)?;
println!("APOD {}: {}", apod.date, apod.title);
Ok(())
}
fn cmd_browse(_settings: Settings, date: NaiveDate) -> Result<(), Box<Error>> {
open::that(apod_url(date).as_str())?;
Ok(())
}
fn make_client() -> APODClient {
APODClient::new("DEMO_KEY".to_string())
}
fn default_apod_directory() -> Option<PathBuf> {
dirs::picture_dir()
.or_else(dirs::home_dir)
.map(|d| d.join("APOD"))
}
enum DateSpec {
Today,
Day(NaiveDate),
}
impl From<DateSpec> for NaiveDate {
fn from(spec: DateSpec) -> Self {
match spec {
DateSpec::Today => Local::today().naive_utc(),
DateSpec::Day(date) => date,
}
}
}
impl FromStr for DateSpec {
type Err = chrono::ParseError;
fn from_str(s: &str) -> Result<DateSpec, Self::Err> {
match s {
"today" => Ok(DateSpec::Today),
_ => NaiveDate::from_str(s).map(DateSpec::Day),
}
}
}
fn main() -> Result<(), Box<Error>> {
let default_dir = default_apod_directory().unwrap().into_os_string();
let date_arg = Arg::with_name("date")
.help("The date to download, in ISO format")
.value_name("YYYY-MM-DD")
.required(true)
.default_value("today");
let matches = app_from_crate!()
.setting(AppSettings::SubcommandRequiredElseHelp)
.arg(
Arg::with_name("directory")
.short("d")
.long("directory")
.help("Directory to write images to")
.value_name("DIRECTORY")
.empty_values(false)
.default_value_os(&default_dir)
.takes_value(true),
)
.subcommand(
SubCommand::with_name("download")
.about("Download the image of the given day")
.arg(date_arg.clone()),
)
.subcommand(
SubCommand::with_name("title")
.about("Show the title of the image at the given day")
.arg(date_arg.clone()),
)
.subcommand(
SubCommand::with_name("browse")
.about("Open the APOD site for the given day in your browser")
.arg(date_arg),
)
.get_matches();
let settings = Settings {
directory: Path::new(matches.value_of("directory").unwrap()),
};
match matches.subcommand() {
("download", Some(dl_matches)) => {
let datespec = value_t_or_exit!(dl_matches.value_of("date"), DateSpec);
cmd_download(settings, datespec.into())
}
("title", Some(title_matches)) => {
let datespec = value_t_or_exit!(title_matches.value_of("date"), DateSpec);
cmd_title(settings, datespec.into())
}
("browse", Some(browse_matches)) => {
let datespec = value_t_or_exit!(browse_matches.value_of("date"), DateSpec);
cmd_browse(settings, datespec.into())
}
(s, _) => panic!("Unexpected subcommand {}", s),
}
}