use super::actions::*; use super::utils::*; use crate::errors::*; use core::ops::Deref; use std::collections::HashSet; use std::fs::{self, File}; use std::io::{self, BufReader, BufWriter, Write}; use chrono::prelude::*; use regex::Regex; use rss::{Channel, Item}; use serde_json; use std::path::PathBuf; #[cfg(target_os = "macos")] const ESCAPE_REGEX: &str = r"/"; #[cfg(target_os = "linux")] const ESCAPE_REGEX: &str = r"/"; #[cfg(target_os = "windows")] const ESCAPE_REGEX: &str = r#"[\\/:*?"<>|]"#; lazy_static! { static ref FILENAME_ESCAPE: Regex = Regex::new(ESCAPE_REGEX).unwrap(); } fn create_new_config_file(path: &PathBuf) -> Result { writeln!( io::stdout().lock(), "Creating new config file at {:?}", &path ) .ok(); let download_limit = 1; let file = File::create(&path)?; let config = Config { auto_download_limit: download_limit, }; serde_yaml::to_writer(file, &config)?; Ok(config) } #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Config { pub auto_download_limit: i64, } impl Config { pub fn new() -> Result { let mut path = get_podcast_dir()?; path.push(".config.yaml"); let config = if path.exists() { let file = File::open(&path)?; match serde_yaml::from_reader(file) { Ok(config) => config, Err(err) => { let mut new_path = path.clone(); new_path.set_extension("yaml.bk"); let stderr = io::stderr(); let mut handle = stderr.lock(); writeln!( &mut handle, "{}\nFailed to open config file, moving to {:?}", err, &new_path ) .ok(); fs::rename(&path, new_path)?; create_new_config_file(&path)? } } } else { create_new_config_file(&path)? }; Ok(config) } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Subscription { pub title: String, pub url: String, pub num_episodes: usize, } impl Subscription { pub fn title(&self) -> &str { &self.title } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct State { pub version: String, pub last_run_time: DateTime, pub subscriptions: Vec, } impl State { pub fn new(version: &str) -> Result { let path = get_sub_file()?; if path.exists() { let file = File::open(&path)?; let mut state: State = serde_json::from_reader(BufReader::new(&file))?; state.version = String::from(version); // Check if a day has passed (86400 seconds) since last launch if 86400 < Utc::now() .signed_duration_since(state.last_run_time) .num_seconds() { update_rss(&mut state); check_for_update(&state.version)?; } state.last_run_time = Utc::now(); state.save()?; Ok(state) } else { writeln!(io::stdout().lock(), "Creating new file: {:?}", &path).ok(); Ok(State { version: String::from(version), last_run_time: Utc::now(), subscriptions: Vec::new(), }) } } pub fn subscriptions(&self) -> &[Subscription] { &self.subscriptions } pub fn subscriptions_mut(&mut self) -> &mut [Subscription] { &mut self.subscriptions } pub fn subscribe(&mut self, url: &str) -> Result<()> { let mut set = HashSet::new(); for sub in self.subscriptions() { set.insert(sub.title.clone()); } let podcast = Podcast::from(Channel::from_url(url)?); 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 save(&self) -> Result<()> { let mut path = get_sub_file()?; path.set_extension("json.tmp"); let file = File::create(&path)?; serde_json::to_writer(BufWriter::new(file), self)?; fs::rename(&path, get_sub_file()?)?; Ok(()) } } #[derive(Clone, Debug, PartialEq)] pub struct Podcast(Channel); impl From for Podcast { fn from(channel: Channel) -> Podcast { Podcast(channel) } } impl Deref for Podcast { type Target = Channel; fn deref(&self) -> &Channel { &self.0 } } 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 { Ok(Podcast::from(Channel::from_url(url)?)) } pub fn from_title(title: &str) -> Result { let mut path = get_xml_dir()?; let mut filename = String::from(title); filename.push_str(".xml"); path.push(filename); let file = File::open(&path)?; Ok(Podcast::from(Channel::read_from(BufReader::new(file))?)) } pub fn episodes(&self) -> Vec { let mut result = Vec::new(); for item in self.0.items().to_owned() { result.push(Episode::from(item)); } result } } #[derive(Clone, Debug, PartialEq)] pub struct Episode(Item); impl From for Episode { fn from(item: Item) -> Episode { Episode(item) } } impl Episode { pub fn title(&self) -> Option { Some( FILENAME_ESCAPE .replace_all(self.0.title()?, "_") .to_string(), ) } pub fn url(&self) -> Option<&str> { match self.0.enclosure() { Some(val) => Some(val.url()), None => None, } } pub fn extension(&self) -> Option { match self.0.enclosure()?.mime_type() { "audio/mpeg" => Some("mp3".into()), "audio/mp4" => Some("m4a".into()), "audio/aac" => Some("m4a".into()), "audio/ogg" => Some("ogg".into()), "audio/vorbis" => Some("ogg".into()), "audio/opus" => Some("opus".into()), _ => find_extension(self.url().unwrap()), } } }