pub mod album;
pub(crate) mod authentication;
mod cli;
mod errors;
pub(crate) mod network;
pub mod upload;
pub mod user;
pub use errors::*;
use std::{
error::Error,
path::Path,
process::ExitCode,
sync::{Arc, LazyLock},
time::Duration,
};
use cli::*;
use album::Album;
use futures_util::{FutureExt, StreamExt, TryStreamExt};
use indicatif::{MultiProgress, ProgressBar};
use keyring::Entry;
use reqwest::Url;
use tokio::sync::OnceCell;
use user::User;
use crate::upload::{upload_temp_file, LitterExpiry};
fn get_username_entry() -> Result<Entry, KeyringError> {
Entry::new("catbox-cli", "username").map_err(KeyringError::KeyringInitilization)
}
fn get_password_entry() -> Result<Entry, KeyringError> {
Entry::new("catbox-cli", "password").map_err(KeyringError::KeyringInitilization)
}
pub static USER_INSTANCE: LazyLock<Arc<UserInstance>> =
LazyLock::new(|| Arc::new(UserInstance::new()));
pub static MULTI_PROGRESS: LazyLock<MultiProgress> = LazyLock::new(MultiProgress::new);
#[derive(Default)]
pub struct UserInstance {
cache: OnceCell<User>,
}
impl UserInstance {
pub fn new() -> Self {
Self {
cache: OnceCell::new(),
}
}
pub async fn get(&self) -> Result<&User, UserError> {
self.cache.get_or_try_init(User::new).await
}
}
pub async fn upload_files<T: AsRef<Path> + Sync>(
paths: impl AsRef<[T]> + Send,
) -> Result<Vec<String>, AppError> {
let user = USER_INSTANCE.get().await?;
futures_util::stream::iter(paths.as_ref())
.map(AsRef::as_ref)
.map(|x| user.upload_file(x).map(move |y| Ok::<_, AppError>((x, y?))))
.buffer_unordered(5)
.map(|x| {
let (path, url) = x?;
MULTI_PROGRESS
.println(format!("{}: {url}", path.display()))
.map_err(AppError::MultiProgressOutputError)?;
Ok(url)
})
.try_collect::<Vec<_>>()
.await
}
pub async fn upload_temp_files<T: AsRef<Path> + Sync>(
paths: impl AsRef<[T]> + Send,
expiry: LitterExpiry,
) -> Result<Vec<String>, AppError> {
futures_util::stream::iter(paths.as_ref())
.map(AsRef::as_ref)
.map(|x| upload_temp_file(x, expiry).map(move |y| Ok::<_, AppError>((x, y?))))
.buffer_unordered(5)
.map(|x| {
let (path, url) = x?;
MULTI_PROGRESS
.println(format!("{}: {url}", path.display()))
.map_err(AppError::MultiProgressOutputError)?;
Ok(url)
})
.try_collect::<Vec<_>>()
.await
}
pub async fn add_to_album(album: String, files: Vec<String>) -> Result<(), AppError> {
let user = USER_INSTANCE.get().await?;
let album = get_album(album)?;
futures_util::stream::iter(files.into_iter().filter_map(|x| {
if x.contains("files.catbox.moe") {
Some(Url::parse(&x).ok()?.path_segments()?.next()?.to_owned())
} else {
Some(x)
}
}))
.map(move |x| {
let album = album.clone();
let pb = ProgressBar::new_spinner();
MULTI_PROGRESS.add(pb.clone());
pb.enable_steady_tick(Duration::from_millis(100));
pb.set_message(format!("Uploading '{x}' to album"));
async move {
let x = user.upload_to_album(&album, &x).await;
pb.finish_and_clear();
x
}
})
.buffer_unordered(5)
.try_collect::<Vec<_>>()
.await?;
Ok(())
}
fn get_album(album: String) -> Result<Album, AppError> {
let album = {
if album.contains("catbox.moe") {
Album::new(
Url::parse(&album).map_err(|source| AppError::InvalidUrl { source, url: album })?,
)
} else {
let url = format!("https://catbox.moe/c/{album}");
Album::new(Url::parse(&url).map_err(|source| AppError::InvalidUrl { source, url })?)
}
};
Ok(album)
}
#[tokio::main]
async fn main() -> ExitCode {
let Err(err) = fake_main().await else {
return ExitCode::SUCCESS;
};
let mut err: &dyn Error = &err;
let mut error_chain = vec![err];
while let Some(x) = err.source() {
error_chain.push(x);
err = x;
}
println!("Error: An error occured in `main`");
println!();
println!("Caused by:");
for (u, error) in error_chain.iter().enumerate() {
println!("{u:>4}: {error}");
}
ExitCode::FAILURE
}
#[allow(clippy::too_many_lines)]
async fn fake_main() -> Result<(), AppError> {
let cli: Cli = argh::from_env();
match cli.command {
CliSubCommands::File(FileCommand {
command:
FileSubCommands::Upload(FileUpload {
paths,
use_litterbox,
expiry,
}),
}) => {
if use_litterbox {
upload_temp_files(paths, expiry.unwrap_or(LitterExpiry::OneHour)).await?;
} else {
upload_files(paths).await?;
}
}
CliSubCommands::File(FileCommand {
command: FileSubCommands::List(FileList {}),
}) => {
let user = USER_INSTANCE.get().await?;
let files = user.fetch_uploaded_files().await?;
if cli.json {
println!("{}", serde_json::to_string_pretty(&files)?);
} else {
for (i, x) in files.into_iter().rev().enumerate() {
println!("File {}: {x}", i + 1);
}
}
}
CliSubCommands::Album(AlbumCommand {
command: AlbumSubCommands::Add(AddFiles { album, files }),
}) => {
add_to_album(album, files).await?;
}
CliSubCommands::Album(AlbumCommand {
command: AlbumSubCommands::Upload(UploadFiles { album, files }),
}) => {
let urls = upload_files(files).await?;
add_to_album(album, urls).await?;
}
CliSubCommands::Album(AlbumCommand {
command: AlbumSubCommands::List(AlbumList { album: Some(album) }),
}) => {
let album = get_album(album)?;
let files = album.fetch_files().await?.urls;
if cli.json {
println!("{}", serde_json::to_string_pretty(&files)?);
} else {
for (i, x) in files.into_iter().rev().enumerate() {
println!("File {}: {x}", i + 1);
}
}
}
CliSubCommands::Album(AlbumCommand {
command: AlbumSubCommands::List(AlbumList { album: None }),
}) => {
let user = USER_INSTANCE.get().await?;
let albums = user.fetch_albums().await?;
if cli.json {
let albums = albums.into_iter().map(|x| x.url).collect::<Vec<_>>();
println!("{}", serde_json::to_string_pretty(&albums)?);
} else {
for (i, x) in user.fetch_albums().await?.into_iter().rev().enumerate() {
println!("Album {}: {}", i + 1, x.url);
}
}
}
CliSubCommands::Config(ConfigCommand {
command: ConfigSubCommands::Save(SaveConfig { username, password }),
}) => {
get_username_entry()?
.set_password(&username)
.map_err(AppError::FailureSettingVariable)?;
get_password_entry()?
.set_password(&password)
.map_err(AppError::FailureSettingVariable)?;
}
CliSubCommands::Config(ConfigCommand {
command: ConfigSubCommands::Delete(DeleteConfig {}),
}) => {
get_username_entry()?
.delete_credential()
.map_err(AppError::FailureSettingVariable)?;
get_password_entry()?
.delete_credential()
.map_err(AppError::FailureSettingVariable)?;
}
}
Ok(())
}