From c8386aa0b9ccc6bf167cef769c4a5d8cfb86e3ef Mon Sep 17 00:00:00 2001 From: njaremko Date: Mon, 17 Jul 2017 18:10:12 -0400 Subject: More features --- src/actions.rs | 26 +++++++++++ src/main.rs | 86 +++++++++++++++++++++++++++------- src/structs.rs | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- src/utils.rs | 34 ++++++++++++++ 4 files changed, 268 insertions(+), 22 deletions(-) create mode 100644 src/actions.rs create mode 100644 src/utils.rs (limited to 'src') diff --git a/src/actions.rs b/src/actions.rs new file mode 100644 index 0000000..0f8d337 --- /dev/null +++ b/src/actions.rs @@ -0,0 +1,26 @@ +use regex::Regex; +use structs::*; + +pub fn list_episodes(state: State, search: &str) { + let re = Regex::new(&search).unwrap(); + for podcast in state.subscriptions() { + if re.is_match(&podcast.name) { + println!("Episodes for {}:", &podcast.name); + match Podcast::from_url(&podcast.url) { + Ok(podcast) => { + for title in podcast.list_episodes() { + println!("{}", title) + } + } + Err(err) => println!("{}", err), + } + + } + } +} + +pub fn list_subscriptions(state: State) { + for podcast in state.subscriptions() { + println!("{}", podcast.name); + } +} diff --git a/src/main.rs b/src/main.rs index 8c0ec89..05c8bf2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,80 @@ extern crate rss; +extern crate regex; +extern crate reqwest; +extern crate serde; +extern crate serde_json; +#[macro_use] +extern crate serde_derive; +extern crate clap; +mod actions; mod structs; +mod utils; -use std::fs::File; -use std::io::BufReader; -use rss::{Channel, Item}; +use actions::*; use structs::*; +use clap::{Arg, App, SubCommand}; fn main() { - let file = File::open("rss.xml").unwrap(); - let channel = Channel::read_from(BufReader::new(file)).unwrap(); - let podcast = Podcast::from(channel); + let mut state = State::new(); - for title in podcast.list_titles() { - println!("{}", title); - } - let ep = &podcast.episodes()[0]; - println!( - "{}", - match ep.download_url() { - Some(val) => val, - None => "", + let matches = App::new("podcast") + .version("1.0") + .author("Nathan J. ") + .about("Does awesome things") + .subcommand( + SubCommand::with_name("list") + .about("list episodes of podcast") + .arg( + Arg::with_name("PODCAST") + .help("Regex for subscribed podcast") + //.required(true) + .index(1), + ), + ) + .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), + ), + ) + .subcommand( + SubCommand::with_name("update").about("update subscribed podcasts"), + ) + .get_matches(); + + match matches.subcommand_name() { + Some("list") => { + let list_matches = matches.subcommand_matches("list").unwrap(); + match list_matches.value_of("PODCAST") { + Some(regex) => list_episodes(state, regex), + None => list_subscriptions(state), + } } - ); + Some("subscribe") => { + state.subscribe( + matches + .subcommand_matches("subscribe") + .unwrap() + .value_of("URL") + .unwrap(), + ) + } + Some("search") => (), + Some("update") => (), + _ => (), + } } diff --git a/src/structs.rs b/src/structs.rs index 626072b..5e50307 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,6 +1,64 @@ -use std::fs::File; -use std::io::BufReader; -use rss::{Channel, Item}; +use reqwest; +use rss::{self, Channel, Item}; +use std::fs::{DirBuilder, File}; +use std::io::{self, Read, Write}; +use utils::*; +use serde_json; +use std::collections::BTreeSet; + +#[derive(Serialize, Deserialize, Clone)] +pub struct Subscription { + pub name: String, + pub url: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct State(Vec); + +impl State { + pub fn new() -> State { + let mut path = get_podcast_dir(); + path.push(".subscriptions"); + if path.exists() { + let mut s = String::new(); + File::open(&path).unwrap().read_to_string(&mut s).unwrap(); + serde_json::from_str(&s).unwrap() + } else { + State(Vec::new()) + } + } + + pub fn subscribe(&mut self, url: &str) { + let mut set = BTreeSet::new(); + for sub in self.subscriptions() { + set.insert(sub.url); + } + if !set.contains(url) { + let channel = Channel::from_url(url).unwrap(); + self.0.push(Subscription { + name: String::from(channel.title()), + url: String::from(url), + }); + } + match self.save() { + Err(err) => println!("{}", err), + _ => (), + } + } + + pub fn subscriptions(&self) -> Vec { + self.0.clone() + } + + pub fn save(&self) -> Result<(), io::Error> { + let mut path = get_podcast_dir(); + path.push(".subscriptions"); + let serialized = serde_json::to_string(self)?; + let mut file = File::create(&path)?; + file.write_all(serialized.as_bytes())?; + Ok(()) + } +} pub struct Podcast(Channel); @@ -19,6 +77,21 @@ impl From for Episode { } impl Podcast { + pub fn title(&self) -> &str { + self.0.title() + } + + pub fn url(&self) -> &str { + self.0.link() + } + + pub fn from_url(url: &str) -> Result { + match Channel::from_url(url) { + Ok(val) => Ok(Podcast::from(val)), + Err(err) => Err(err), + } + } + pub fn episodes(&self) -> Vec { let mut result = Vec::new(); @@ -29,8 +102,7 @@ impl Podcast { result } - - pub fn list_titles(&self) -> Vec<&str> { + pub fn list_episodes(&self) -> Vec<&str> { let mut result = Vec::new(); let items = self.0.items(); @@ -42,13 +114,73 @@ impl Podcast { } result } + + pub fn download(&self) { + let mut path = get_podcast_dir(); + path.push(self.title()); + + DirBuilder::new().recursive(true).create(path).unwrap(); + + let downloaded = already_downloaded(self.title()); + + for ep in self.episodes() { + if let Some(ep_title) = ep.title() { + if !downloaded.contains(ep_title) { + match ep.download(self.title()) { + Err(err) => println!("{}", err), + _ => (), + } + } + } + } + } } impl Episode { + pub fn title(&self) -> Option<&str> { + self.0.title() + } + pub fn download_url(&self) -> Option<&str> { match self.0.enclosure() { Some(val) => Some(val.url()), - None => None, + None => None, + } + } + + fn download_extension(&self) -> Option<&str> { + match self.0.enclosure() { + Some(enclosure) => { + match enclosure.mime_type() { + "audio/mpeg" => Some(".mp3"), + "audio/mp4" => Some(".m4a"), + "audio/ogg" => Some(".ogg"), + _ => None, + } + } + None => None, + } + } + + pub fn download(&self, podcast_name: &str) -> Result<(), io::Error> { + let mut path = get_podcast_dir(); + path.push(podcast_name); + + if let Some(url) = self.download_url() { + if let Some(title) = self.title() { + println!("Downloading: {}", title); + let mut filename = String::from(title); + filename.push_str(self.download_extension().unwrap()); + path.push(filename); + + let mut file = File::create(&path)?; + let mut resp = reqwest::get(url).unwrap(); + let mut content: Vec = Vec::new(); + resp.read_to_end(&mut content)?; + file.write_all(&content)?; + return Ok(()); + } } + Ok(()) } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..ab4876f --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,34 @@ +use std::fs; +use std::path::PathBuf; +use std::collections::BTreeSet; +use std::env; + + +pub fn already_downloaded(dir: &str) -> BTreeSet { + let mut result = BTreeSet::new(); + + let mut path = get_podcast_dir(); + path.push(dir); + + if let Ok(entries) = fs::read_dir(path) { + for entry in entries { + if let Ok(entry) = entry { + match entry.file_name().into_string() { + Ok(val) => { + result.insert(String::from(val.trim_right_matches(".mp3"))); + } + Err(err) => { + println!("OsString: {:?} couldn't be converted to String", err); + } + } + } + } + } + result +} + +pub fn get_podcast_dir() -> PathBuf { + let mut path = env::home_dir().unwrap(); + path.push("Podcasts"); + path +} -- cgit v1.2.3