Add a command to synchronise date of births
This commit is contained in:
		
							parent
							
								
									5eb1e3bba4
								
							
						
					
					
						commit
						bc09979a62
					
				
					 6 changed files with 392 additions and 5 deletions
				
			
		
							
								
								
									
										19
									
								
								src/args.rs
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								src/args.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,3 +1,5 @@
 | 
			
		|||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use clap::{command, Parser, Subcommand};
 | 
			
		||||
 | 
			
		||||
#[derive(Parser)]
 | 
			
		||||
| 
						 | 
				
			
			@ -9,12 +11,20 @@ pub(crate) struct Opts {
 | 
			
		|||
    #[arg(short, long)]
 | 
			
		||||
    pub api_key: String,
 | 
			
		||||
 | 
			
		||||
    #[arg(short, long)]
 | 
			
		||||
    pub dry_run: bool,
 | 
			
		||||
 | 
			
		||||
    #[command(subcommand)]
 | 
			
		||||
    pub command: Commands,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Subcommand)]
 | 
			
		||||
pub(crate) enum Commands {
 | 
			
		||||
    /// People related commands
 | 
			
		||||
    People {
 | 
			
		||||
        #[command(subcommand)]
 | 
			
		||||
        people_command: PeopleCommands,
 | 
			
		||||
    },
 | 
			
		||||
    /// Server related commands
 | 
			
		||||
    Server {
 | 
			
		||||
        #[command(subcommand)]
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +32,15 @@ pub(crate) enum Commands {
 | 
			
		|||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Subcommand)]
 | 
			
		||||
pub(crate) enum PeopleCommands {
 | 
			
		||||
    /// Synchronises date of births from a vcard file
 | 
			
		||||
    SyncDateOfBirths {
 | 
			
		||||
        #[arg(short, long)]
 | 
			
		||||
        vcard_file: PathBuf,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Subcommand)]
 | 
			
		||||
pub(crate) enum ServerCommands {
 | 
			
		||||
    /// Fetches the version of the server
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1,2 @@
 | 
			
		|||
pub mod server_version;
 | 
			
		||||
pub mod sync_date_of_birth;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										137
									
								
								src/commands/sync_date_of_birth.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/commands/sync_date_of_birth.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,137 @@
 | 
			
		|||
use std::{fs, path::PathBuf};
 | 
			
		||||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use chrono::{Datelike, NaiveDate};
 | 
			
		||||
use log::*;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
use vcard4::{
 | 
			
		||||
    parse_loose,
 | 
			
		||||
    property::{DateAndOrTime, DateTimeOrTextProperty},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    types::{PersonResponseDto, PersonUpdateDto},
 | 
			
		||||
    Client,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub async fn sync_date_of_birth(
 | 
			
		||||
    client: &Client,
 | 
			
		||||
    vcard_file: &PathBuf,
 | 
			
		||||
    dry_run: bool,
 | 
			
		||||
) -> Result<()> {
 | 
			
		||||
    let server_contacts = fetch_all_contacts(client).await?;
 | 
			
		||||
 | 
			
		||||
    let filtered_contacts: Vec<_> = server_contacts
 | 
			
		||||
        .iter()
 | 
			
		||||
        .filter(|p| !p.name.is_empty())
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    let contacts = fs::read_to_string(vcard_file)?;
 | 
			
		||||
    let cards = parse_loose(contacts)?;
 | 
			
		||||
 | 
			
		||||
    let cards_with_bday: Vec<_> = cards.iter().filter(|c| c.bday.is_some()).collect();
 | 
			
		||||
 | 
			
		||||
    info!(
 | 
			
		||||
        "{} vcards with date of births, {} persons with names fetched from Immich",
 | 
			
		||||
        cards_with_bday.len(),
 | 
			
		||||
        filtered_contacts.len()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let mut updated_dobs = 0;
 | 
			
		||||
 | 
			
		||||
    for card in cards_with_bday {
 | 
			
		||||
        let name = card.formatted_name.first().map(|text| text.value.clone());
 | 
			
		||||
        match name {
 | 
			
		||||
            Some(formatted_name) => {
 | 
			
		||||
                let contact = filtered_contacts.iter().find(|c| c.name == formatted_name);
 | 
			
		||||
                match contact {
 | 
			
		||||
                    Some(c) => match card.bday.as_ref().unwrap() {
 | 
			
		||||
                        DateTimeOrTextProperty::DateTime(dt) => {
 | 
			
		||||
                            let bday = vcard_date_to_naive_date(dt.value.first().unwrap().clone())?;
 | 
			
		||||
                            if c.birth_date.as_ref().map_or(false, |bdate| bday == *bdate) {
 | 
			
		||||
                                debug!(
 | 
			
		||||
                                    "{} already has the proper date of birth, skipping",
 | 
			
		||||
                                    formatted_name
 | 
			
		||||
                                );
 | 
			
		||||
                            } else if bday.year() > 0 {
 | 
			
		||||
                                update_person_bday(c, bday, client, dry_run).await?;
 | 
			
		||||
                                updated_dobs += 1;
 | 
			
		||||
                            } else {
 | 
			
		||||
                                debug!(
 | 
			
		||||
                                    "{} has an incomplete date of birth, skipping",
 | 
			
		||||
                                    formatted_name
 | 
			
		||||
                                );
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        DateTimeOrTextProperty::Text(_) => todo!(),
 | 
			
		||||
                    },
 | 
			
		||||
                    None => debug!("No Immich match for {}", formatted_name),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            None => todo!(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    info!("Updated {} persons", updated_dobs);
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn vcard_date_to_naive_date(src: DateAndOrTime) -> Result<NaiveDate> {
 | 
			
		||||
    match src {
 | 
			
		||||
        DateAndOrTime::Date(date) => {
 | 
			
		||||
            Ok(
 | 
			
		||||
                NaiveDate::from_ymd_opt(date.year(), date.month() as u32, date.day().into())
 | 
			
		||||
                    .expect(&format!("Unable to parse {}", date)),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        DateAndOrTime::DateTime(datetime) => Ok(NaiveDate::from_ymd_opt(
 | 
			
		||||
            datetime.year(),
 | 
			
		||||
            datetime.month() as u32,
 | 
			
		||||
            datetime.day().into(),
 | 
			
		||||
        )
 | 
			
		||||
        .expect(&format!("Unable to parse {}", datetime))),
 | 
			
		||||
        DateAndOrTime::Time(_time) => todo!(),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn update_person_bday(
 | 
			
		||||
    person: &PersonResponseDto,
 | 
			
		||||
    bday: NaiveDate,
 | 
			
		||||
    client: &Client,
 | 
			
		||||
    dry_run: bool,
 | 
			
		||||
) -> Result<()> {
 | 
			
		||||
    let update = PersonUpdateDto {
 | 
			
		||||
        feature_face_asset_id: None,
 | 
			
		||||
        is_hidden: None,
 | 
			
		||||
        name: None,
 | 
			
		||||
        birth_date: Some(bday),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    info!("Updating {}: {:?}", person.name, update);
 | 
			
		||||
 | 
			
		||||
    if !dry_run {
 | 
			
		||||
        client
 | 
			
		||||
            .update_person(&Uuid::parse_str(&person.id)?, &update)
 | 
			
		||||
            .await?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn fetch_all_contacts(client: &Client) -> Result<Vec<PersonResponseDto>> {
 | 
			
		||||
    let mut all_people = Vec::new();
 | 
			
		||||
    let mut page_number = 1;
 | 
			
		||||
    let mut has_next_page = true;
 | 
			
		||||
 | 
			
		||||
    while has_next_page {
 | 
			
		||||
        let fetched = client
 | 
			
		||||
            .get_all_people(Some(page_number.into()), None, Some(true))
 | 
			
		||||
            .await?;
 | 
			
		||||
        all_people.extend(fetched.people.clone());
 | 
			
		||||
        has_next_page = fetched.has_next_page.expect("Missing has_next_page");
 | 
			
		||||
        page_number += 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(all_people)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								src/main.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -2,9 +2,10 @@ include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
 | 
			
		|||
 | 
			
		||||
use crate::args::Commands;
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use args::ServerCommands;
 | 
			
		||||
use args::{PeopleCommands, ServerCommands};
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
use commands::server_version::server_version;
 | 
			
		||||
use commands::sync_date_of_birth::sync_date_of_birth;
 | 
			
		||||
use reqwest::header;
 | 
			
		||||
 | 
			
		||||
mod args;
 | 
			
		||||
| 
						 | 
				
			
			@ -12,11 +13,18 @@ mod commands;
 | 
			
		|||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() {
 | 
			
		||||
    pretty_env_logger::init();
 | 
			
		||||
 | 
			
		||||
    let args = args::Opts::parse();
 | 
			
		||||
 | 
			
		||||
    let client = get_client(&args.server_url, &args.api_key).unwrap();
 | 
			
		||||
 | 
			
		||||
    let res = match &args.command {
 | 
			
		||||
        Commands::People { people_command } => match people_command {
 | 
			
		||||
            PeopleCommands::SyncDateOfBirths { vcard_file } => {
 | 
			
		||||
                sync_date_of_birth(&client, vcard_file, args.dry_run).await
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        Commands::Server { server_command } => match server_command {
 | 
			
		||||
            ServerCommands::Version {} => server_version(&client).await,
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue