<commit_before>//! Provides functions related to factorial calculations (e.g. binomial
//! coefficient, factorial, multinomial)

use crate::error::StatsError;
use crate::function::gamma;
use crate::Result;
use core::f64;

/// The maximum factorial representable
/// by a 64-bit floating point without
/// overflowing
pub const MAX_ARG: u64 = 170;

/// Computes the factorial function `x -> x!` for
/// `170 >= x >= 0`. All factorials larger than `170!`
/// will overflow an `f64`.
///
/// # Remarks
///
/// Returns `f64::INFINITY` if `x > 170`
pub fn factorial(x: u64) -> f64 {
    if x > MAX_ARG {
        f64::INFINITY
    } else {
        FCACHE[x as usize]
    }
}

/// Computes the logarithmic factorial function `x -> ln(x!)`
/// for `x >= 0`.
///
/// # Remarks
///
/// Returns `0.0` if `x <= 1`
pub fn ln_factorial(x: u64) -> f64 {
    if x <= 1 {
        0.0
    } else if x > MAX_ARG {
        gamma::ln_gamma(x as f64 + 1.0)
    } else {
        FCACHE[x as usize].ln()
    }
}

/// Computes the binomial coefficient `n choose k`
/// where `k` and `n` are non-negative values.
///
/// # Remarks
///
/// Returns `0.0` if `k > n`
pub fn binomial(n: u64, k: u64) -> f64 {
    if k > n {
        0.0
    } else {
        (0.5 + (ln_factorial(n) - ln_factorial(k) - ln_factorial(n - k)).exp()).floor()
    }
}

/// Computes the natural logarithm of the binomial coefficient
/// `ln(n choose k)` where `k` and `n` are non-negative values
///
/// # Remarks
///
/// Returns `f64::NEG_INFINITY` if `k > n`
pub fn ln_binomial(n: u64, k: u64) -> f64 {
    if k > n {
        f64::NEG_INFINITY
    } else {
        ln_factorial(n) - ln_factorial(k) - ln_factorial(n - k)
    }
}

/// Computes the multinomial coefficient: `n choose n1, n2, n3, ...`
///
/// # Panics
///
/// If the elements in `ni` do not sum to `n`
pub fn multinomial(n: u64, ni: &[u64]) -> f64 {
    checked_multinomial(n, ni).unwrap()
}

/// Computes the multinomial coefficient: `n choose n1, n2, n3, ...`
///
/// # Errors
///
/// If the elements in `ni` do not sum to `n`
pub fn checked_multinomial(n: u64, ni: &[u64]) -> Result<f64> {
    let (sum, ret) = ni.iter().fold((0, ln_factorial(n)), |acc, &x| {
        (acc.0 + x, acc.1 - ln_factorial(x))
    });
    if sum != n {
        Err(StatsError::ContainerExpectedSumVar("ni", "n"))
    } else {
        Ok((0.5 + ret.exp()).floor())
    }
}

// Initialization for pre-computed cache of 171 factorial
// values 0!...170!
lazy_static! {
    static ref FCACHE: [f64; MAX_ARGS + 1] = {
        let mut fcache = [1.0; MAX_ARGS + 1];
        fcache
            .iter_mut()
            .enumerate()
            .skip(1)
            .fold(1.0, |acc, (i, elt)| {
                let fac = acc * i as f64;
                *elt = fac;
                fac
            });
        fcache
    };
}

#[rustfmt::skip]
#[cfg(test)]
mod tests {
    use core::{f64, u64};

    #[test]
    fn test_factorial_and_ln_factorial() {
        let mut factorial = 1.0;
        for i in 1..171 {
            factorial *= i as f64;
            assert_eq!(super::factorial(i), factorial);
            assert_eq!(super::ln_factorial(i), factorial.ln());
        }
    }

    #[test]
    fn test_factorial_overflow() {
        assert_eq!(super::factorial(172), f64::INFINITY);
        assert_eq!(super::factorial(u64::MAX), f64::INFINITY);
    }

    #[test]
    fn test_ln_factorial_does_not_overflow() {
        assert_eq!(super::ln_factorial(1 << 10), 6078.2118847500501140);
        assert_almost_eq!(super::ln_factorial(1 << 12), 29978.648060844048236, 1e-11);
        assert_eq!(super::ln_factorial(1 << 15), 307933.81973375485425);
        assert_eq!(super::ln_factorial(1 << 17), 1413421.9939462073242);
    }

    #[test]
    fn test_binomial() {
        assert_eq!(super::binomial(1, 1), 1.0);
        assert_eq!(super::binomial(5, 2), 10.0);
        assert_eq!(super::binomial(7, 3), 35.0);
        assert_eq!(super::binomial(1, 0), 1.0);
        assert_eq!(super::binomial(0, 1), 0.0);
        assert_eq!(super::binomial(5, 7), 0.0);
    }

    #[test]
    fn test_ln_binomial() {
        assert_eq!(super::ln_binomial(1, 1), 1f64.ln());
        assert_almost_eq!(super::ln_binomial(5, 2), 10f64.ln(), 1e-14);
        assert_almost_eq!(super::ln_binomial(7, 3), 35f64.ln(), 1e-14);
        assert_eq!(super::ln_binomial(1, 0), 1f64.ln());
        assert_eq!(super::ln_binomial(0, 1), 0f64.ln());
        assert_eq!(super::ln_binomial(5, 7), 0f64.ln());
    }

    #[test]
    fn test_multinomial() {
        assert_eq!(1.0, super::multinomial(1, &[1, 0]));
        assert_eq!(10.0, super::multinomial(5, &[3, 2]));
        assert_eq!(10.0, super::multinomial(5, &[2, 3]));
        assert_eq!(35.0, super::multinomial(7, &[3, 4]));
    }

    #[test]
    #[should_panic]
    fn test_multinomial_bad_ni() {
        super::multinomial(1, &[1, 1]);
    }

    #[test]
    fn test_checked_multinomial_bad_ni() {
        assert!(super::checked_multinomial(1, &[1, 1]).is_err());
    }
}
<commit_msg>fix: simplify conversions, simplify code<commit_after>//! Provides functions related to factorial calculations (e.g. binomial
//! coefficient, factorial, multinomial)

use crate::error::StatsError;
use crate::function::gamma;
use crate::Result;
use core::f64::INFINITY as INF;

/// The maximum factorial representable
/// by a 64-bit floating point without
/// overflowing
pub const MAX_FACTORIAL: usize = 170;

/// Computes the factorial function `x -> x!` for
/// `170 >= x >= 0`. All factorials larger than `170!`
/// will overflow an `f64`.
///
/// # Remarks
///
/// Returns `f64::INFINITY` if `x > 170`
pub fn factorial(x: u64) -> f64 {
    let x = x as usize;
    FCACHE.get(x).map_or(INF, |&fac| fac)
}

/// Computes the logarithmic factorial function `x -> ln(x!)`
/// for `x >= 0`.
///
/// # Remarks
///
/// Returns `0.0` if `x <= 1`
pub fn ln_factorial(x: u64) -> f64 {
    let x = x as usize;
    FCACHE
        .get(x)
        .map_or_else(|| gamma::ln_gamma(x as f64 + 1.0), |&fac| fac.ln())
}

/// Computes the binomial coefficient `n choose k`
/// where `k` and `n` are non-negative values.
///
/// # Remarks
///
/// Returns `0.0` if `k > n`
pub fn binomial(n: u64, k: u64) -> f64 {
    if k > n {
        0.0
    } else {
        (0.5 + (ln_factorial(n) - ln_factorial(k) - ln_factorial(n - k)).exp()).floor()
    }
}

/// Computes the natural logarithm of the binomial coefficient
/// `ln(n choose k)` where `k` and `n` are non-negative values
///
/// # Remarks
///
/// Returns `f64::NEG_INFINITY` if `k > n`
pub fn ln_binomial(n: u64, k: u64) -> f64 {
    if k > n {
        f64::NEG_INFINITY
    } else {
        ln_factorial(n) - ln_factorial(k) - ln_factorial(n - k)
    }
}

/// Computes the multinomial coefficient: `n choose n1, n2, n3, ...`
///
/// # Panics
///
/// If the elements in `ni` do not sum to `n`
pub fn multinomial(n: u64, ni: &[u64]) -> f64 {
    checked_multinomial(n, ni).unwrap()
}

/// Computes the multinomial coefficient: `n choose n1, n2, n3, ...`
///
/// # Errors
///
/// If the elements in `ni` do not sum to `n`
pub fn checked_multinomial(n: u64, ni: &[u64]) -> Result<f64> {
    let (sum, ret) = ni.iter().fold((0, ln_factorial(n)), |acc, &x| {
        (acc.0 + x, acc.1 - ln_factorial(x))
    });
    if sum != n {
        Err(StatsError::ContainerExpectedSumVar("ni", "n"))
    } else {
        Ok((0.5 + ret.exp()).floor())
    }
}

// Initialization for pre-computed cache of 171 factorial
// values 0!...170!
lazy_static! {
    static ref FCACHE: [f64; MAX_FACTORIAL + 1] = {
        let mut fcache = [1.0; MAX_FACTORIAL + 1];
        fcache
            .iter_mut()
            .enumerate()
            .skip(1)
            .fold(1.0, |acc, (i, elt)| {
                let fac = acc * i as f64;
                *elt = fac;
                fac
            });
        fcache
    };
}

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

    #[test]
    fn test_factorial_and_ln_factorial() {
        let mut fac = 1.0;
        assert_eq!(factorial(0), fac);
        for i in 1..171 {
            fac *= i as f64;
            assert_eq!(factorial(i), fac);
            assert_eq!(ln_factorial(i), fac.ln());
        }
    }

    #[test]
    fn test_factorial_overflow() {
        assert_eq!(factorial(172), INF);
        assert_eq!(factorial(u64::MAX), INF);
    }

    #[test]
    fn test_ln_factorial_does_not_overflow() {
        assert_eq!(ln_factorial(1 << 10), 6078.2118847500501140);
        assert_almost_eq!(ln_factorial(1 << 12), 29978.648060844048236, 1e-11);
        assert_eq!(ln_factorial(1 << 15), 307933.81973375485425);
        assert_eq!(ln_factorial(1 << 17), 1413421.9939462073242);
    }

    #[test]
    fn test_binomial() {
        assert_eq!(binomial(1, 1), 1.0);
        assert_eq!(binomial(5, 2), 10.0);
        assert_eq!(binomial(7, 3), 35.0);
        assert_eq!(binomial(1, 0), 1.0);
        assert_eq!(binomial(0, 1), 0.0);
        assert_eq!(binomial(5, 7), 0.0);
    }

    #[test]
    fn test_ln_binomial() {
        assert_eq!(ln_binomial(1, 1), 1f64.ln());
        assert_almost_eq!(ln_binomial(5, 2), 10f64.ln(), 1e-14);
        assert_almost_eq!(ln_binomial(7, 3), 35f64.ln(), 1e-14);
        assert_eq!(ln_binomial(1, 0), 1f64.ln());
        assert_eq!(ln_binomial(0, 1), 0f64.ln());
        assert_eq!(ln_binomial(5, 7), 0f64.ln());
    }

    #[test]
    fn test_multinomial() {
        assert_eq!(1.0, multinomial(1, &[1, 0]));
        assert_eq!(10.0, multinomial(5, &[3, 2]));
        assert_eq!(10.0, multinomial(5, &[2, 3]));
        assert_eq!(35.0, multinomial(7, &[3, 4]));
    }

    #[test]
    #[should_panic]
    fn test_multinomial_bad_ni() {
        multinomial(1, &[1, 1]);
    }

    #[test]
    fn test_checked_multinomial_bad_ni() {
        assert!(checked_multinomial(1, &[1, 1]).is_err());
    }
}
<|endoftext|>


<commit_before>use super::{SeasonState, SeriesConfig};
use backend::{AnimeEntry, AnimeInfo, SyncBackend};
use error::SeriesError;
use input::{self, Answer};
use regex::Regex;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs;
use std::ops::Range;
use std::path::{Path, PathBuf};
use toml;

pub struct FolderData {
    pub series_info: SeriesInfo,
    pub savefile: SaveData,
    pub path: PathBuf,
}

impl FolderData {
    pub fn load_dir(path: &Path) -> Result<FolderData, SeriesError> {
        let mut savefile = SaveData::from_dir(path)?;
        let series_info = FolderData::load_series_info(path, &mut savefile)?;

        Ok(FolderData {
            series_info,
            savefile,
            path: PathBuf::from(path),
        })
    }

    fn load_series_info(path: &Path, savefile: &mut SaveData) -> Result<SeriesInfo, SeriesError> {
        let mut ep_data = parse_episode_files_until_valid(path, &mut savefile.episode_matcher)?;

        if let Some(info) = SeriesInfo::select_from_save(&mut ep_data, savefile) {
            return Ok(info);
        }

        let info = prompt_select_series_info(ep_data)?;
        savefile.files_title = Some(info.name.clone());

        Ok(info)
    }

    pub fn populate_season_data<B>(&mut self, config: &SeriesConfig<B>) -> Result<(), SeriesError>
    where
        B: SyncBackend,
    {
        let num_seasons = self.seasons().len();

        if num_seasons > config.season_num {
            return Ok(());
        }

        for cur_season in num_seasons..=config.season_num {
            let info = AnimeInfo::default();
            let entry = AnimeEntry::new(info);

            let mut season = SeasonState {
                state: entry,
                needs_info: true,
                needs_sync: config.offline_mode,
            };

            season.sync_info_from_remote(config, &self, cur_season)?;

            self.seasons_mut().push(season);
        }

        Ok(())
    }

    pub fn calculate_season_offset(&self, mut range: Range<usize>) -> u32 {
        let num_seasons = self.savefile.season_states.len();
        range.start = num_seasons.min(range.start);
        range.end = num_seasons.min(range.end);

        let mut offset = 0;

        for i in range {
            let season = &self.savefile.season_states[i];

            match season.state.info.episodes {
                Some(eps) => offset += eps,
                None => return offset,
            }
        }

        offset
    }

    pub fn try_remove_dir(&self) {
        let path = self.path.to_string_lossy();

        println!("WARNING: {} will be deleted", path);
        println!("is this ok? (y/N)");

        match input::read_yn(Answer::No) {
            Ok(true) => match fs::remove_dir_all(&self.path) {
                Ok(_) => (),
                Err(err) => {
                    eprintln!("failed to remove directory: {}", err);
                }
            },
            Ok(false) => (),
            Err(err) => {
                eprintln!("failed to read input: {}", err);
            }
        }
    }

    pub fn get_episode(&self, episode: u32) -> Result<&PathBuf, SeriesError> {
        self.series_info.get_episode(episode)
    }

    pub fn save(&self) -> Result<(), SeriesError> {
        self.savefile.write_to_file()
    }

    pub fn seasons(&self) -> &Vec<SeasonState> {
        &self.savefile.season_states
    }

    pub fn seasons_mut(&mut self) -> &mut Vec<SeasonState> {
        &mut self.savefile.season_states
    }
}

#[derive(Serialize, Deserialize)]
pub struct SaveData {
    pub episode_matcher: Option<String>,
    pub files_title: Option<String>,
    pub season_states: Vec<SeasonState>,
    #[serde(skip)]
    pub path: PathBuf,
}

impl SaveData {
    const DATA_FILE_NAME: &'static str = ".anup";

    pub fn new(path: PathBuf) -> SaveData {
        SaveData {
            episode_matcher: None,
            files_title: None,
            season_states: Vec::new(),
            path,
        }
    }

    pub fn from_dir(path: &Path) -> Result<SaveData, SeriesError> {
        let path = PathBuf::from(path).join(SaveData::DATA_FILE_NAME);

        if !path.exists() {
            return Ok(SaveData::new(path));
        }

        let file_contents = fs::read_to_string(&path)?;

        let mut save_data: SaveData = toml::from_str(&file_contents)?;
        save_data.path = path;

        Ok(save_data)
    }

    pub fn write_to_file(&self) -> Result<(), SeriesError> {
        let toml = toml::to_string(self)?;
        fs::write(&self.path, toml)?;

        Ok(())
    }
}

pub struct SeriesInfo {
    pub name: String,
    pub episodes: HashMap<u32, PathBuf>,
}

impl SeriesInfo {
    pub fn get_episode(&self, episode: u32) -> Result<&PathBuf, SeriesError> {
        self.episodes
            .get(&episode)
            .ok_or_else(|| SeriesError::EpisodeNotFound(episode))
    }

    pub fn select_from_save(
        ep_data: &mut SeriesEpisodes,
        savefile: &SaveData,
    ) -> Option<SeriesInfo> {
        if let Some(name) = &savefile.files_title {
            let entry = ep_data.remove_entry(name);

            if let Some((name, episodes)) = entry {
                return Some(SeriesInfo { name, episodes });
            }
        }

        None
    }
}

struct EpisodeFile {
    series_name: String,
    episode_num: u32,
}

impl EpisodeFile {
    fn parse(path: &Path, matcher: &Regex) -> Result<EpisodeFile, SeriesError> {
        // Replace certain characters with spaces since they can prevent proper series
        // identification or prevent it from being found on a sync backend
        let filename = path
            .file_name()
            .and_then(|path| path.to_str())
            .ok_or(SeriesError::UnableToGetFilename)?;

        let caps = matcher
            .captures(&filename)
            .ok_or(SeriesError::EpisodeRegexCaptureFailed)?;

        let series_name = {
            let raw_name = caps
                .name("name")
                .map(|c| c.as_str())
                .ok_or_else(|| SeriesError::UnknownRegexCapture("name".into()))?;

            raw_name
                .replace('.', " ")
                .replace(" -", ":") // Dashes typically represent a colon in file names
                .replace('_', " ")
                .trim()
                .to_string()
        };

        let episode = caps
            .name("episode")
            .ok_or_else(|| SeriesError::UnknownRegexCapture("episode".into()))
            .and_then(|cap| {
                cap.as_str()
                    .parse()
                    .map_err(SeriesError::EpisodeNumParseFailed)
            })?;

        Ok(EpisodeFile {
            series_name,
            episode_num: episode,
        })
    }
}

type EpisodePaths = HashMap<u32, PathBuf>;
type SeriesEpisodes = HashMap<String, EpisodePaths>;

pub fn prompt_select_series_info(info: SeriesEpisodes) -> Result<SeriesInfo, SeriesError> {
    if info.is_empty() {
        return Err(SeriesError::NoSeriesFound);
    }

    let mut info = info
        .into_iter()
        .map(|(name, eps)| SeriesInfo {
            name,
            episodes: eps,
        }).collect::<Vec<_>>();

    if info.len() == 1 {
        return Ok(info.swap_remove(0));
    }

    println!("multiple series found in directory");
    println!("please enter the number next to the episode files you want to use:");

    for (i, series) in info.iter().enumerate() {
        println!("{} [{}]", 1 + i, series.name);
    }

    let index = input::read_range(1, info.len())? - 1;
    let series = info.swap_remove(index);

    Ok(series)
}

// This default pattern will match episodes in several common formats, such as:
// [Group] Series Name - 01.mkv
// [Group]_Series_Name_-_01.mkv
// [Group].Series.Name.-.01.mkv
// [Group] Series Name - 01 [tag 1][tag 2].mkv
// [Group]_Series_Name_-_01_[tag1][tag2].mkv
// [Group].Series.Name.-.01.[tag1][tag2].mkv
// Series Name - 01.mkv
// Series_Name_-_01.mkv
// Series.Name.-.01.mkv
const EP_FORMAT_REGEX: &str =
    r"(?:\[.+?\](?:\s+|_+|\.+))?(?P<name>.+?)(?:\s*|_*|\.*)-(?:\s*|_*|\.*)(?P<episode>\d+).*?\..+?";

fn format_episode_parser_regex<'a, S>(pattern: Option<S>) -> Result<Cow<'a, Regex>, SeriesError>
where
    S: AsRef<str>,
{
    lazy_static! {
        static ref EP_FORMAT: Regex = Regex::new(EP_FORMAT_REGEX).unwrap();
    }

    match pattern {
        Some(pattern) => {
            let pattern = pattern
                .as_ref()
                .replace("{name}", "(?P<name>.+?)")
                .replace("{episode}", r"(?P<episode>\d+)");

            let regex = Regex::new(&pattern)?;
            Ok(Cow::Owned(regex))
        }
        None => Ok(Cow::Borrowed(&*EP_FORMAT)),
    }
}

pub fn parse_episode_files<S>(
    path: &Path,
    pattern: Option<S>,
) -> Result<SeriesEpisodes, SeriesError>
where
    S: AsRef<str>,
{
    if !path.is_dir() {
        return Err(SeriesError::NotADirectory(path.to_string_lossy().into()));
    }

    let pattern = format_episode_parser_regex(pattern)?;
    let mut data = HashMap::new();

    for entry in fs::read_dir(path).map_err(SeriesError::Io)? {
        let entry = entry.map_err(SeriesError::Io)?.path();

        if !entry.is_file() {
            continue;
        }

        let episode = match EpisodeFile::parse(&entry, pattern.as_ref()) {
            Ok(episode) => episode,
            Err(SeriesError::EpisodeRegexCaptureFailed) => continue,
            Err(err) => return Err(err),
        };

        let series = data.entry(episode.series_name).or_insert_with(HashMap::new);
        series.insert(episode.episode_num, entry);
    }

    if data.is_empty() {
        return Err(SeriesError::NoSeriesFound);
    }

    Ok(data)
}

pub fn parse_episode_files_until_valid<S>(
    path: &Path,
    pattern: &mut Option<S>,
) -> Result<SeriesEpisodes, SeriesError>
where
    S: AsRef<str> + From<String>,
{
    loop {
        match parse_episode_files(path, pattern.as_ref()) {
            Ok(data) => break Ok(data),
            Err(SeriesError::NoSeriesFound) => {
                println!("no series found");
                println!("you will now be prompted to enter a custom regex pattern");
                println!("when entering the pattern, please mark the series name and episode number with {{name}} and {{episode}}, respectively");
                println!("example:");
                println!("  filename: [SubGroup] Series Name - Ep01.mkv");
                println!(r"  pattern: \[.+?\] {{name}} - Ep{{episode}}.mkv");
                println!("please enter your custom pattern:");

                *pattern = Some(input::read_line()?.into());
            }
            Err(err @ SeriesError::Regex(_)) | Err(err @ SeriesError::UnknownRegexCapture(_)) => {
                eprintln!("error parsing regex pattern: {}", err);
                println!("please try again:");

                *pattern = Some(input::read_line()?.into());
            }
            Err(err) => return Err(err),
        }
    }
}
<commit_msg>[series] Make the episode marker optional for custom patterns<commit_after>use super::{SeasonState, SeriesConfig};
use backend::{AnimeEntry, AnimeInfo, SyncBackend};
use error::SeriesError;
use input::{self, Answer};
use regex::Regex;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs;
use std::ops::Range;
use std::path::{Path, PathBuf};
use toml;

pub struct FolderData {
    pub series_info: SeriesInfo,
    pub savefile: SaveData,
    pub path: PathBuf,
}

impl FolderData {
    pub fn load_dir(path: &Path) -> Result<FolderData, SeriesError> {
        let mut savefile = SaveData::from_dir(path)?;
        let series_info = FolderData::load_series_info(path, &mut savefile)?;

        Ok(FolderData {
            series_info,
            savefile,
            path: PathBuf::from(path),
        })
    }

    fn load_series_info(path: &Path, savefile: &mut SaveData) -> Result<SeriesInfo, SeriesError> {
        let mut ep_data = parse_episode_files_until_valid(path, &mut savefile.episode_matcher)?;

        if let Some(info) = SeriesInfo::select_from_save(&mut ep_data, savefile) {
            return Ok(info);
        }

        let info = prompt_select_series_info(ep_data)?;
        savefile.files_title = Some(info.name.clone());

        Ok(info)
    }

    pub fn populate_season_data<B>(&mut self, config: &SeriesConfig<B>) -> Result<(), SeriesError>
    where
        B: SyncBackend,
    {
        let num_seasons = self.seasons().len();

        if num_seasons > config.season_num {
            return Ok(());
        }

        for cur_season in num_seasons..=config.season_num {
            let info = AnimeInfo::default();
            let entry = AnimeEntry::new(info);

            let mut season = SeasonState {
                state: entry,
                needs_info: true,
                needs_sync: config.offline_mode,
            };

            season.sync_info_from_remote(config, &self, cur_season)?;

            self.seasons_mut().push(season);
        }

        Ok(())
    }

    pub fn calculate_season_offset(&self, mut range: Range<usize>) -> u32 {
        let num_seasons = self.savefile.season_states.len();
        range.start = num_seasons.min(range.start);
        range.end = num_seasons.min(range.end);

        let mut offset = 0;

        for i in range {
            let season = &self.savefile.season_states[i];

            match season.state.info.episodes {
                Some(eps) => offset += eps,
                None => return offset,
            }
        }

        offset
    }

    pub fn try_remove_dir(&self) {
        let path = self.path.to_string_lossy();

        println!("WARNING: {} will be deleted", path);
        println!("is this ok? (y/N)");

        match input::read_yn(Answer::No) {
            Ok(true) => match fs::remove_dir_all(&self.path) {
                Ok(_) => (),
                Err(err) => {
                    eprintln!("failed to remove directory: {}", err);
                }
            },
            Ok(false) => (),
            Err(err) => {
                eprintln!("failed to read input: {}", err);
            }
        }
    }

    pub fn get_episode(&self, episode: u32) -> Result<&PathBuf, SeriesError> {
        self.series_info.get_episode(episode)
    }

    pub fn save(&self) -> Result<(), SeriesError> {
        self.savefile.write_to_file()
    }

    pub fn seasons(&self) -> &Vec<SeasonState> {
        &self.savefile.season_states
    }

    pub fn seasons_mut(&mut self) -> &mut Vec<SeasonState> {
        &mut self.savefile.season_states
    }
}

#[derive(Serialize, Deserialize)]
pub struct SaveData {
    pub episode_matcher: Option<String>,
    pub files_title: Option<String>,
    pub season_states: Vec<SeasonState>,
    #[serde(skip)]
    pub path: PathBuf,
}

impl SaveData {
    const DATA_FILE_NAME: &'static str = ".anup";

    pub fn new(path: PathBuf) -> SaveData {
        SaveData {
            episode_matcher: None,
            files_title: None,
            season_states: Vec::new(),
            path,
        }
    }

    pub fn from_dir(path: &Path) -> Result<SaveData, SeriesError> {
        let path = PathBuf::from(path).join(SaveData::DATA_FILE_NAME);

        if !path.exists() {
            return Ok(SaveData::new(path));
        }

        let file_contents = fs::read_to_string(&path)?;

        let mut save_data: SaveData = toml::from_str(&file_contents)?;
        save_data.path = path;

        Ok(save_data)
    }

    pub fn write_to_file(&self) -> Result<(), SeriesError> {
        let toml = toml::to_string(self)?;
        fs::write(&self.path, toml)?;

        Ok(())
    }
}

pub struct SeriesInfo {
    pub name: String,
    pub episodes: HashMap<u32, PathBuf>,
}

impl SeriesInfo {
    pub fn get_episode(&self, episode: u32) -> Result<&PathBuf, SeriesError> {
        self.episodes
            .get(&episode)
            .ok_or_else(|| SeriesError::EpisodeNotFound(episode))
    }

    pub fn select_from_save(
        ep_data: &mut SeriesEpisodes,
        savefile: &SaveData,
    ) -> Option<SeriesInfo> {
        if let Some(name) = &savefile.files_title {
            let entry = ep_data.remove_entry(name);

            if let Some((name, episodes)) = entry {
                return Some(SeriesInfo { name, episodes });
            }
        }

        None
    }
}

struct EpisodeFile {
    series_name: String,
    episode_num: u32,
}

impl EpisodeFile {
    fn parse(path: &Path, matcher: &Regex) -> Result<EpisodeFile, SeriesError> {
        // Replace certain characters with spaces since they can prevent proper series
        // identification or prevent it from being found on a sync backend
        let filename = path
            .file_name()
            .and_then(|path| path.to_str())
            .ok_or(SeriesError::UnableToGetFilename)?;

        let caps = matcher
            .captures(&filename)
            .ok_or(SeriesError::EpisodeRegexCaptureFailed)?;

        let series_name = {
            let raw_name = caps
                .name("name")
                .map(|c| c.as_str())
                .ok_or_else(|| SeriesError::UnknownRegexCapture("name".into()))?;

            raw_name
                .replace('.', " ")
                .replace(" -", ":") // Dashes typically represent a colon in file names
                .replace('_', " ")
                .trim()
                .to_string()
        };

        let episode = match caps.name("episode") {
            Some(capture) => capture
                .as_str()
                .parse()
                .map_err(SeriesError::EpisodeNumParseFailed)?,
            None => 1,
        };

        Ok(EpisodeFile {
            series_name,
            episode_num: episode,
        })
    }
}

type EpisodePaths = HashMap<u32, PathBuf>;
type SeriesEpisodes = HashMap<String, EpisodePaths>;

pub fn prompt_select_series_info(info: SeriesEpisodes) -> Result<SeriesInfo, SeriesError> {
    if info.is_empty() {
        return Err(SeriesError::NoSeriesFound);
    }

    let mut info = info
        .into_iter()
        .map(|(name, eps)| SeriesInfo {
            name,
            episodes: eps,
        }).collect::<Vec<_>>();

    if info.len() == 1 {
        return Ok(info.swap_remove(0));
    }

    println!("multiple series found in directory");
    println!("please enter the number next to the episode files you want to use:");

    for (i, series) in info.iter().enumerate() {
        println!("{} [{}]", 1 + i, series.name);
    }

    let index = input::read_range(1, info.len())? - 1;
    let series = info.swap_remove(index);

    Ok(series)
}

// This default pattern will match episodes in several common formats, such as:
// [Group] Series Name - 01.mkv
// [Group]_Series_Name_-_01.mkv
// [Group].Series.Name.-.01.mkv
// [Group] Series Name - 01 [tag 1][tag 2].mkv
// [Group]_Series_Name_-_01_[tag1][tag2].mkv
// [Group].Series.Name.-.01.[tag1][tag2].mkv
// Series Name - 01.mkv
// Series_Name_-_01.mkv
// Series.Name.-.01.mkv
const EP_FORMAT_REGEX: &str =
    r"(?:\[.+?\](?:\s+|_+|\.+))?(?P<name>.+?)(?:\s*|_*|\.*)-(?:\s*|_*|\.*)(?P<episode>\d+).*?\..+?";

fn format_episode_parser_regex<'a, S>(pattern: Option<S>) -> Result<Cow<'a, Regex>, SeriesError>
where
    S: AsRef<str>,
{
    lazy_static! {
        static ref EP_FORMAT: Regex = Regex::new(EP_FORMAT_REGEX).unwrap();
    }

    match pattern {
        Some(pattern) => {
            let pattern = pattern
                .as_ref()
                .replace("{name}", "(?P<name>.+?)")
                .replace("{episode}", r"(?P<episode>\d+)");

            let regex = Regex::new(&pattern)?;
            Ok(Cow::Owned(regex))
        }
        None => Ok(Cow::Borrowed(&*EP_FORMAT)),
    }
}

pub fn parse_episode_files<S>(
    path: &Path,
    pattern: Option<S>,
) -> Result<SeriesEpisodes, SeriesError>
where
    S: AsRef<str>,
{
    if !path.is_dir() {
        return Err(SeriesError::NotADirectory(path.to_string_lossy().into()));
    }

    let pattern = format_episode_parser_regex(pattern)?;
    let mut data = HashMap::new();

    for entry in fs::read_dir(path).map_err(SeriesError::Io)? {
        let entry = entry.map_err(SeriesError::Io)?.path();

        if !entry.is_file() {
            continue;
        }

        let episode = match EpisodeFile::parse(&entry, pattern.as_ref()) {
            Ok(episode) => episode,
            Err(SeriesError::EpisodeRegexCaptureFailed) => continue,
            Err(err) => return Err(err),
        };

        let series = data.entry(episode.series_name).or_insert_with(HashMap::new);
        series.insert(episode.episode_num, entry);
    }

    if data.is_empty() {
        return Err(SeriesError::NoSeriesFound);
    }

    Ok(data)
}

pub fn parse_episode_files_until_valid<S>(
    path: &Path,
    pattern: &mut Option<S>,
) -> Result<SeriesEpisodes, SeriesError>
where
    S: AsRef<str> + From<String>,
{
    loop {
        match parse_episode_files(path, pattern.as_ref()) {
            Ok(data) => break Ok(data),
            Err(SeriesError::NoSeriesFound) => {
                eprintln!("error: no series found");
                println!("you will now be prompted to enter a custom regex pattern");
                println!("when entering the pattern, please mark the series name and episode number with {{name}} and {{episode}}, respectively");
                println!("note: if you're trying to play a single file (like a movie) and it doesn't have an episode number, you can omit the {{episode}} marker");
                println!("example:");
                println!("  filename: [SubGroup] Series Name - Ep01 [1080p].mkv");
                println!(r"  pattern: \[.+?\] {{name}} - Ep{{episode}} \[1080p\].mkv");
                println!("please enter your custom pattern:");

                *pattern = Some(input::read_line()?.into());
            }
            Err(err @ SeriesError::Regex(_)) | Err(err @ SeriesError::UnknownRegexCapture(_)) => {
                eprintln!("error parsing regex pattern: {}", err);
                println!("please try again:");

                *pattern = Some(input::read_line()?.into());
            }
            Err(err) => return Err(err),
        }
    }
}
<|endoftext|>


<commit_before>use super::{search_for_series_info, SeasonState, SeriesConfig};
use backend::{AnimeEntry, AnimeInfo, SyncBackend};
use error::SeriesError;
use input::{self, Answer};
use regex::Regex;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs;
use std::ops::Range;
use std::path::{Path, PathBuf};
use toml;

pub struct FolderData {
    pub episodes: EpisodeData,
    pub savefile: SaveData,
    pub path: PathBuf,
}

impl FolderData {
    pub fn load_dir(path: &Path) -> Result<FolderData, SeriesError> {
        let mut savefile = SaveData::from_dir(path)?;
        let episodes = EpisodeData::parse_until_valid_pattern(path, &mut savefile.episode_matcher)?;

        Ok(FolderData {
            episodes,
            savefile,
            path: PathBuf::from(path),
        })
    }

    pub fn save(&self) -> Result<(), SeriesError> {
        self.savefile.write_to_file()
    }

    pub fn populate_season_data<B>(&mut self, config: &SeriesConfig<B>) -> Result<(), SeriesError>
    where
        B: SyncBackend,
    {
        let num_seasons = self.seasons().len();

        if num_seasons > config.season_num {
            return Ok(());
        }

        for cur_season in num_seasons..=config.season_num {
            let info = self.fetch_series_info(config, cur_season)?;
            let entry = AnimeEntry::new(info);

            let season = SeasonState {
                state: entry,
                needs_info: config.offline_mode,
                needs_sync: config.offline_mode,
            };

            self.seasons_mut().push(season);
        }

        Ok(())
    }

    pub fn fetch_series_info<B>(
        &mut self,
        config: &SeriesConfig<B>,
        cur_season: usize,
    ) -> Result<AnimeInfo, SeriesError>
    where
        B: SyncBackend,
    {
        if config.offline_mode {
            // Return existing data if we already have it, otherwise return barebones info
            if self.seasons().len() > config.season_num {
                let info = self.seasons()[config.season_num].state.info.clone();
                Ok(info)
            } else {
                let mut info = AnimeInfo::default();
                info.title = self.episodes.series_name.clone();

                Ok(info)
            }
        } else {
            search_for_series_info(&config.sync_service, &self.episodes.series_name, cur_season)
        }
    }

    pub fn calculate_season_offset(&self, mut range: Range<usize>) -> u32 {
        let num_seasons = self.savefile.season_states.len();
        range.start = num_seasons.min(range.start);
        range.end = num_seasons.min(range.end);

        let mut offset = 0;

        for i in range {
            let season = &self.savefile.season_states[i];

            match season.state.info.episodes {
                Some(eps) => offset += eps,
                None => return offset,
            }
        }

        offset
    }

    pub fn try_remove_dir(&self) {
        let path = self.path.to_string_lossy();

        println!("WARNING: {} will be deleted", path);
        println!("is this ok? (y/N)");

        match input::read_yn(Answer::No) {
            Ok(true) => match fs::remove_dir_all(&self.path) {
                Ok(_) => (),
                Err(err) => {
                    eprintln!("failed to remove directory: {}", err);
                }
            },
            Ok(false) => (),
            Err(err) => {
                eprintln!("failed to read input: {}", err);
            }
        }
    }

    pub fn seasons(&self) -> &Vec<SeasonState> {
        &self.savefile.season_states
    }

    pub fn seasons_mut(&mut self) -> &mut Vec<SeasonState> {
        &mut self.savefile.season_states
    }
}

type SeriesName = String;
type EpisodeNum = u32;

#[derive(Debug)]
pub struct EpisodeData {
    pub series_name: String,
    pub episodes: HashMap<EpisodeNum, PathBuf>,
    pub custom_format: Option<String>,
}

impl EpisodeData {
    // This default pattern will match episodes in several common formats, such as:
    // [Group] Series Name - 01.mkv
    // [Group]_Series_Name_-_01.mkv
    // [Group].Series.Name.-.01.mkv
    // [Group] Series Name - 01 [tag 1][tag 2].mkv
    // [Group]_Series_Name_-_01_[tag1][tag2].mkv
    // [Group].Series.Name.-.01.[tag1][tag2].mkv
    // Series Name - 01.mkv
    // Series_Name_-_01.mkv
    // Series.Name.-.01.mkv
    pub const EP_FORMAT_REGEX: &'static str =
        r"(?:\[.+?\](?:\s+|_+|\.+))?(?P<name>.+?)(?:\s*|_*|\.*)-(?:\s*|_*|\.*)(?P<episode>\d+).*?\..+?";

    fn get_matcher<'a, S>(custom_format: Option<S>) -> Result<Cow<'a, Regex>, SeriesError>
    where
        S: AsRef<str>,
    {
        lazy_static! {
            static ref EP_FORMAT: Regex = Regex::new(EpisodeData::EP_FORMAT_REGEX).unwrap();
        }

        match custom_format {
            Some(raw_pattern) => {
                let pattern = raw_pattern
                    .as_ref()
                    .replace("{name}", "(?P<name>.+?)")
                    .replace("{episode}", r"(?P<episode>\d+)");

                let regex = Regex::new(&pattern)?;
                Ok(Cow::Owned(regex))
            }
            None => Ok(Cow::Borrowed(&*EP_FORMAT)),
        }
    }

    pub fn parse_dir<S>(dir: &Path, custom_format: Option<S>) -> Result<EpisodeData, SeriesError>
    where
        S: AsRef<str>,
    {
        if !dir.is_dir() {
            return Err(SeriesError::NotADirectory(
                dir.file_name()
                    .map(|n| n.to_string_lossy().into_owned())
                    .unwrap_or_else(|| "err".into()),
            ));
        }

        let matcher = EpisodeData::get_matcher(custom_format)?;

        let mut series_name = None;
        let mut episodes = HashMap::new();

        for entry in fs::read_dir(dir).map_err(SeriesError::Io)? {
            let path = entry.map_err(SeriesError::Io)?.path();

            if !path.is_file() {
                continue;
            }

            match EpisodeData::parse_filename(&path, matcher.as_ref()) {
                Ok((ep_name, ep_num)) => {
                    match series_name {
                        Some(ref series_name) if &ep_name != series_name => {
                            return Err(SeriesError::MultipleSeriesFound);
                        }
                        Some(_) => (),
                        None => series_name = Some(ep_name),
                    }

                    episodes.insert(ep_num, path);
                }
                Err(SeriesError::EpisodeRegexCaptureFailed) => continue,
                Err(e) => return Err(e),
            }
        }

        let name = series_name.ok_or(SeriesError::NoEpisodesFound)?;

        Ok(EpisodeData {
            series_name: name,
            episodes,
            custom_format: None,
        })
    }

    fn parse_filename(
        path: &Path,
        matcher: &Regex,
    ) -> Result<(SeriesName, EpisodeNum), SeriesError> {
        // Replace certain characters with spaces since they can prevent proper series
        // identification or prevent it from being found on a sync backend
        let filename = path
            .file_name()
            .and_then(|path| path.to_str())
            .ok_or(SeriesError::UnableToGetFilename)?;

        let caps = matcher
            .captures(&filename)
            .ok_or(SeriesError::EpisodeRegexCaptureFailed)?;

        let series_name = {
            let raw_name = caps
                .name("name")
                .map(|c| c.as_str())
                .ok_or_else(|| SeriesError::UnknownRegexCapture("name".into()))?;

            raw_name
                .replace('.', " ")
                .replace(" -", ":") // Dashes typically represent a colon in file names
                .replace('_', " ")
                .trim()
                .to_string()
        };

        let episode = caps
            .name("episode")
            .ok_or_else(|| SeriesError::UnknownRegexCapture("episode".into()))
            .and_then(|cap| {
                cap.as_str()
                    .parse()
                    .map_err(SeriesError::EpisodeNumParseFailed)
            })?;

        Ok((series_name, episode))
    }

    pub fn parse_until_valid_pattern(
        path: &Path,
        pattern: &mut Option<String>,
    ) -> Result<EpisodeData, SeriesError> {
        loop {
            match EpisodeData::parse_dir(path, pattern.as_ref()) {
                Ok(data) => break Ok(data),
                Err(SeriesError::NoEpisodesFound) => {
                    println!("no episodes found");
                    println!("you will now be prompted to enter a custom regex pattern");
                    println!("when entering the pattern, please mark the series name and episode number with {{name}} and {{episode}}, respectively");
                    println!("example:");
                    println!("  filename: [SubGroup] Series Name - Ep01.mkv");
                    println!(r"  pattern: \[.+?\] {{name}} - Ep{{episode}}.mkv");
                    println!("please enter your custom pattern:");

                    *pattern = Some(input::read_line()?);
                }
                Err(err @ SeriesError::Regex(_))
                | Err(err @ SeriesError::UnknownRegexCapture(_)) => {
                    eprintln!("error parsing regex pattern: {}", err);
                    println!("please try again:");

                    *pattern = Some(input::read_line()?);
                }
                Err(err) => return Err(err),
            }
        }
    }

    pub fn get_episode(&self, episode: u32) -> Result<&PathBuf, SeriesError> {
        self.episodes
            .get(&episode)
            .ok_or_else(|| SeriesError::EpisodeNotFound(episode))
    }
}

#[derive(Serialize, Deserialize)]
pub struct SaveData {
    pub episode_matcher: Option<String>,
    pub season_states: Vec<SeasonState>,
    #[serde(skip)]
    pub path: PathBuf,
}

impl SaveData {
    const DATA_FILE_NAME: &'static str = ".anup";

    pub fn new(path: PathBuf) -> SaveData {
        SaveData {
            episode_matcher: None,
            season_states: Vec::new(),
            path,
        }
    }

    pub fn from_dir(path: &Path) -> Result<SaveData, SeriesError> {
        let path = PathBuf::from(path).join(SaveData::DATA_FILE_NAME);

        if !path.exists() {
            return Ok(SaveData::new(path));
        }

        let file_contents = fs::read_to_string(&path)?;

        let mut save_data: SaveData = toml::from_str(&file_contents)?;
        save_data.path = path;

        Ok(save_data)
    }

    pub fn write_to_file(&self) -> Result<(), SeriesError> {
        let toml = toml::to_string_pretty(self)?;
        fs::write(&self.path, toml)?;

        Ok(())
    }
}
<commit_msg>[series] Get initial series info from SeasonState::sync_info_from_remote<commit_after>use super::{SeasonState, SeriesConfig};
use backend::{AnimeEntry, AnimeInfo, SyncBackend};
use error::SeriesError;
use input::{self, Answer};
use regex::Regex;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs;
use std::ops::Range;
use std::path::{Path, PathBuf};
use toml;

pub struct FolderData {
    pub episodes: EpisodeData,
    pub savefile: SaveData,
    pub path: PathBuf,
}

impl FolderData {
    pub fn load_dir(path: &Path) -> Result<FolderData, SeriesError> {
        let mut savefile = SaveData::from_dir(path)?;
        let episodes = EpisodeData::parse_until_valid_pattern(path, &mut savefile.episode_matcher)?;

        Ok(FolderData {
            episodes,
            savefile,
            path: PathBuf::from(path),
        })
    }

    pub fn save(&self) -> Result<(), SeriesError> {
        self.savefile.write_to_file()
    }

    pub fn populate_season_data<B>(&mut self, config: &SeriesConfig<B>) -> Result<(), SeriesError>
    where
        B: SyncBackend,
    {
        let num_seasons = self.seasons().len();

        if num_seasons > config.season_num {
            return Ok(());
        }

        for cur_season in num_seasons..=config.season_num {
            let info = AnimeInfo::default();
            let entry = AnimeEntry::new(info);

            let mut season = SeasonState {
                state: entry,
                needs_info: true,
                needs_sync: config.offline_mode,
            };

            season.sync_info_from_remote(config, &self, cur_season)?;

            self.seasons_mut().push(season);
        }

        Ok(())
    }

    pub fn calculate_season_offset(&self, mut range: Range<usize>) -> u32 {
        let num_seasons = self.savefile.season_states.len();
        range.start = num_seasons.min(range.start);
        range.end = num_seasons.min(range.end);

        let mut offset = 0;

        for i in range {
            let season = &self.savefile.season_states[i];

            match season.state.info.episodes {
                Some(eps) => offset += eps,
                None => return offset,
            }
        }

        offset
    }

    pub fn try_remove_dir(&self) {
        let path = self.path.to_string_lossy();

        println!("WARNING: {} will be deleted", path);
        println!("is this ok? (y/N)");

        match input::read_yn(Answer::No) {
            Ok(true) => match fs::remove_dir_all(&self.path) {
                Ok(_) => (),
                Err(err) => {
                    eprintln!("failed to remove directory: {}", err);
                }
            },
            Ok(false) => (),
            Err(err) => {
                eprintln!("failed to read input: {}", err);
            }
        }
    }

    pub fn seasons(&self) -> &Vec<SeasonState> {
        &self.savefile.season_states
    }

    pub fn seasons_mut(&mut self) -> &mut Vec<SeasonState> {
        &mut self.savefile.season_states
    }
}

type SeriesName = String;
type EpisodeNum = u32;

#[derive(Debug)]
pub struct EpisodeData {
    pub series_name: String,
    pub episodes: HashMap<EpisodeNum, PathBuf>,
    pub custom_format: Option<String>,
}

impl EpisodeData {
    // This default pattern will match episodes in several common formats, such as:
    // [Group] Series Name - 01.mkv
    // [Group]_Series_Name_-_01.mkv
    // [Group].Series.Name.-.01.mkv
    // [Group] Series Name - 01 [tag 1][tag 2].mkv
    // [Group]_Series_Name_-_01_[tag1][tag2].mkv
    // [Group].Series.Name.-.01.[tag1][tag2].mkv
    // Series Name - 01.mkv
    // Series_Name_-_01.mkv
    // Series.Name.-.01.mkv
    pub const EP_FORMAT_REGEX: &'static str =
        r"(?:\[.+?\](?:\s+|_+|\.+))?(?P<name>.+?)(?:\s*|_*|\.*)-(?:\s*|_*|\.*)(?P<episode>\d+).*?\..+?";

    fn get_matcher<'a, S>(custom_format: Option<S>) -> Result<Cow<'a, Regex>, SeriesError>
    where
        S: AsRef<str>,
    {
        lazy_static! {
            static ref EP_FORMAT: Regex = Regex::new(EpisodeData::EP_FORMAT_REGEX).unwrap();
        }

        match custom_format {
            Some(raw_pattern) => {
                let pattern = raw_pattern
                    .as_ref()
                    .replace("{name}", "(?P<name>.+?)")
                    .replace("{episode}", r"(?P<episode>\d+)");

                let regex = Regex::new(&pattern)?;
                Ok(Cow::Owned(regex))
            }
            None => Ok(Cow::Borrowed(&*EP_FORMAT)),
        }
    }

    pub fn parse_dir<S>(dir: &Path, custom_format: Option<S>) -> Result<EpisodeData, SeriesError>
    where
        S: AsRef<str>,
    {
        if !dir.is_dir() {
            return Err(SeriesError::NotADirectory(
                dir.file_name()
                    .map(|n| n.to_string_lossy().into_owned())
                    .unwrap_or_else(|| "err".into()),
            ));
        }

        let matcher = EpisodeData::get_matcher(custom_format)?;

        let mut series_name = None;
        let mut episodes = HashMap::new();

        for entry in fs::read_dir(dir).map_err(SeriesError::Io)? {
            let path = entry.map_err(SeriesError::Io)?.path();

            if !path.is_file() {
                continue;
            }

            match EpisodeData::parse_filename(&path, matcher.as_ref()) {
                Ok((ep_name, ep_num)) => {
                    match series_name {
                        Some(ref series_name) if &ep_name != series_name => {
                            return Err(SeriesError::MultipleSeriesFound);
                        }
                        Some(_) => (),
                        None => series_name = Some(ep_name),
                    }

                    episodes.insert(ep_num, path);
                }
                Err(SeriesError::EpisodeRegexCaptureFailed) => continue,
                Err(e) => return Err(e),
            }
        }

        let name = series_name.ok_or(SeriesError::NoEpisodesFound)?;

        Ok(EpisodeData {
            series_name: name,
            episodes,
            custom_format: None,
        })
    }

    fn parse_filename(
        path: &Path,
        matcher: &Regex,
    ) -> Result<(SeriesName, EpisodeNum), SeriesError> {
        // Replace certain characters with spaces since they can prevent proper series
        // identification or prevent it from being found on a sync backend
        let filename = path
            .file_name()
            .and_then(|path| path.to_str())
            .ok_or(SeriesError::UnableToGetFilename)?;

        let caps = matcher
            .captures(&filename)
            .ok_or(SeriesError::EpisodeRegexCaptureFailed)?;

        let series_name = {
            let raw_name = caps
                .name("name")
                .map(|c| c.as_str())
                .ok_or_else(|| SeriesError::UnknownRegexCapture("name".into()))?;

            raw_name
                .replace('.', " ")
                .replace(" -", ":") // Dashes typically represent a colon in file names
                .replace('_', " ")
                .trim()
                .to_string()
        };

        let episode = caps
            .name("episode")
            .ok_or_else(|| SeriesError::UnknownRegexCapture("episode".into()))
            .and_then(|cap| {
                cap.as_str()
                    .parse()
                    .map_err(SeriesError::EpisodeNumParseFailed)
            })?;

        Ok((series_name, episode))
    }

    pub fn parse_until_valid_pattern(
        path: &Path,
        pattern: &mut Option<String>,
    ) -> Result<EpisodeData, SeriesError> {
        loop {
            match EpisodeData::parse_dir(path, pattern.as_ref()) {
                Ok(data) => break Ok(data),
                Err(SeriesError::NoEpisodesFound) => {
                    println!("no episodes found");
                    println!("you will now be prompted to enter a custom regex pattern");
                    println!("when entering the pattern, please mark the series name and episode number with {{name}} and {{episode}}, respectively");
                    println!("example:");
                    println!("  filename: [SubGroup] Series Name - Ep01.mkv");
                    println!(r"  pattern: \[.+?\] {{name}} - Ep{{episode}}.mkv");
                    println!("please enter your custom pattern:");

                    *pattern = Some(input::read_line()?);
                }
                Err(err @ SeriesError::Regex(_))
                | Err(err @ SeriesError::UnknownRegexCapture(_)) => {
                    eprintln!("error parsing regex pattern: {}", err);
                    println!("please try again:");

                    *pattern = Some(input::read_line()?);
                }
                Err(err) => return Err(err),
            }
        }
    }

    pub fn get_episode(&self, episode: u32) -> Result<&PathBuf, SeriesError> {
        self.episodes
            .get(&episode)
            .ok_or_else(|| SeriesError::EpisodeNotFound(episode))
    }
}

#[derive(Serialize, Deserialize)]
pub struct SaveData {
    pub episode_matcher: Option<String>,
    pub season_states: Vec<SeasonState>,
    #[serde(skip)]
    pub path: PathBuf,
}

impl SaveData {
    const DATA_FILE_NAME: &'static str = ".anup";

    pub fn new(path: PathBuf) -> SaveData {
        SaveData {
            episode_matcher: None,
            season_states: Vec::new(),
            path,
        }
    }

    pub fn from_dir(path: &Path) -> Result<SaveData, SeriesError> {
        let path = PathBuf::from(path).join(SaveData::DATA_FILE_NAME);

        if !path.exists() {
            return Ok(SaveData::new(path));
        }

        let file_contents = fs::read_to_string(&path)?;

        let mut save_data: SaveData = toml::from_str(&file_contents)?;
        save_data.path = path;

        Ok(save_data)
    }

    pub fn write_to_file(&self) -> Result<(), SeriesError> {
        let toml = toml::to_string_pretty(self)?;
        fs::write(&self.path, toml)?;

        Ok(())
    }
}
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2016 Boucher, Antoni <bouanto@zoho.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

use std::cell::Cell;

use gtk::{BoxExt, ContainerExt, EditableExt, Entry, EntryExt, Label, WidgetExt};
use gtk::Orientation::Horizontal;

pub type StatusBarWidget = HBox;
pub type HBox = ::gtk::Box;

/// The window status bar.
pub struct StatusBar {
    colon_label: Label,
    entry: Entry,
    entry_shown: Cell<bool>,
    hbox: HBox,
}

impl StatusBar {
    /// Create a new status bar.
    pub fn new() -> Self {
        let hbox = HBox::new(Horizontal, 0);

        let colon_label = Label::new(Some(":"));
        hbox.add(&colon_label);

        let entry = Entry::new();
        entry.set_has_frame(false);
        entry.set_hexpand(true);
        hbox.add(&entry);

        StatusBar {
            colon_label: colon_label,
            entry: entry,
            entry_shown: Cell::new(false),
            hbox: hbox,
        }
    }

    /// Add an item.
    pub fn add_item(&self, item: &StatusBarItem) {
        item.label.show();
        if item.left {
            self.hbox.pack_start(item, false, false, 3);
        }
        else {
            self.hbox.pack_end(item, false, false, 3);
        }
    }

    /// Connect the active entry event.
    pub fn connect_activate<F: Fn(Option<String>) + 'static>(&self, callback: F) {
        self.entry.connect_activate(move |entry| callback(entry.get_text()));
    }

    /// Get whether the entry is shown or not.
    pub fn entry_shown(&self) -> bool {
        self.entry_shown.get()
    }

    /// Hide all the widgets.
    pub fn hide(&self) {
        self.hide_entry();
    }

    /// Hide the entry.
    pub fn hide_entry(&self) {
        self.entry_shown.set(false);
        self.entry.hide();
        self.colon_label.hide();
    }

    /// Set the text of the command entry.
    pub fn set_command(&self, command: &str) {
        self.entry.set_text(command);
        self.entry.set_position(command.len() as i32);
    }

    /// Show the entry.
    pub fn show_entry(&self) {
        self.entry_shown.set(true);
        self.entry.set_text("");
        self.entry.show();
        self.entry.grab_focus();
        self.colon_label.show();
    }
}

is_widget!(StatusBar, hbox);

/// A status bar text item.
pub struct StatusBarItem {
    label: Label,
    left: bool,
}

impl StatusBarItem {
    /// Create a new status bar item.
    #[allow(new_without_default)]
    pub fn new() -> Self {
        StatusBarItem {
            label: Label::new(None),
            left: false,
        }
    }

    /// Set the item to be on the left.
    pub fn left(mut self) -> Self {
        self.left = true;
        self
    }

    /// Set the item text.
    pub fn set_text(&self, text: &str) {
        self.label.set_text(text);
        self.label.show();
    }
}

is_widget!(StatusBarItem, label);
<commit_msg>Adjusted the style of the command input text.<commit_after>/*
 * Copyright (c) 2016 Boucher, Antoni <bouanto@zoho.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

use std::cell::Cell;

use gtk::{BoxExt, ContainerExt, CssProvider, EditableExt, Entry, EntryExt, Label, WidgetExt, STYLE_PROVIDER_PRIORITY_APPLICATION};
use gtk::Orientation::Horizontal;

pub type StatusBarWidget = HBox;
pub type HBox = ::gtk::Box;

/// The window status bar.
pub struct StatusBar {
    colon_label: Label,
    entry: Entry,
    entry_shown: Cell<bool>,
    hbox: HBox,
}

impl StatusBar {
    /// Create a new status bar.
    pub fn new() -> Self {
        let hbox = HBox::new(Horizontal, 0);
        hbox.set_size_request(1, 20);

        let colon_label = Label::new(Some(":"));
        hbox.add(&colon_label);

        let entry = Entry::new();
        StatusBar::adjust_entry(&entry);
        hbox.add(&entry);

        StatusBar {
            colon_label: colon_label,
            entry: entry,
            entry_shown: Cell::new(false),
            hbox: hbox,
        }
    }

    /// Add an item.
    pub fn add_item(&self, item: &StatusBarItem) {
        item.label.show();
        if item.left {
            self.hbox.pack_start(item, false, false, 3);
        }
        else {
            self.hbox.pack_end(item, false, false, 3);
        }
    }

    /// Adjust the look of the entry.
    fn adjust_entry(entry: &Entry) {
        let style_context = entry.get_style_context().unwrap();
        style_context.add_class("mg-input-command");
        let style = ".mg-input-command {
            box-shadow: none;
            padding: 0;
        }";
        let provider = CssProvider::new();
        provider.load_from_data(style).unwrap();
        style_context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_APPLICATION);
        entry.set_has_frame(false);
        entry.set_hexpand(true);
    }

    /// Connect the active entry event.
    pub fn connect_activate<F: Fn(Option<String>) + 'static>(&self, callback: F) {
        self.entry.connect_activate(move |entry| callback(entry.get_text()));
    }

    /// Get whether the entry is shown or not.
    pub fn entry_shown(&self) -> bool {
        self.entry_shown.get()
    }

    /// Hide all the widgets.
    pub fn hide(&self) {
        self.hide_entry();
    }

    /// Hide the entry.
    pub fn hide_entry(&self) {
        self.entry_shown.set(false);
        self.entry.hide();
        self.colon_label.hide();
    }

    /// Set the text of the command entry.
    pub fn set_command(&self, command: &str) {
        self.entry.set_text(command);
        self.entry.set_position(command.len() as i32);
    }

    /// Show the entry.
    pub fn show_entry(&self) {
        self.entry_shown.set(true);
        self.entry.set_text("");
        self.entry.show();
        self.entry.grab_focus();
        self.colon_label.show();
    }
}

is_widget!(StatusBar, hbox);

/// A status bar text item.
pub struct StatusBarItem {
    label: Label,
    left: bool,
}

impl StatusBarItem {
    /// Create a new status bar item.
    #[allow(new_without_default)]
    pub fn new() -> Self {
        StatusBarItem {
            label: Label::new(None),
            left: false,
        }
    }

    /// Set the item to be on the left.
    pub fn left(mut self) -> Self {
        self.left = true;
        self
    }

    /// Set the item text.
    pub fn set_text(&self, text: &str) {
        self.label.set_text(text);
        self.label.show();
    }
}

is_widget!(StatusBarItem, label);
<|endoftext|>


<commit_before>//! An abstraction of a plist file as a stream of events. Used to support multiple encodings.

mod binary_reader;
pub use self::binary_reader::BinaryReader;

mod binary_writer;
pub use self::binary_writer::BinaryWriter;

mod xml_reader;
pub use self::xml_reader::XmlReader;

mod xml_writer;
pub use self::xml_writer::XmlWriter;

use std::{
    io::{self, Read, Seek, SeekFrom},
    vec,
};

use crate::{
    dictionary,
    error::{Error, ErrorKind},
    Date, Integer, Uid, Value,
};

/// An encoding of a plist as a flat structure.
///
/// Output by the event readers.
///
/// Dictionary keys and values are represented as pairs of values e.g.:
///
/// ```ignore rust
/// StartDictionary
/// String("Height") // Key
/// Real(181.2)      // Value
/// String("Age")    // Key
/// Integer(28)      // Value
/// EndDictionary
/// ```
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
    // While the length of an array or dict cannot be feasably greater than max(usize) this better
    // conveys the concept of an effectively unbounded event stream.
    StartArray(Option<u64>),
    StartDictionary(Option<u64>),
    EndCollection,

    Boolean(bool),
    Data(Vec<u8>),
    Date(Date),
    Integer(Integer),
    Real(f64),
    String(String),
    Uid(Uid),

    #[doc(hidden)]
    __Nonexhaustive,
}

/// An `Event` stream returned by `Value::into_events`.
pub struct IntoEvents {
    stack: Vec<StackItem>,
}

enum StackItem {
    Root(Value),
    Array(vec::IntoIter<Value>),
    Dict(dictionary::IntoIter),
    DictValue(Value),
}

impl IntoEvents {
    pub(crate) fn new(value: Value) -> IntoEvents {
        IntoEvents {
            stack: vec![StackItem::Root(value)],
        }
    }
}

impl Iterator for IntoEvents {
    type Item = Event;

    fn next(&mut self) -> Option<Event> {
        fn handle_value(value: Value, stack: &mut Vec<StackItem>) -> Event {
            match value {
                Value::Array(array) => {
                    let len = array.len();
                    let iter = array.into_iter();
                    stack.push(StackItem::Array(iter));
                    Event::StartArray(Some(len as u64))
                }
                Value::Dictionary(dict) => {
                    let len = dict.len();
                    let iter = dict.into_iter();
                    stack.push(StackItem::Dict(iter));
                    Event::StartDictionary(Some(len as u64))
                }
                Value::Boolean(value) => Event::Boolean(value),
                Value::Data(value) => Event::Data(value),
                Value::Date(value) => Event::Date(value),
                Value::Real(value) => Event::Real(value),
                Value::Integer(value) => Event::Integer(value),
                Value::String(value) => Event::String(value),
                Value::Uid(value) => Event::Uid(value),
                Value::__Nonexhaustive => unreachable!(),
            }
        }

        Some(match self.stack.pop()? {
            StackItem::Root(value) => handle_value(value, &mut self.stack),
            StackItem::Array(mut array) => {
                if let Some(value) = array.next() {
                    // There might still be more items in the array so return it to the stack.
                    self.stack.push(StackItem::Array(array));
                    handle_value(value, &mut self.stack)
                } else {
                    Event::EndCollection
                }
            }
            StackItem::Dict(mut dict) => {
                if let Some((key, value)) = dict.next() {
                    // There might still be more items in the dictionary so return it to the stack.
                    self.stack.push(StackItem::Dict(dict));
                    // The next event to be returned must be the dictionary value.
                    self.stack.push(StackItem::DictValue(value));
                    // Return the key event now.
                    Event::String(key)
                } else {
                    Event::EndCollection
                }
            }
            StackItem::DictValue(value) => handle_value(value, &mut self.stack),
        })
    }
}

pub struct Reader<R: Read + Seek>(ReaderInner<R>);

enum ReaderInner<R: Read + Seek> {
    Uninitialized(Option<R>),
    Xml(XmlReader<R>),
    Binary(BinaryReader<R>),
}

impl<R: Read + Seek> Reader<R> {
    pub fn new(reader: R) -> Reader<R> {
        Reader(ReaderInner::Uninitialized(Some(reader)))
    }

    fn is_binary(reader: &mut R) -> Result<bool, Error> {
        fn from_io_offset_0(err: io::Error) -> Error {
            ErrorKind::Io(err).with_byte_offset(0)
        }

        reader.seek(SeekFrom::Start(0)).map_err(from_io_offset_0)?;
        let mut magic = [0; 8];
        reader.read_exact(&mut magic).map_err(from_io_offset_0)?;
        reader.seek(SeekFrom::Start(0)).map_err(from_io_offset_0)?;

        Ok(&magic == b"bplist00")
    }
}

impl<R: Read + Seek> Iterator for Reader<R> {
    type Item = Result<Event, Error>;

    fn next(&mut self) -> Option<Result<Event, Error>> {
        let mut reader = match self.0 {
            ReaderInner::Xml(ref mut parser) => return parser.next(),
            ReaderInner::Binary(ref mut parser) => return parser.next(),
            ReaderInner::Uninitialized(ref mut reader) => reader.take().unwrap(),
        };

        let event_reader = match Reader::is_binary(&mut reader) {
            Ok(true) => ReaderInner::Binary(BinaryReader::new(reader)),
            Ok(false) => ReaderInner::Xml(XmlReader::new(reader)),
            Err(err) => {
                ::std::mem::replace(&mut self.0, ReaderInner::Uninitialized(Some(reader)));
                return Some(Err(err));
            }
        };

        ::std::mem::replace(&mut self.0, event_reader);

        self.next()
    }
}

/// Supports writing event streams in different plist encodings.
pub trait Writer: private::Sealed {
    fn write(&mut self, event: &Event) -> Result<(), Error> {
        match event {
            Event::StartArray(len) => self.write_start_array(*len),
            Event::StartDictionary(len) => self.write_start_dictionary(*len),
            Event::EndCollection => self.write_end_collection(),
            Event::Boolean(value) => self.write_boolean(*value),
            Event::Data(value) => self.write_data(value),
            Event::Date(value) => self.write_date(*value),
            Event::Integer(value) => self.write_integer(*value),
            Event::Real(value) => self.write_real(*value),
            Event::String(value) => self.write_string(value),
            Event::Uid(value) => self.write_uid(*value),
            Event::__Nonexhaustive => unreachable!(),
        }
    }

    fn write_start_array(&mut self, len: Option<u64>) -> Result<(), Error>;
    fn write_start_dictionary(&mut self, len: Option<u64>) -> Result<(), Error>;
    fn write_end_collection(&mut self) -> Result<(), Error>;

    fn write_boolean(&mut self, value: bool) -> Result<(), Error>;
    fn write_data(&mut self, value: &[u8]) -> Result<(), Error>;
    fn write_date(&mut self, value: Date) -> Result<(), Error>;
    fn write_integer(&mut self, value: Integer) -> Result<(), Error>;
    fn write_real(&mut self, value: f64) -> Result<(), Error>;
    fn write_string(&mut self, value: &str) -> Result<(), Error>;
    fn write_uid(&mut self, value: Uid) -> Result<(), Error>;
}

pub(crate) mod private {
    use std::io::Write;

    pub trait Sealed {}

    impl<W: Write> Sealed for super::BinaryWriter<W> {}
    impl<W: Write> Sealed for super::XmlWriter<W> {}
}
<commit_msg>Fix warning about unneeded `mem::replace`.<commit_after>//! An abstraction of a plist file as a stream of events. Used to support multiple encodings.

mod binary_reader;
pub use self::binary_reader::BinaryReader;

mod binary_writer;
pub use self::binary_writer::BinaryWriter;

mod xml_reader;
pub use self::xml_reader::XmlReader;

mod xml_writer;
pub use self::xml_writer::XmlWriter;

use std::{
    io::{self, Read, Seek, SeekFrom},
    vec,
};

use crate::{
    dictionary,
    error::{Error, ErrorKind},
    Date, Integer, Uid, Value,
};

/// An encoding of a plist as a flat structure.
///
/// Output by the event readers.
///
/// Dictionary keys and values are represented as pairs of values e.g.:
///
/// ```ignore rust
/// StartDictionary
/// String("Height") // Key
/// Real(181.2)      // Value
/// String("Age")    // Key
/// Integer(28)      // Value
/// EndDictionary
/// ```
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
    // While the length of an array or dict cannot be feasably greater than max(usize) this better
    // conveys the concept of an effectively unbounded event stream.
    StartArray(Option<u64>),
    StartDictionary(Option<u64>),
    EndCollection,

    Boolean(bool),
    Data(Vec<u8>),
    Date(Date),
    Integer(Integer),
    Real(f64),
    String(String),
    Uid(Uid),

    #[doc(hidden)]
    __Nonexhaustive,
}

/// An `Event` stream returned by `Value::into_events`.
pub struct IntoEvents {
    stack: Vec<StackItem>,
}

enum StackItem {
    Root(Value),
    Array(vec::IntoIter<Value>),
    Dict(dictionary::IntoIter),
    DictValue(Value),
}

impl IntoEvents {
    pub(crate) fn new(value: Value) -> IntoEvents {
        IntoEvents {
            stack: vec![StackItem::Root(value)],
        }
    }
}

impl Iterator for IntoEvents {
    type Item = Event;

    fn next(&mut self) -> Option<Event> {
        fn handle_value(value: Value, stack: &mut Vec<StackItem>) -> Event {
            match value {
                Value::Array(array) => {
                    let len = array.len();
                    let iter = array.into_iter();
                    stack.push(StackItem::Array(iter));
                    Event::StartArray(Some(len as u64))
                }
                Value::Dictionary(dict) => {
                    let len = dict.len();
                    let iter = dict.into_iter();
                    stack.push(StackItem::Dict(iter));
                    Event::StartDictionary(Some(len as u64))
                }
                Value::Boolean(value) => Event::Boolean(value),
                Value::Data(value) => Event::Data(value),
                Value::Date(value) => Event::Date(value),
                Value::Real(value) => Event::Real(value),
                Value::Integer(value) => Event::Integer(value),
                Value::String(value) => Event::String(value),
                Value::Uid(value) => Event::Uid(value),
                Value::__Nonexhaustive => unreachable!(),
            }
        }

        Some(match self.stack.pop()? {
            StackItem::Root(value) => handle_value(value, &mut self.stack),
            StackItem::Array(mut array) => {
                if let Some(value) = array.next() {
                    // There might still be more items in the array so return it to the stack.
                    self.stack.push(StackItem::Array(array));
                    handle_value(value, &mut self.stack)
                } else {
                    Event::EndCollection
                }
            }
            StackItem::Dict(mut dict) => {
                if let Some((key, value)) = dict.next() {
                    // There might still be more items in the dictionary so return it to the stack.
                    self.stack.push(StackItem::Dict(dict));
                    // The next event to be returned must be the dictionary value.
                    self.stack.push(StackItem::DictValue(value));
                    // Return the key event now.
                    Event::String(key)
                } else {
                    Event::EndCollection
                }
            }
            StackItem::DictValue(value) => handle_value(value, &mut self.stack),
        })
    }
}

pub struct Reader<R: Read + Seek>(ReaderInner<R>);

enum ReaderInner<R: Read + Seek> {
    Uninitialized(Option<R>),
    Xml(XmlReader<R>),
    Binary(BinaryReader<R>),
}

impl<R: Read + Seek> Reader<R> {
    pub fn new(reader: R) -> Reader<R> {
        Reader(ReaderInner::Uninitialized(Some(reader)))
    }

    fn is_binary(reader: &mut R) -> Result<bool, Error> {
        fn from_io_offset_0(err: io::Error) -> Error {
            ErrorKind::Io(err).with_byte_offset(0)
        }

        reader.seek(SeekFrom::Start(0)).map_err(from_io_offset_0)?;
        let mut magic = [0; 8];
        reader.read_exact(&mut magic).map_err(from_io_offset_0)?;
        reader.seek(SeekFrom::Start(0)).map_err(from_io_offset_0)?;

        Ok(&magic == b"bplist00")
    }
}

impl<R: Read + Seek> Iterator for Reader<R> {
    type Item = Result<Event, Error>;

    fn next(&mut self) -> Option<Result<Event, Error>> {
        let mut reader = match self.0 {
            ReaderInner::Xml(ref mut parser) => return parser.next(),
            ReaderInner::Binary(ref mut parser) => return parser.next(),
            ReaderInner::Uninitialized(ref mut reader) => reader.take().unwrap(),
        };

        match Reader::is_binary(&mut reader) {
            Ok(true) => self.0 = ReaderInner::Binary(BinaryReader::new(reader)),
            Ok(false) => self.0 = ReaderInner::Xml(XmlReader::new(reader)),
            Err(err) => {
                self.0 = ReaderInner::Uninitialized(Some(reader));
                return Some(Err(err));
            }
        }

        self.next()
    }
}

/// Supports writing event streams in different plist encodings.
pub trait Writer: private::Sealed {
    fn write(&mut self, event: &Event) -> Result<(), Error> {
        match event {
            Event::StartArray(len) => self.write_start_array(*len),
            Event::StartDictionary(len) => self.write_start_dictionary(*len),
            Event::EndCollection => self.write_end_collection(),
            Event::Boolean(value) => self.write_boolean(*value),
            Event::Data(value) => self.write_data(value),
            Event::Date(value) => self.write_date(*value),
            Event::Integer(value) => self.write_integer(*value),
            Event::Real(value) => self.write_real(*value),
            Event::String(value) => self.write_string(value),
            Event::Uid(value) => self.write_uid(*value),
            Event::__Nonexhaustive => unreachable!(),
        }
    }

    fn write_start_array(&mut self, len: Option<u64>) -> Result<(), Error>;
    fn write_start_dictionary(&mut self, len: Option<u64>) -> Result<(), Error>;
    fn write_end_collection(&mut self) -> Result<(), Error>;

    fn write_boolean(&mut self, value: bool) -> Result<(), Error>;
    fn write_data(&mut self, value: &[u8]) -> Result<(), Error>;
    fn write_date(&mut self, value: Date) -> Result<(), Error>;
    fn write_integer(&mut self, value: Integer) -> Result<(), Error>;
    fn write_real(&mut self, value: f64) -> Result<(), Error>;
    fn write_string(&mut self, value: &str) -> Result<(), Error>;
    fn write_uid(&mut self, value: Uid) -> Result<(), Error>;
}

pub(crate) mod private {
    use std::io::Write;

    pub trait Sealed {}

    impl<W: Write> Sealed for super::BinaryWriter<W> {}
    impl<W: Write> Sealed for super::XmlWriter<W> {}
}
<|endoftext|>


<commit_before>
use std::str::Chars;
use std::iter::{Iterator};

macro_rules! declare_keywords {(
    $( ($name:ident, $word:expr); )*
) => {
    #[derive(Copy, Clone, PartialEq, Eq, Debug)]
    pub enum Keyword {
        $( $name, )*
    }

    impl Keyword {
        // pub fn word(&self) -> &'static str {
        //     match *self {
        //         $( Keyword::$name => $word, )*
        //     }
        // }

        pub fn from_str(s: &String) -> Option<Self> {
            match &**s {
                $( $word => Some(Keyword::$name), )*
                _ => None,
            }
        }
    }
}}

declare_keywords! {
    (Public,        "public");
    (Private,       "private");
    (Protected,     "protected");
    (Class,         "class");
    (Static,        "static");
    (Import,        "import");

    // control structures
    (Do,     "do");
    (While,  "while");
    (For,    "for");
    (If,     "if");
    (Else,   "else");
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DelimToken {
    Paren,      // round ( )
    Bracket,    // square [ ]
    Brace,      // curly { }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Token {
    Keyword(Keyword),
    Whitespace(bool),
    Comment,
    Dot,
    OpenDelim(DelimToken),
    CloseDelim(DelimToken),
    Word(String),
    Other(String),
}

impl Token {
    pub fn is_real(&self) -> bool {
        match *self {
            Token::Whitespace(false) | Token::Comment => false,
            _ => true,
        }
    }
}

#[derive(Debug)]
pub struct TokenSpan {
    pub tok: Token,
    /// Byte position, half open: inclusive-exclusive
    pub span: (i64, i64),
}

pub struct Tokenizer<'a> {
    chs: Chars<'a>,

    curr: Option<char>,
    peek: Option<char>,
    curr_pos: i64,
    peek_pos: i64,
}

impl<'a> Tokenizer<'a> {
    pub fn new(content: &'a String) -> Tokenizer<'a> {
        let iter = content.chars();
        let mut tok = Tokenizer {
            chs: iter,
            curr: None,
            peek: None,
            curr_pos: -1,
            peek_pos: -1,
        };
        tok.bump();
        tok.bump();
        tok
    }

    fn bump(&mut self) {
        self.curr = self.peek;
        self.curr_pos = self.peek_pos;
        self.peek = self.chs.next();
        match self.peek {
            Some(c) => self.peek_pos += c.len_utf8() as i64,
            _ => {}
        };
    }

    fn skip_whitespace(&mut self) -> bool {
        let mut newline_found = false;
        while is_whitespace(self.curr.unwrap_or('x')) {
            if self.curr.unwrap_or('x') == '\n' && !newline_found {
                newline_found = true;
            }
            // else if newline_found {
            //     break;
            // }
            self.bump();
        }
        newline_found
    }

    fn skip_comment(&mut self) {
        // We know curr == '/' and peek is either '*' or '/'.
        // Note: `self.peek.is_some()` implies `self.curr.is_some()`
        if self.peek.unwrap() == '*' {
            while self.peek.is_some() &&
                !(self.curr.unwrap() == '*' && self.peek.unwrap() == '/') {
                self.bump();
            }
            self.bump();
            self.bump();
        } else {
            while self.curr.is_some() && self.curr.unwrap() != '\n' {
                self.bump();
            }
            self.bump();
        }
    }

    fn scan_real(&mut self) -> String {
        let mut s = String::new();
        // Break if its whitespace or None (whitespace in that case)
        loop {
            match self.curr.unwrap_or(' ') {
                'a' ... 'z' | 'A' ... 'Z' => {
                    s.push(self.curr.unwrap());
                },
                _ => break,
            }
            self.bump();
        }
        s
    }

    fn scan_string(&mut self) -> String {
        let mut s = String::new();
        // Break if its whitespace or None (whitespace in that case)
        loop {
            match self.curr.unwrap_or(' ') {
                c if is_whitespace(c) => break,
                c => {
                    s.push(c);
                },
            }
            self.bump();
        }
        s
    }

    // fn scan_string_literal(&mut self) -> String {

    // }
}

impl<'a> Iterator for Tokenizer<'a> {
    type Item = TokenSpan;

    fn next(&mut self) -> Option<TokenSpan> {
        let before_pos = self.curr_pos;
        let p = self.peek.unwrap_or('\x00');

        if self.curr.is_none() {
            return None;
        }

        let t = match self.curr.unwrap() {
            c if is_whitespace(c) => {
                Token::Whitespace(self.skip_whitespace())
            },
            '/' if p == '/' || p == '*' => {
                self.skip_comment();
                Token::Comment
            },
            '.' => {
                self.bump();
                Token::Dot
            },
            '(' => { self.bump(); Token::OpenDelim(DelimToken::Paren) },
            ')' => { self.bump(); Token::CloseDelim(DelimToken::Paren) },
            '[' => { self.bump(); Token::OpenDelim(DelimToken::Bracket) },
            ']' => { self.bump(); Token::CloseDelim(DelimToken::Bracket) },
            '{' => { self.bump(); Token::OpenDelim(DelimToken::Brace) },
            '}' => { self.bump(); Token::CloseDelim(DelimToken::Brace) },

            // '"' =>

            'a' ... 'z' | 'A'... 'Z' => {
                let w = self.scan_real();
                match Keyword::from_str(&w) {
                    Some(kw) => Token::Keyword(kw),
                    None => Token::Word(w),
                }
            },
            _ => Token::Other(self.scan_string()),
        };

        Some(TokenSpan {
            tok: t,
            span: (before_pos, self.curr_pos)
        })

        // match self.curr {
        //     Some(_) => Some(Token {
        //         ty: TokenType::Other(s),
        //         span: (0, 0),
        //     }),
        //     None => None,
        // }
        // self.chs.next()
    }
}

fn is_whitespace(c: char) -> bool {
    match c {
        ' ' | '\n' | '\t' | '\r' => true,
        _ => false,
    }
}
<commit_msg>Add string literal parsing and semicolon<commit_after>
use std::str::Chars;
use std::iter::{Iterator};

macro_rules! declare_keywords {(
    $( ($name:ident, $word:expr); )*
) => {
    #[derive(Copy, Clone, PartialEq, Eq, Debug)]
    pub enum Keyword {
        $( $name, )*
    }

    impl Keyword {
        // pub fn word(&self) -> &'static str {
        //     match *self {
        //         $( Keyword::$name => $word, )*
        //     }
        // }

        pub fn from_str(s: &String) -> Option<Self> {
            match &**s {
                $( $word => Some(Keyword::$name), )*
                _ => None,
            }
        }
    }
}}

declare_keywords! {
    (Public,        "public");
    (Private,       "private");
    (Protected,     "protected");
    (Class,         "class");
    (Static,        "static");
    (Import,        "import");

    // control structures
    (Do,     "do");
    (While,  "while");
    (For,    "for");
    (If,     "if");
    (Else,   "else");
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DelimToken {
    Paren,      // round ( )
    Bracket,    // square [ ]
    Brace,      // curly { }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Lit {
    Str(String),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Token {
    Keyword(Keyword),
    Whitespace(bool),
    Comment,
    Dot,
    Comma,
    Semi,
    OpenDelim(DelimToken),
    CloseDelim(DelimToken),
    Literal(Lit),
    Word(String),
    Other(String),
}

impl Token {
    pub fn is_real(&self) -> bool {
        match *self {
            Token::Whitespace(false) | Token::Comment => false,
            _ => true,
        }
    }
}

#[derive(Debug)]
pub struct TokenSpan {
    pub tok: Token,
    /// Byte position, half open: inclusive-exclusive
    pub span: (i64, i64),
}

pub struct Tokenizer<'a> {
    chs: Chars<'a>,

    last: Option<char>,
    curr: Option<char>,
    peek: Option<char>,
    curr_pos: i64,
    peek_pos: i64,
}

impl<'a> Tokenizer<'a> {
    pub fn new(content: &'a String) -> Tokenizer<'a> {
        let iter = content.chars();
        let mut tok = Tokenizer {
            chs: iter,
            last: None,
            curr: None,
            peek: None,
            curr_pos: -1,
            peek_pos: -1,
        };
        tok.bump();
        tok.bump();
        tok
    }

    fn bump(&mut self) {
        self.last = self.curr;
        self.curr = self.peek;
        self.curr_pos = self.peek_pos;
        self.peek = self.chs.next();
        match self.peek {
            Some(c) => self.peek_pos += c.len_utf8() as i64,
            _ => {}
        };
    }

    fn skip_whitespace(&mut self) -> bool {
        let mut newline_found = false;
        while is_whitespace(self.curr.unwrap_or('x')) {
            if self.curr.unwrap_or('x') == '\n' && !newline_found {
                newline_found = true;
            }
            // else if newline_found {
            //     break;
            // }
            self.bump();
        }
        newline_found
    }

    fn skip_comment(&mut self) {
        // We know curr == '/' and peek is either '*' or '/'.
        // Note: `self.peek.is_some()` implies `self.curr.is_some()`
        if self.peek.unwrap() == '*' {
            while self.peek.is_some() &&
                !(self.curr.unwrap() == '*' && self.peek.unwrap() == '/') {
                self.bump();
            }
            self.bump();
            self.bump();
        } else {
            while self.curr.is_some() && self.curr.unwrap() != '\n' {
                self.bump();
            }
            self.bump();
        }
    }

    fn scan_real(&mut self) -> String {
        let mut s = String::new();
        // Break if its whitespace or None (whitespace in that case)
        loop {
            match self.curr.unwrap_or(' ') {
                'a' ... 'z' | 'A' ... 'Z' => {
                    s.push(self.curr.unwrap());
                },
                _ => break,
            }
            self.bump();
        }
        s
    }

    fn scan_string(&mut self) -> String {
        let mut s = String::new();
        // Break if its whitespace or None (whitespace in that case)
        loop {
            match self.curr.unwrap_or(' ') {
                c if is_whitespace(c) => break,
                c => {
                    s.push(c);
                },
            }
            self.bump();
        }
        s
    }

    fn scan_string_literal(&mut self) -> String {
        // `curr` is '"'. Note: After one bump, `last` != None
        self.bump();

        let mut s = String::new();
        loop {
            match self.curr {
                Some(c) if c == '"' && self.last.unwrap() != '\\' => break,
                None => break,  // TODO: This should not happen!
                Some(c) => {
                    s.push(c);
                    self.bump();
                },
            }
        }
        self.bump();    // Remove last '"'
        s
    }
}

impl<'a> Iterator for Tokenizer<'a> {
    type Item = TokenSpan;

    fn next(&mut self) -> Option<TokenSpan> {
        let before_pos = self.curr_pos;
        let p = self.peek.unwrap_or('\x00');

        if self.curr.is_none() {
            return None;
        }

        let t = match self.curr.unwrap() {
            c if is_whitespace(c) => {
                Token::Whitespace(self.skip_whitespace())
            },
            '/' if p == '/' || p == '*' => {
                self.skip_comment();
                Token::Comment
            },
            '.' => { self.bump(); Token::Dot },
            ',' => { self.bump(); Token::Comma },
            ';' => { self.bump(); Token::Semi },

            '(' => { self.bump(); Token::OpenDelim(DelimToken::Paren) },
            ')' => { self.bump(); Token::CloseDelim(DelimToken::Paren) },
            '[' => { self.bump(); Token::OpenDelim(DelimToken::Bracket) },
            ']' => { self.bump(); Token::CloseDelim(DelimToken::Bracket) },
            '{' => { self.bump(); Token::OpenDelim(DelimToken::Brace) },
            '}' => { self.bump(); Token::CloseDelim(DelimToken::Brace) },

            '"' => Token::Literal(Lit::Str(self.scan_string_literal())),

            'a' ... 'z' | 'A'... 'Z' => {
                let w = self.scan_real();
                match Keyword::from_str(&w) {
                    Some(kw) => Token::Keyword(kw),
                    None => Token::Word(w),
                }
            },
            _ => Token::Other(self.scan_string()),
        };

        Some(TokenSpan {
            tok: t,
            span: (before_pos, self.curr_pos)
        })

        // match self.curr {
        //     Some(_) => Some(Token {
        //         ty: TokenType::Other(s),
        //         span: (0, 0),
        //     }),
        //     None => None,
        // }
        // self.chs.next()
    }
}

fn is_whitespace(c: char) -> bool {
    match c {
        ' ' | '\n' | '\t' | '\r' => true,
        _ => false,
    }
}
<|endoftext|>