use crate::prelude::*;
use gazelle_api::GazelleClientTrait;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
#[injectable]
pub(crate) struct VerifyCommand {
verify_options: Ref<VerifyOptions>,
source_provider: Ref<SourceProvider>,
api: Ref<Box<dyn GazelleClientTrait + Send + Sync>>,
targets: Ref<TargetFormatProvider>,
paths: Ref<PathManager>,
}
impl VerifyCommand {
pub(crate) async fn execute_cli(&self) -> Result<bool, Failure<VerifyAction>> {
let source = match self.source_provider.get_from_options().await {
Ok(Ok(source)) => source,
Ok(Err(issue)) => {
let status = VerifyStatus::from_issue(issue);
warn!("{} for transcoding unknown", "Unsuitable".bold());
if let Some(issues) = &status.issues {
for issue in issues {
warn!("{issue}");
}
}
return Ok(false);
}
Err(e) => return Err(Failure::new(VerifyAction::GetSource, e)),
};
let result = self.execute(&source).await;
let id = source.to_string();
if result.verified() {
info!("{} {id}", "Verified".bold());
} else {
warn!("{} for transcoding {id}", "Unsuitable".bold());
for issue in &result.issues {
warn!("{issue}");
}
}
Ok(result.verified())
}
pub(crate) async fn execute(&self, source: &Source) -> VerifySuccess {
debug!("{} {}", "Verifying".bold(), source);
let mut issues: Vec<SourceIssue> = Vec::new();
issues.append(&mut self.api_checks(source));
issues.append(&mut self.flac_checks(source));
issues.append(&mut self.hash_check(source).await);
VerifySuccess { issues }
}
fn api_checks(&self, source: &Source) -> Vec<SourceIssue> {
let mut issues: Vec<SourceIssue> = Vec::new();
if source.group.category_name != "Music" {
issues.push(SourceIssue::Category {
actual: source.group.category_name.clone(),
});
}
if source.torrent.scene {
issues.push(SourceIssue::Scene);
}
if source.torrent.lossy_master_approved == Some(true) {
issues.push(SourceIssue::LossyMaster);
}
if source.torrent.lossy_web_approved == Some(true) {
issues.push(SourceIssue::LossyWeb);
}
if source.torrent.trumpable == Some(true) {
issues.push(SourceIssue::Trumpable);
}
if source.torrent.remastered == Some(false) {
issues.push(SourceIssue::Unconfirmed);
}
let excluded_tags: Vec<String> = self
.verify_options
.exclude_tags
.clone()
.unwrap_or_default()
.into_iter()
.filter(|x| source.group.tags.contains(x))
.collect();
if !excluded_tags.is_empty() {
issues.push(SourceIssue::Excluded {
tags: excluded_tags,
});
}
let target_formats = self.targets.get(source.format, &source.existing);
if target_formats.is_empty() {
issues.push(SourceIssue::Existing {
formats: source.existing.clone(),
});
}
issues
}
#[allow(
clippy::cast_sign_loss,
clippy::cast_possible_wrap,
clippy::as_conversions
)]
fn flac_checks(&self, source: &Source) -> Vec<SourceIssue> {
if !source.directory.is_dir() {
return vec![SourceIssue::MissingDirectory {
path: source.directory.clone(),
}];
}
let flacs = Collector::get_flacs_with_context(&source.directory);
if flacs.is_empty() {
return vec![SourceIssue::NoFlacs {
path: source.directory.clone(),
}];
}
let mut issues: Vec<SourceIssue> = Vec::new();
let api_flacs = source.torrent.get_flacs();
if flacs.len() != api_flacs.len() {
issues.push(SourceIssue::FlacCount {
expected: api_flacs.len(),
actual: flacs.len(),
});
}
issues.append(&mut VerifyCommand::subdirectory_checks(&flacs));
let max_target = self
.targets
.get_max_path_length(source.format, &source.existing);
let output_dir = self.paths.get_output_dir();
let mut too_long = false;
for flac in flacs {
if let Some(max_target) = max_target {
let path = self
.paths
.get_transcode_path(source, max_target, &flac)
.strip_prefix(output_dir.clone())
.expect("should be able to strip prefix from transcode path")
.to_path_buf();
let length = path.to_string_lossy().chars().count() as isize;
let excess = length - MAX_PATH_LENGTH;
if excess > 0 {
let excess = excess as usize;
issues.push(SourceIssue::Length { path, excess });
Shortener::suggest_track_name(&flac);
too_long = true;
}
}
let tags = TagVerifier::execute(&flac, source)
.unwrap_or(vec!["failed to retrieve tags".to_owned()]);
if !tags.is_empty() {
issues.push(SourceIssue::MissingTags {
path: flac.path.clone(),
tags,
});
}
for error in StreamVerifier::execute(&flac) {
issues.push(error);
}
}
if too_long {
Shortener::suggest_album_name(source);
}
issues
}
async fn hash_check(&self, source: &Source) -> Vec<SourceIssue> {
if self.verify_options.no_hash_check {
debug!("{} hash check due to settings", "Skipped".bold());
return Vec::new();
}
let torrent_path = self.paths.get_source_torrent_path(source);
if !torrent_path.is_file() {
trace!(
"{} torrent file as it's not cached: {}",
"Downloading".bold(),
torrent_path.display()
);
let mut file = match File::create_new(&torrent_path).await {
Ok(file) => file,
Err(e) => {
return vec![SourceIssue::Error {
domain: "File System".to_owned(),
details: e.to_string(),
}];
}
};
let buffer = match self.api.download_torrent(source.torrent.id).await {
Ok(buffer) => buffer,
#[expect(
deprecated,
reason = "SourceIssue::Api is kept for deserialization compatibility"
)]
Err(e) => return vec![SourceIssue::api(e.into())],
};
if let Err(e) = file.write_all(&buffer).await {
return vec![SourceIssue::Error {
domain: "File System".to_owned(),
details: e.to_string(),
}];
}
if let Err(e) = file.flush().await {
return vec![SourceIssue::Error {
domain: "File System".to_owned(),
details: e.to_string(),
}];
}
}
TorrentVerifier::execute(&torrent_path, &source.directory)
.await
.unwrap_or_else(|e| {
Some(SourceIssue::Error {
domain: "Torrent".to_owned(),
details: e.to_string(),
})
})
.map_or_else(Vec::new, |x| vec![x])
}
pub fn subdirectory_checks(flacs: &[FlacFile]) -> Vec<SourceIssue> {
let flac_sub_dirs: Vec<_> = flacs.iter().map(|x| &x.sub_dir).collect();
if let Some(prefix) = Shortener::longest_common_prefix(&flac_sub_dirs) {
return vec![SourceIssue::UnnecessaryDirectory { prefix }];
}
vec![]
}
}