diff options
| -rw-r--r-- | CHANGELOG | 4 | ||||
| -rw-r--r-- | Cargo.toml | 4 | ||||
| -rw-r--r-- | src/actions.rs | 118 | ||||
| -rw-r--r-- | src/main.rs | 224 | ||||
| -rw-r--r-- | src/match_handler.rs | 103 | ||||
| -rw-r--r-- | src/migration_handler.rs | 16 | ||||
| -rw-r--r-- | src/parser.rs | 137 | ||||
| -rw-r--r-- | src/structs.rs | 154 | ||||
| -rw-r--r-- | src/utils.rs | 34 | 
9 files changed, 436 insertions, 358 deletions
| @@ -1,3 +1,7 @@ +0.8.0 +- Add a few subcommands / subcommand shortcuts +- Internal cleanup +  0.6.0  - Update to rust 2018 edition @@ -1,7 +1,7 @@  [package]  name = "podcast"  edition = "2018" -version = "0.7.5" +version = "0.8.0"  authors = ["Nathan Jaremko <njaremko@gmail.com>"]  description = "A command line podcast manager"  license = "GPL-3.0" @@ -31,5 +31,5 @@ rss = {version = "1.6", features = ["from_url"] }  serde = "1.0"  serde_derive = "1.0"  serde_json = "1.0" -yaml-rust = "0.4" +serde_yaml = "0.8"  toml = "0.4" diff --git a/src/actions.rs b/src/actions.rs index ca4f3c0..0ecb083 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,5 +1,5 @@ -use super::structs::*; -use super::utils::*; +use crate::structs::*; +use crate::utils::*;  use std::collections::HashSet;  use std::fs::{self, DirBuilder, File}; @@ -16,12 +16,9 @@ use toml;  pub fn list_episodes(search: &str) -> Result<()> {      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) -        .chain_err(|| UNABLE_TO_CREATE_DIRECTORY)?; +    let path = get_xml_dir()?; +    create_dir_if_not_exist(&path)?; +      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()) { @@ -56,12 +53,16 @@ pub fn subscribe_rss(url: &str) -> Result<Channel> {  pub fn download_rss(config: &Config, url: &str) -> Result<()> {      println!("Downloading episode(s)...");      let channel = download_rss_feed(url)?; -    let download_limit = config.auto_download_limit as usize; -    if download_limit > 0 { +    let mut download_limit = config.auto_download_limit as usize; +    if 0 < download_limit  {          let podcast = Podcast::from(channel);          let episodes = podcast.episodes(); +        if episodes.len() < download_limit +        { +            download_limit = episodes.len() +        }          episodes[..download_limit].par_iter().for_each(|ep| { -            if let Err(err) = ep.download(podcast.title()) { +            if let Err(err) = download(podcast.title(), ep) {                  eprintln!("Error downloading {}: {}", podcast.title(), err);              }          }); @@ -72,10 +73,7 @@ pub fn download_rss(config: &Config, url: &str) -> Result<()> {  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)?; +    create_dir_if_not_exist(&path)?;      let mut titles = HashSet::new();      for entry in fs::read_dir(&path).chain_err(|| UNABLE_TO_READ_DIRECTORY)? { @@ -93,20 +91,21 @@ pub fn update_subscription(sub: &mut Subscription) -> Result<()> {          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(); + +    let mut podcast_rss_path = get_xml_dir()?; +    podcast_rss_path.push(&filename); + +    let mut file = File::create(&podcast_rss_path).unwrap();      file.write_all(&content).unwrap(); -    if podcast.episodes().len() > sub.num_episodes { +    if sub.num_episodes < podcast.episodes().len() {          podcast.episodes()[..podcast.episodes().len() - sub.num_episodes]              .par_iter()              .for_each(|ep: &Episode| { -                if let Err(err) = ep.download(podcast.title()) { +                if let Err(err) = download(podcast.title(), ep) {                      eprintln!("Error downloading {}: {}", podcast.title(), err);                  }              }); @@ -127,7 +126,7 @@ pub fn update_rss(state: &mut State) {  pub fn list_subscriptions(state: &State) -> Result<()> {      let stdout = io::stdout();      let mut handle = stdout.lock(); -    for podcast in &state.subscriptions() { +    for podcast in state.subscriptions() {          writeln!(&mut handle, "{}", &podcast.title).ok();      }      Ok(()) @@ -159,8 +158,7 @@ pub fn download_episode_by_num(state: &State, p_search: &str, e_search: &str) ->                  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()) +                download(podcast.title(), &episodes[episodes.len() - ep_num])                      .chain_err(|| "unable to download episode")?;              }          } @@ -168,8 +166,11 @@ pub fn download_episode_by_num(state: &State, p_search: &str, e_search: &str) ->          {              let stdout = io::stdout();              let mut handle = stdout.lock(); -            writeln!(&mut handle, "Failed to parse episode number...").ok(); -            writeln!(&mut handle, "Attempting to find episode by name...").ok(); +            writeln!( +                &mut handle, +                "Failed to parse episode number...\nAttempting to find episode by name..." +            ) +            .ok();          }          download_episode_by_name(state, p_search, e_search, false)              .chain_err(|| "Failed to download episode.")?; @@ -178,6 +179,47 @@ pub fn download_episode_by_num(state: &State, p_search: &str, e_search: &str) ->      Ok(())  } +pub fn download(podcast_name: &str, episode: &Episode) -> Result<()> { +    let stdout = io::stdout(); + +    let mut path = get_podcast_dir()?; +    path.push(podcast_name); +    create_dir_if_not_exist(&path)?; + +    if let Some(url) = episode.url() { +            if let Some(title) = episode.title() { +                let mut filename = title; +                filename.push_str( +                    episode.extension() +                        .chain_err(|| "unable to retrieve extension")?, +                ); +                path.push(filename); +                if !path.exists() { +                    { +                        let mut handle = stdout.lock(); +                        writeln!(&mut handle, "Downloading: {:?}", &path).ok(); +                    } +                    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) +                        .chain_err(|| UNABLE_TO_READ_RESPONSE_TO_END)?; +                    file.write_all(&content) +                        .chain_err(|| UNABLE_TO_WRITE_FILE)?; +                } else { +                    let mut handle = stdout.lock(); +                    writeln!( +                        &mut handle, +                        "File already exists: {:?}", +                        &path +                    ) +                    .ok(); +                } +            } +        } +    Ok(()) +} +  pub fn download_episode_by_name(      state: &State,      p_search: &str, @@ -194,29 +236,27 @@ pub fn download_episode_by_name(              if download_all {                  episodes                      .iter() -                    .filter(|ep| { -                        ep.title() -                            .unwrap_or_else(|| "".to_string()) -                            .contains(e_search) -                    }) +                    .filter(|ep| ep.title().is_some()) +                    .filter(|ep| ep.title().unwrap().contains(e_search))                      .for_each(|ep| { -                        ep.download(podcast.title()).unwrap_or_else(|_| { +                        download(podcast.title(), ep).unwrap_or_else(|_| {                              eprintln!("Error downloading episode: {}", podcast.title())                          });                      })              } else {                  let filtered_episodes: Vec<&Episode> = episodes                      .iter() +                    .filter(|ep| ep.title().is_some())                      .filter(|ep| {                          ep.title() -                            .unwrap_or_else(|| "".to_string()) +                            .unwrap()                              .to_lowercase()                              .contains(&e_search.to_lowercase())                      })                      .collect();                  if let Some(ep) = filtered_episodes.first() { -                    ep.download(podcast.title()) +                    download(podcast.title(), ep)                          .chain_err(|| "unable to download episode")?;                  }              } @@ -343,7 +383,7 @@ pub fn play_episode_by_num(state: &State, p_search: &str, ep_num_string: &str) -          {              let stdout = io::stdout();              let mut handle = stdout.lock(); -            writeln!(&mut handle, "Failed to parse episode number...").ok(); +            writeln!(&mut handle, "Failed to parse episode index number...").ok();              writeln!(&mut handle, "Attempting to find episode by name...").ok();          }          play_episode_by_name(state, p_search, ep_num_string) @@ -415,7 +455,6 @@ pub fn check_for_update(version: &str) -> Result<()> {              .text()              .chain_err(|| "unable to convert response to text")?; -    //println!("{}", resp);      let config = resp          .parse::<toml::Value>()          .chain_err(|| "unable to parse toml")?; @@ -464,7 +503,8 @@ fn launch_vlc(url: &str) -> Result<()> {  pub fn remove_podcast(state: &mut State, p_search: &str) -> Result<()> {      if p_search == "*" { -        return Podcast::delete_all(); +        state.subscriptions = vec![]; +        return delete_all();      }      let re_pod = Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?; @@ -473,7 +513,7 @@ pub fn remove_podcast(state: &mut State, p_search: &str) -> Result<()> {          let title = state.subscriptions[subscription].title.clone();          if re_pod.is_match(&title) {              state.subscriptions.remove(subscription); -            Podcast::delete(&title)?; +            delete(&title)?;          }      }      Ok(()) @@ -483,7 +523,7 @@ pub fn print_completion(arg: &str) {      let zsh = r#"#compdef podcast  #autoload -# Copyright (C) 2017: +# Copyright (C) 2019:  #    Nathan Jaremko <njaremko@gmail.com>  # All Rights Reserved.  # This file is licensed under the GPLv2+. Please see COPYING for more information. diff --git a/src/main.rs b/src/main.rs index 5f6cfc6..2e312e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,236 +14,30 @@ extern crate rss;  #[macro_use]  extern crate serde_derive;  extern crate serde_json; +extern crate serde_yaml;  extern crate toml; -extern crate yaml_rust;  pub mod actions; +pub mod match_handler; +pub mod migration_handler; +pub mod parser;  pub mod structs;  pub mod utils;  pub mod errors { -    // Create the Error, ErrorKind, ResultExt, and Result types      error_chain! {}  } -use self::actions::*;  use self::errors::*;  use self::structs::*; -use self::utils::*; -use clap::{App, Arg, SubCommand}; - -const VERSION: &str = "0.7.5"; +const VERSION: &str = "0.8.0";  fn main() -> Result<()> { -    create_directories().chain_err(|| "unable to create directories")?; +    utils::create_directories().chain_err(|| "unable to create directories")?; +    migration_handler::migrate_old_subscriptions()?;      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>") -        .about("A command line podcast manager") -        .subcommand( -            SubCommand::with_name("download") -                .about("download episodes of podcast") -                .arg( -                    Arg::with_name("PODCAST") -                        .help("Regex for subscribed podcast") -                        .required(true) -                        .index(1), -                ) -                .arg( -                    Arg::with_name("EPISODE") -                        .required(false) -                        .help("Episode index") -                        .index(2), -                ) -                .arg( -                    Arg::with_name("name") -                        .short("e") -                        .long("episode") -                        .help("Download using episode name instead of number") -                        .required(false), -                ) -                .arg( -                    Arg::with_name("all") -                        .short("a") -                        .long("all") -                        .help("Download all matching episodes") -                        .required(false), -                ), -        ) -        .subcommand( -            SubCommand::with_name("ls") -                .about("list episodes of podcast") -                .arg( -                    Arg::with_name("PODCAST") -                        .help("Regex for subscribed podcast") -                        .index(1), -                ), -        ) -        .subcommand( -            SubCommand::with_name("list") -                .about("list episodes of podcast") -                .arg( -                    Arg::with_name("PODCAST") -                        .help("Regex for subscribed podcast") -                        .index(1), -                ), -        ) -        .subcommand( -            SubCommand::with_name("play") -                .about("play an episode") -                .arg( -                    Arg::with_name("PODCAST") -                        .help("Regex for subscribed podcast") -                        .required(true) -                        .index(1), -                ) -                .arg( -                    Arg::with_name("EPISODE") -                        .help("Episode index") -                        .required(false) -                        .index(2), -                ) -                .arg( -                    Arg::with_name("name") -                        .short("e") -                        .long("episode") -                        .help("Play using episode name instead of number") -                        .required(false), -                ), -        ) -        .subcommand( -            SubCommand::with_name("search") -                .about("searches for podcasts") -                .arg( -                    Arg::with_name("debug") -                        .short("d") -                        .help("print debug information verbosely"), -                ), -        ) -        .subcommand( -            SubCommand::with_name("subscribe") -                .about("subscribes to a podcast RSS feed") -                .arg( -                    Arg::with_name("URL") -                        .help("URL to RSS feed") -                        .required(true) -                        .index(1), -                ) -                .arg( -                    Arg::with_name("download") -                        .short("d") -                        .long("download") -                        .help("auto download based on config"), -                ), -        ) -        .subcommand(SubCommand::with_name("refresh").about("refresh subscribed podcasts")) -        .subcommand(SubCommand::with_name("update").about("check for updates")) -        .subcommand( -            SubCommand::with_name("rm") -                .about("unsubscribe from a podcast") -                .arg(Arg::with_name("PODCAST").help("Podcast to delete").index(1)), -        ) -        .subcommand( -            SubCommand::with_name("completion") -                .about("install shell completion") -                .arg( -                    Arg::with_name("SHELL") -                        .help("Shell to print completion for") -                        .index(1), -                ), -        ) -        .get_matches(); - -    match matches.subcommand_name() { -        Some("download") => { -            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)? -                    } else if download_matches.occurrences_of("name") > 0 { -                        download_episode_by_name( -                            &state, -                            podcast, -                            ep, -                            download_matches.occurrences_of("all") > 0, -                        )? -                    } else { -                        download_episode_by_num(&state, podcast, ep)? -                    } -                } -                None => download_all(&state, podcast)?, -            } -        } -        Some("ls") | Some("list") => { -            let list_matches = matches -                .subcommand_matches("ls") -                .or_else(|| matches.subcommand_matches("list")) -                .chain_err(|| "unable to find subcommand matches")?; -            match list_matches.value_of("PODCAST") { -                Some(regex) => list_episodes(regex)?, -                None => list_subscriptions(&state)?, -            } -        } -        Some("play") => { -            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) => { -                    if play_matches.occurrences_of("name") > 0 { -                        play_episode_by_name(&state, podcast, episode)? -                    } else { -                        play_episode_by_num(&state, podcast, episode)? -                    } -                } -                None => play_latest(&state, podcast)?, -            } -        } -        Some("subscribe") => { -            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)?; -            } else { -                subscribe_rss(url)?; -            } -        } -        Some("search") => println!("This feature is coming soon..."), -        Some("rm") => { -            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") -                .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)?, -        _ => (), -    } +    let matches = parser::get_matches(&VERSION); +    match_handler::handle_matches(&VERSION, &mut state, &config, &matches)?;      state.save().chain_err(|| "unable to save state")  } diff --git a/src/match_handler.rs b/src/match_handler.rs new file mode 100644 index 0000000..3fd7c6a --- /dev/null +++ b/src/match_handler.rs @@ -0,0 +1,103 @@ +use clap::ArgMatches; + +use crate::actions::*; +use crate::errors::*; +use crate::structs::*; + +pub fn handle_matches( +    version: &str, +    state: &mut State, +    config: &Config, +    matches: &ArgMatches, +) -> Result<()> { +    match matches.subcommand_name() { +        Some("download") => { +            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)? +                    } else if download_matches.occurrences_of("name") > 0 { +                        download_episode_by_name( +                            &state, +                            podcast, +                            ep, +                            download_matches.occurrences_of("all") > 0, +                        )? +                    } else { +                        download_episode_by_num(&state, podcast, ep)? +                    } +                } +                None => download_all(&state, podcast)?, +            } +        } +        Some("ls") | Some("list") => { +            let list_matches = matches +                .subcommand_matches("ls") +                .or_else(|| matches.subcommand_matches("list")) +                .chain_err(|| "unable to find subcommand matches")?; +            match list_matches.value_of("PODCAST") { +                Some(regex) => list_episodes(regex)?, +                None => list_subscriptions(&state)?, +            } +        } +        Some("play") => { +            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) => { +                    if play_matches.occurrences_of("name") > 0 { +                        play_episode_by_name(&state, podcast, episode)? +                    } else { +                        play_episode_by_num(&state, podcast, episode)? +                    } +                } +                None => play_latest(&state, podcast)?, +            } +        } +        Some("sub") | Some("subscribe") => { +            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)?; +            } else { +                subscribe_rss(url)?; +            } +        } +        Some("search") => println!("This feature is coming soon..."), +        Some("rm") => { +            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(state, regex)? +        } +        Some("completion") => { +            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(state), +        Some("update") => check_for_update(version)?, +        _ => (), +    }; +    Ok(()) +} diff --git a/src/migration_handler.rs b/src/migration_handler.rs new file mode 100644 index 0000000..73a0241 --- /dev/null +++ b/src/migration_handler.rs @@ -0,0 +1,16 @@ +use crate::errors::*; +use crate::utils::*; +use std::fs; + +pub fn migrate_old_subscriptions() -> Result<()> { +    let path = get_podcast_dir()?; +    let mut old_path = path.clone(); +    old_path.push(".subscriptions"); +    if old_path.exists() { +        println!("Migrating old subscriptions file..."); +        let new_path = get_sub_file()?; +        fs::rename(&old_path, &new_path) +            .chain_err(|| format!("Unable to move {:?} to {:?}", &old_path, &new_path))?; +    } +    Ok(()) +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..ce5ca93 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,137 @@ +use clap::{App, Arg, ArgMatches, SubCommand}; + +pub fn get_matches<'a>(version: &str) -> ArgMatches<'a> { +    App::new("podcast") +        .version(version) +        .author("Nathan J. <njaremko@gmail.com>") +        .about("A command line podcast manager") +        .subcommand( +            SubCommand::with_name("download") +                .about("download episodes of podcast") +                .arg( +                    Arg::with_name("PODCAST") +                        .help("Regex for subscribed podcast") +                        .required(true) +                        .index(1), +                ) +                .arg( +                    Arg::with_name("EPISODE") +                        .required(false) +                        .help("Episode index") +                        .index(2), +                ) +                .arg( +                    Arg::with_name("name") +                        .short("e") +                        .long("episode") +                        .help("Download using episode name instead of index number") +                        .required(false), +                ) +                .arg( +                    Arg::with_name("all") +                        .short("a") +                        .long("all") +                        .help("Download all matching episodes") +                        .required(false), +                ), +        ) +        .subcommand( +            SubCommand::with_name("ls") +                .about("list episodes of podcast") +                .arg( +                    Arg::with_name("PODCAST") +                        .help("Regex for subscribed podcast") +                        .index(1), +                ), +        ) +        .subcommand( +            SubCommand::with_name("list") +                .about("list episodes of podcast") +                .arg( +                    Arg::with_name("PODCAST") +                        .help("Regex for subscribed podcast") +                        .index(1), +                ), +        ) +        .subcommand( +            SubCommand::with_name("play") +                .about("play an episode") +                .arg( +                    Arg::with_name("PODCAST") +                        .help("Regex for subscribed podcast") +                        .required(true) +                        .index(1), +                ) +                .arg( +                    Arg::with_name("EPISODE") +                        .help("Episode index") +                        .required(false) +                        .index(2), +                ) +                .arg( +                    Arg::with_name("name") +                        .short("e") +                        .long("episode") +                        .help("Play using episode name instead of index number") +                        .required(false), +                ), +        ) +        .subcommand( +            SubCommand::with_name("search") +                .about("searches for podcasts") +                .arg( +                    Arg::with_name("debug") +                        .short("d") +                        .help("print debug information verbosely"), +                ), +        ) +        .subcommand( +            SubCommand::with_name("subscribe") +                .about("subscribes to a podcast RSS feed") +                .arg( +                    Arg::with_name("URL") +                        .help("URL to RSS feed") +                        .required(true) +                        .index(1), +                ) +                .arg( +                    Arg::with_name("download") +                        .short("d") +                        .long("download") +                        .help("auto download based on config"), +                ), +        ) +        .subcommand( +            SubCommand::with_name("sub") +                .about("subscribes to a podcast RSS feed") +                .arg( +                    Arg::with_name("URL") +                        .help("URL to RSS feed") +                        .required(true) +                        .index(1), +                ) +                .arg( +                    Arg::with_name("download") +                        .short("d") +                        .long("download") +                        .help("auto download based on config"), +                ), +        ) +        .subcommand(SubCommand::with_name("refresh").about("refresh subscribed podcasts")) +        .subcommand(SubCommand::with_name("update").about("check for updates")) +        .subcommand( +            SubCommand::with_name("rm") +                .about("unsubscribe from a podcast") +                .arg(Arg::with_name("PODCAST").help("Podcast to delete").index(1)), +        ) +        .subcommand( +            SubCommand::with_name("completion") +                .about("install shell completion") +                .arg( +                    Arg::with_name("SHELL") +                        .help("Shell to print completion for") +                        .index(1), +                ), +        ) +        .get_matches() +} diff --git a/src/structs.rs b/src/structs.rs index 9b921ee..56c1674 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -3,16 +3,15 @@ use super::utils::*;  use crate::errors::*;  use std::collections::BTreeSet; -use std::fs::{self, DirBuilder, File}; -use std::io::{self, BufReader, Read, Write}; +use std::fs::{self, File}; +use std::io::{self, BufReader, Write};  use chrono::prelude::*;  use rayon::prelude::*;  use regex::Regex; -use reqwest;  use rss::{Channel, Item};  use serde_json; -use yaml_rust::YamlLoader; +use std::path::PathBuf;  #[cfg(target_os = "macos")]  static ESCAPE_REGEX: &str = r"/"; @@ -25,6 +24,7 @@ lazy_static! {      static ref FILENAME_ESCAPE: Regex = Regex::new(ESCAPE_REGEX).unwrap();  } +#[derive(Debug, PartialEq, Serialize, Deserialize)]  pub struct Config {      pub auto_download_limit: i64,  } @@ -32,41 +32,47 @@ pub struct Config {  impl Config {      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) -                .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() { -                    download_limit = val; +        path.push(".config.yaml"); +        let config = if path.exists() { +            let file = File::open(&path).chain_err(|| UNABLE_TO_OPEN_FILE)?; +            match serde_yaml::from_reader(file) { +                Ok(config) => config, +                Err(err) => { +                    let mut new_path = path.clone(); +                    new_path.set_extension("yaml.bk"); +                    eprintln!("{}", err); +                    eprintln!("Failed to open config file, moving to {:?}", &new_path); +                    fs::rename(&path, new_path) +                        .chain_err(|| "Failed to move old config file...")?; +                    create_new_config_file(&path)?                  }              }          } else { -            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)?; -        } -        Ok(Config { -            auto_download_limit: download_limit, -        }) +            create_new_config_file(&path)? +        }; +        Ok(config)      }  } -#[derive(Serialize, Deserialize, Clone, Debug)] +fn create_new_config_file(path: &PathBuf) -> Result<Config> { +    println!("Creating new config file at {:?}", &path); +    let download_limit = 1; +    let file = File::create(&path).chain_err(|| UNABLE_TO_CREATE_FILE)?; +    let config = Config { +        auto_download_limit: download_limit, +    }; +    serde_yaml::to_writer(file, &config).chain_err(|| UNABLE_TO_WRITE_FILE)?; +    Ok(config) +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]  pub struct Subscription {      pub title: String,      pub url: String,      pub num_episodes: usize,  } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]  pub struct State {      pub version: String,      pub last_run_time: DateTime<Utc>, @@ -75,21 +81,15 @@ pub struct State {  impl State {      pub fn new(version: &str) -> Result<State> { -        let mut path = get_podcast_dir()?; -        path.push(".subscriptions"); +        let path = get_sub_file()?;          if path.exists() { -            let mut s = String::new(); -            { -                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) { +            let file = File::open(&path).chain_err(|| UNABLE_TO_OPEN_FILE)?; +            let mut state: State = match serde_json::from_reader(&file) {                  Ok(val) => val,                  // This will happen if the struct has changed between versions                  Err(_) => { -                    let v: serde_json::Value = -                        serde_json::from_str(&s).chain_err(|| "unable to read json from string")?; +                    let v: serde_json::Value = serde_json::from_reader(&file) +                        .chain_err(|| "unable to read json from string")?;                      State {                          version: String::from(version),                          last_run_time: Utc::now(), @@ -103,10 +103,9 @@ impl State {              };              state.version = String::from(version);              // Check if a day has passed (86400 seconds) since last launch -            if Utc::now() +            if 86400 < Utc::now()                  .signed_duration_since(state.last_run_time)                  .num_seconds() -                > 86400              {                  update_rss(&mut state);                  check_for_update(&state.version)?; @@ -115,6 +114,7 @@ impl State {              state.save()?;              Ok(state)          } else { +            println!("Creating new file {:?}", &path);              Ok(State {                  version: String::from(version),                  last_run_time: Utc::now(), @@ -126,7 +126,7 @@ impl State {      pub fn subscribe(&mut self, url: &str) -> Result<()> {          let mut set = BTreeSet::new();          for sub in self.subscriptions() { -            set.insert(sub.title); +            set.insert(sub.title.clone());          }          let podcast = Podcast::from(Channel::from_url(url).unwrap());          if !set.contains(podcast.title()) { @@ -139,20 +139,26 @@ impl State {          self.save()      } -    pub fn subscriptions(&self) -> Vec<Subscription> { -        self.subscriptions.clone() +    pub fn subscriptions(&self) -> &[Subscription] { +        &self.subscriptions +    } + +    pub fn subscriptions_mut(&mut self) -> &mut [Subscription] { +        &mut self.subscriptions      }      pub fn save(&self) -> Result<()> { -        let mut path = get_podcast_dir()?; -        path.push(".subscriptions.tmp"); +        let mut path = get_sub_file()?; +        path.set_extension("json.tmp");          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")?; +        let sub_file_path = get_sub_file()?; +        fs::rename(&path, &sub_file_path) +            .chain_err(|| format!("unable to rename file {:?} to {:?}", &path, &sub_file_path))?;          Ok(())      }  } @@ -210,25 +216,26 @@ impl Podcast {          let mut filename = String::from(title);          filename.push_str(".xml");          path.push(filename); -          fs::remove_file(path).chain_err(|| UNABLE_TO_REMOVE_FILE)      }      pub fn delete_all() -> Result<()> { -        let path = get_xml_dir()?; -        fs::remove_dir_all(path).chain_err(|| UNABLE_TO_READ_DIRECTORY) +        fs::remove_dir_all(get_xml_dir()?).chain_err(|| UNABLE_TO_READ_DIRECTORY)      }      pub fn episodes(&self) -> Vec<Episode> {          let mut result = Vec::new(); -        for item in self.0.items().to_vec() { +        for item in self.0.items().to_owned() {              result.push(Episode::from(item));          }          result      }      pub fn download(&self) -> Result<()> { -        print!("You are about to download all episodes of {} (y/n): ", self.title()); +        print!( +            "You are about to download all episodes of {} (y/n): ", +            self.title() +        );          io::stdout().flush().ok();          let mut input = String::new();          io::stdin() @@ -246,7 +253,7 @@ impl Podcast {                  self.episodes().par_iter().for_each(|i| {                      if let Some(ep_title) = i.title() {                          if !downloaded.contains(&ep_title) { -                            if let Err(err) = i.download(self.title()) { +                            if let Err(err) = download(self.title(), i) {                                  eprintln!("{}", err);                              }                          } @@ -255,7 +262,7 @@ impl Podcast {              }              Err(_) => {                  self.episodes().par_iter().for_each(|i| { -                    if let Err(err) = i.download(self.title()) { +                    if let Err(err) = download(self.title(), i) {                          eprintln!("{}", err);                      }                  }); @@ -275,7 +282,7 @@ impl Podcast {          episode_numbers.par_iter().for_each(|ep_num| {              if let Some(ep_title) = episodes[episodes.len() - ep_num].title() {                  if !downloaded.contains(&ep_title) { -                    if let Err(err) = episodes[episodes.len() - ep_num].download(self.title()) { +                    if let Err(err) = download(self.title(), &episodes[episodes.len() - ep_num]) {                          eprintln!("{}", err);                      }                  } @@ -309,43 +316,4 @@ impl Episode {              _ => find_extension(self.url().unwrap()),          }      } - -    pub fn download(&self, podcast_name: &str) -> Result<()> { -        let stdout = io::stdout(); - -        let mut path = get_podcast_dir()?; -        path.push(podcast_name); -        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 = title; -                filename.push_str( -                    self.extension() -                        .chain_err(|| "unable to retrieve extension")?, -                ); -                path.push(filename); -                if !path.exists() { -                    { -                        let mut handle = stdout.lock(); -                        writeln!(&mut handle, "Downloading: {}", path.to_str().unwrap()).ok(); -                    } -                    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) -                        .chain_err(|| UNABLE_TO_READ_RESPONSE_TO_END)?; -                    file.write_all(&content) -                        .chain_err(|| UNABLE_TO_WRITE_FILE)?; -                } else { -                    let mut handle = stdout.lock(); -                    writeln!(&mut handle, "File already exists: {}", path.to_str().chain_err(|| UNABLE_TO_CONVERT_TO_STR)?).ok(); -                } -            } -        } -        Ok(()) -    }  } diff --git a/src/utils.rs b/src/utils.rs index 657ff5b..5c3b2db 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -12,6 +12,7 @@ use rss::Channel;  pub const UNABLE_TO_PARSE_REGEX: &str = "unable to parse regex";  pub const UNABLE_TO_OPEN_FILE: &str = "unable to open file";  pub const UNABLE_TO_CREATE_FILE: &str = "unable to create file"; +pub const UNABLE_TO_READ_FILE: &str = "unable to read file";  pub const UNABLE_TO_WRITE_FILE: &str = "unable to write file";  pub const UNABLE_TO_READ_FILE_TO_STRING: &str = "unable to read file to string";  pub const UNABLE_TO_READ_DIRECTORY: &str = "unable to read directory"; @@ -25,6 +26,7 @@ pub const UNABLE_TO_CREATE_CHANNEL_FROM_RESPONSE: &str =      "unable to create channel from http response";  pub const UNABLE_TO_CREATE_CHANNEL_FROM_FILE: &str = "unable to create channel from xml file";  pub const UNABLE_TO_RETRIEVE_PODCAST_BY_TITLE: &str = "unable to retrieve podcast by title"; +  pub fn trim_extension(filename: &str) -> Option<String> {      let name = String::from(filename);      let index = name.rfind('.')?; @@ -59,15 +61,32 @@ pub fn get_podcast_dir() -> Result<PathBuf> {      }  } -pub fn create_directories() -> Result<()> { -    let mut path = get_podcast_dir()?; -    path.push(".rss"); +pub fn create_dir_if_not_exist(path: &PathBuf) -> Result<()> {      DirBuilder::new()          .recursive(true)          .create(&path) -        .chain_err(|| "unable to create directory") +        .chain_err(|| UNABLE_TO_CREATE_DIRECTORY)?; +        Ok(()) +} + +pub fn create_directories() -> Result<()> { +    let mut path = get_podcast_dir()?; +    path.push(".rss"); +    create_dir_if_not_exist(&path)  } +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); +        fs::remove_file(path).chain_err(|| UNABLE_TO_REMOVE_FILE) +    } + +    pub fn delete_all() -> Result<()> { +        fs::remove_dir_all(get_xml_dir()?).chain_err(|| UNABLE_TO_READ_DIRECTORY) +    } +  pub fn already_downloaded(dir: &str) -> Result<HashSet<String>> {      let mut result = HashSet::new(); @@ -95,7 +114,7 @@ pub fn already_downloaded(dir: &str) -> Result<HashSet<String>> {  pub fn get_sub_file() -> Result<PathBuf> {      let mut path = get_podcast_dir()?; -    path.push(".subscriptions"); +    path.push(".subscriptions.json");      Ok(path)  } @@ -108,10 +127,7 @@ pub fn get_xml_dir() -> Result<PathBuf> {  pub fn download_rss_feed(url: &str) -> Result<Channel> {      let mut path = get_podcast_dir()?;      path.push(".rss"); -    DirBuilder::new() -        .recursive(true) -        .create(&path) -        .chain_err(|| "unable to open directory")?; +    create_dir_if_not_exist(&path)?;      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) | 
