use crate::prelude::*;
use lofty::tag::Tag;
use std::fs::create_dir_all;
use std::process::Stdio;
use tokio::fs::{copy, hard_link};
use tokio::join;
pub(crate) struct TranscodeJob {
pub id: String,
pub variant: Variant,
pub tags: Option<Tag>,
pub exclude_vorbis_comments: Vec<String>,
}
impl TranscodeJob {
pub(crate) async fn execute(self) -> Result<(), Failure<TranscodeAction>> {
let output_path = match &self.variant {
Variant::Transcode(_, encode) => encode.output.clone(),
Variant::Resample(resample) => resample.output.clone(),
Variant::Include(include) => include.output.clone(),
};
let output_dir = output_path
.parent()
.expect("output path should have a parent");
create_dir_all(output_dir).map_err(Failure::wrap_with_path(
TranscodeAction::CreateOutputDirectory,
output_dir,
))?;
match self.variant {
Variant::Transcode(decode, encode) => execute_transcode(decode, encode).await?,
Variant::Resample(resample) => {
execute_resample(resample).await?;
exclude_vorbis_comments_from_flac(&output_path, &self.exclude_vorbis_comments)
.map_err(Failure::wrap(TranscodeAction::ExcludeVorbisComments))?;
}
Variant::Include(include) => execute_include(include).await?,
}
if let Some(mut tags) = self.tags {
exclude_tags(&mut tags, &vorbis_keys(&self.exclude_vorbis_comments));
save_id3v2_deterministic(tags, &output_path).map_err(Failure::wrap_with_path(
TranscodeAction::WriteTags,
&output_path,
))?;
}
Ok(())
}
}
async fn execute_transcode(decode: Decode, encode: Encode) -> Result<(), Failure<TranscodeAction>> {
let decode_info = decode.to_info();
let encode_info = encode.to_info();
trace!("Executing transcode: {decode_info} | {encode_info}");
let decode_program = decode_info.program.clone();
let mut decode_command = decode_info
.to_command()
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.map_err(Failure::wrap_with(TranscodeAction::SpawnDecode, |f| {
f.with("program", &decode_program)
}))?;
let pipe: Stdio = decode_command
.stdout
.take()
.expect("should be able to take stdout")
.try_into()
.expect("should be able to convert stdout to pipe");
let encode_program = encode_info.program.clone();
let encode_command = encode_info
.to_command()
.stdin(pipe)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(Failure::wrap_with(TranscodeAction::SpawnEncode, |f| {
f.with("program", &encode_program)
}))?;
let (decode_result, encode_output) =
join!(decode_command.wait(), encode_command.wait_with_output());
let decode_exit = decode_result.map_err(Failure::wrap(TranscodeAction::WaitDecode))?;
let encode_output = encode_output.map_err(Failure::wrap(TranscodeAction::WaitEncode))?;
if !decode_exit.success() {
warn!("Decode ({decode_program}) was not successful: {decode_exit}");
}
require_success(encode_output, &encode_program).map_err(Failure::wrap_with_path(
TranscodeAction::Transcode,
encode_program,
))?;
Ok(())
}
async fn execute_resample(resample: Resample) -> Result<(), Failure<TranscodeAction>> {
let output = resample.output.clone();
let info = resample.to_info();
trace!("Executing resample: {info}");
info.to_command()
.run()
.await
.map_err(Failure::wrap_with_path(TranscodeAction::Resample, &output))?;
Ok(())
}
async fn execute_include(include: Include) -> Result<(), Failure<TranscodeAction>> {
let verb = if include.hard_link {
hard_link(&include.input, &include.output)
.await
.map_err(Failure::wrap_with_path(
TranscodeAction::HardLinkFlac,
&include.output,
))?;
"Hard Linked"
} else {
copy(&include.input, &include.output)
.await
.map_err(Failure::wrap_with_path(
TranscodeAction::CopyFlac,
&include.output,
))?;
"Copied"
};
trace!(
"{} {} to {}",
verb.bold(),
&include.input.display(),
&include.output.display()
);
Ok(())
}