aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNathan Jaremko2019-02-24 16:52:01 -0500
committerNathan Jaremko2019-02-24 17:53:19 -0500
commit22e617eec8e2d8da36788ae40fb53c2ed2ebe734 (patch)
tree99ecbf8a7df65c9748b73debf9ece08ad9858315
parente54af75fa1fe7f5e9da3bd858058ab491efea77a (diff)
downloadpodcast-22e617eec8e2d8da36788ae40fb53c2ed2ebe734.tar.bz2
Improve code0.8.0
-rw-r--r--CHANGELOG4
-rw-r--r--Cargo.toml4
-rw-r--r--src/actions.rs118
-rw-r--r--src/main.rs224
-rw-r--r--src/match_handler.rs103
-rw-r--r--src/migration_handler.rs16
-rw-r--r--src/parser.rs137
-rw-r--r--src/structs.rs154
-rw-r--r--src/utils.rs34
9 files changed, 436 insertions, 358 deletions
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 <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)