caesura 0.22.3

An all-in-one command line tool to **transcode FLAC** audio files and **upload to gazelle** based indexers/trackers.
Documentation
use crate::errors::{command_error, error, io_error, OutputHandler};
use crate::transcode::{Decode, Encode, Resample, Variant};
use lofty::config::WriteOptions;
use lofty::prelude::TagExt;
use lofty::tag::ItemKey::{Popularimeter, Work};
use lofty::tag::Tag;
use log::{trace, warn};
use rogue_logging::Error;
use std::fs::create_dir_all;
use std::process::Stdio;
use tokio::join;

pub struct TranscodeJob {
    pub id: String,
    pub variant: Variant,
    pub tags: Option<Tag>,
}

impl TranscodeJob {
    pub async fn execute(self) -> Result<(), Error> {
        let output_path = match &self.variant {
            Variant::Transcode(_, encode) => encode.output.clone(),
            Variant::Resample(resample) => resample.output.clone(),
        };
        let output_dir = output_path
            .parent()
            .expect("output path should have a parent");
        create_dir_all(output_dir).map_err(|e| io_error(e, "create transcode output directory"))?;
        match self.variant {
            Variant::Transcode(decode, encode) => execute_transcode(decode, encode).await?,
            Variant::Resample(resample) => execute_resample(resample).await?,
        };
        if let Some(mut tags) = self.tags {
            let exclude = [Popularimeter, Work];
            for key in exclude {
                if let Some(value) = tags.get_string(&key) {
                    trace!("Excluding invalid {key:?} value: {value}");
                    tags.remove_key(&key);
                }
            }
            tags.save_to_path(&output_path, WriteOptions::default())
                .map_err(|e| error("write tags", e.to_string()))?;
        }
        Ok(())
    }
}

async fn execute_transcode(decode: Decode, encode: Encode) -> Result<(), Error> {
    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(|e| command_error(e, "spawn decode", &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(|e| command_error(e, "spawn decode", &encode_program))?;
    let (decode_result, encode_output) =
        join!(decode_command.wait(), encode_command.wait_with_output());
    let decode_exit = decode_result.map_err(|e| io_error(e, "wait for decode"))?;
    let encode_output = encode_output.map_err(|e| io_error(e, "wait for encode"))?;
    if !decode_exit.success() {
        warn!("Decode was not successful: {decode_exit}");
    }
    OutputHandler::execute(encode_output, "execute resample job", "transcode")?;
    Ok(())
}

async fn execute_resample(resample: Resample) -> Result<(), Error> {
    let info = resample.to_info();
    trace!("Executing resample: {info}");
    let program = info.program.clone();
    let output = info
        .to_command()
        .output()
        .await
        .map_err(|e| command_error(e, "execute resample job", &program))?;
    OutputHandler::execute(output, "execute resample job", "transcode")?;
    Ok(())
}