use std::{
convert::AsRef,
default::Default,
ffi::OsStr,
fmt::Write as fmt_Write,
fs,
io::Write,
path::{Path, PathBuf},
};
use anyhow::{anyhow, Context as _};
use crate::{
align::align_available_files,
lang::Lang,
srt::{Subtitle, SubtitleFile},
time::{Period, ToTimestamp},
video::{Extraction, ExtractionSpec, Id3Metadata, Video},
Result,
};
pub fn os_str_to_string(os_str: &OsStr) -> String {
os_str.to_string_lossy().into_owned()
}
pub struct LanguageResources {
pub subtitles: SubtitleFile,
pub language: Option<Lang>,
}
impl LanguageResources {
fn new(subtitles: SubtitleFile) -> LanguageResources {
let language = subtitles.detect_language();
LanguageResources {
subtitles: subtitles,
language: language,
}
}
}
pub struct Exporter {
video: Video,
foreign: LanguageResources,
native: Option<LanguageResources>,
file_stem: String,
dir: PathBuf,
extractions: Vec<Extraction>,
}
impl Exporter {
pub fn new(
video: Video,
foreign_subtitles: SubtitleFile,
native_subtitles: Option<SubtitleFile>,
label: &str,
) -> Result<Exporter> {
let foreign = LanguageResources::new(foreign_subtitles);
let native = native_subtitles.map(|subs| LanguageResources::new(subs));
let file_stem = os_str_to_string(video.file_stem());
let dir = Path::new("./").join(format!("{}_{}", &file_stem, label));
if fs::metadata(&dir).is_ok() {
return Err(anyhow!(
"Directory already exists: {}",
&dir.to_string_lossy()
));
}
fs::create_dir_all(&dir)
.with_context(|| format!("could not create {}", dir.display()))?;
Ok(Exporter {
video: video,
foreign: foreign,
native: native,
file_stem: file_stem,
dir: dir,
extractions: vec![],
})
}
pub fn file_stem(&self) -> &str {
&self.file_stem
}
pub fn title(&self) -> &str {
&self.file_stem
}
pub fn video(&self) -> &Video {
&self.video
}
pub fn foreign(&self) -> &LanguageResources {
&self.foreign
}
pub fn native(&self) -> Option<&LanguageResources> {
self.native.as_ref()
}
pub fn align(&self) -> Vec<(Option<Subtitle>, Option<Subtitle>)> {
align_available_files(
&self.foreign.subtitles,
self.native.as_ref().map(|n| &n.subtitles),
)
}
fn media_path<T: ToTimestamp>(
&self,
timestamp: T,
lang: Option<Lang>,
extension: &str,
) -> PathBuf {
let mut file_name =
format!("{}_{}", &self.file_stem, timestamp.to_file_timestamp());
if let Some(l) = lang {
write!(&mut file_name, ".{}", l.as_str()).unwrap();
}
write!(&mut file_name, ".{}", extension).unwrap();
self.dir.join(file_name)
}
pub fn schedule_image_export(&mut self, time: f32) -> String {
let path = self.media_path(time, None, "jpg");
self.extractions.push(Extraction {
path: path.clone(),
spec: ExtractionSpec::Image(time),
});
os_str_to_string(path.file_name().unwrap())
}
pub fn schedule_audio_export(
&mut self,
lang: Option<Lang>,
period: Period,
) -> String {
self.schedule_audio_export_ext(lang, period, Default::default())
}
pub fn schedule_audio_export_ext(
&mut self,
lang: Option<Lang>,
period: Period,
metadata: Id3Metadata,
) -> String {
let path = self.media_path(period, lang, "mp3");
let stream = lang.and_then(|l| self.video.audio_for(l));
self.extractions.push(Extraction {
path: path.clone(),
spec: ExtractionSpec::Audio(stream, period, metadata),
});
os_str_to_string(path.file_name().unwrap())
}
pub fn export_data_file<P>(&self, rel_path: P, data: &[u8]) -> Result<()>
where
P: AsRef<Path>,
{
let path = self.dir.join(rel_path.as_ref());
let mut f = fs::File::create(&path)
.with_context(|| format!("could not open {}", path.display()))?;
f.write_all(data)
.with_context(|| format!("could not write to {}", path.display()))?;
Ok(())
}
pub fn finish_exports(&mut self) -> Result<()> {
self.video.extract(&self.extractions)?;
Ok(())
}
}