include!(concat!(env!("OUT_DIR"), "/codegen.rs")); use crate::args::Commands; use args::{AssetsCommands, PeopleCommands, ServerCommands}; use clap::Parser; use color_eyre::eyre::{Result, WrapErr}; use color_eyre::Section; use commands::delete_assets::delete_assets; use commands::list_assets::list_assets; use commands::missing_date_of_birth::missing_date_of_birth; use commands::server_features::server_features; use commands::server_version::server_version; use commands::sync_date_of_birth::sync_date_of_birth; use config::Config; use context::Context; use directories::ProjectDirs; use figment::providers::{Env, Format, Serialized, Toml}; use figment::Figment; use figment_file_provider_adapter::FileAdapter; use log::*; use reqwest::header; mod args; mod commands; mod config; mod context; mod utils; #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; pretty_env_logger::init(); let mut conf_extractor = Figment::new(); if let Some(project_dirs) = ProjectDirs::from("fr", "enoent", "Immich Tools") { let config_file_path = project_dirs.config_dir().join("config.toml"); if config_file_path.exists() { debug!("Reading configuration from {:?}", config_file_path); conf_extractor = conf_extractor.merge(FileAdapter::wrap(Toml::file_exact(config_file_path))); } } else { warn!("Unable to determine configuration file path"); } let args = args::Opts::parse(); let conf: Config = conf_extractor .merge(FileAdapter::wrap(Env::prefixed("IMMICH_TOOLS_"))) .merge(Serialized::defaults(&args)) .extract() .wrap_err_with(|| "Invalid configuration or insufficient command line arguments")?; let client = get_client(&conf.server_url, &conf.api_key)?; let ctx = Context::builder() .client(client) .dry_run(args.dry_run) .no_confirm(args.no_confirm) .build(); validate_client_connection(&ctx.client).await?; match &args.command { Commands::Assets { assets_command } => match assets_command { AssetsCommands::Delete { offline } => delete_assets(ctx, *offline).await, AssetsCommands::List { offline } => list_assets(ctx, *offline).await, }, Commands::People { people_command } => match people_command { PeopleCommands::MissingDateOfBirths {} => missing_date_of_birth(ctx).await, PeopleCommands::SyncDateOfBirths { vcard: _ } => { sync_date_of_birth(ctx, &conf.people.sync_date_of_births.vcard).await } }, Commands::Server { server_command } => match server_command { ServerCommands::Features {} => server_features(ctx).await, ServerCommands::Version {} => server_version(ctx).await, }, } } fn get_client(url: &str, api_key: &str) -> Result { let mut headers = header::HeaderMap::new(); let mut auth_value = header::HeaderValue::from_str(api_key)?; auth_value.set_sensitive(true); headers.insert("x-api-key", auth_value); Ok(Client::new_with_client( url, reqwest::Client::builder() .default_headers(headers) .build() .unwrap(), )) } async fn validate_client_connection(client: &Client) -> Result<()> { client .validate_access_token() .await .wrap_err_with(|| "Unable to connect to Immich") .with_suggestion(|| "The API key might be invalid") .with_suggestion(|| "There server URL might be invalid, it should likely end in /api") .with_note(|| format!("The provided server URL was {}", client.baseurl())) .with_note(|| { format!( "The API version for this client is {}, make sure the server version is compatible", client.api_version() ) })?; Ok(()) }