DP6ASS5FJGSZUG2B4SQOKZVHFAVZLEHFVEWBNFG3BHMC6FZAJNHQC
QG7TWI4VFXEVO3WTXGUPKUE5V5VKEJBWDGIE3IWM2G5L7RQRR2AAC
OX53NPUOF2CYET46DF6Y22DVQSKQKR2KW3TGYDAXILEBYVJO2EPQC
L3I4GC7R74HPQS3VCQ44UTBAKKBOW6UDVXV2EF7XSWH7H2Z3RRJQC
FBQOBNZ6JJQXSHYQK7MCFA4U7NBNB47FXED7Y7HPRTOQVXJFIAGAC
XIHPYOWDLQY2MVMVUQPH23O3TBALRG4G2CHSLWSCAYMY5NVJ32WQC
IFBRAMVLQ4Z6BAEMWDIXD2V5HSZK4DHRWYZNB32IBY7ZRTNZJVCQC
O53GR2OQHGRKAVJT2RVPRHYFB54W5LM4DQYT7EYVGKU7HDK5CJJQC
FRLZDOAN7A3N623TLAPO66JVWBLBI45AG6P3DMQMDKGEZ2OBPYAAC
UIMZBURR7KOWSREO4GDH5C2LZDUTEZBKQNYWBYSFGUTRYJ4GKSNQC
JYSIHNS67XTGAR4HN7ZHWFMGGYSK5IY6J6EHO4YUZOR7UMMWAORQC
3YR56Y65UIAL3J7PUXWVJMOOHYZYDIX4V54OT2TJPZ25WQ6MXHCQC
2CKX4R6ONNXDXGRYZ5NZEBJZFX5Z6BYPGNJ7LMXUHHFB4MUFJRCAC
5JMYBRF3UYX4LFH7JK6S4BEDKRVKDFIL4YKTCWKMKP4TMNNGQFKQC
NE63ERXN7OUYSQ4PGPIQIIKEYD7OAOWXXSGMTORD6RJNUZHRLVJAC
ZD3G3BCXBEXELHH3KMXMDUQUJ4BXIF4ZSZKFIJJCRO7IDK4XPOWAC
Y6BVNXQ747WQKVB4JO3AHYOMBWNNJNPC6SG2UUCCZRIIXDMXJYDQC
JDSHG6Q3GBJBEMIQRIDLYSCRUIH6PZD5P2DF6VGXAW5RRZAK73FQC
NUYITHNW2PMOOUBX2ZT3JHSK3MQUFP4W3GEZ5HFJFDOMT56R4QSAC
DSOQZYPHBDUFWKMMDXJZCHLPP7GRHW6PS7EGTF42M6OKZCEXSY3AC
EZMX4SYFEBYNJVQETRVAYONU5MIMQYTTSA5DRMTQET5B7CL6CI6AC
ZYS43ILR4OXI7S2AYNGYSTK3IU2UVELIWVCCWDS7RVZQDSNJMDHQC
BSPWOOHZMN3RAOHGJ2A3XKUOUCFFAOXS7YR67E3AARPPPIA5YPDAC
O3ZP3X5CVIYUAPZJFQD7E4LBE4MA2K4DS5WABI4KWKOG3POBJ2YQC
VRLKU5J6FEMVVZIK5ABNDM5KYIGJ56YFJW52CBVTW3EH35FOHU6AC
S3YILTUU7CUF6AOV2OHCCXIR37C6B5LTFW4OHJNHEJNMA7UOFKBAC
EQUMHANDFLYXP666ZZ4ZSNASEDMA6MP4RZQQZNWUR27WGXGINUQAC
RNYYKYFGUE2PWL6MFY5MN7OLB5UZNXQ6EEFM7HROMLYIK6OSC5IQC
OWYYKTVHXMHHF6B44IN2SMYQ4MJ46LSKQT3OYPHONLQ6R7TSRMTAC
L7S4333LAJBFHRBEI5KB27CQJ3VVWSFZ6HOJ4FPFDDNAE4PUH6GAC
ZSO6T3AM36JCKVGEZEIC6NA7WWESSW73TVM67W3BOWUPOSBHZEFQC
ADXMUSFXOKBPCHW4XS3T4KLWMPGQPYDMZW6YFFSHZPAQKEGIKCBQC
TE6ACJLE7GUBYSV7B77L5YFKUMTV2PYR7CQRKG6A3XZ6YZWBR7TAC
USEXBPODUNF4Y7KLGC7SFQZAARXFMTAHK4OKO6HR7G3UPZY4C42QC
ORRUAQNB2SPMP67NJKXTJRMWOXVNOONLWE7VXTU2EH726QV3SV2AC
GE7XXDPC73SUY6I6F3X6I4QFQO2BCPHD4MZALYOWG7H7SRE5XAFQC
UX4YE27CC5I4OFW5DMEKHTIGUUGMCAM6X5S4NXPTS7XK4MJLUKHQC
GG5VBVLAQAQFIPOQGYSCC2T5RXXQMDPKXZFLCYGTUNE5DLRQYFDQC
M4FCDZ745GHHL3OLH64EVYOEOEGGGVIBCVFGX5JUJDJRE5OLCXLQC
SBPKWZNQF5BWAJ7SZHWVK5BG6DTVJNDYND6UG5PDZCWZ2W4W2HXQC
6FJACP6KUOZ4HWK4PSS5PFPGDYXZSCAWSKIARWBDGCZTPJWXA62AC
THSENT35O3PIXQ343QPPE3DJGR4YVULN6YPS5ETW5PXSVGZZQIZAC
HSDBPX2AMUS4NRA52EHIYOR7H37ABNGJWBJKPQABFMFDU7EITSIAC
JJ4SMY257MAHSJSZH5PJZMLBH3GJX5VKH2ZZSBGWLL7FWP7OA7TQC
J64KBLKALQ3HQCY4HJU5H6WBXTATS7TKBYNNUUSNJE7JLWLYO66QC
476KTQSS5NXVCTVLVZQRGSYD5OAFBYG75VTSWBN26Q45RSMRT5YQC
5GQNHICLSFAA7ZUFXUCNACCPAIIGK4DV2QPTONDNXLS4TJJTOFHAC
TV3GOKIHRVTPGEFYLMTNFQYZBNJRL46ZLOKSQACHVIGW4IWOMDFQC
MQKOX2CQ7AON24UJC7RORAC7Y2UROROVG7BBKLVWURPXKY75JV5AC
76TBVFPIFU3LSMXY5NAHZBH6HRJLSLK43PGOPL6QQ2YYVBJ64QAQC
5FEMSWRS6SMVKBJAV4IYJLEJ2CML6QNZM75UGQFIIMAR5FBAACXAC
DQ3IKTJCRSC74OONPXMNOMRGCV4MOAYQIMM7IQD6Z7KFJGP6MG7AC
EEJ6CBJRTXLPQP44I2RLWVLJBX565DXXAWU4JIWNA3MMNE7WB5LQC
JPN37V6Q35ZAW7A2DTGX2WJ3IJ66BAGHXHWXOGHQRHGFAOETFQ7AC
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)?;
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();
}
}
}
const VERSION: &str = "0.7.5";
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)?;
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(())
}
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(())
}
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()
}
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)?
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)]
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) {
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
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))?;
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(())
}