use super::actions::*;
use super::utils::*;
use anyhow::Result;
use core::ops::Deref;
use std::fs::{self, File};
use std::io::{self, BufReader, BufWriter, Write};
use crate::{download, utils};
use bloom::ASMS;
use chrono::prelude::*;
use regex::Regex;
use reqwest::header;
use rss::{Channel, Item};
use semver_parser::version;
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();
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Config {
pub auto_download_limit: Option<i64>,
pub download_subscription_limit: Option<i64>,
pub quiet: Option<bool>,
pub filename_pattern: Option<String>,
}
impl Default for Config {
fn default() -> Self {
Config {
auto_download_limit: Some(1),
download_subscription_limit: Some(1),
quiet: Some(false),
filename_pattern: Some("{number}-{title}".to_string()),
}
}
}
impl Config {
pub fn load() -> Result<Option<Config>> {
let mut path = get_podcast_dir()?;
path.push(".config.yaml");
if path.exists() {
let file = File::open(&path)?;
return Ok(Some(serde_yaml::from_reader(file)?));
}
Ok(None)
}
}
#[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 PublicState {
pub version: String,
pub last_run_time: DateTime<Utc>,
pub config: Config,
pub subscriptions: Vec<Subscription>,
}
impl From<State> for PublicState {
fn from(internal_state: State) -> Self {
PublicState {
version: internal_state.version,
last_run_time: internal_state.last_run_time,
config: internal_state.config,
subscriptions: internal_state.subscriptions,
}
}
}
impl PublicState {
pub fn save(&self) -> Result<()> {
let mut path = config_path()?;
path.set_extension("json.tmp");
let file = File::create(&path)?;
serde_json::to_writer_pretty(BufWriter::new(file), self)?;
fs::rename(&path, config_path()?)?;
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct State {
pub version: String,
pub last_run_time: DateTime<Utc>,
pub config: Config,
pub subscriptions: Vec<Subscription>,
pub client: reqwest::Client,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct ParseState {
version: Option<String>,
last_run_time: Option<DateTime<Utc>>,
config: Option<Config>,
subscriptions: Option<Vec<Subscription>>,
}
impl From<ParseState> for State {
fn from(internal_state: ParseState) -> Self {
State {
version: internal_state.version.unwrap(),
last_run_time: internal_state.last_run_time.unwrap_or_else(Utc::now),
config: internal_state.config.unwrap_or_default(),
subscriptions: internal_state.subscriptions.unwrap_or_default(),
client: reqwest::Client::new(),
}
}
}
impl State {
pub async fn new(version: &str, config: Config) -> Result<State> {
let config_path = config_path()?;
let legacy_subscription_path = get_sub_file()?;
if legacy_subscription_path.exists() {
std::fs::rename(&legacy_subscription_path, &config_path)?;
}
if config_path.exists() {
let file = File::open(&config_path)?;
let parse_state: ParseState = serde_json::from_reader(BufReader::new(&file))?;
let mut state: State = parse_state.into();
state.version = String::from(version);
if 0 < Utc::now()
.signed_duration_since(state.last_run_time)
.num_days()
{
state.check_for_update().await?;
state.update_rss().await?;
}
state.last_run_time = Utc::now();
Ok(state)
} else {
writeln!(io::stdout().lock(), "Creating new file: {:?}", &config_path).ok();
Ok(State {
version: String::from(version),
last_run_time: Utc::now(),
subscriptions: Vec::new(),
config,
client: reqwest::Client::new(),
})
}
}
pub async fn subscribe(&mut self, url: &str) -> Result<()> {
let existing_subscriptions = if self.subscriptions.is_empty() {
10
} else {
self.subscriptions.len()
};
let mut bloom_filter = bloom::BloomFilter::with_rate(0.1, existing_subscriptions as u32);
for sub in &self.subscriptions {
bloom_filter.insert(&sub.title);
}
let resp = reqwest::get(url).await?.bytes().await?;
let channel = Channel::read_from(BufReader::new(&resp[..]))?;
let podcast = Podcast::from(channel);
if !bloom_filter.contains(&podcast.title()) {
self.subscriptions.push(Subscription {
title: String::from(podcast.title()),
url: String::from(url),
num_episodes: podcast.episodes().len(),
});
}
let episodes = download::download_rss(self, url).await?;
download::download_episodes(episodes).await?;
Ok(())
}
pub async fn update_rss(&mut self) -> Result<()> {
println!("Checking for new episodes...");
let mut d_vec = vec![];
for (index, sub) in self.subscriptions.iter().enumerate() {
d_vec.push(update_subscription(self, index, sub, &self.config));
}
let new_subscriptions = futures::future::join_all(d_vec).await;
for c in &new_subscriptions {
match c {
Ok([index, new_ep_count]) => {
self.subscriptions[*index].num_episodes = *new_ep_count;
}
Err(err) => {
println!("Error: {}", err);
}
}
}
println!("Done.");
Ok(())
}
pub async fn check_for_update(&self) -> Result<()> {
println!("Checking for updates...");
let resp: String =
reqwest::get("https://raw.githubusercontent.com/njaremko/podcast/master/Cargo.toml")
.await?
.text()
.await?;
let config = resp.parse::<toml::Value>()?;
let latest = config["package"]["version"]
.as_str()
.unwrap_or_else(|| panic!("Cargo.toml didn't have a version {:?}", config));
let local_version = match version::parse(&self.version) {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to parse version {}: {}", &self.version, e);
return Ok(());
}
};
let remote_version = match version::parse(latest) {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to parse version {}: {}", &self.version, e);
return Ok(());
}
};
if local_version < remote_version {
println!("New version available: {} -> {}", &self.version, latest);
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Download {
pub title: String,
pub path: PathBuf,
pub url: String,
pub size: u64,
}
impl Download {
pub async fn new(
state: &State,
podcast: &Podcast,
episode: &Episode,
) -> Result<Option<Download>> {
let mut path = utils::get_podcast_dir()?;
path.push(podcast.title());
utils::create_dir_if_not_exist(&path)?;
let pattern = state.config.filename_pattern.as_ref();
if let (Some(title), Some(url)) = (episode.title(), episode.url()) {
let mut filename;
if let Some(pattern) = pattern {
filename = pattern.replace("{title}", &title);
filename = filename.replace(
"{number}",
&podcast
.episodes()
.iter()
.rev()
.position(|e| e == episode)
.map(|x| (x + 1).to_string())
.unwrap_or_else(|| "".into()),
);
} else {
filename = title.clone();
}
if let Some(ext) = episode.extension() {
filename = utils::append_extension(&filename, &ext);
}
path.push(&filename);
let head_resp = state.client.head(url).send().await?;
let total_size = head_resp
.headers()
.get(header::CONTENT_LENGTH)
.and_then(|ct_len| ct_len.to_str().ok())
.and_then(|ct_len| ct_len.parse().ok())
.unwrap_or(0);
if !path.exists() {
return Ok(Some(Download {
title,
path,
url: url.into(),
size: total_size,
}));
}
}
Ok(None)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Podcast(Channel);
impl From<Channel> 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 async fn from_url(url: &str) -> Result<Podcast> {
let content = reqwest::get(url).await?.bytes().await?;
Ok(Podcast::from(Channel::read_from(&content[..])?))
}
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)?;
Ok(Podcast::from(Channel::read_from(BufReader::new(file))?))
}
pub fn episodes(&self) -> Vec<Episode> {
let mut result = Vec::new();
for item in self.0.items().iter().cloned() {
result.push(Episode::from(item));
}
result
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Episode(Item);
impl From<Item> for Episode {
fn from(item: Item) -> Episode {
Episode(item)
}
}
impl Episode {
pub fn title(&self) -> Option<String> {
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<String> {
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()),
}
}
}