autorip 0.1.1

Composes other programs to automatically rip optical media
Documentation
use std::{
    env, fs,
    path::{Path, PathBuf},
    process::Command,
};

use anyhow::{anyhow, bail, Result};
use regex::Regex;
use uuid::Uuid;

use super::{
    capture_group_as_str, file::File, get_stdout, title_from_label, Preset,
};

pub(crate) struct Bluray {
    pub(crate) duration: Option<u32>,
    pub(crate) title_hint: String,
}

impl Bluray {
    pub(crate) fn new(path: impl Into<PathBuf>) -> Result<Self> {
        let path = path.into();
        let makemkv_info = Bluray::makemkv_info(&path).ok();
        let duration = makemkv_info
            .as_ref()
            .and_then(|i| Bluray::duration(i.as_ref()).ok());
        let title_hint = makemkv_info
            .as_ref()
            .map(|i| Bluray::title_hint(i))
            .transpose()?
            .map_or_else(
                || title_from_label(&path),
                |v| Ok(String::from(v)),
            )?;

        Ok(Self {
            duration,
            title_hint,
        })
    }

    pub(crate) fn rip(&self, preset: &Preset) -> Result<PathBuf> {
        const HOUR_IN_SECS: u32 = 60 * 60;

        let uuid = Uuid::new_v4().to_string();
        let dest = env::temp_dir().join("autorip").join(uuid);

        let _output = Command::new("makemkvcon")
            .arg("--minlength")
            .arg(self.duration.unwrap_or(HOUR_IN_SECS).to_string())
            .args(
                "--robot --decrypt --directio=true mkv disc:0 all"
                    .split_whitespace(),
            )
            .arg(&dest)
            .output()?;

        let mut files: Vec<_> = fs::read_dir(&dest)?
            .filter_map(|direntry| {
                direntry.ok().and_then(|de| {
                    let p = de.path();
                    if p.ends_with(".mkv") {
                        Some(p)
                    } else {
                        None
                    }
                })
            })
            .collect();

        let mkvfile = match files.len() {
            0 => bail!("No mk4 files in {:?}", &dest),
            1 => files.remove(0),
            _ => bail!("Too many mk4 files in {:?}", dest),
        };

        File::new(mkvfile)?.rip(preset)
    }

    fn duration(makemkv_output: &str) -> Result<u32> {
        let re = Regex::new(
            r#"TINFO:0,9,0,"(?P<hours>\d{1,2}):(?P<minutes>\d{2}):(?P<seconds>\d{2})"$"#,
        )
        .expect("failed to compile duration regex");
        if let Some(duration_str) = re.captures(makemkv_output) {
            if let (Some(hours), Some(minutes), Some(seconds)) = (
                duration_str.name("hours"),
                duration_str.name("minutes"),
                duration_str.name("seconds"),
            ) {
                if let (Ok(hours), Ok(minutes), Ok(seconds)) = (
                    hours.as_str().parse::<u32>(),
                    minutes.as_str().parse::<u32>(),
                    seconds.as_str().parse::<u32>(),
                ) {
                    return Ok(hours * 3600 + minutes + 60 + seconds);
                }
                bail!(
                    "Unable to parse as duration: {}",
                    duration_str
                        .get(0)
                        .map_or("no duration present", |v| v.as_str())
                )
            }
        };
        bail!("Unable to parse duration")
    }

    fn makemkv_info(path: &Path) -> Result<String> {
        // TODO: Better handling for /dev/dvd19
        // Hopefully most people won't have more than 9 optical drives
        let disc_number = match path.to_str().and_then(|s| s.chars().last()) {
            Some(num @ '0'..='9') => num,
            _ => '0',
        };
        let cmd = Command::new("makemkvcon")
            .args("-r info".split_whitespace())
            .arg(format!("disc:{disc_number}"))
            .output()?;
        get_stdout(cmd).map(String::from)
    }

    fn title_hint(makemkv_info: &str) -> Result<&str> {
        let re = Regex::new(r#"CINFO:30,0,"(?P<title>.+?)"$"#)
            .expect("Unable to compile regex for title_hint");
        re.captures(makemkv_info)
            .ok_or_else(|| anyhow!("No title found"))
            .and_then(|ref cap| capture_group_as_str(cap, "title"))
    }
}