podcast 0.9.1

A command line podcast manager
use std::collections::HashSet;
use std::env;
use std::fs::{self, DirBuilder, File};
use std::io::{BufReader, Read, Write};
use std::path::PathBuf;

use crate::errors::*;
use dirs;
use reqwest;
use rss::Channel;

pub const UNABLE_TO_PARSE_REGEX: &str = "unable to parse regex";
pub const UNABLE_TO_OPEN_FILE: &str = "unable to open file";
pub const UNABLE_TO_CREATE_FILE: &str = "unable to create file";
pub const UNABLE_TO_READ_FILE: &str = "unable to read file";
pub const UNABLE_TO_WRITE_FILE: &str = "unable to write file";
pub const UNABLE_TO_READ_FILE_TO_STRING: &str = "unable to read file to string";
pub const UNABLE_TO_READ_DIRECTORY: &str = "unable to read directory";
pub const UNABLE_TO_READ_ENTRY: &str = "unable to read entry";
pub const UNABLE_TO_CREATE_DIRECTORY: &str = "unable to create directory";
pub const UNABLE_TO_READ_RESPONSE_TO_END: &str = "unable to read response to end";
pub const UNABLE_TO_GET_HTTP_RESPONSE: &str = "unable to get http response";
pub const UNABLE_TO_CONVERT_TO_STR: &str = "unable to convert to &str";
pub const UNABLE_TO_REMOVE_FILE: &str = "unable to remove file";
pub const UNABLE_TO_CREATE_CHANNEL_FROM_RESPONSE: &str =
    "unable to create channel from http response";
pub const UNABLE_TO_CREATE_CHANNEL_FROM_FILE: &str = "unable to create channel from xml file";
pub const UNABLE_TO_RETRIEVE_PODCAST_BY_TITLE: &str = "unable to retrieve podcast by title";

const UNSUBSCRIBE_NOTE: &str = "Note: this does NOT delete any downloaded podcasts";

pub fn trim_extension(filename: &str) -> Option<String> {
    let name = String::from(filename);
    let index = name.rfind('.')?;
    Some(String::from(&name[0..index]))
}

pub fn find_extension(input: &str) -> Option<&str> {
    let tmp = String::from(input);
    if tmp.ends_with(".mp3") {
        Some(".mp3")
    } else if tmp.ends_with(".m4a") {
        Some(".m4a")
    } else if tmp.ends_with(".wav") {
        Some(".wav")
    } else if tmp.ends_with(".ogg") {
        Some(".ogg")
    } else if tmp.ends_with(".opus") {
        Some(".opus")
    } else {
        None
    }
}

pub fn get_podcast_dir() -> Result<PathBuf> {
    match env::var_os("PODCAST") {
        Some(val) => Ok(PathBuf::from(val)),
        None => {
            let mut path = dirs::home_dir().chain_err(|| "Couldn't find your home directory")?;
            path.push("Podcasts");
            Ok(path)
        }
    }
}

pub fn create_dir_if_not_exist(path: &PathBuf) -> Result<()> {
    DirBuilder::new()
        .recursive(true)
        .create(&path)
        .chain_err(|| UNABLE_TO_CREATE_DIRECTORY)?;
    Ok(())
}

pub fn create_directories() -> Result<()> {
    let mut path = get_podcast_dir()?;
    path.push(".rss");
    create_dir_if_not_exist(&path)
}

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);
    println!("Removing '{}' from subscriptions...", &title);
    println!("{}", UNSUBSCRIBE_NOTE);
    fs::remove_file(path).chain_err(|| UNABLE_TO_REMOVE_FILE)
}


pub fn delete_all() -> Result<()> {
    println!("Removing all subscriptions...");
    println!("{}", UNSUBSCRIBE_NOTE);
    fs::remove_dir_all(get_xml_dir()?).chain_err(|| UNABLE_TO_READ_DIRECTORY)
}

pub fn already_downloaded(dir: &str) -> Result<HashSet<String>> {
    let mut result = HashSet::new();

    let mut path = get_podcast_dir()?;
    path.push(dir);

    let entries = fs::read_dir(path).chain_err(|| "unable to read directory")?;
    for entry in entries {
        let entry = entry.chain_err(|| "unable to read entry")?;
        match entry.file_name().into_string() {
            Ok(name) => {
                let index = name.find('.').chain_err(|| "unable to find string index")?;
                result.insert(String::from(&name[0..index]));
            }
            Err(_) => {
                eprintln!(
                    "OsString: {:?} couldn't be converted to String",
                    entry.file_name()
                );
            }
        }
    }
    Ok(result)
}

pub fn get_sub_file() -> Result<PathBuf> {
    let mut path = get_podcast_dir()?;
    path.push(".subscriptions.json");
    Ok(path)
}

pub fn get_xml_dir() -> Result<PathBuf> {
    let mut path = get_podcast_dir()?;
    path.push(".rss");
    Ok(path)
}

pub fn download_rss_feed(url: &str) -> Result<Channel> {
    println!("Downloading RSS feed...");
    let mut path = get_podcast_dir()?;
    path.push(".rss");
    create_dir_if_not_exist(&path)?;
    let mut resp = reqwest::get(url).chain_err(|| "unable to open url")?;
    let mut content: Vec<u8> = Vec::new();
    resp.read_to_end(&mut content)
        .chain_err(|| "unable to read http response to end")?;
    let channel = Channel::read_from(BufReader::new(&content[..]))
        .chain_err(|| "unable to create channel from xml http response")?;
    let mut filename = String::from(channel.title());
    filename.push_str(".xml");
    path.push(filename);
    let mut file = File::create(&path).chain_err(|| "unable to create file")?;
    file.write_all(&content)
        .chain_err(|| "unable to write file")?;
    Ok(channel)
}

pub fn parse_download_episodes(e_search: &str) -> Result<Vec<usize>> {
    let input = String::from(e_search);
    let mut ranges = Vec::<(usize, usize)>::new();
    let mut elements = Vec::<usize>::new();
    let comma_separated: Vec<&str> = input.split(',').collect();
    for elem in comma_separated {
        let temp = String::from(elem);
        if temp.contains('-') {
            let range: Vec<usize> = elem
                .split('-')
                .map(|i| i.parse::<usize>().chain_err(|| "unable to parse number"))
                .collect::<Result<Vec<usize>>>()
                .chain_err(|| "unable to collect ranges")?;
            ranges.push((range[0], range[1]));
        } else {
            elements.push(
                elem.parse::<usize>()
                    .chain_err(|| "unable to parse number")?,
            );
        }
    }

    for range in ranges {
        // Add 1 to upper range to include given episode in the download
        for num in range.0..=range.1 {
            elements.push(num);
        }
    }
    elements.dedup();
    Ok(elements)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_find_extension_mp3() {
        assert_eq!(find_extension("test.mp3"), Some(".mp3"))
    }

    #[test]
    fn test_find_extension_m4a() {
        assert_eq!(find_extension("test.m4a"), Some(".m4a"))
    }

    #[test]
    fn test_find_extension_wav() {
        assert_eq!(find_extension("test.wav"), Some(".wav"))
    }

    #[test]
    fn test_find_extension_ogg() {
        assert_eq!(find_extension("test.ogg"), Some(".ogg"))
    }

    #[test]
    fn test_find_extension_opus() {
        assert_eq!(find_extension("test.opus"), Some(".opus"))
    }

    #[test]
    fn test_find_extension_invalid() {
        assert_eq!(find_extension("test.taco"), None)
    }

    #[test]
    fn test_trim_extension() {
        assert_eq!(trim_extension("test.taco"), Some(String::from("test")))
    }

    #[test]
    fn test_trim_extension_invalid() {
        assert_eq!(trim_extension("test"), None)
    }
}