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> {
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"))
}
}