use std::{
borrow::Cow,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{anyhow, bail, Result};
use regex::Regex;
use super::{capture_group_as_str, get_stderr, title_from_label, Preset};
pub(crate) struct Dvd {
pub(crate) path: PathBuf,
handbrake_info: Option<String>,
pub(crate) duration: Option<u32>,
pub(crate) title_hint: String,
}
impl Dvd {
pub(crate) fn new(path: impl Into<PathBuf>) -> Result<Self> {
let path = path.into();
let handbrake_info = Dvd::handbrake_info(&path).ok().map(String::from);
let duration =
handbrake_info.as_ref().and_then(|i| Dvd::duration(i).ok());
let title_hint = title_from_label(&path).map(String::from)?;
Ok(Self {
path,
handbrake_info,
duration,
title_hint,
})
}
pub(crate) fn handbrake_info(path: &Path) -> Result<String> {
get_stderr(
Command::new("HandBrakeCLI")
.args("--scan --main-feature".split_whitespace())
.arg("--input")
.arg(path)
.output()?,
)
.map(String::from)
}
fn duration(handbrake_info: impl AsRef<str>) -> Result<u32> {
let mut main_feature_flag = false;
for line in handbrake_info.as_ref().lines() {
if line.starts_with("Found main feature") {
main_feature_flag = true;
}
if main_feature_flag && line.trim().starts_with("+ duration: ") {
if let Some(duration) = line.split_whitespace().next_back() {
let mut d = duration.split(':');
let opt_as_u32 = |v: &str| v.parse::<u32>().ok();
if let (Some(hours), Some(minutes), Some(seconds)) = (
d.next().and_then(opt_as_u32),
d.next().and_then(opt_as_u32),
d.next().and_then(opt_as_u32),
) {
return Ok(hours + minutes + seconds);
}
}
}
}
bail!("Unable to get duration")
}
fn subtitles(handbrake_info: &str) -> Result<Vec<String>> {
let all_subtitles_re = Regex::new(r"(?mx) # set debug mode and multiline
(?s:^\s+\+\ Main\ Feature$.*?) # with dotall, only match after ` + Main Feature` and consume extra lines
(?:^\s+\+\ subtitle\ tracks:$\n) # only match after ` + subtitle tracks:`
(?P<subtitles>(^\s+\+\ \d+,.*?$\n)*) # multiple lines starting with ` + 3,`
").expect("failed to compile all_subtitles_re");
let each_subtitle_re = Regex::new(r"(?mx) # set debug mode
\s+\+\ # spaces, plus, trailing space
(?P<number>\d+),\ # `3, ` subtitle number, don't capture the comma or trailing space
(?P<description>.+?$) # `English (Wide Screen) [VOBSUB]` take everything else until the newline
").expect("failed to compile each_subtitle_re");
let pgs_subtitle_re = Regex::new(r".*\[PGS\]$")
.expect("failed to compile pgs_subtitle_re");
let all_subtitles =
if let Some(m) = all_subtitles_re.captures(handbrake_info) {
m.name("subtitles")
.ok_or_else(|| {
anyhow!("did not find capture group `subtitles`")
})?
.as_str()
} else {
return Ok(vec![]);
};
let mut subtitles = Vec::new();
for cap in each_subtitle_re.captures_iter(all_subtitles) {
let description = capture_group_as_str(&cap, "description")?;
if pgs_subtitle_re.is_match(description) {
continue;
}
let sub_number: usize =
capture_group_as_str(&cap, "number")?.parse()?;
subtitles.push((sub_number, description));
}
Ok(subtitles.iter().map(|(u, _)| u.to_string()).collect())
}
pub(crate) fn rip(&self, preset: &Preset) -> Result<PathBuf> {
let outfile = &self.title_hint;
let subtitles: Cow<str> = self
.handbrake_info
.as_ref()
.and_then(|i| Dvd::subtitles(i).ok())
.map_or_else(
|| Cow::Borrowed("scan"),
|subts| Cow::Owned(String::from("scan,") + &subts.join(",")),
);
Command::new("HandBrakeCLI")
.args(
"
--native-language=eng
--subtitle-default=1
--subtitle-forced=1
--main-feature
--optimize
"
.split_whitespace(),
)
.arg("--subitle")
.arg(&*subtitles)
.arg("--preset")
.arg(preset.to_string())
.arg("--input")
.arg(&self.path)
.arg("--output")
.arg(outfile)
.status()?;
Ok(outfile.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_subtitles() {
let tests = [
(
include_str!(
"../../tests/files/Kill Bill Vol 2.handbrakeinfo"
),
9,
),
(include_str!("../../tests/files/Toy Story.handbrakeinfo"), 5),
(
include_str!(
"../../tests/files/The Art of Racing in the \
Rain.handbrakeinfo"
),
9,
),
(
include_str!(
"../../tests/files/We Were Soldiers.handbrakeinfo"
),
5,
),
(
include_str!(
"../../tests/files/Kill Bill FAKEPGS.handbrakeinfo"
),
7,
),
];
for test in tests {
let subs = Dvd::subtitles(test.0).unwrap();
assert_eq!(subs.len(), test.1);
}
}
}