From 22e617eec8e2d8da36788ae40fb53c2ed2ebe734 Mon Sep 17 00:00:00 2001 From: Nathan Jaremko Date: Sun, 24 Feb 2019 16:52:01 -0500 Subject: Improve code --- CHANGELOG | 4 + Cargo.toml | 4 +- src/actions.rs | 118 ++++++++++++++++--------- src/main.rs | 224 ++--------------------------------------------- src/match_handler.rs | 103 ++++++++++++++++++++++ src/migration_handler.rs | 16 ++++ src/parser.rs | 137 +++++++++++++++++++++++++++++ src/structs.rs | 154 +++++++++++++------------------- src/utils.rs | 34 +++++-- 9 files changed, 436 insertions(+), 358 deletions(-) create mode 100644 src/match_handler.rs create mode 100644 src/migration_handler.rs create mode 100644 src/parser.rs diff --git a/CHANGELOG b/CHANGELOG index cc6daef..4ba9f1d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +0.8.0 +- Add a few subcommands / subcommand shortcuts +- Internal cleanup + 0.6.0 - Update to rust 2018 edition diff --git a/Cargo.toml b/Cargo.toml index 837683b..5b71faf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "podcast" edition = "2018" -version = "0.7.5" +version = "0.8.0" authors = ["Nathan Jaremko "] 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 { 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 = 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::() .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 # 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. ") - .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. ") + .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 { 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 { + 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, @@ -75,21 +81,15 @@ pub struct State { impl State { pub fn new(version: &str) -> Result { - 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 { - 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 { 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 = 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 { let name = String::from(filename); let index = name.rfind('.')?; @@ -59,15 +61,32 @@ pub fn get_podcast_dir() -> Result { } } -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> { let mut result = HashSet::new(); @@ -95,7 +114,7 @@ pub fn already_downloaded(dir: &str) -> Result> { pub fn get_sub_file() -> Result { 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 { pub fn download_rss_feed(url: &str) -> Result { 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 = Vec::new(); resp.read_to_end(&mut content) -- cgit v1.2.3