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());
}
}
}
}