aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG5
-rw-r--r--Cargo.toml6
-rw-r--r--src/actions.rs485
-rw-r--r--src/arg_parser.rs96
-rw-r--r--src/command_handler.rs63
-rw-r--r--src/commands.rs12
-rw-r--r--src/download.rs199
-rw-r--r--src/main.rs42
-rw-r--r--src/match_handler.rs112
-rw-r--r--src/migration_handler.rs9
-rw-r--r--src/playback.rs183
-rw-r--r--src/structs.rs214
-rw-r--r--src/utils.rs124
13 files changed, 764 insertions, 786 deletions
diff --git a/CHANGELOG b/CHANGELOG
index c54a320..eb6cb3d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,8 @@
+0.10.0
+- Partial re-write of the application to be idiomatic
+- Improves performance throughout the application (downloads are also faster)
+- Changed from error-chain to failure for error handling
+
0.9.1
- Improve unsubscribe messages
diff --git a/Cargo.toml b/Cargo.toml
index c36fabf..ffd75cd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "podcast"
edition = "2018"
-version = "0.9.1"
+version = "0.10.0"
authors = ["Nathan Jaremko <njaremko@gmail.com>"]
description = "A command line podcast manager"
license = "GPL-3.0"
@@ -22,8 +22,8 @@ name = "podcast"
chrono = { version = "0.4", features = ["serde"] }
clap = "2.32"
dirs = "1.0"
-error-chain = "0.12"
-lazy_static = "1.2"
+failure = "0.1"
+lazy_static = "1.3"
rayon = "1.0"
regex = "1.1"
reqwest = "0.9"
diff --git a/src/actions.rs b/src/actions.rs
index c0518c5..a44a9cf 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -1,12 +1,12 @@
+use crate::download;
+use crate::errors::*;
use crate::structs::*;
use crate::utils::*;
use std::collections::HashSet;
-use std::fs::{self, DirBuilder, File};
-use std::io::{self, BufReader, Read, Write};
-use std::process::Command;
+use std::fs::{self, File};
+use std::io::{self, BufReader, BufWriter, Write};
-use crate::errors::*;
use clap::App;
use clap::Shell;
use rayon::prelude::*;
@@ -17,77 +17,54 @@ use std::path::PathBuf;
use toml;
pub fn list_episodes(search: &str) -> Result<()> {
- let re = Regex::new(&format!("(?i){}", &search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?;
+ let re = Regex::new(&format!("(?i){}", &search))?;
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)?;
+ for entry in fs::read_dir(&path)? {
+ let entry = entry?;
if re.is_match(&entry.file_name().into_string().unwrap()) {
- let file = File::open(&entry.path()).chain_err(|| UNABLE_TO_OPEN_FILE)?;
- let channel = Channel::read_from(BufReader::new(file))
- .chain_err(|| UNABLE_TO_CREATE_CHANNEL_FROM_FILE)?;
+ let file = File::open(&entry.path())?;
+ let channel = Channel::read_from(BufReader::new(file))?;
let podcast = Podcast::from(channel);
let episodes = podcast.episodes();
let stdout = io::stdout();
let mut handle = stdout.lock();
- for (num, ep) in episodes.iter().enumerate() {
- writeln!(
- &mut handle,
- "({}) {}",
- episodes.len() - num,
- ep.title()
- .chain_err(|| "unable to retrieve episode title")?
- )
- .ok();
- }
+ episodes
+ .iter()
+ .filter(|ep| ep.title().is_some())
+ .enumerate()
+ .for_each(|(num, ep)| {
+ writeln!(
+ &mut handle,
+ "({}) {}",
+ episodes.len() - num,
+ ep.title().unwrap()
+ )
+ .ok();
+ });
return Ok(());
}
}
Ok(())
}
-pub fn download_rss(config: &Config, url: &str) -> Result<()> {
- let channel = download_rss_feed(url)?;
- let mut download_limit = config.auto_download_limit as usize;
- if 0 < download_limit {
- println!("Subscribe auto-download limit set to: {}", download_limit);
- println!("Downloading episode(s)...");
- 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) = download(podcast.title(), ep) {
- eprintln!("Error downloading {}: {}", podcast.title(), err);
- }
- });
- }
- Ok(())
-}
-
pub fn update_subscription(sub: &mut Subscription) -> Result<()> {
+ println!("Updating {}", sub.title);
let mut path: PathBuf = get_podcast_dir()?;
path.push(&sub.title);
create_dir_if_not_exist(&path)?;
let mut titles = HashSet::new();
- for entry in fs::read_dir(&path).chain_err(|| UNABLE_TO_READ_DIRECTORY)? {
- let unwrapped_entry = &entry.chain_err(|| UNABLE_TO_READ_ENTRY)?;
+ for entry in fs::read_dir(&path)? {
+ let unwrapped_entry = &entry?;
titles.insert(trim_extension(
&unwrapped_entry.file_name().into_string().unwrap(),
));
}
- let mut resp = reqwest::get(&sub.url).chain_err(|| UNABLE_TO_GET_HTTP_RESPONSE)?;
- let mut content: Vec<u8> = Vec::new();
- resp.read_to_end(&mut content)
- .chain_err(|| UNABLE_TO_READ_RESPONSE_TO_END)?;
- let podcast = Podcast::from(
- Channel::read_from(BufReader::new(&content[..]))
- .chain_err(|| UNABLE_TO_CREATE_CHANNEL_FROM_RESPONSE)?,
- );
+ let resp = reqwest::get(&sub.url)?;
+ let podcast = Podcast::from(Channel::read_from(BufReader::new(resp))?);
let mut filename = String::from(podcast.title());
filename.push_str(".xml");
@@ -95,17 +72,16 @@ pub fn update_subscription(sub: &mut Subscription) -> Result<()> {
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();
+ let file = File::create(&podcast_rss_path)?;
+ (*podcast).write_to(BufWriter::new(file))?;
if sub.num_episodes < podcast.episodes().len() {
- podcast.episodes()[..podcast.episodes().len() - sub.num_episodes]
+ podcast.episodes()
+ [..podcast.episodes().len() - sub.num_episodes]
.par_iter()
- .for_each(|ep: &Episode| {
- if let Err(err) = download(podcast.title(), ep) {
- eprintln!("Error downloading {}: {}", podcast.title(), err);
- }
- });
+ .map(|ep| download::download(podcast.title(), ep))
+ .flat_map(|e| e.err())
+ .for_each(|err| eprintln!("Error: {}", err));
}
sub.num_episodes = podcast.episodes().len();
Ok(())
@@ -114,327 +90,18 @@ pub fn update_subscription(sub: &mut Subscription) -> Result<()> {
pub fn update_rss(state: &mut State) {
println!("Checking for new episodes...");
let _result: Vec<Result<()>> = state
- .subscriptions
+ .subscriptions_mut()
.par_iter_mut()
.map(|sub: &mut Subscription| update_subscription(sub))
.collect();
+ println!("Done.");
}
pub fn list_subscriptions(state: &State) -> Result<()> {
let stdout = io::stdout();
let mut handle = stdout.lock();
- for podcast in state.subscriptions() {
- writeln!(&mut handle, "{}", &podcast.title).ok();
- }
- Ok(())
-}
-
-pub fn download_range(state: &State, p_search: &str, e_search: &str) -> Result<()> {
- let re_pod = Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?;
-
- for subscription in &state.subscriptions {
- if re_pod.is_match(&subscription.title) {
- let podcast = Podcast::from_title(&subscription.title)
- .chain_err(|| UNABLE_TO_RETRIEVE_PODCAST_BY_TITLE)?;
- let episodes_to_download = parse_download_episodes(e_search)
- .chain_err(|| "unable to parse episodes to download")?;
- podcast
- .download_specific(&episodes_to_download)
- .chain_err(|| "unable to download episodes")?;
- }
- }
- Ok(())
-}
-
-pub fn download_episode_by_num(state: &State, p_search: &str, e_search: &str) -> Result<()> {
- let re_pod = Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?;
-
- if let Ok(ep_num) = e_search.parse::<usize>() {
- for subscription in &state.subscriptions {
- if re_pod.is_match(&subscription.title) {
- let podcast = Podcast::from_title(&subscription.title)
- .chain_err(|| UNABLE_TO_RETRIEVE_PODCAST_BY_TITLE)?;
- let episodes = podcast.episodes();
- download(podcast.title(), &episodes[episodes.len() - ep_num])
- .chain_err(|| "unable to download episode")?;
- }
- }
- } else {
- {
- let stdout = io::stdout();
- let mut handle = stdout.lock();
- 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.")?;
- }
-
- 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,
- e_search: &str,
- download_all: bool,
-) -> Result<()> {
- let re_pod = Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?;
-
- for subscription in &state.subscriptions {
- if re_pod.is_match(&subscription.title) {
- let podcast = Podcast::from_title(&subscription.title)
- .chain_err(|| UNABLE_TO_RETRIEVE_PODCAST_BY_TITLE)?;
- let episodes = podcast.episodes();
- if download_all {
- episodes
- .iter()
- .filter(|ep| ep.title().is_some())
- .filter(|ep| ep.title().unwrap().contains(e_search))
- .for_each(|ep| {
- 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()
- .to_lowercase()
- .contains(&e_search.to_lowercase())
- })
- .collect();
-
- if let Some(ep) = filtered_episodes.first() {
- download(podcast.title(), ep).chain_err(|| "unable to download episode")?;
- }
- }
- }
- }
- Ok(())
-}
-
-pub fn download_all(state: &State, p_search: &str) -> Result<()> {
- let re_pod = Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?;
-
- for subscription in &state.subscriptions {
- if re_pod.is_match(&subscription.title) {
- let podcast = Podcast::from_title(&subscription.title)
- .chain_err(|| UNABLE_TO_RETRIEVE_PODCAST_BY_TITLE)?;
- podcast
- .download()
- .chain_err(|| "unable to download podcast")?;
- }
- }
- Ok(())
-}
-
-pub fn play_latest(state: &State, p_search: &str) -> Result<()> {
- let re_pod: Regex =
- Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?;
- let mut path: PathBuf = get_xml_dir()?;
- DirBuilder::new()
- .recursive(true)
- .create(&path)
- .chain_err(|| UNABLE_TO_CREATE_DIRECTORY)?;
- for subscription in &state.subscriptions {
- if re_pod.is_match(&subscription.title) {
- let mut filename: String = subscription.title.clone();
- filename.push_str(".xml");
- path.push(filename);
-
- let mut file: File = File::open(&path).chain_err(|| UNABLE_TO_OPEN_FILE)?;
- let mut content: Vec<u8> = Vec::new();
- file.read_to_end(&mut content)
- .chain_err(|| "unable to read file to end")?;
-
- let podcast: Podcast = Podcast::from(
- Channel::read_from(content.as_slice())
- .chain_err(|| UNABLE_TO_CREATE_CHANNEL_FROM_FILE)?,
- );
- let episodes = podcast.episodes();
- let episode = episodes[0].clone();
-
- filename = episode
- .title()
- .chain_err(|| "unable to retrieve episode name")?;
- filename.push_str(
- episode
- .extension()
- .chain_err(|| "unable to retrieve episode extension")?,
- );
- path = get_podcast_dir()?;
- path.push(podcast.title());
- path.push(filename);
- if path.exists() {
- launch_player(
- path.to_str()
- .chain_err(|| "unable to convert path to &str")?,
- )?;
- } else {
- launch_player(
- episode
- .url()
- .chain_err(|| "unable to retrieve episode url")?,
- )?;
- }
- return Ok(());
- }
- }
- Ok(())
-}
-
-pub fn play_episode_by_num(state: &State, p_search: &str, ep_num_string: &str) -> Result<()> {
- let re_pod: Regex =
- Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?;
- if let Ok(ep_num) = ep_num_string.parse::<usize>() {
- let mut path: PathBuf = get_xml_dir()?;
- if let Err(err) = DirBuilder::new().recursive(true).create(&path) {
- eprintln!(
- "Couldn't create directory: {}\nReason: {}",
- path.to_str().unwrap(),
- err
- );
- return Ok(());
- }
- for subscription in &state.subscriptions {
- if re_pod.is_match(&subscription.title) {
- let mut filename: String = subscription.title.clone();
- filename.push_str(".xml");
- path.push(filename);
-
- let mut file: File = File::open(&path).unwrap();
- let mut content: Vec<u8> = Vec::new();
- file.read_to_end(&mut content).unwrap();
-
- let podcast = Podcast::from(Channel::read_from(content.as_slice()).unwrap());
- let episodes = podcast.episodes();
- let episode = episodes[episodes.len() - ep_num].clone();
-
- filename = episode.title().unwrap();
- filename.push_str(episode.extension().unwrap());
- path = get_podcast_dir()?;
- path.push(podcast.title());
- path.push(filename);
- if path.exists() {
- launch_player(path.to_str().chain_err(|| UNABLE_TO_CONVERT_TO_STR)?)?;
- } else {
- launch_player(
- episode
- .url()
- .chain_err(|| "unable to retrieve episode url")?,
- )?;
- }
- return Ok(());
- }
- }
- } else {
- {
- let stdout = io::stdout();
- let mut handle = stdout.lock();
- 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)
- .chain_err(|| "Failed to play episode by name.")?;
- }
- Ok(())
-}
-
-pub fn play_episode_by_name(state: &State, p_search: &str, ep_string: &str) -> Result<()> {
- let re_pod: Regex =
- Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?;
- let mut path: PathBuf = get_xml_dir()?;
- if let Err(err) = DirBuilder::new().recursive(true).create(&path) {
- eprintln!(
- "Couldn't create directory: {}\nReason: {}",
- path.to_str().unwrap(),
- err
- );
- return Ok(());
- }
- for subscription in &state.subscriptions {
- if re_pod.is_match(&subscription.title) {
- let mut filename: String = subscription.title.clone();
- filename.push_str(".xml");
- path.push(filename);
-
- let mut file: File = File::open(&path).unwrap();
- let mut content: Vec<u8> = Vec::new();
- file.read_to_end(&mut content).unwrap();
-
- let podcast = Podcast::from(Channel::read_from(content.as_slice()).unwrap());
- let episodes = podcast.episodes();
- let filtered_episodes: Vec<&Episode> = episodes
- .iter()
- .filter(|ep| {
- ep.title()
- .unwrap_or_else(|| "".to_string())
- .to_lowercase()
- .contains(&ep_string.to_lowercase())
- })
- .collect();
- if let Some(episode) = filtered_episodes.first() {
- filename = episode.title().unwrap();
- filename.push_str(episode.extension().unwrap());
- path = get_podcast_dir()?;
- path.push(podcast.title());
- path.push(filename);
- if path.exists() {
- launch_player(path.to_str().chain_err(|| UNABLE_TO_CONVERT_TO_STR)?)?;
- } else {
- launch_player(
- episode
- .url()
- .chain_err(|| "unable to retrieve episode url")?,
- )?;
- }
- }
- return Ok(());
- }
+ for subscription in state.subscriptions() {
+ writeln!(&mut handle, "{}", subscription.title())?;
}
Ok(())
}
@@ -442,53 +109,13 @@ pub fn play_episode_by_name(state: &State, p_search: &str, ep_string: &str) -> R
pub fn check_for_update(version: &str) -> Result<()> {
println!("Checking for updates...");
let resp: String =
- reqwest::get("https://raw.githubusercontent.com/njaremko/podcast/master/Cargo.toml")
- .chain_err(|| UNABLE_TO_GET_HTTP_RESPONSE)?
- .text()
- .chain_err(|| "unable to convert response to text")?;
+ reqwest::get("https://raw.githubusercontent.com/njaremko/podcast/master/Cargo.toml")?
+ .text()?;
- let config = resp
- .parse::<toml::Value>()
- .chain_err(|| "unable to parse toml")?;
- let latest = config["package"]["version"]
- .as_str()
- .chain_err(|| UNABLE_TO_CONVERT_TO_STR)?;
+ let config = resp.parse::<toml::Value>()?;
+ let latest = config["package"]["version"].as_str().expect(&format!("Cargo.toml didn't have a version {:?}", config));
if version != latest {
- println!("New version avaliable: {} -> {}", version, latest);
- }
- Ok(())
-}
-
-fn launch_player(url: &str) -> Result<()> {
- if launch_mpv(url).is_err() {
- return launch_vlc(url);
- }
- Ok(())
-}
-
-fn launch_mpv(url: &str) -> Result<()> {
- if let Err(err) = Command::new("mpv")
- .args(&["--audio-display=no", "--ytdl=no", url])
- .status()
- {
- match err.kind() {
- io::ErrorKind::NotFound => {
- eprintln!("Couldn't open mpv\nTrying vlc...");
- }
- _ => eprintln!("Error: {}", err),
- }
- }
- Ok(())
-}
-
-fn launch_vlc(url: &str) -> Result<()> {
- if let Err(err) = Command::new("vlc").args(&["-I ncurses", url]).status() {
- match err.kind() {
- io::ErrorKind::NotFound => {
- eprintln!("Couldn't open vlc...aborting");
- }
- _ => eprintln!("Error: {}", err),
- }
+ println!("New version available: {} -> {}", version, latest);
}
Ok(())
}
@@ -499,7 +126,7 @@ pub fn remove_podcast(state: &mut State, p_search: &str) -> Result<()> {
return delete_all();
}
- let re_pod = Regex::new(&format!("(?i){}", &p_search)).chain_err(|| UNABLE_TO_PARSE_REGEX)?;
+ let re_pod = Regex::new(&format!("(?i){}", &p_search))?;
for subscription in 0..state.subscriptions.len() {
let title = state.subscriptions[subscription].title.clone();
@@ -513,11 +140,23 @@ pub fn remove_podcast(state: &mut State, p_search: &str) -> Result<()> {
pub fn print_completion(app: &mut App, arg: &str) {
match arg {
- "zsh" => app.gen_completions_to("podcast", Shell::Zsh, &mut io::stdout()),
- "bash" => app.gen_completions_to("podcast", Shell::Bash, &mut io::stdout()),
- "powershell" => app.gen_completions_to("podcast", Shell::PowerShell, &mut io::stdout()),
- "fish" => app.gen_completions_to("podcast", Shell::Fish, &mut io::stdout()),
- "elvish" => app.gen_completions_to("podcast", Shell::Elvish, &mut io::stdout()),
- other => eprintln!("Completions are not available for {}", other),
+ "zsh" => {
+ app.gen_completions_to("podcast", Shell::Zsh, &mut io::stdout());
+ }
+ "bash" => {
+ app.gen_completions_to("podcast", Shell::Bash, &mut io::stdout());
+ }
+ "powershell" => {
+ app.gen_completions_to("podcast", Shell::PowerShell, &mut io::stdout());
+ }
+ "fish" => {
+ app.gen_completions_to("podcast", Shell::Fish, &mut io::stdout());
+ }
+ "elvish" => {
+ app.gen_completions_to("podcast", Shell::Elvish, &mut io::stdout());
+ }
+ other => {
+ println!("Completions are not available for {}", other);
+ }
}
}
diff --git a/src/arg_parser.rs b/src/arg_parser.rs
new file mode 100644
index 0000000..8f9650a
--- /dev/null
+++ b/src/arg_parser.rs
@@ -0,0 +1,96 @@
+use clap::{App, ArgMatches};
+
+use std::env;
+use std::path::Path;
+
+use crate::actions::*;
+use crate::download;
+use crate::errors::*;
+use crate::playback;
+use crate::structs::*;
+
+pub fn download(state: &mut State, matches: &ArgMatches) -> Result<()> {
+ let download_matches = matches.subcommand_matches("download").unwrap();
+ let podcast = download_matches.value_of("PODCAST").unwrap();
+ match download_matches.value_of("EPISODE") {
+ Some(ep) => {
+ if String::from(ep).contains(|c| c == '-' || c == ',') {
+ download::download_range(&state, podcast, ep)?
+ } else if download_matches.occurrences_of("name") > 0 {
+ download::download_episode_by_name(
+ &state,
+ podcast,
+ ep,
+ download_matches.occurrences_of("all") > 0,
+ )?
+ } else {
+ download::download_episode_by_num(&state, podcast, ep)?
+ }
+ }
+ None => download::download_all(&state, podcast)?,
+ }
+ Ok(())
+}
+
+pub fn list(state: &mut State, matches: &ArgMatches) -> Result<()> {
+ let list_matches = matches
+ .subcommand_matches("ls")
+ .or_else(|| matches.subcommand_matches("list"))
+ .unwrap();
+ match list_matches.value_of("PODCAST") {
+ Some(regex) => list_episodes(regex)?,
+ None => list_subscriptions(&state)?,
+ }
+ Ok(())
+}
+
+pub fn play(state: &mut State, matches: &ArgMatches) -> Result<()> {
+ let play_matches = matches.subcommand_matches("play").unwrap();
+ let podcast = play_matches.value_of("PODCAST").unwrap();
+ match play_matches.value_of("EPISODE") {
+ Some(episode) => {
+ if play_matches.occurrences_of("name") > 0 {
+ playback::play_episode_by_name(&state, podcast, episode)?
+ } else {
+ playback::play_episode_by_num(&state, podcast, episode)?
+ }
+ }
+ None => playback::play_latest(&state, podcast)?,
+ }
+ Ok(())
+}
+
+pub fn subscribe(state: &mut State, config: &Config, matches: &ArgMatches) -> Result<()> {
+ let subscribe_matches = matches
+ .subcommand_matches("sub")
+ .or_else(|| matches.subcommand_matches("subscribe"))
+ .unwrap();
+ let url = subscribe_matches.value_of("URL").unwrap();
+ state.subscribe(url)?;
+ download::download_rss(&config, url)?;
+ Ok(())
+}
+
+pub fn remove(state: &mut State, matches: &ArgMatches) -> Result<()> {
+ let rm_matches = matches.subcommand_matches("rm").unwrap();
+ let regex = rm_matches.value_of("PODCAST").unwrap();
+ remove_podcast(state, regex)?;
+ Ok(())
+}
+
+pub fn complete(app: &mut App, matches: &ArgMatches) -> Result<()> {
+ let matches = matches.subcommand_matches("completion").unwrap();
+ match matches.value_of("SHELL") {
+ Some(shell) => print_completion(app, shell),
+ None => {
+ let shell_path_env = env::var("SHELL");
+ if let Ok(p) = shell_path_env {
+ let shell_path = Path::new(&p);
+ if let Some(shell) = shell_path.file_name() {
+ print_completion(app, shell.to_str().unwrap())
+ }
+ }
+ }
+ }
+ Ok(())
+}
diff --git a/src/command_handler.rs b/src/command_handler.rs
new file mode 100644
index 0000000..742b79e
--- /dev/null
+++ b/src/command_handler.rs
@@ -0,0 +1,63 @@
+use clap::{App, ArgMatches};
+
+use crate::actions::*;
+use crate::arg_parser;
+use crate::commands;
+use crate::errors::*;
+use crate::structs::*;
+
+pub fn parse_sub_command(matches: &ArgMatches) -> commands::Command {
+ match matches.subcommand_name() {
+ Some("download") => commands::Command::Download,
+ Some("ls") | Some("list") => commands::Command::List,
+ Some("play") => commands::Command::Play,
+ Some("sub") | Some("subscribe") => commands::Command::Subscribe,
+ Some("search") => commands::Command::Search,
+ Some("rm") => commands::Command::Remove,
+ Some("completion") => commands::Command::Complete,
+ Some("refresh") => commands::Command::Refresh,
+ Some("update") => commands::Command::Update,
+ _ => commands::Command::NoMatch,
+ }
+}
+
+pub fn handle_matches(
+ version: &str,
+ state: &mut State,
+ config: &Config,
+ app: &mut App,
+ matches: &ArgMatches,
+) -> Result<()> {
+ let command = parse_sub_command(matches);
+ match command {
+ commands::Command::Download => {
+ arg_parser::download(state, matches)?;
+ }
+ commands::Command::List => {
+ arg_parser::list(state, matches)?;
+ }
+ commands::Command::Play => {
+ arg_parser::play(state, matches)?;
+ }
+ commands::Command::Subscribe => {
+ arg_parser::subscribe(state, config, matches)?;
+ }
+ commands::Command::Search => {
+ println!("This feature is coming soon...");
+ }
+ commands::Command::Remove => {
+ arg_parser::remove(state, matches)?;
+ }
+ commands::Command::Complete => {
+ arg_parser::complete(app, matches)?;
+ }
+ commands::Command::Refresh => {
+ update_rss(state);
+ }
+ commands::Command::Update => {
+ check_for_update(version)?;
+ }
+ _ => (),
+ };
+ Ok(())
+}
diff --git a/src/commands.rs b/src/commands.rs
new file mode 100644
index 0000000..f7c19b6
--- /dev/null
+++ b/src/commands.rs
@@ -0,0 +1,12 @@
+pub enum Command {
+ Download,
+ List,
+ Play,
+ Subscribe,
+ Search,
+ Remove,
+ Complete,
+ Refresh,
+ Update,
+ NoMatch,
+}
diff --git a/src/download.rs b/src/download.rs
new file mode 100644
index 0000000..69dc853
--- /dev/null
+++ b/src/download.rs
@@ -0,0 +1,199 @@
+use crate::structs::*;
+use crate::utils::*;
+
+use std::collections::HashSet;
+use std::fs::File;
+use std::io::{self, BufReader, BufWriter, Write};
+
+use failure::Error;
+use rayon::prelude::*;
+use regex::Regex;
+use reqwest;
+
+pub fn download_range(state: &State, p_search: &str, e_search: &str) -> Result<(), Error> {
+ let re_pod = Regex::new(&format!("(?i){}", &p_search))?;
+
+ for subscription in &state.subscriptions {
+ if re_pod.is_match(&subscription.title) {
+ let podcast = Podcast::from_title(&subscription.title)?;
+ let downloaded = already_downloaded(podcast.title())?;
+ let episodes = podcast.episodes();
+ let episodes_to_download = parse_download_episodes(e_search)?;
+
+ episodes_to_download
+ .par_iter()
+ .map(|ep_num| &episodes[episodes.len() - ep_num])
+ .filter(|e| e.title().is_some())
+ .filter(|e| !downloaded.contains(&e.title().unwrap()))
+ .map(|ep| download(podcast.title(), ep))
+ .flat_map(|e| e.err())
+ .for_each(|err| eprintln!("Error: {}", err));
+ }
+ }
+ Ok(())
+}
+
+pub fn download_episode_by_num(state: &State, p_search: &str, e_search: &str) -> Result<(), Error> {
+ let re_pod = Regex::new(&format!("(?i){}", &p_search))?;
+
+ if let Ok(ep_num) = e_search.parse::<usize>() {
+ for subscription in &state.subscriptions {
+ if re_pod.is_match(&subscription.title) {
+ let podcast = Podcast::from_title(&subscription.title)?;
+ let episodes = podcast.episodes();
+ download(podcast.title(), &episodes[episodes.len() - ep_num])?;
+ }
+ }
+ } else {
+ eprintln!("Failed to parse episode number...\nAttempting to find episode by name...");
+ download_episode_by_name(state, p_search, e_search, false)?;
+ }
+
+ Ok(())
+}
+
+pub fn download(podcast_name: &str, episode: &Episode) -> Result<(), Error> {
+ let mut path = get_podcast_dir()?;
+ path.push(podcast_name);
+ create_dir_if_not_exist(&path)?;
+
+ if let (Some(title), Some(url)) = (episode.title(), episode.url()) {
+ path.push(title);
+ episode.extension().map(|ext| path.set_extension(ext));
+ if !path.exists() {
+ println!("Downloading: {:?}", &path);
+ let resp = reqwest::get(url)?;
+ let file = File::create(&path)?;
+ let mut reader = BufReader::new(resp);
+ let mut writer = BufWriter::new(file);
+ io::copy(&mut reader, &mut writer)?;
+ } else {
+ eprintln!("File already exists: {:?}", &path);
+ }
+ }
+ Ok(())
+}
+
+pub fn download_episode_by_name(
+ state: &State,
+ p_search: &str,
+ e_search: &str,
+ download_all: bool,
+) -> Result<(), Error> {
+ let re_pod = Regex::new(&format!("(?i){}", &p_search))?;
+
+ for subscription in &state.subscriptions {
+ if re_pod.is_match(&subscription.title) {
+ let podcast = Podcast::from_title(&subscription.title)?;
+ let episodes = podcast.episodes();
+ let filtered_episodes =
+ episodes
+ .iter()
+ .filter(|ep| ep.title().is_some())
+ .filter(|ep| {
+ ep.title()
+ .unwrap()
+ .to_lowercase()
+ .contains(&e_search.to_lowercase())
+ });
+
+ if download_all {
+ filtered_episodes
+ .map(|ep| download(podcast.title(), ep))
+ .flat_map(|e| e.err())
+ .for_each(|err| eprintln!("Error: {}", err));
+ } else {
+ filtered_episodes
+ .take(1)
+ .map(|ep| download(podcast.title(), ep))
+ .flat_map(|e| e.err())
+ .for_each(|err| eprintln!("Error: {}", err));
+ }
+ }
+ }
+ Ok(())
+}
+
+pub fn download_all(state: &State, p_search: &str) -> Result<(), Error> {
+ let re_pod = Regex::new(&format!("(?i){}", &p_search))?;
+
+ for subscription in &state.subscriptions {
+ if re_pod.is_match(&subscription.title) {
+ let podcast = Podcast::from_title(&subscription.title)?;
+ print!(
+ "You are about to download all episodes of {} (y/n): ",
+ podcast.title()
+ );
+ io::stdout().flush().ok();
+ let mut input = String::new();
+ io::stdin().read_line(&mut input)?;
+ if input.to_lowercase().trim() != "y" {
+ return Ok(());
+ }
+
+ let mut path = get_podcast_dir()?;
+ path.push(podcast.title());
+
+ already_downloaded(podcast.title()).map(|downloaded| {
+ podcast
+ .episodes()
+ .par_iter()
+ .filter(|e| e.title().is_some())
+ .filter(|e| !downloaded.contains(&e.title().unwrap()))
+ .map(|e| download(podcast.title(), e))
+ .flat_map(|e| e.err())
+ .for_each(|err| eprintln!("Error: {}", err))
+ })?;
+ }
+ }
+ Ok(())
+}
+
+pub fn download_rss(config: &Config, url: &str) -> Result<(), Error> {
+ let channel = download_rss_feed(url)?;
+ let mut download_limit = config.auto_download_limit as usize;
+ if 0 < download_limit {
+ println!(
+ "Subscribe auto-download limit set to: {}\nDownloading episode(s)...",
+ 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()
+ .map(|ep| download(podcast.title(), ep))
+ .flat_map(|e| e.err())
+ .for_each(|err| eprintln!("Error downloading {}: {}", podcast.title(), err));
+ }
+ Ok(())
+}
+
+fn parse_download_episodes(e_search: &str) -> Result<HashSet<usize>, Error> {
+ let input = String::from(e_search);
+ let mut ranges = Vec::<(usize, usize)>::new();
+ let mut elements = HashSet::<usize>::new();
+ let comma_separated: Vec<&str> = input.split(',').collect();
+ for elem in comma_separated {
+ if elem.contains('-') {
+ let range: Vec<usize> = elem
+ .split('-')
+ .map(|i| i.parse::<usize>())
+ .collect::<Result<Vec<usize>, std::num::ParseIntError>>()?;
+ ranges.push((range[0], range[1]));
+ } else {
+ elements.insert(elem.parse::<usize>()?);
+ }
+ }
+
+ for range in ranges {
+ // Include given episode in the download
+ for num in range.0..=range.1 {
+ elements.insert(num);
+ }
+ }
+ Ok(elements)
+}
diff --git a/src/main.rs b/src/main.rs
index b9085bb..bdf913a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,10 +1,11 @@
-#![recursion_limit = "1024"]
+#![feature(impl_trait_in_bindings)]
extern crate chrono;
extern crate clap;
extern crate dirs;
+#[allow(unused_imports)]
#[macro_use]
-extern crate error_chain;
+extern crate failure;
#[macro_use]
extern crate lazy_static;
extern crate rayon;
@@ -17,28 +18,35 @@ extern crate serde_json;
extern crate serde_yaml;
extern crate toml;
-pub mod actions;
-pub mod match_handler;
-pub mod migration_handler;
-pub mod parser;
-pub mod structs;
-pub mod utils;
-pub mod errors {
- error_chain! {}
+mod actions;
+mod arg_parser;
+mod command_handler;
+mod commands;
+mod download;
+mod migration_handler;
+mod parser;
+mod playback;
+mod structs;
+mod utils;
+
+mod errors {
+ use failure::Error;
+ use std::result;
+ pub type Result<T> = result::Result<T, Error>;
}
-use self::errors::*;
use self::structs::*;
+use errors::Result;
-const VERSION: &str = "0.9.1";
+const VERSION: &str = "0.10.0";
fn main() -> Result<()> {
- 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")?;
+ utils::create_directories()?;
+ migration_handler::migrate()?;
+ let mut state = State::new(VERSION)?;
let config = Config::new()?;
let mut app = parser::get_app(&VERSION);
let matches = app.clone().get_matches();
- match_handler::handle_matches(&VERSION, &mut state, &config, &mut app, &matches)?;
- state.save().chain_err(|| "unable to save state")
+ command_handler::handle_matches(&VERSION, &mut state, &config, &mut app, &matches)?;
+ state.save()
}
diff --git a/src/match_handler.rs b/src/match_handler.rs
deleted file mode 100644
index ac09d15..0000000
--- a/src/match_handler.rs
+++ /dev/null
@@ -1,112 +0,0 @@
-use clap::{App, ArgMatches};
-
-use std::env;
-use std::path::Path;
-
-use crate::actions::*;
-use crate::errors::*;
-use crate::structs::*;
-
-pub fn handle_matches(
- version: &str,
- state: &mut State,
- config: &Config,
- app: &mut App,
- 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("sub")
- .or_else(|| 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")?;
- download_rss(&config, 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(app, shell),
- None => {
- let shell_path_env = env::var("SHELL");
- if let Ok(p) = shell_path_env {
- let shell_path = Path::new(&p);
- if let Some(shell) = shell_path.file_name() {
- print_completion(app, shell.to_str().chain_err(|| format!("Unable to convert {:?} to string", shell))?)
- }
- }
- }
- }
- }
- Some("refresh") => update_rss(state),
- Some("update") => check_for_update(version)?,
- _ => (),
- };
- Ok(())
-}
diff --git a/src/migration_handler.rs b/src/migration_handler.rs
index 73a0241..9f8fbc4 100644
--- a/src/migration_handler.rs
+++ b/src/migration_handler.rs
@@ -2,15 +2,18 @@ use crate::errors::*;
use crate::utils::*;
use std::fs;
-pub fn migrate_old_subscriptions() -> Result<()> {
+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))?;
+ fs::rename(&old_path, &new_path)?;
}
Ok(())
}
+
+pub fn migrate() -> Result<()> {
+ migrate_old_subscriptions()
+}
diff --git a/src/playback.rs b/src/playback.rs
new file mode 100644
index 0000000..05196dd
--- /dev/null
+++ b/src/playback.rs
@@ -0,0 +1,183 @@
+use crate::errors::*;
+use crate::structs::*;
+use crate::utils::*;
+
+use std::fs::{DirBuilder, File};
+use std::io::{self, BufReader, Read, Write};
+use std::process::Command;
+
+use regex::Regex;
+use rss::Channel;
+use std::path::PathBuf;
+
+fn launch_player(url: &str) -> Result<()> {
+ if launch_mpv(url).is_err() {
+ return launch_vlc(url);
+ }
+ Ok(())
+}
+
+fn launch_mpv(url: &str) -> Result<()> {
+ if let Err(err) = Command::new("mpv")
+ .args(&["--audio-display=no", "--ytdl=no", url])
+ .status()
+ {
+ let stderr = io::stderr();
+ let mut handle = stderr.lock();
+ match err.kind() {
+ io::ErrorKind::NotFound => {
+ writeln!(&mut handle, "Couldn't open mpv\nTrying vlc...").ok()
+ }
+ _ => writeln!(&mut handle, "Error: {}", err).ok(),
+ };
+ }
+ Ok(())
+}
+
+fn launch_vlc(url: &str) -> Result<()> {
+ if let Err(err) = Command::new("vlc").args(&["-I ncurses", url]).status() {
+ let stderr = io::stderr();
+ let mut handle = stderr.lock();
+ match err.kind() {
+ io::ErrorKind::NotFound => writeln!(&mut handle, "Couldn't open vlc...aborting").ok(),
+ _ => writeln!(&mut handle, "Error: {}", err).ok(),
+ };
+ }
+ Ok(())
+}
+
+pub fn play_latest(state: &State, p_search: &str) -> Result<()> {
+ let re_pod: Regex = Regex::new(&format!("(?i){}", &p_search))?;
+ let mut path: PathBuf = get_xml_dir()?;
+ DirBuilder::new().recursive(true).create(&path)?;
+ for subscription in &state.subscriptions {
+ if re_pod.is_match(&subscription.title) {
+ let mut filename: String = subscription.title.clone();
+ filename.push_str(".xml");
+ path.push(filename);
+
+ let file: File = File::open(&path)?;
+ let podcast: Podcast = Podcast::from(Channel::read_from(BufReader::new(file))?);
+ let episodes = podcast.episodes();
+ let episode = episodes[0].clone();
+
+ filename = episode.title().unwrap();
+ filename.push_str(&episode.extension().unwrap());
+ path = get_podcast_dir()?;
+ path.push(podcast.title());
+ path.push(filename);
+ if path.exists() {
+ launch_player(path.to_str().unwrap())?;
+ } else {
+ launch_player(episode.url().unwrap())?;
+ }
+ return Ok(());
+ }
+ }
+ Ok(())
+}
+
+pub fn play_episode_by_num(state: &State, p_search: &str, ep_num_string: &str) -> Result<()> {
+ let re_pod: Regex = Regex::new(&format!("(?i){}", &p_search))?;
+ if let Ok(ep_num) = ep_num_string.parse::<usize>() {
+ let mut path: PathBuf = get_xml_dir()?;
+ let stderr = io::stderr();
+ let mut handle = stderr.lock();
+ if let Err(err) = DirBuilder::new().recursive(true).create(&path) {
+ writeln!(
+ &mut handle,
+ "Couldn't create directory: {}\nReason: {}",
+ path.to_str().unwrap(),
+ err
+ )
+ .ok();
+ return Ok(());
+ }
+ for subscription in &state.subscriptions {
+ if re_pod.is_match(&subscription.title) {
+ let mut filename: String = subscription.title.clone();
+ filename.push_str(".xml");
+ path.push(filename);
+
+ let file: File = File::open(&path).unwrap();
+ let podcast = Podcast::from(Channel::read_from(BufReader::new(file)).unwrap());
+ let episodes = podcast.episodes();
+ let episode = episodes[episodes.len() - ep_num].clone();
+
+ filename = episode.title().unwrap();
+ filename.push_str(&episode.extension().unwrap());
+ path = get_podcast_dir()?;
+ path.push(podcast.title());
+ path.push(filename);
+ if path.exists() {
+ launch_player(path.to_str().unwrap())?;
+ } else {
+ launch_player(episode.url().unwrap())?;
+ }
+ return Ok(());
+ }
+ }
+ } else {
+ {
+ let stdout = io::stdout();
+ let mut handle = stdout.lock();
+ 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)?;
+ }
+ Ok(())
+}
+
+pub fn play_episode_by_name(state: &State, p_search: &str, ep_string: &str) -> Result<()> {
+ let re_pod: Regex = Regex::new(&format!("(?i){}", &p_search))?;
+ let mut path: PathBuf = get_xml_dir()?;
+ if let Err(err) = DirBuilder::new().recursive(true).create(&path) {
+ let stderr = io::stderr();
+ let mut handle = stderr.lock();
+ writeln!(
+ &mut handle,
+ "Couldn't create directory: {:?}\nReason: {}",
+ path, err
+ )
+ .ok();
+ return Ok(());
+ }
+ for subscription in &state.subscriptions {
+ if re_pod.is_match(&subscription.title) {
+ let mut filename: String = subscription.title.clone();
+ filename.push_str(".xml");
+ path.push(filename);
+
+ let mut file: File = File::open(&path).unwrap();
+ let mut content: Vec<u8> = Vec::new();
+ file.read_to_end(&mut content).unwrap();
+
+ let podcast = Podcast::from(Channel::read_from(content.as_slice()).unwrap());
+ let episodes = podcast.episodes();
+ let filtered_episodes: Vec<&Episode> = episodes
+ .iter()
+ .filter(|ep| {
+ ep.title()
+ .unwrap_or_else(|| "".to_string())
+ .to_lowercase()
+ .contains(&ep_string.to_lowercase())
+ })
+ .collect();
+ if let Some(episode) = filtered_episodes.first() {
+ filename = episode.title().unwrap();
+ filename.push_str(&episode.extension().unwrap());
+ path = get_podcast_dir()?;
+ path.push(podcast.title());
+ path.push(filename);
+ if path.exists() {
+ launch_player(path.to_str().unwrap())?;
+ } else {
+ launch_player(episode.url().unwrap())?;
+ }
+ }
+ return Ok(());
+ }
+ }
+ Ok(())
+}
diff --git a/src/structs.rs b/src/structs.rs
index 1c3ceaf..2524034 100644
--- a/src/structs.rs
+++ b/src/structs.rs
@@ -1,30 +1,46 @@
use super::actions::*;
use super::utils::*;
use crate::errors::*;
+use core::ops::Deref;
-use std::collections::BTreeSet;
+use std::collections::HashSet;
use std::fs::{self, File};
-use std::io::{self, BufReader, Write};
+use std::io::{self, BufReader, BufWriter, Write};
use chrono::prelude::*;
-use rayon::prelude::*;
use regex::Regex;
use rss::{Channel, Item};
use serde_json;
use std::path::PathBuf;
#[cfg(target_os = "macos")]
-static ESCAPE_REGEX: &str = r"/";
+const ESCAPE_REGEX: &str = r"/";
#[cfg(target_os = "linux")]
-static ESCAPE_REGEX: &str = r"/";
+const ESCAPE_REGEX: &str = r"/";
#[cfg(target_os = "windows")]
-static ESCAPE_REGEX: &str = r#"[\\/:*?"<>|]"#;
+const ESCAPE_REGEX: &str = r#"[\\/:*?"<>|]"#;
lazy_static! {
static ref FILENAME_ESCAPE: Regex = Regex::new(ESCAPE_REGEX).unwrap();
}
-#[derive(Debug, PartialEq, Serialize, Deserialize)]
+fn create_new_config_file(path: &PathBuf) -> Result<Config> {
+ writeln!(
+ io::stdout().lock(),
+ "Creating new config file at {:?}",
+ &path
+ )
+ .ok();
+ let download_limit = 1;
+ let file = File::create(&path)?;
+ let config = Config {
+ auto_download_limit: download_limit,
+ };
+ serde_yaml::to_writer(file, &config)?;
+ Ok(config)
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Config {
pub auto_download_limit: i64,
}
@@ -34,16 +50,21 @@ impl Config {
let mut path = get_podcast_dir()?;
path.push(".config.yaml");
let config = if path.exists() {
- let file = File::open(&path).chain_err(|| UNABLE_TO_OPEN_FILE)?;
+ let file = File::open(&path)?;
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...")?;
+ let stderr = io::stderr();
+ let mut handle = stderr.lock();
+ writeln!(
+ &mut handle,
+ "{}\nFailed to open config file, moving to {:?}",
+ err, &new_path
+ )
+ .ok();
+ fs::rename(&path, new_path)?;
create_new_config_file(&path)?
}
}
@@ -54,17 +75,6 @@ impl Config {
}
}
-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,
@@ -72,6 +82,12 @@ pub struct Subscription {
pub num_episodes: usize,
}
+impl Subscription {
+ pub fn title(&self) -> &str {
+ &self.title
+ }
+}
+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct State {
pub version: String,
@@ -83,24 +99,8 @@ impl State {
pub fn new(version: &str) -> Result<State> {
let path = get_sub_file()?;
if path.exists() {
- 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_reader(&file)
- .chain_err(|| "unable to read json from string")?;
- State {
- version: String::from(version),
- last_run_time: Utc::now(),
- subscriptions: match serde_json::from_value(v["subscriptions"].clone()) {
- Ok(val) => val,
- Err(_) => serde_json::from_value(v["subs"].clone())
- .chain_err(|| "unable to parse value from json")?,
- },
- }
- }
- };
+ let file = File::open(&path)?;
+ let mut state: State = serde_json::from_reader(BufReader::new(&file))?;
state.version = String::from(version);
// Check if a day has passed (86400 seconds) since last launch
if 86400
@@ -115,7 +115,7 @@ impl State {
state.save()?;
Ok(state)
} else {
- println!("Creating new file {:?}", &path);
+ writeln!(io::stdout().lock(), "Creating new file: {:?}", &path).ok();
Ok(State {
version: String::from(version),
last_run_time: Utc::now(),
@@ -124,12 +124,20 @@ impl State {
}
}
+ pub fn subscriptions(&self) -> &[Subscription] {
+ &self.subscriptions
+ }
+
+ pub fn subscriptions_mut(&mut self) -> &mut [Subscription] {
+ &mut self.subscriptions
+ }
+
pub fn subscribe(&mut self, url: &str) -> Result<()> {
- let mut set = BTreeSet::new();
+ let mut set = HashSet::new();
for sub in self.subscriptions() {
set.insert(sub.title.clone());
}
- let podcast = Podcast::from(Channel::from_url(url).unwrap());
+ let podcast = Podcast::from(Channel::from_url(url)?);
if !set.contains(podcast.title()) {
self.subscriptions.push(Subscription {
title: String::from(podcast.title()),
@@ -140,45 +148,30 @@ impl State {
self.save()
}
- 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_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)?;
- }
- 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))?;
+ let file = File::create(&path)?;
+ serde_json::to_writer(BufWriter::new(file), self)?;
+ fs::rename(&path, get_sub_file()?)?;
Ok(())
}
}
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq)]
pub struct Podcast(Channel);
-#[derive(Clone, Debug)]
-pub struct Episode(Item);
-
impl From<Channel> for Podcast {
fn from(channel: Channel) -> Podcast {
Podcast(channel)
}
}
-impl From<Item> for Episode {
- fn from(item: Item) -> Episode {
- Episode(item)
+impl Deref for Podcast {
+ type Target = Channel;
+
+ fn deref(&self) -> &Channel {
+ &self.0
}
}
@@ -194,9 +187,7 @@ impl Podcast {
#[allow(dead_code)]
pub fn from_url(url: &str) -> Result<Podcast> {
- Ok(Podcast::from(
- Channel::from_url(url).chain_err(|| UNABLE_TO_CREATE_CHANNEL_FROM_RESPONSE)?,
- ))
+ Ok(Podcast::from(Channel::from_url(url)?))
}
pub fn from_title(title: &str) -> Result<Podcast> {
@@ -205,11 +196,8 @@ impl Podcast {
filename.push_str(".xml");
path.push(filename);
- let file = File::open(&path).chain_err(|| UNABLE_TO_OPEN_FILE)?;
- Ok(Podcast::from(
- Channel::read_from(BufReader::new(file))
- .chain_err(|| UNABLE_TO_CREATE_CHANNEL_FROM_FILE)?,
- ))
+ let file = File::open(&path)?;
+ Ok(Podcast::from(Channel::read_from(BufReader::new(file))?))
}
pub fn episodes(&self) -> Vec<Episode> {
@@ -219,65 +207,14 @@ impl Podcast {
}
result
}
+}
- pub fn download(&self) -> Result<()> {
- print!(
- "You are about to download all episodes of {} (y/n): ",
- self.title()
- );
- io::stdout().flush().ok();
- let mut input = String::new();
- io::stdin()
- .read_line(&mut input)
- .chain_err(|| "unable to read stdin")?;
- if input.to_lowercase().trim() != "y" {
- return Ok(());
- }
-
- let mut path = get_podcast_dir()?;
- path.push(self.title());
-
- match already_downloaded(self.title()) {
- Ok(downloaded) => {
- self.episodes().par_iter().for_each(|i| {
- if let Some(ep_title) = i.title() {
- if !downloaded.contains(&ep_title) {
- if let Err(err) = download(self.title(), i) {
- eprintln!("{}", err);
- }
- }
- }
- });
- }
- Err(_) => {
- self.episodes().par_iter().for_each(|i| {
- if let Err(err) = download(self.title(), i) {
- eprintln!("{}", err);
- }
- });
- }
- }
-
- Ok(())
- }
-
- pub fn download_specific(&self, episode_numbers: &[usize]) -> Result<()> {
- let mut path = get_podcast_dir()?;
- path.push(self.title());
-
- let downloaded = already_downloaded(self.title())?;
- let episodes = self.episodes();
+#[derive(Clone, Debug, PartialEq)]
+pub struct Episode(Item);
- 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) = download(self.title(), &episodes[episodes.len() - ep_num]) {
- eprintln!("{}", err);
- }
- }
- }
- });
- Ok(())
+impl From<Item> for Episode {
+ fn from(item: Item) -> Episode {
+ Episode(item)
}
}
@@ -297,11 +234,14 @@ impl Episode {
}
}
- pub fn extension(&self) -> Option<&str> {
+ pub fn extension(&self) -> Option<String> {
match self.0.enclosure()?.mime_type() {
- "audio/mpeg" => Some(".mp3"),
- "audio/mp4" => Some(".m4a"),
- "audio/ogg" => Some(".ogg"),
+ "audio/mpeg" => Some("mp3".into()),
+ "audio/mp4" => Some("m4a".into()),
+ "audio/aac" => Some("m4a".into()),
+ "audio/ogg" => Some("ogg".into()),
+ "audio/vorbis" => Some("ogg".into()),
+ "audio/opus" => Some("opus".into()),
_ => find_extension(self.url().unwrap()),
}
}
diff --git a/src/utils.rs b/src/utils.rs
index f495805..e00df98 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,7 +1,7 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, DirBuilder, File};
-use std::io::{BufReader, Read, Write};
+use std::io::{self, BufReader, Read, Write};
use std::path::PathBuf;
use crate::errors::*;
@@ -9,24 +9,6 @@ use dirs;
use reqwest;
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";
-pub const UNABLE_TO_READ_ENTRY: &str = "unable to read entry";
-pub const UNABLE_TO_CREATE_DIRECTORY: &str = "unable to create directory";
-pub const UNABLE_TO_READ_RESPONSE_TO_END: &str = "unable to read response to end";
-pub const UNABLE_TO_GET_HTTP_RESPONSE: &str = "unable to get http response";
-pub const UNABLE_TO_CONVERT_TO_STR: &str = "unable to convert to &str";
-pub const UNABLE_TO_REMOVE_FILE: &str = "unable to remove file";
-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";
-
const UNSUBSCRIBE_NOTE: &str = "Note: this does NOT delete any downloaded podcasts";
pub fn trim_extension(filename: &str) -> Option<String> {
@@ -35,28 +17,19 @@ pub fn trim_extension(filename: &str) -> Option<String> {
Some(String::from(&name[0..index]))
}
-pub fn find_extension(input: &str) -> Option<&str> {
- let tmp = String::from(input);
- if tmp.ends_with(".mp3") {
- Some(".mp3")
- } else if tmp.ends_with(".m4a") {
- Some(".m4a")
- } else if tmp.ends_with(".wav") {
- Some(".wav")
- } else if tmp.ends_with(".ogg") {
- Some(".ogg")
- } else if tmp.ends_with(".opus") {
- Some(".opus")
- } else {
- None
+pub fn find_extension(input: &str) -> Option<String> {
+ let s: Vec<String> = input.split(".").map(|s| s.to_string()).collect();
+ if s.len() > 1 {
+ return s.last().cloned();
}
+ None
}
pub fn get_podcast_dir() -> Result<PathBuf> {
match env::var_os("PODCAST") {
Some(val) => Ok(PathBuf::from(val)),
None => {
- let mut path = dirs::home_dir().chain_err(|| "Couldn't find your home directory")?;
+ let mut path = dirs::home_dir().unwrap();
path.push("Podcasts");
Ok(path)
}
@@ -64,15 +37,13 @@ pub fn get_podcast_dir() -> Result<PathBuf> {
}
pub fn create_dir_if_not_exist(path: &PathBuf) -> Result<()> {
- DirBuilder::new()
- .recursive(true)
- .create(&path)
- .chain_err(|| UNABLE_TO_CREATE_DIRECTORY)?;
+ DirBuilder::new().recursive(true).create(&path)?;
Ok(())
}
pub fn create_directories() -> Result<()> {
let mut path = get_podcast_dir()?;
+ writeln!(io::stdout().lock(), "Using PODCAST dir: {:?}", &path).ok();
path.push(".rss");
create_dir_if_not_exist(&path)
}
@@ -84,14 +55,15 @@ pub fn delete(title: &str) -> Result<()> {
path.push(filename);
println!("Removing '{}' from subscriptions...", &title);
println!("{}", UNSUBSCRIBE_NOTE);
- fs::remove_file(path).chain_err(|| UNABLE_TO_REMOVE_FILE)
+ fs::remove_file(path)?;
+ Ok(())
}
-
pub fn delete_all() -> Result<()> {
println!("Removing all subscriptions...");
println!("{}", UNSUBSCRIBE_NOTE);
- fs::remove_dir_all(get_xml_dir()?).chain_err(|| UNABLE_TO_READ_DIRECTORY)
+ fs::remove_dir_all(get_xml_dir()?)?;
+ Ok(())
}
pub fn already_downloaded(dir: &str) -> Result<HashSet<String>> {
@@ -100,12 +72,12 @@ pub fn already_downloaded(dir: &str) -> Result<HashSet<String>> {
let mut path = get_podcast_dir()?;
path.push(dir);
- let entries = fs::read_dir(path).chain_err(|| "unable to read directory")?;
+ let entries = fs::read_dir(path)?;
for entry in entries {
- let entry = entry.chain_err(|| "unable to read entry")?;
+ let entry = entry?;
match entry.file_name().into_string() {
Ok(name) => {
- let index = name.find('.').chain_err(|| "unable to find string index")?;
+ let index = name.find('.').unwrap();
result.insert(String::from(&name[0..index]));
}
Err(_) => {
@@ -136,85 +108,55 @@ pub fn download_rss_feed(url: &str) -> Result<Channel> {
let mut path = get_podcast_dir()?;
path.push(".rss");
create_dir_if_not_exist(&path)?;
- let mut resp = reqwest::get(url).chain_err(|| "unable to open url")?;
+ let mut resp = reqwest::get(url)?;
let mut content: Vec<u8> = Vec::new();
- resp.read_to_end(&mut content)
- .chain_err(|| "unable to read http response to end")?;
- let channel = Channel::read_from(BufReader::new(&content[..]))
- .chain_err(|| "unable to create channel from xml http response")?;
+ resp.read_to_end(&mut content)?;
+ let channel = Channel::read_from(BufReader::new(&content[..]))?;
let mut filename = String::from(channel.title());
filename.push_str(".xml");
path.push(filename);
- let mut file = File::create(&path).chain_err(|| "unable to create file")?;
- file.write_all(&content)
- .chain_err(|| "unable to write file")?;
+ let mut file = File::create(&path)?;
+ file.write_all(&content)?;
Ok(channel)
}
-pub fn parse_download_episodes(e_search: &str) -> Result<Vec<usize>> {
- let input = String::from(e_search);
- let mut ranges = Vec::<(usize, usize)>::new();
- let mut elements = Vec::<usize>::new();
- let comma_separated: Vec<&str> = input.split(',').collect();
- for elem in comma_separated {
- let temp = String::from(elem);
- if temp.contains('-') {
- let range: Vec<usize> = elem
- .split('-')
- .map(|i| i.parse::<usize>().chain_err(|| "unable to parse number"))
- .collect::<Result<Vec<usize>>>()
- .chain_err(|| "unable to collect ranges")?;
- ranges.push((range[0], range[1]));
- } else {
- elements.push(
- elem.parse::<usize>()
- .chain_err(|| "unable to parse number")?,
- );
- }
- }
-
- for range in ranges {
- // Add 1 to upper range to include given episode in the download
- for num in range.0..=range.1 {
- elements.push(num);
- }
- }
- elements.dedup();
- Ok(elements)
-}
-
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_extension_mp3() {
- assert_eq!(find_extension("test.mp3"), Some(".mp3"))
+ assert_eq!(find_extension("test.mp3"), Some("mp3".into()))
}
#[test]
fn test_find_extension_m4a() {
- assert_eq!(find_extension("test.m4a"), Some(".m4a"))
+ assert_eq!(find_extension("test.m4a"), Some("m4a".into()))
}
#[test]
fn test_find_extension_wav() {
- assert_eq!(find_extension("test.wav"), Some(".wav"))
+ assert_eq!(find_extension("test.wav"), Some("wav".into()))
}
#[test]
fn test_find_extension_ogg() {
- assert_eq!(find_extension("test.ogg"), Some(".ogg"))
+ assert_eq!(find_extension("test.ogg"), Some("ogg".into()))
}
#[test]
fn test_find_extension_opus() {
- assert_eq!(find_extension("test.opus"), Some(".opus"))
+ assert_eq!(find_extension("test.opus"), Some("opus".into()))
+ }
+
+ #[test]
+ fn test_find_weird_extension() {
+ assert_eq!(find_extension("test.taco"), Some("taco".into()))
}
#[test]
- fn test_find_extension_invalid() {
- assert_eq!(find_extension("test.taco"), None)
+ fn test_find_no_extension() {
+ assert_eq!(find_extension("test"), None)
}
#[test]