use crate::errors::AppError;
use crate::formats::{TargetFormat, TargetFormatProvider};
use crate::fs::{Collector, PathManager};
use crate::imdl::ImdlCommand;
use crate::jobs::JobRunner;
use crate::logging::Colors;
use crate::naming::join_humanized;
use crate::options::{Options, SharedOptions, SourceArg, TargetOptions};
use crate::queue::TimeStamp;
use crate::source::*;
use crate::transcode::{
AdditionalJobFactory, TranscodeFormatStatus, TranscodeJobFactory, TranscodeStatus,
};
use colored::Colorize;
use di::{injectable, Ref, RefMut};
use log::*;
use std::collections::BTreeSet;
use tokio::fs::copy;
#[injectable]
pub struct TranscodeCommand {
arg: Ref<SourceArg>,
shared_options: Ref<SharedOptions>,
target_options: Ref<TargetOptions>,
source_provider: RefMut<SourceProvider>,
paths: Ref<PathManager>,
targets: Ref<TargetFormatProvider>,
transcode_job_factory: Ref<TranscodeJobFactory>,
additional_job_factory: Ref<AdditionalJobFactory>,
runner: Ref<JobRunner>,
}
impl TranscodeCommand {
pub async fn execute_cli(&self) -> Result<bool, AppError> {
if !self.arg.validate()
|| !self.shared_options.validate()
|| !self.target_options.validate()
{
return Ok(false);
}
let source = self
.source_provider
.write()
.expect("Source provider should be writeable")
.get_from_options()
.await
.map_err(|e| AppError::else_explained("get source from options", e.to_string()))?;
let status = self.execute(&source).await;
if let Some(error) = &status.error {
error!("{error}");
}
Ok(status.success)
}
#[must_use]
pub async fn execute(&self, source: &Source) -> TranscodeStatus {
let targets = self.targets.get(source.format, &source.existing);
let mut status = TranscodeStatus {
success: false,
formats: None,
additional: None,
completed: TimeStamp::now(),
error: None,
};
if targets.is_empty() {
status.error = Some(AppError::else_explained(
"transcode",
"No transcodes to perform".to_owned(),
));
return status;
}
let formats: Vec<TranscodeFormatStatus> = targets
.iter()
.map(|&format| TranscodeFormatStatus {
format,
path: self.paths.get_transcode_target_dir(source, format),
})
.collect();
status.formats = Some(formats);
let targets = self.skip_completed(source, &targets).await;
if targets.is_empty() {
status.success = true;
return status;
}
if let Err(error) = self.execute_transcode(source, &targets).await {
status.error = Some(error);
status.completed = TimeStamp::now();
return status;
}
if let Err(error) = self.execute_additional(source, &targets).await {
status.error = Some(error);
status.completed = TimeStamp::now();
return status;
}
if let Err(error) = self.execute_torrent(source, &targets).await {
status.error = Some(error);
status.completed = TimeStamp::now();
return status;
}
status.success = true;
status
}
#[must_use]
async fn skip_completed(
&self,
source: &Source,
targets: &BTreeSet<TargetFormat>,
) -> BTreeSet<TargetFormat> {
let mut out: BTreeSet<TargetFormat> = BTreeSet::new();
for target in targets {
if let Ok(Some(path)) = self
.paths
.get_or_duplicate_existing_torrent_path(source, *target)
.await
{
debug!("{} existing {target} transcode", "Found".bold());
trace!("{}", path.display());
} else {
out.insert(*target);
}
}
out
}
async fn execute_transcode(
&self,
source: &Source,
targets: &BTreeSet<TargetFormat>,
) -> Result<(), AppError> {
let flacs = Collector::get_flacs(&source.directory);
info!(
"{} to {} for {} FLACs in {}",
"Transcoding".bold(),
join_humanized(targets),
flacs.len().to_string().gray(),
source
);
for target in targets {
let jobs = self.transcode_job_factory.create(&flacs, source, *target)?;
self.runner.add(jobs);
}
self.runner.execute().await?;
info!("{} {}", "Transcoded".bold(), source);
Ok(())
}
async fn execute_additional(
&self,
source: &Source,
targets: &BTreeSet<TargetFormat>,
) -> Result<(), AppError> {
let files = Collector::get_additional(&source.directory);
debug!(
"{} {} additional files",
"Adding".bold(),
files.len().to_string().gray()
);
for target in targets {
let jobs = self.additional_job_factory.create(&files, source, *target);
self.runner.add_without_publish(jobs);
}
self.runner.execute_without_publish().await?;
debug!("{} additional files {}", "Added".bold(), source);
Ok(())
}
async fn execute_torrent(
&self,
source: &Source,
targets: &BTreeSet<TargetFormat>,
) -> Result<(), AppError> {
debug!("{} torrents {}", "Creating".bold(), source);
for target in targets {
let content_dir = self.paths.get_transcode_target_dir(source, *target);
let path_without_indexer = self.paths.get_torrent_path(source, *target, false);
let announce_url = self
.shared_options
.announce_url
.clone()
.expect("announce_url should be set");
let indexer = self
.shared_options
.indexer
.clone()
.expect("indexer should be set");
ImdlCommand::create(&content_dir, &path_without_indexer, announce_url, indexer).await?;
trace!(
"{} torrent {}",
"Created".bold(),
path_without_indexer.display()
);
let path_with_indexer = self.paths.get_torrent_path(source, *target, true);
copy(&path_without_indexer, &path_with_indexer)
.await
.or_else(|e| AppError::io(e, "copy torrent file"))?;
trace!(
"{} torrent {}",
"Created".bold(),
path_with_indexer.display()
);
}
debug!("{} torrents {}", "Created".bold(), source);
Ok(())
}
}