diff options
| -rw-r--r-- | CHANGELOG | 3 | ||||
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | src/actions.rs | 338 | ||||
| -rw-r--r-- | src/main.rs | 96 | ||||
| -rw-r--r-- | src/structs.rs | 168 | ||||
| -rw-r--r-- | src/utils.rs | 121 | 
6 files changed, 391 insertions, 338 deletions
| @@ -1,3 +1,6 @@ +0.5.4 +- Improve error handling throughout the application (using error-chain) +  0.5.0  - Fix downloading all episodes of podcast not working if folder didn't exist  - Confirm before downloading all episodes of a podcast @@ -1,6 +1,6 @@  [package]  name = "podcast" -version = "0.5.3" +version = "0.5.4"  authors = ["Nathan Jaremko <njaremko@gmail.com>"]  description = "A command line podcast manager"  license = "GPL-3.0" @@ -20,6 +20,7 @@ name = "podcast"  [dependencies]  chrono = { version = "0.4", features = ["serde"] }  clap = "2.28" +error-chain = "0.11"  rayon = "0.9"  regex = "0.2"  reqwest = "0.8" diff --git a/src/actions.rs b/src/actions.rs index 3c1a6e9..8adea4c 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -6,25 +6,31 @@ use std::fs::{self, DirBuilder, File};  use std::io::{self, BufReader, Read, Write};  use std::process::Command; +use errors::*;  use rayon::prelude::*;  use regex::Regex;  use reqwest;  use rss::Channel; +use std::path::PathBuf;  use toml; -pub fn list_episodes(search: &str) { +pub fn list_episodes(search: &str) -> Result<()> {      let stdout = io::stdout();      let mut handle = stdout.lock(); -    let re = Regex::new(&format!("(?i){}", &search)).expect("Failed to parse regex"); -    let mut path = get_podcast_dir(); +    let re = Regex::new(&format!("(?i){}", &search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?; +    let mut path = get_podcast_dir()?;      path.push(".rss"); -    DirBuilder::new().recursive(true).create(&path).unwrap(); -    for entry in fs::read_dir(&path).unwrap() { -        let entry = entry.unwrap(); +    DirBuilder::new() +        .recursive(true) +        .create(&path) +        .chain_err(|| UNABLE_TO_CREATE_DIRECTORY)?; +    for entry in fs::read_dir(&path).chain_err(|| UNABLE_TO_READ_DIRECTORY)? { +        let entry = entry.chain_err(|| UNABLE_TO_READ_ENTRY)?;          if re.is_match(&entry.file_name().into_string().unwrap()) { -            let file = File::open(&entry.path()).unwrap(); -            let channel = Channel::read_from(BufReader::new(file)).unwrap(); +            let file = File::open(&entry.path()).chain_err(|| UNABLE_TO_OPEN_FILE)?; +            let channel = Channel::read_from(BufReader::new(file)) +                .chain_err(|| UNABLE_TO_CREATE_CHANNEL_FROM_FILE)?;              let podcast = Podcast::from(channel);              let episodes = podcast.episodes();              for (num, ep) in episodes.iter().enumerate() { @@ -32,200 +38,217 @@ pub fn list_episodes(search: &str) {                      &mut handle,                      "({}) {}\n",                      episodes.len() - num, -                    ep.title().unwrap() -                ).is_ok(); +                    ep.title().chain_err(|| "unable to retrieve episode title")? +                ).chain_err(|| "unable to write to stdout")?              } -            return; +            return Ok(());          }      } +    Ok(())  } -pub fn subscribe_rss(url: &str) { +pub fn subscribe_rss(url: &str) -> Result<Channel> {      println!("Downloading RSS feed..."); -    if let Err(err) = download_rss_feed(url) { -        eprintln!("Error: {}", err); -    } +    download_rss_feed(url)  } -pub fn download_rss(config: &Config, url: &str) { +pub fn download_rss(config: &Config, url: &str) -> Result<()> {      println!("Downloading episode(s)..."); -    match download_rss_feed(url) { -        Ok(channel) => { -            let download_limit = config.auto_download_limit as usize; -            if download_limit > 0 { -                let podcast = Podcast::from(channel); -                let episodes = podcast.episodes(); -                episodes[..download_limit].par_iter().for_each(|ep| { -                    if let Err(err) = ep.download(podcast.title()) { -                        eprintln!("Error downloading {}: {}", podcast.title(), err); -                    } -                }); +    let channel = download_rss_feed(url)?; +    let download_limit = config.auto_download_limit as usize; +    if download_limit > 0 { +        let podcast = Podcast::from(channel); +        let episodes = podcast.episodes(); +        episodes[..download_limit].par_iter().for_each(|ep| { +            if let Err(err) = ep.download(podcast.title()) { +                eprintln!("Error downloading {}: {}", podcast.title(), err);              } -        } -        Err(err) => eprintln!("Error: {}", err), +        });      } +    Ok(()) +} + +pub fn update_subscription(sub: &mut Subscription) -> Result<()> { +    let mut path: PathBuf = get_podcast_dir()?; +    path.push(&sub.title); +    DirBuilder::new() +        .recursive(true) +        .create(&path) +        .chain_err(|| UNABLE_TO_CREATE_DIRECTORY)?; + +    let mut titles = HashSet::new(); +    for entry in fs::read_dir(&path).chain_err(|| UNABLE_TO_READ_DIRECTORY)? { +        let unwrapped_entry = &entry.chain_err(|| UNABLE_TO_READ_ENTRY)?; +        titles.insert(trim_extension(&unwrapped_entry +            .file_name() +            .into_string() +            .unwrap())); +    } + +    let mut resp = reqwest::get(&sub.url).chain_err(|| UNABLE_TO_GET_HTTP_RESPONSE)?; +    let mut content: Vec<u8> = Vec::new(); +    resp.read_to_end(&mut content) +        .chain_err(|| UNABLE_TO_READ_RESPONSE_TO_END)?; +    let podcast = Podcast::from(Channel::read_from(BufReader::new(&content[..])) +        .chain_err(|| UNABLE_TO_CREATE_CHANNEL_FROM_RESPONSE)?); +    path = get_podcast_dir()?; +    path.push(".rss"); + +    let mut filename = String::from(podcast.title()); +    filename.push_str(".xml"); +    path.push(&filename); +    let mut file = File::create(&path).unwrap(); +    file.write_all(&content).unwrap(); + +    if podcast.episodes().len() > sub.num_episodes { +        podcast.episodes()[..podcast.episodes().len() - sub.num_episodes] +            .par_iter() +            .for_each(|ep: &Episode| { +                if let Err(err) = ep.download(podcast.title()) { +                    eprintln!("Error downloading {}: {}", podcast.title(), err); +                } +            }); +    } +    sub.num_episodes = podcast.episodes().len(); +    Ok(())  }  pub fn update_rss(state: &mut State) {      println!("Checking for new episodes..."); -    state.subscriptions.par_iter_mut().for_each(|sub| { -        let mut path = get_podcast_dir(); -        path.push(&sub.title); -        DirBuilder::new().recursive(true).create(&path).unwrap(); - -        let mut titles = HashSet::new(); -        for entry in fs::read_dir(&path).unwrap() { -            let entry = entry.unwrap(); -            titles.insert(trim_extension(&entry.file_name().into_string().unwrap())); -        } - -        let mut resp = reqwest::get(&sub.url).unwrap(); -        let mut content: Vec<u8> = Vec::new(); -        resp.read_to_end(&mut content).unwrap(); -        let podcast = Podcast::from(Channel::read_from(BufReader::new(&content[..])).unwrap()); -        path = get_podcast_dir(); -        path.push(".rss"); - -        let mut filename = String::from(podcast.title()); -        filename.push_str(".xml"); -        path.push(&filename); -        let mut file = File::create(&path).unwrap(); -        file.write_all(&content).unwrap(); - -        if podcast.episodes().len() > sub.num_episodes { -            podcast.episodes()[..podcast.episodes().len() - sub.num_episodes] -                .par_iter() -                .for_each(|ep| { -                    if let Err(err) = ep.download(podcast.title()) { -                        eprintln!("Error downloading {}: {}", podcast.title(), err); -                    } -                }); -        } -        sub.num_episodes = podcast.episodes().len(); -    }); +    let _result: Vec<Result<()>> = state +        .subscriptions +        .par_iter_mut() +        .map(|sub: &mut Subscription| update_subscription(sub)) +        .collect();  } -pub fn list_subscriptions(state: &State) { +pub fn list_subscriptions(state: &State) -> Result<()> {      let stdout = io::stdout();      let mut handle = stdout.lock();      for podcast in &state.subscriptions() { -        write!(&mut handle, "{}\n", &podcast.title).is_ok(); +        write!(&mut handle, "{}\n", &podcast.title).chain_err(|| "unable to write to stdout")?;      } +    Ok(())  } -pub fn download_range(state: &State, p_search: &str, e_search: &str) { -    let re_pod = Regex::new(&format!("(?i){}", &p_search)).expect("Failed to parse regex"); +pub fn download_range(state: &State, p_search: &str, e_search: &str) -> Result<()> { +    let re_pod = Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?;      for subscription in &state.subscriptions {          if re_pod.is_match(&subscription.title) { -            match Podcast::from_title(&subscription.title) { -                Ok(podcast) => match parse_download_episodes(e_search) { -                    Ok(episodes_to_download) => { -                        if let Err(err) = podcast.download_specific(&episodes_to_download) { -                            eprintln!("Error: {}", err); -                        } -                    } -                    Err(err) => eprintln!("Error: {}", err), -                }, -                Err(err) => eprintln!("Error: {}", err), -            } +            let podcast = Podcast::from_title(&subscription.title) +                .chain_err(|| UNABLE_TO_RETRIEVE_PODCAST_BY_TITLE)?; +            let episodes_to_download = parse_download_episodes(e_search) +                .chain_err(|| "unable to parse episodes to download")?; +            podcast +                .download_specific(&episodes_to_download) +                .chain_err(|| "unable to download episodes")?;          }      } +    Ok(())  } -pub fn download_episode(state: &State, p_search: &str, e_search: &str) { -    let re_pod = Regex::new(&format!("(?i){}", &p_search)).expect("Failed to parse regex"); -    let ep_num = e_search.parse::<usize>().unwrap(); +pub fn download_episode(state: &State, p_search: &str, e_search: &str) -> Result<()> { +    let re_pod = Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?; +    let ep_num = e_search +        .parse::<usize>() +        .chain_err(|| "unable to parse number")?;      for subscription in &state.subscriptions {          if re_pod.is_match(&subscription.title) { -            match Podcast::from_title(&subscription.title) { -                Ok(podcast) => { -                    let episodes = podcast.episodes(); -                    if let Err(err) = episodes[episodes.len() - ep_num].download(podcast.title()) { -                        eprintln!("{}", err); -                    } -                } -                Err(err) => eprintln!("Error: {}", err), -            } +            let podcast = Podcast::from_title(&subscription.title) +                .chain_err(|| UNABLE_TO_RETRIEVE_PODCAST_BY_TITLE)?; +            let episodes = podcast.episodes(); +            episodes[episodes.len() - ep_num] +                .download(podcast.title()) +                .chain_err(|| "unable to download episode")?;          }      } +    Ok(())  } -pub fn download_all(state: &State, p_search: &str) { -    let re_pod = Regex::new(&format!("(?i){}", &p_search)).expect("Failed to parse regex"); +pub fn download_all(state: &State, p_search: &str) -> Result<()> { +    let re_pod = Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?;      for subscription in &state.subscriptions {          if re_pod.is_match(&subscription.title) { -            match Podcast::from_title(&subscription.title) { -                Ok(podcast) => if let Err(err) = podcast.download() { -                    eprintln!("{}", err); -                }, -                Err(err) => eprintln!("Error: {}", err), -            } +            let podcast = Podcast::from_title(&subscription.title) +                .chain_err(|| UNABLE_TO_RETRIEVE_PODCAST_BY_TITLE)?; +            podcast +                .download() +                .chain_err(|| "unable to download podcast")?;          }      } +    Ok(())  } -pub fn play_latest(state: &State, p_search: &str) { -    let re_pod = Regex::new(&format!("(?i){}", &p_search)).expect("Failed to parse regex"); -    let mut path = get_xml_dir(); -    if let Err(err) = DirBuilder::new().recursive(true).create(&path) { -        eprintln!( -            "Couldn't create directory: {}\nReason: {}", -            path.to_str().unwrap(), -            err -        ); -        return; -    } +pub fn play_latest(state: &State, p_search: &str) -> Result<()> { +    let re_pod: Regex = +        Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?; +    let mut path: PathBuf = get_xml_dir()?; +    DirBuilder::new() +        .recursive(true) +        .create(&path) +        .chain_err(|| UNABLE_TO_CREATE_DIRECTORY)?;      for subscription in &state.subscriptions {          if re_pod.is_match(&subscription.title) { -            let mut filename = subscription.title.clone(); +            let mut filename: String = subscription.title.clone();              filename.push_str(".xml");              path.push(filename); -            let mut file = File::open(&path).unwrap(); +            let mut file: File = File::open(&path).chain_err(|| UNABLE_TO_OPEN_FILE)?;              let mut content: Vec<u8> = Vec::new(); -            file.read_to_end(&mut content).unwrap(); +            file.read_to_end(&mut content) +                .chain_err(|| "unable to read file to end")?; -            let podcast = Podcast::from(Channel::read_from(content.as_slice()).unwrap()); +            let podcast: Podcast = Podcast::from(Channel::read_from(content.as_slice()) +                .chain_err(|| UNABLE_TO_CREATE_CHANNEL_FROM_FILE)?);              let episodes = podcast.episodes();              let episode = episodes[0].clone(); -            filename = String::from(episode.title().unwrap()); -            filename.push_str(episode.extension().unwrap()); -            path = get_podcast_dir(); +            filename = String::from(episode +                .title() +                .chain_err(|| "unable to retrieve episode name")?); +            filename.push_str(episode +                .extension() +                .chain_err(|| "unable to retrieve episode extension")?); +            path = get_podcast_dir()?;              path.push(podcast.title());              path.push(filename);              if path.exists() { -                launch_player(path.to_str().unwrap()); +                launch_player(path.to_str() +                    .chain_err(|| "unable to convert path to &str")?)?;              } else { -                launch_player(episode.url().unwrap()); +                launch_player(episode +                    .url() +                    .chain_err(|| "unable to retrieve episode url")?)?;              } -            return; +            return Ok(());          }      } +    Ok(())  } -pub fn play_episode(state: &State, p_search: &str, ep_num_string: &str) { -    let re_pod = Regex::new(&format!("(?i){}", &p_search)).expect("Failed to parse regex"); -    let ep_num = ep_num_string.parse::<usize>().unwrap(); -    let mut path = get_xml_dir(); +pub fn play_episode(state: &State, p_search: &str, ep_num_string: &str) -> Result<()> { +    let re_pod: Regex = +        Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?; +    let ep_num: usize = ep_num_string.parse::<usize>().unwrap(); +    let mut path: PathBuf = get_xml_dir()?;      if let Err(err) = DirBuilder::new().recursive(true).create(&path) {          eprintln!(              "Couldn't create directory: {}\nReason: {}",              path.to_str().unwrap(),              err          ); -        return; +        return Ok(());      }      for subscription in &state.subscriptions {          if re_pod.is_match(&subscription.title) { -            let mut filename = subscription.title.clone(); +            let mut filename: String = subscription.title.clone();              filename.push_str(".xml");              path.push(filename); -            let mut file = File::open(&path).unwrap(); +            let mut file: File = File::open(&path).unwrap();              let mut content: Vec<u8> = Vec::new();              file.read_to_end(&mut content).unwrap(); @@ -235,46 +258,48 @@ pub fn play_episode(state: &State, p_search: &str, ep_num_string: &str) {              filename = String::from(episode.title().unwrap());              filename.push_str(episode.extension().unwrap()); -            path = get_podcast_dir(); +            path = get_podcast_dir()?;              path.push(podcast.title());              path.push(filename);              if path.exists() { -                launch_player(path.to_str().unwrap()); +                launch_player(path.to_str().chain_err(|| UNABLE_TO_CONVERT_TO_STR)?)?;              } else { -                launch_player(episode.url().unwrap()); +                launch_player(episode.url().chain_err(|| "unable to retrieve episode url")?)?;              } -            return; +            return Ok(());          }      } +    Ok(())  } -pub fn check_for_update(version: &str) { +pub fn check_for_update(version: &str) -> Result<()> {      println!("Checking for updates...");      let resp: String = reqwest::get(          "https://raw.githubusercontent.com/njaremko/podcast/master/Cargo.toml", -    ).unwrap() +    ).chain_err(|| UNABLE_TO_GET_HTTP_RESPONSE)?          .text() -        .unwrap(); +        .chain_err(|| "unable to convert response to text")?;      //println!("{}", resp); -    match resp.parse::<toml::Value>() { -        Ok(config) => { -            let latest = config["package"]["version"].as_str().unwrap(); -            if version != latest { -                println!("New version avaliable: {} -> {}", version, latest); -            } -        } -        Err(err) => eprintln!("{}", err), +    let config = resp.parse::<toml::Value>() +        .chain_err(|| "unable to parse toml")?; +    let latest = config["package"]["version"] +        .as_str() +        .chain_err(|| UNABLE_TO_CONVERT_TO_STR)?; +    if version != latest { +        println!("New version avaliable: {} -> {}", version, latest);      } +    Ok(())  } -fn launch_player(url: &str) { +fn launch_player(url: &str) -> Result<()> {      if launch_mpv(url).is_err() { -        launch_vlc(url) +        return launch_vlc(url);      } +    Ok(())  } -fn launch_mpv(url: &str) -> io::Result<()> { +fn launch_mpv(url: &str) -> Result<()> {      if let Err(err) = Command::new("mpv")          .args(&["--audio-display=no", "--ytdl=no", url])          .status() @@ -282,7 +307,6 @@ fn launch_mpv(url: &str) -> io::Result<()> {          match err.kind() {              io::ErrorKind::NotFound => {                  eprintln!("Couldn't open mpv\nTrying vlc..."); -                return Err(err);              }              _ => eprintln!("Error: {}", err),          } @@ -290,39 +314,33 @@ fn launch_mpv(url: &str) -> io::Result<()> {      Ok(())  } -fn launch_vlc(url: &str) { +fn launch_vlc(url: &str) -> Result<()> {      if let Err(err) = Command::new("vlc").args(&["-I ncurses", url]).status() {          match err.kind() {              io::ErrorKind::NotFound => { -                eprintln!("vlc not found in PATH\nAborting..."); +                eprintln!("Couldn't open vlc...aborting");              }              _ => eprintln!("Error: {}", err),          }      } +    Ok(())  } -pub fn remove_podcast(state: &mut State, p_search: &str) { +pub fn remove_podcast(state: &mut State, p_search: &str) -> Result<()> {      if p_search == "*" { -        match Podcast::delete_all() { -            Ok(_) => println!("Success"), -            Err(err) => eprintln!("Error: {}", err), -        } -        return; +        return Podcast::delete_all();      } -    let re_pod = Regex::new(&format!("(?i){}", &p_search)).expect("Failed to parse regex"); +    let re_pod = Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?;      for subscription in 0..state.subscriptions.len() {          let title = state.subscriptions[subscription].title.clone();          if re_pod.is_match(&title) {              state.subscriptions.remove(subscription); -            match Podcast::delete(&title) { -                Ok(_) => println!("Success"), -                Err(err) => eprintln!("Error: {}", err), -            } -            break; +            Podcast::delete(&title)?;          }      } +    Ok(())  }  pub fn print_completion(arg: &str) { diff --git a/src/main.rs b/src/main.rs index 68c3193..35498df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,9 @@ +#![recursion_limit = "1024"] +  extern crate chrono;  extern crate clap; +#[macro_use] +extern crate error_chain;  extern crate rayon;  extern crate regex;  extern crate reqwest; @@ -13,28 +17,24 @@ extern crate yaml_rust;  mod actions;  mod structs;  mod utils; +mod errors { +    // Create the Error, ErrorKind, ResultExt, and Result types +    error_chain!{} +}  use actions::*; -use utils::*; +use errors::*;  use structs::*; +use utils::*;  use clap::{App, Arg, SubCommand}; -const VERSION: &str = "0.5.3"; +const VERSION: &str = "0.5.4"; -fn main() { -    if let Err(err) = create_directories() { -        eprintln!("{}", err); -        return; -    } -    let mut state = match State::new(VERSION) { -        Ok(val) => val, -        Err(err) => { -            eprintln!("{}", err); -            return; -        } -    }; -    let config = Config::new(); +fn main() -> Result<()> { +    create_directories().chain_err(|| "unable to create directories")?; +    let mut state = State::new(VERSION).chain_err(|| "unable to load state")?; +    let config = Config::new()?;      let matches = App::new("podcast")          .version(VERSION)          .author("Nathan J. <njaremko@gmail.com>") @@ -129,65 +129,77 @@ fn main() {      match matches.subcommand_name() {          Some("download") => { -            let download_matches = matches.subcommand_matches("download").unwrap(); -            let podcast = download_matches.value_of("PODCAST").unwrap(); +            let download_matches = matches +                .subcommand_matches("download") +                .chain_err(|| "unable to find subcommand matches")?; +            let podcast = download_matches +                .value_of("PODCAST") +                .chain_err(|| "unable to find subcommand match")?;              match download_matches.value_of("EPISODE") {                  Some(ep) => if String::from(ep).contains(|c| c == '-' || c == ',') { -                    download_range(&state, podcast, ep) +                    download_range(&state, podcast, ep)?                  } else { -                    download_episode(&state, podcast, ep) +                    download_episode(&state, podcast, ep)?                  }, -                None => download_all(&state, podcast), +                None => download_all(&state, podcast)?,              }          }          Some("ls") | Some("list") => {              let list_matches = matches                  .subcommand_matches("ls")                  .or(matches.subcommand_matches("list")) -                .unwrap(); +                .chain_err(|| "unable to find subcommand matches")?;              match list_matches.value_of("PODCAST") { -                Some(regex) => list_episodes(regex), -                None => list_subscriptions(&state), +                Some(regex) => list_episodes(regex)?, +                None => list_subscriptions(&state)?,              }          }          Some("play") => { -            let play_matches = matches.subcommand_matches("play").unwrap(); -            let podcast = play_matches.value_of("PODCAST").unwrap(); +            let play_matches = matches +                .subcommand_matches("play") +                .chain_err(|| "unable to find subcommand matches")?; +            let podcast = play_matches +                .value_of("PODCAST") +                .chain_err(|| "unable to find subcommand match")?;              match play_matches.value_of("EPISODE") { -                Some(episode) => play_episode(&state, podcast, episode), -                None => play_latest(&state, podcast), +                Some(episode) => play_episode(&state, podcast, episode)?, +                None => play_latest(&state, podcast)?,              }          }          Some("subscribe") => { -            let subscribe_matches = matches.subcommand_matches("subscribe").unwrap(); -            let url = subscribe_matches.value_of("URL").unwrap(); -            state.subscribe(url); +            let subscribe_matches = matches +                .subcommand_matches("subscribe") +                .chain_err(|| "unable to find subcommand matches")?; +            let url = subscribe_matches +                .value_of("URL") +                .chain_err(|| "unable to find subcommand match")?; +            state.subscribe(url).chain_err(|| "unable to subscribe")?;              if subscribe_matches.occurrences_of("download") > 0 { -                download_rss(&config, url); +                download_rss(&config, url)?;              } else { -                subscribe_rss(url); +                subscribe_rss(url)?;              }          }          Some("search") => println!("This feature is coming soon..."),          Some("rm") => { -            let rm_matches = matches.subcommand_matches("rm").unwrap(); -            match rm_matches.value_of("PODCAST") { -                Some(regex) => remove_podcast(&mut state, regex), -                None => println!(), -            } +            let rm_matches = matches +                .subcommand_matches("rm") +                .chain_err(|| "unable to find subcommand matches")?; +            let regex = rm_matches.value_of("PODCAST").chain_err(|| "")?; +            remove_podcast(&mut state, regex)?          }          Some("completion") => { -            let matches = matches.subcommand_matches("completion").unwrap(); +            let matches = matches +                .subcommand_matches("completion") +                .chain_err(|| "unable to find subcommand matches")?;              match matches.value_of("SHELL") {                  Some(shell) => print_completion(shell),                  None => print_completion(""),              }          }          Some("refresh") => update_rss(&mut state), -        Some("update") => check_for_update(VERSION), +        Some("update") => check_for_update(VERSION)?,          _ => (),      } -    if let Err(err) = state.save() { -        eprintln!("{}", err); -    } +    state.save().chain_err(|| "unable to save state")  } diff --git a/src/structs.rs b/src/structs.rs index ac6832e..03a9932 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,14 +1,15 @@  use actions::*; +use errors::*;  use utils::*;  use std::collections::BTreeSet; -use std::fs::{self, remove_dir_all, remove_file, DirBuilder, File}; +use std::fs::{self, DirBuilder, File};  use std::io::{self, BufReader, Read, Write};  use chrono::prelude::*;  use rayon::prelude::*;  use reqwest; -use rss::{self, Channel, Item}; +use rss::{Channel, Item};  use serde_json;  use yaml_rust::YamlLoader; @@ -17,14 +18,18 @@ pub struct Config {  }  impl Config { -    pub fn new() -> Config { -        let mut path = get_podcast_dir(); +    pub fn new() -> Result<Config> { +        let mut path = get_podcast_dir()?;          let mut download_limit = 1;          path.push(".config");          if path.exists() {              let mut s = String::new(); -            File::open(&path).unwrap().read_to_string(&mut s).unwrap(); -            let config = YamlLoader::load_from_str(&s).unwrap(); +            File::open(&path) +                .chain_err(|| UNABLE_TO_OPEN_FILE)? +                .read_to_string(&mut s) +                .chain_err(|| UNABLE_TO_READ_FILE_TO_STRING)?; +            let config = +                YamlLoader::load_from_str(&s).chain_err(|| "unable to load yaml from string")?;              if !config.is_empty() {                  let doc = &config[0];                  if let Some(val) = doc["auto_download_limit"].as_i64() { @@ -32,12 +37,13 @@ impl Config {                  }              }          } else { -            let mut file = File::create(&path).unwrap(); -            file.write_all(b"auto_download_limit: 1").unwrap(); +            let mut file = File::create(&path).chain_err(|| UNABLE_TO_CREATE_FILE)?; +            file.write_all(b"auto_download_limit: 1") +                .chain_err(|| UNABLE_TO_WRITE_FILE)?;          } -        Config { +        Ok(Config {              auto_download_limit: download_limit, -        } +        })      }  } @@ -56,29 +62,27 @@ pub struct State {  }  impl State { -    pub fn new(version: &str) -> Result<State, String> { -        let mut path = get_podcast_dir(); +    pub fn new(version: &str) -> Result<State> { +        let mut path = get_podcast_dir()?;          path.push(".subscriptions");          if path.exists() {              let mut s = String::new(); -            let mut file = match File::open(&path) { -                Ok(val) => val, -                Err(err) => return Err(format!("{}", err)), -            }; -            if let Err(err) = file.read_to_string(&mut s) { -                return Err(format!("{}", err)); -            }; +            let mut file = File::open(&path).chain_err(|| UNABLE_TO_OPEN_FILE)?; +            file.read_to_string(&mut s) +                .chain_err(|| UNABLE_TO_READ_FILE_TO_STRING)?;              let mut state: State = match serde_json::from_str(&s) {                  Ok(val) => val,                  // This will happen if the struct has changed between versions                  Err(_) => { -                    let v: serde_json::Value = serde_json::from_str(&s).unwrap(); +                    let v: serde_json::Value = +                        serde_json::from_str(&s).chain_err(|| "unable to read json from string")?;                      State {                          version: String::from(version),                          last_run_time: Utc::now(),                          subscriptions: match serde_json::from_value(v["subscriptions"].clone()) {                              Ok(val) => val, -                            Err(_) => serde_json::from_value(v["subs"].clone()).unwrap(), +                            Err(_) => serde_json::from_value(v["subs"].clone()) +                                .chain_err(|| "unable to parse value from json")?,                          },                      }                  } @@ -90,12 +94,10 @@ impl State {                  .num_seconds() > 86400              {                  update_rss(&mut state); -                check_for_update(&state.version); +                check_for_update(&state.version)?;              }              state.last_run_time = Utc::now(); -            if let Err(err) = state.save() { -                eprintln!("{}", err); -            } +            state.save()?;              Ok(state)          } else {              Ok(State { @@ -106,7 +108,7 @@ impl State {          }      } -    pub fn subscribe(&mut self, url: &str) { +    pub fn subscribe(&mut self, url: &str) -> Result<()> {          let mut set = BTreeSet::new();          for sub in self.subscriptions() {              set.insert(sub.title); @@ -119,22 +121,21 @@ impl State {                  num_episodes: podcast.episodes().len(),              });          } -        if let Err(err) = self.save() { -            eprintln!("{}", err); -        } +        self.save()      }      pub fn subscriptions(&self) -> Vec<Subscription> {          self.subscriptions.clone()      } -    pub fn save(&self) -> Result<(), io::Error> { -        let mut path = get_podcast_dir(); +    pub fn save(&self) -> Result<()> { +        let mut path = get_podcast_dir()?;          path.push(".subscriptions.tmp"); -        let serialized = serde_json::to_string(self)?; -        let mut file = File::create(&path)?; -        file.write_all(serialized.as_bytes())?; -        fs::rename(&path, get_sub_file())?; +        let serialized = serde_json::to_string(self).chain_err(|| "unable to serialize state")?; +        let mut file = File::create(&path).chain_err(|| UNABLE_TO_CREATE_FILE)?; +        file.write_all(serialized.as_bytes()) +            .chain_err(|| UNABLE_TO_WRITE_FILE)?; +        fs::rename(&path, get_sub_file()?).chain_err(|| "unable to rename file")?;          Ok(())      }  } @@ -168,61 +169,57 @@ impl Podcast {      }      #[allow(dead_code)] -    pub fn from_url(url: &str) -> Result<Podcast, rss::Error> { -        match Channel::from_url(url) { -            Ok(val) => Ok(Podcast::from(val)), -            Err(err) => Err(err), -        } +    pub fn from_url(url: &str) -> Result<Podcast> { +        Ok( +            Podcast::from(Channel::from_url(url).chain_err(|| UNABLE_TO_CREATE_CHANNEL_FROM_RESPONSE)?), +        )      } -    pub fn from_title(title: &str) -> Result<Podcast, String> { -        let mut path = get_xml_dir(); +    pub fn from_title(title: &str) -> Result<Podcast> { +        let mut path = get_xml_dir()?;          let mut filename = String::from(title);          filename.push_str(".xml");          path.push(filename); -        match File::open(&path) { -            Ok(file) => match Channel::read_from(BufReader::new(file)) { -                Ok(podcast) => Ok(Podcast::from(podcast)), -                Err(err) => Err(format!("Error: {}", err)), -            }, -            Err(err) => Err(format!("Error: {}", err)), -        } +        let file = File::open(&path).chain_err(|| UNABLE_TO_OPEN_FILE)?; +        Ok(Podcast::from(Channel::read_from(BufReader::new(file)) +            .chain_err(|| UNABLE_TO_CREATE_CHANNEL_FROM_FILE)?))      } -    pub fn delete(title: &str) -> io::Result<()> { -        let mut path = get_xml_dir(); +    pub fn delete(title: &str) -> Result<()> { +        let mut path = get_xml_dir()?;          let mut filename = String::from(title);          filename.push_str(".xml");          path.push(filename); -        remove_file(path) +        fs::remove_file(path).chain_err(|| UNABLE_TO_REMOVE_FILE)      } -    pub fn delete_all() -> io::Result<()> { -        let path = get_xml_dir(); -        remove_dir_all(path) +    pub fn delete_all() -> Result<()> { +        let path = get_xml_dir()?; +        fs::remove_dir_all(path).chain_err(|| UNABLE_TO_READ_DIRECTORY)      }      pub fn episodes(&self) -> Vec<Episode> {          let mut result = Vec::new(); - -        let items = self.0.items().to_vec(); -        for item in items { +        for item in self.0.items().to_vec() {              result.push(Episode::from(item));          }          result      } -    pub fn download(&self) -> Result<(), io::Error> { +    pub fn download(&self) -> Result<()> {          print!("You are about to download all episodes (y/n): ");          io::stdout().flush().ok();          let mut input = String::new(); -        if io::stdin().read_line(&mut input).is_ok() && input.to_lowercase().trim() != "y" { +        io::stdin() +            .read_line(&mut input) +            .chain_err(|| "unable to read stdin")?; +        if input.to_lowercase().trim() != "y" {              return Ok(());          } -        let mut path = get_podcast_dir(); +        let mut path = get_podcast_dir()?;          path.push(self.title());          match already_downloaded(self.title()) { @@ -231,7 +228,7 @@ impl Podcast {                      if let Some(ep_title) = i.title() {                          if !downloaded.contains(ep_title) {                              if let Err(err) = i.download(self.title()) { -                                println!("{}", err); +                                eprintln!("{}", err);                              }                          }                      } @@ -240,7 +237,7 @@ impl Podcast {              Err(_) => {                  self.episodes().par_iter().for_each(|i| {                      if let Err(err) = i.download(self.title()) { -                        println!("{}", err); +                        eprintln!("{}", err);                      }                  });              } @@ -249,8 +246,8 @@ impl Podcast {          Ok(())      } -    pub fn download_specific(&self, episode_numbers: &[usize]) -> Result<(), io::Error> { -        let mut path = get_podcast_dir(); +    pub fn download_specific(&self, episode_numbers: &[usize]) -> Result<()> { +        let mut path = get_podcast_dir()?;          path.push(self.title());          let downloaded = already_downloaded(self.title())?; @@ -282,38 +279,41 @@ impl Episode {      }      pub fn extension(&self) -> Option<&str> { -        match self.0.enclosure() { -            Some(enclosure) => match enclosure.mime_type() { -                "audio/mpeg" => Some(".mp3"), -                "audio/mp4" => Some(".m4a"), -                "audio/ogg" => Some(".ogg"), -                _ => find_extension(self.url().unwrap()), -            }, -            None => None, +        match self.0.enclosure()?.mime_type() { +            "audio/mpeg" => Some(".mp3"), +            "audio/mp4" => Some(".m4a"), +            "audio/ogg" => Some(".ogg"), +            _ => find_extension(self.url().unwrap()),          }      } -    pub fn download(&self, podcast_name: &str) -> Result<(), io::Error> { -        let mut path = get_podcast_dir(); +    pub fn download(&self, podcast_name: &str) -> Result<()> { +        let mut path = get_podcast_dir()?;          path.push(podcast_name); -        DirBuilder::new().recursive(true).create(&path).unwrap(); +        DirBuilder::new() +            .recursive(true) +            .create(&path) +            .chain_err(|| UNABLE_TO_CREATE_DIRECTORY)?;          if let Some(url) = self.url() {              if let Some(title) = self.title() {                  let mut filename = String::from(title); -                filename.push_str(self.extension().unwrap()); +                filename.push_str(self.extension() +                    .chain_err(|| "unable to retrieve extension")?);                  path.push(filename);                  if !path.exists() {                      println!("Downloading: {}", path.to_str().unwrap()); -                    let mut file = File::create(&path)?; -                    let mut resp = reqwest::get(url).unwrap(); +                    let mut file = File::create(&path).chain_err(|| UNABLE_TO_CREATE_FILE)?; +                    let mut resp = reqwest::get(url).chain_err(|| UNABLE_TO_GET_HTTP_RESPONSE)?;                      let mut content: Vec<u8> = Vec::new(); -                    resp.read_to_end(&mut content)?; -                    file.write_all(&content)?; -                    return Ok(()); +                    resp.read_to_end(&mut content) +                        .chain_err(|| UNABLE_TO_READ_RESPONSE_TO_END)?; +                    file.write_all(&content).chain_err(|| UNABLE_TO_WRITE_FILE)?;                  } else { -                    println!("File already exists: {}", path.to_str().unwrap()); -                    return Ok(()); +                    println!( +                        "File already exists: {}", +                        path.to_str().chain_err(|| UNABLE_TO_CONVERT_TO_STR)? +                    );                  }              }          } diff --git a/src/utils.rs b/src/utils.rs index 597d9b6..4648903 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,19 +1,34 @@  use std::collections::HashSet;  use std::env;  use std::fs::{self, DirBuilder, File}; -use std::io::{self, BufReader, Read, Write}; -use std::num::ParseIntError; +use std::io::{BufReader, Read, Write};  use std::path::PathBuf; +use errors::*;  use reqwest;  use rss::Channel; +pub const UNABLE_TO_PARSE_REGEX: &'static str = "unable to parse regex"; +pub const UNABLE_TO_OPEN_FILE: &'static str = "unable to open file"; +pub const UNABLE_TO_CREATE_FILE: &'static str = "unable to create file"; +pub const UNABLE_TO_WRITE_FILE: &'static str = "unable to write file"; +pub const UNABLE_TO_READ_FILE_TO_STRING: &'static str = "unable to read file to string"; +pub const UNABLE_TO_READ_DIRECTORY: &'static str = "unable to read directory"; +pub const UNABLE_TO_READ_ENTRY: &'static str = "unable to read entry"; +pub const UNABLE_TO_CREATE_DIRECTORY: &'static str = "unable to create directory"; +pub const UNABLE_TO_READ_RESPONSE_TO_END: &'static str = "unable to read response to end"; +pub const UNABLE_TO_GET_HTTP_RESPONSE: &'static str = "unable to get http response"; +pub const UNABLE_TO_CONVERT_TO_STR: &'static str = "unable to convert to &str"; +pub const UNABLE_TO_REMOVE_FILE: &'static str = "unable to remove file"; +pub const UNABLE_TO_CREATE_CHANNEL_FROM_RESPONSE: &'static str = +    "unable to create channel from http response"; +pub const UNABLE_TO_CREATE_CHANNEL_FROM_FILE: &'static str = +    "unable to create channel from xml file"; +pub const UNABLE_TO_RETRIEVE_PODCAST_BY_TITLE: &'static str = "unable to retrieve podcast by title";  pub fn trim_extension(filename: &str) -> Option<String> {      let name = String::from(filename); -    match name.rfind('.') { -        Some(index) => Some(String::from(&name[0..index])), -        None => None, -    } +    let index = name.rfind('.')?; +    Some(String::from(&name[0..index]))  }  pub fn find_extension(input: &str) -> Option<&str> { @@ -33,35 +48,42 @@ pub fn find_extension(input: &str) -> Option<&str> {      }  } -pub fn create_directories() -> Result<(), String> { -    let mut path = get_podcast_dir(); -    path.push(".rss"); -    if let Err(err) = DirBuilder::new().recursive(true).create(&path) { -        return Err(format!( -            "Couldn't create directory: {}\nReason: {}", -            path.to_str().unwrap(), -            err -        )); +pub fn get_podcast_dir() -> Result<PathBuf> { +    match env::var_os("PODCAST") { +        Some(val) => Ok(PathBuf::from(val)), +        None => { +            let mut path = env::home_dir().chain_err(|| "Couldn't find your home directory")?; +            path.push("Podcasts"); +            Ok(path) +        }      } -    Ok(())  } -pub fn already_downloaded(dir: &str) -> Result<HashSet<String>, io::Error> { +pub fn create_directories() -> Result<()> { +    let mut path = get_podcast_dir()?; +    path.push(".rss"); +    DirBuilder::new() +        .recursive(true) +        .create(&path) +        .chain_err(|| "unable to create directory") +} + +pub fn already_downloaded(dir: &str) -> Result<HashSet<String>> {      let mut result = HashSet::new(); -    let mut path = get_podcast_dir(); +    let mut path = get_podcast_dir()?;      path.push(dir); -    let entries = fs::read_dir(path)?; +    let entries = fs::read_dir(path).chain_err(|| "unable to read directory")?;      for entry in entries { -        let entry = entry?; +        let entry = entry.chain_err(|| "unable to read entry")?;          match entry.file_name().into_string() {              Ok(name) => { -                let index = name.find('.').unwrap(); +                let index = name.find('.').chain_err(|| "unable to find string index")?;                  result.insert(String::from(&name[0..index]));              }              Err(_) => { -                println!( +                eprintln!(                      "OsString: {:?} couldn't be converted to String",                      entry.file_name()                  ); @@ -71,46 +93,41 @@ pub fn already_downloaded(dir: &str) -> Result<HashSet<String>, io::Error> {      Ok(result)  } -pub fn get_podcast_dir() -> PathBuf { -    match env::var_os("PODCAST") { -        Some(val) => PathBuf::from(val), -        None => { -            let mut path = env::home_dir().expect("Couldn't find your home directory"); -            path.push("Podcasts"); -            path -        } -    } -} - -pub fn get_sub_file() -> PathBuf { -    let mut path = get_podcast_dir(); +pub fn get_sub_file() -> Result<PathBuf> { +    let mut path = get_podcast_dir()?;      path.push(".subscriptions"); -    path +    Ok(path)  } -pub fn get_xml_dir() -> PathBuf { -    let mut path = get_podcast_dir(); +pub fn get_xml_dir() -> Result<PathBuf> { +    let mut path = get_podcast_dir()?;      path.push(".rss"); -    path +    Ok(path)  } -pub fn download_rss_feed(url: &str) -> Result<Channel, String> { -    let mut path = get_podcast_dir(); +pub fn download_rss_feed(url: &str) -> Result<Channel> { +    let mut path = get_podcast_dir()?;      path.push(".rss"); -    DirBuilder::new().recursive(true).create(&path).unwrap(); -    let mut resp = reqwest::get(url).unwrap(); +    DirBuilder::new() +        .recursive(true) +        .create(&path) +        .chain_err(|| "unable to open directory")?; +    let mut resp = reqwest::get(url).chain_err(|| "unable to open url")?;      let mut content: Vec<u8> = Vec::new(); -    resp.read_to_end(&mut content).unwrap(); -    let channel = Channel::read_from(BufReader::new(&content[..])).unwrap(); +    resp.read_to_end(&mut content) +        .chain_err(|| "unable to read http response to end")?; +    let channel = Channel::read_from(BufReader::new(&content[..])) +        .chain_err(|| "unable to create channel from xml http response")?;      let mut filename = String::from(channel.title());      filename.push_str(".xml");      path.push(filename); -    let mut file = File::create(&path).unwrap(); -    file.write_all(&content).unwrap(); +    let mut file = File::create(&path).chain_err(|| "unable to create file")?; +    file.write_all(&content) +        .chain_err(|| "unable to write file")?;      Ok(channel)  } -pub fn parse_download_episodes(e_search: &str) -> Result<Vec<usize>, ParseIntError> { +pub fn parse_download_episodes(e_search: &str) -> Result<Vec<usize>> {      let input = String::from(e_search);      let mut ranges = Vec::<(usize, usize)>::new();      let mut elements = Vec::<usize>::new(); @@ -119,11 +136,13 @@ pub fn parse_download_episodes(e_search: &str) -> Result<Vec<usize>, ParseIntErr          let temp = String::from(elem);          if temp.contains('-') {              let range: Vec<usize> = elem.split('-') -                .map(|i| i.parse::<usize>().unwrap()) -                .collect(); +                .map(|i| i.parse::<usize>().chain_err(|| "unable to parse number")) +                .collect::<Result<Vec<usize>>>() +                .chain_err(|| "unable to collect ranges")?;              ranges.push((range[0], range[1]));          } else { -            elements.push(elem.parse::<usize>()?); +            elements.push(elem.parse::<usize>() +                .chain_err(|| "unable to parse number")?);          }      } | 
