ite 0.5.3

Command line ID3 tag editor
use clap::CommandFactory;
use clap::Parser;
use clap_complete::Shell;
use id3::Content;
use id3::Frame;
use id3::Tag;
use id3::TagLike;
use id3::frame::Picture;
use id3::frame::PictureType;
use libite::Arguments;
use libite::Command;
use libite::CompleteOptions;
use libite::Configurations;
use libite::Error;
use libite::FrameGroup;
use libite::FrameId;
use libite::GetCommandConfigurations;
use libite::GetOptions;
use libite::PrintCommandConfigurations;
use libite::PrintOptions;
use std::io::stdout;
use std::path::Path;
use std::path::PathBuf;
use tokio::fs::File;
use tokio::fs::read_dir;
use tokio::io::AsyncWriteExt;
use tokio::task::spawn_blocking;
async fn complete(
	command: &mut clap::Command,
	shell: Shell,
	options: &CompleteOptions,
) -> Result<(), String> {
	if let Some(path) = &options.output {
		clap_complete::generate(
			shell,
			command,
			"ite",
			&mut File::create(path).await.map_err(|err| err.to_string())?.into_std().await,
		);
		return Ok(());
	}
	clap_complete::generate(shell, command, "ite", &mut stdout());
	Ok(())
}
async fn file_paths_recursive_search<A: AsRef<Path>>(path: A) -> Result<Vec<PathBuf>, String> {
	let path = path.as_ref();
	if path.is_file() {
		return Ok(vec![path.to_owned()]);
	}
	let mut paths = Vec::new();
	let mut reader = read_dir(path).await.map_err(|err| err.to_string())?;
	while let Some(entry) = reader.next_entry().await.map_err(|err| err.to_string())? {
		let path = entry.path();
		if path.is_dir() {
			paths.extend(Box::pin(file_paths_recursive_search(path)).await?);
			continue;
		}
		paths.push(path);
	}
	Ok(paths)
}
async fn get<A: AsRef<Path>>(
	path: A,
	id: FrameId,
	options: &GetOptions,
	configurations: &GetCommandConfigurations,
) -> Result<(), String> {
	async fn content_get(
		id: FrameId,
		frame: &Frame,
		options: &GetOptions,
	) -> Result<Option<String>, String> {
		let content = frame.content();
		let group = id.as_frame_group();
		let output = &options.output;
		let content = match group {
			FrameGroup::AttachedPicture => {
				let content = &content.picture().unwrap().data;
				if output.is_none() {
					return Ok(Some(
						String::from_utf8(content.to_owned())
							.map_err(|err| err.to_string())?
							.to_string(),
					));
				}
				content.to_owned()
			}
			FrameGroup::Comments
			| FrameGroup::SynchronisedLyricsText
			| FrameGroup::TextInformationFrames
			| FrameGroup::UnsynchronisedLyricsTextTranscription
			| FrameGroup::UrlLinkFrames => {
				let content = content.to_string();
				if output.is_none() {
					return Ok(Some(content));
				}
				content.into_bytes()
			}
			_ => {
				return Err(format!(
					"{}: {}",
					group.as_str(),
					Error::NotSupportedFrameGroup.as_str()
				));
			}
		};
		File::create(output.to_owned().unwrap())
			.await
			.map_err(|err| err.to_string())?
			.write_all(&content)
			.await
			.map_err(|err| err.to_string())?;
		Ok(None)
	}
	let path = path.as_ref();
	let tag = Tag::async_read_from_path(path).await.map_err(|err| err.to_string())?;
	let frame = if let Some(frame) = Tag::get(&tag, id.as_str()) {
		frame
	} else {
		return Ok(());
	};
	let content = if let Some(content) = content_get(id.to_owned(), frame, options)
		.await
		.map_err(|err| format!("{}: {err}", id.as_str()))?
	{
		content
	} else {
		return Ok(());
	};
	let group = id.as_frame_group();
	let group = group.as_str();
	let name = id.name();
	let id = id.as_str();
	print!(
		"{}",
		configurations
			.frame_format
			.to_owned()
			.unwrap_or_else(|| GetCommandConfigurations::default_frame_format())
			.replace("%c", &content)
			.replace("%g", group)
			.replace("%i", id)
			.replace("%n", name)
			.replace("%{content}", &content)
			.replace("%{c}", &content)
			.replace("%{group}", group)
			.replace("%{g}", group)
			.replace("%{id}", id)
			.replace("%{i}", id)
			.replace("%{name}", name)
			.replace("%{n}", name)
			.replace("%{}", "%")
	);
	Ok(())
}
async fn print(
	paths: &[PathBuf],
	ids: &[FrameId],
	options: &PrintOptions,
	configurations: &PrintCommandConfigurations,
) {
	async fn at_file_path<A: AsRef<Path>>(
		path: A,
		ids: &[FrameId],
		options: &PrintOptions,
		configurations: &PrintCommandConfigurations,
	) -> Result<(), String> {
		let path = path.as_ref();
		let path_str = path.to_string_lossy();
		let path_str = path_str.as_ref();
		let input_format = configurations
			.input_format
			.to_owned()
			.unwrap_or_else(|| PrintCommandConfigurations::default_input_format())
			.replace("%i", path_str)
			.replace("%{input}", path_str)
			.replace("%{i}", path_str)
			.replace("%{}", "%");
		let mut input_format = input_format.splitn(2, "%{&&}");
		if let Some(format) = input_format.next() {
			print!("{format}");
		}
		let tag = Tag::async_read_from_path(path).await.map_err(|err| err.to_string())?;
		for id in ids {
			let content = if let Some(frame) = Tag::get(&tag, id.as_str()) {
				match id.as_frame_group() {
					FrameGroup::Comments
					| FrameGroup::SynchronisedLyricsText
					| FrameGroup::TextInformationFrames
					| FrameGroup::UnsynchronisedLyricsTextTranscription
					| FrameGroup::UrlLinkFrames => {
						let content = frame.content().to_string();
						configurations
							.frame_content_format
							.to_owned()
							.unwrap_or_else(|| {
								PrintCommandConfigurations::default_frame_content_format()
							})
							.replace("%c", &content)
							.replace("%{content}", &content)
							.replace("%{c}", &content)
							.replace("%{}", "%")
					}
					_ => configurations.existing_frame_content_format.to_owned().unwrap_or_else(
						|| PrintCommandConfigurations::default_existing_frame_content_format(),
					),
				}
			} else if options.existing {
				continue;
			} else {
				configurations.empty_frame_content_format.to_owned().unwrap_or_else(|| {
					PrintCommandConfigurations::default_empty_frame_content_format()
				})
			};
			let group = id.as_frame_group();
			let group = group.as_str();
			let name = id.name();
			let id = id.as_str();
			print!(
				"{}",
				configurations
					.frame_format
					.to_owned()
					.unwrap_or_else(|| PrintCommandConfigurations::default_frame_format())
					.replace("%c", &content)
					.replace("%g", group)
					.replace("%i", id)
					.replace("%n", name)
					.replace("%{content}", &content)
					.replace("%{c}", &content)
					.replace("%{group}", group)
					.replace("%{g}", group)
					.replace("%{id}", id)
					.replace("%{input}", path_str)
					.replace("%{i}", id)
					.replace("%{name}", name)
					.replace("%{n}", name)
					.replace("%{}", "%")
			);
		}
		if let Some(format) = input_format.next() {
			print!("{format}");
		}
		Ok(())
	}
	for path in paths {
		let file_paths = match file_paths_recursive_search(path).await {
			Err(err) => {
				eprintln!("\x1b[1;31merror\x1b[39m:\x1b[22m {}: {err}", path.display());
				continue;
			}
			Ok(paths) => paths,
		};
		for file_path in &file_paths {
			if let Err(err) = at_file_path(file_path, ids, options, configurations).await {
				eprintln!("\x1b[1;31merror\x1b[39m:\x1b[22m {}: {err}", file_path.display());
			}
		}
	}
}
async fn remove(paths: &[PathBuf], ids: &[FrameId]) {
	async fn at_file_path<A: AsRef<Path>>(path: A, ids: &[FrameId]) -> Result<(), String> {
		let path = path.as_ref();
		let mut tag = Tag::async_read_from_path(path).await.map_err(|err| err.to_string())?;
		for id in ids {
			Tag::remove(&mut tag, id.as_str());
		}
		spawn_blocking({
			let path = path.to_owned();
			move || tag.write_to_path(path, tag.version()).map_err(|err| err.to_string())
		})
		.await
		.map_err(|err| err.to_string())??;
		Ok(())
	}
	for path in paths {
		let file_paths = match file_paths_recursive_search(path).await {
			Err(err) => {
				eprintln!("\x1b[1;31merror\x1b[39m:\x1b[22m {}: {err}", path.display());
				continue;
			}
			Ok(paths) => paths,
		};
		for file_path in &file_paths {
			if let Err(err) = at_file_path(file_path, ids).await {
				eprintln!("\x1b[1;31merror\x1b[39m:\x1b[22m {}: {err}", file_path.display());
			}
		}
	}
}
pub(crate) async fn run() -> Result<(), String> {
	let configurations = Configurations::parse().await?;
	let frame_configurations = configurations.frame_configurations.unwrap_or_default();
	match Arguments::parse().command {
		Command::Complete { options, shell } => {
			complete(&mut Arguments::command(), shell, &options).await?;
		}
		Command::Get { frame, input, options } => {
			get(
				input,
				frame.try_into_frame_id(&frame_configurations)?,
				&options,
				&configurations.get_command_configurations.unwrap_or_default(),
			)
			.await?;
		}
		Command::Print { frames, inputs, options } => {
			print(
				&inputs,
				&frames.try_into_frame_id_vec(&frame_configurations)?,
				&options,
				&configurations.print_command_configurations.unwrap_or_default(),
			)
			.await;
		}
		Command::Remove { frames, inputs } => {
			remove(&inputs, &frames.try_into_frame_id_vec(&frame_configurations)?).await;
		}
		Command::Set { frame, frame_content, inputs } => {
			set(
				&inputs,
				frame.try_into_frame_id(&frame_configurations)?,
				&frame_content.try_into_u8_vec().await?,
			)
			.await;
		}
	}
	Ok(())
}
async fn set(paths: &[PathBuf], id: FrameId, content: &[u8]) {
	async fn at_file_path<A: AsRef<Path>>(
		path: A,
		id: FrameId,
		content: &[u8],
	) -> Result<(), String> {
		let path = path.as_ref();
		let mut tag = Tag::async_read_from_path(path).await.map_err(|err| err.to_string())?;
		let group = id.as_frame_group();
		match group {
			FrameGroup::AttachedPicture => {
				Tag::add_frame(
					&mut tag,
					Picture {
						data: content.to_owned(),
						description: String::new(),
						mime_type: "image/png".to_owned(),
						picture_type: PictureType::Other,
					},
				);
			}
			FrameGroup::TextInformationFrames | FrameGroup::UrlLinkFrames => {
				match id {
					FrameId::Txxx | FrameId::Wxxx => {
						return Err(Error::NotSupportedFrame.as_str().to_owned());
					}
					_ => (),
				}
				Tag::add_frame(
					&mut tag,
					Frame::with_content(
						id.as_str(),
						Content::Text(
							String::from_utf8(content.to_owned()).map_err(|err| err.to_string())?,
						),
					),
				);
			}
			_ => {
				return Err(format!(
					"{}: {}",
					group.as_str(),
					Error::NotSupportedFrameGroup.as_str()
				));
			}
		};
		spawn_blocking({
			let path = path.to_owned();
			move || tag.write_to_path(path, tag.version()).map_err(|err| err.to_string())
		})
		.await
		.map_err(|err| err.to_string())??;
		Ok(())
	}
	for path in paths {
		let file_paths = match file_paths_recursive_search(path).await {
			Err(err) => {
				eprintln!("\x1b[1;31merror\x1b[39m:\x1b[22m {}: {err}", path.display());
				continue;
			}
			Ok(paths) => paths,
		};
		for file_path in &file_paths {
			if let Err(err) = at_file_path(file_path, id.to_owned(), content).await {
				eprintln!("\x1b[1;31merror\x1b[39m:\x1b[22m {}: {err}", file_path.display());
			}
		}
	}
}