aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml8
-rw-r--r--src/actions.rs26
-rw-r--r--src/main.rs86
-rw-r--r--src/structs.rs144
-rw-r--r--src/utils.rs34
5 files changed, 275 insertions, 23 deletions
diff --git a/Cargo.toml b/Cargo.toml
index e538ec4..31cc6ce 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
+}