diff options
| author | njaremko | 2017-07-17 18:10:12 -0400 | 
|---|---|---|
| committer | njaremko | 2017-07-17 18:10:12 -0400 | 
| commit | c8386aa0b9ccc6bf167cef769c4a5d8cfb86e3ef (patch) | |
| tree | f83bf72473c11d3b67639078a8e0b8160b5031a5 | |
| parent | 3483b857cf687fe3d1568b82ca7bb8ffec938a7c (diff) | |
| download | podcast-c8386aa0b9ccc6bf167cef769c4a5d8cfb86e3ef.tar.bz2 | |
More features
| -rw-r--r-- | Cargo.toml | 8 | ||||
| -rw-r--r-- | src/actions.rs | 26 | ||||
| -rw-r--r-- | src/main.rs | 86 | ||||
| -rw-r--r-- | src/structs.rs | 144 | ||||
| -rw-r--r-- | src/utils.rs | 34 | 
5 files changed, 275 insertions, 23 deletions
| @@ -4,4 +4,10 @@ version = "0.1.0"  authors = ["njaremko <njaremko@gmail.com>"]  [dependencies] -rss = "0.7" +serde = "1.0.10" +serde_json = "1.0.2" +regex = "0.2" +serde_derive = "1.0.10" +rss = {version = "0.7", features = ["from_url"] } +reqwest = "0.7.1" +clap = "2.25.0" 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. <njaremko@gmail.com>") +        .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<Subscription>); + +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<Subscription> { +        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<Item> 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<Podcast, rss::Error> { +        match Channel::from_url(url) { +            Ok(val) => Ok(Podcast::from(val)), +            Err(err) => Err(err), +        } +    } +      pub fn episodes(&self) -> Vec<Episode> {          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<u8> = 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<String> { +    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 +} | 
