use actions::*;
use errors::*;
use utils::*;
use std::collections::BTreeSet;
use std::fs::{self, DirBuilder, File};
use std::io::{self, BufReader, Read, Write};
use chrono::prelude::*;
use rayon::prelude::*;
use reqwest;
use rss::{Channel, Item};
use serde_json;
use yaml_rust::YamlLoader;
pub struct Config {
pub auto_download_limit: i64,
}
impl Config {
pub fn new() -> Result<Config> {
let mut path = get_podcast_dir()?;
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;
}
}
} else {
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,
})
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct Subscription {
pub title: String,
pub url: String,
pub num_episodes: usize,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct State {
pub version: String,
pub last_run_time: DateTime<Utc>,
pub subscriptions: Vec<Subscription>,
}
impl State {
pub fn new(version: &str) -> Result<State> {
let mut path = get_podcast_dir()?;
path.push(".subscriptions");
if path.exists() {
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) {
Ok(val) => val,
Err(_) => {
let v: serde_json::Value =
serde_json::from_str(&s).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")?,
},
}
}
};
state.version = String::from(version);
if Utc::now()
.signed_duration_since(state.last_run_time)
.num_seconds() > 86400
{
update_rss(&mut state);
check_for_update(&state.version)?;
}
state.last_run_time = Utc::now();
state.save()?;
Ok(state)
} else {
Ok(State {
version: String::from(version),
last_run_time: Utc::now(),
subscriptions: Vec::new(),
})
}
}
pub fn subscribe(&mut self, url: &str) -> Result<()> {
let mut set = BTreeSet::new();
for sub in self.subscriptions() {
set.insert(sub.title);
}
let podcast = Podcast::from(Channel::from_url(url).unwrap());
if !set.contains(podcast.title()) {
self.subscriptions.push(Subscription {
title: String::from(podcast.title()),
url: String::from(url),
num_episodes: podcast.episodes().len(),
});
}
self.save()
}
pub fn subscriptions(&self) -> Vec<Subscription> {
self.subscriptions.clone()
}
pub fn save(&self) -> Result<()> {
let mut path = get_podcast_dir()?;
path.push(".subscriptions.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)?;
fs::rename(&path, get_sub_file()?).chain_err(|| "unable to rename file")?;
Ok(())
}
}
#[derive(Clone)]
pub struct Podcast(Channel);
#[derive(Clone)]
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 Podcast {
pub fn title(&self) -> &str {
self.0.title()
}
#[allow(dead_code)]
pub fn url(&self) -> &str {
self.0.link()
}
#[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)?),
)
}
pub fn from_title(title: &str) -> Result<Podcast> {
let mut path = get_xml_dir()?;
let mut filename = String::from(title);
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)?))
}
pub fn delete(title: &str) -> Result<()> {
let mut path = get_xml_dir()?;
let mut filename = String::from(title);
filename.push_str(".xml");
path.push(filename);
fs::remove_file(path).chain_err(|| UNABLE_TO_REMOVE_FILE)
}
pub fn delete_all() -> Result<()> {
let path = get_xml_dir()?;
fs::remove_dir_all(path).chain_err(|| UNABLE_TO_READ_DIRECTORY)
}
pub fn episodes(&self) -> Vec<Episode> {
let mut result = Vec::new();
for item in self.0.items().to_vec() {
result.push(Episode::from(item));
}
result
}
pub fn download(&self) -> Result<()> {
print!("You are about to download all episodes (y/n): ");
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) = i.download(self.title()) {
eprintln!("{}", err);
}
}
}
});
}
Err(_) => {
self.episodes().par_iter().for_each(|i| {
if let Err(err) = i.download(self.title()) {
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();
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) = episodes[episodes.len() - ep_num].download(self.title()) {
println!("{}", err);
}
}
}
});
Ok(())
}
}
impl Episode {
pub fn title(&self) -> Option<&str> {
self.0.title()
}
pub fn url(&self) -> Option<&str> {
match self.0.enclosure() {
Some(val) => Some(val.url()),
None => None,
}
}
pub fn extension(&self) -> Option<&str> {
match self.0.enclosure()?.mime_type() {
"audio/mpeg" => Some(".mp3"),
"audio/mp4" => Some(".m4a"),
"audio/ogg" => Some(".ogg"),
_ => find_extension(self.url().unwrap()),
}
}
pub fn download(&self, podcast_name: &str) -> Result<()> {
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 = String::from(title);
filename.push_str(self.extension()
.chain_err(|| "unable to retrieve extension")?);
path.push(filename);
if !path.exists() {
println!("Downloading: {}", path.to_str().unwrap());
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 {
println!(
"File already exists: {}",
path.to_str().chain_err(|| UNABLE_TO_CONVERT_TO_STR)?
);
}
}
}
Ok(())
}
}