use std::{fs, path::PathBuf}; use chrono::{Datelike, NaiveDate}; use color_eyre::eyre::{Context as _, ContextCompat, Result}; use log::*; use vcard4::{ parse_loose, property::{DateAndOrTime, DateTimeOrTextProperty}, }; use crate::{ actions::{ action::Action, fetch_all_persons::FetchAllPersons, update_person_date_of_birth::{UpdatePersonDateOfBirth, UpdatePersonDateOfBirthArgs}, }, context::Context, }; pub async fn sync_date_of_birth(ctx: Context, vcard_file: &PathBuf) -> Result<()> { let fetch_action = FetchAllPersons::new(()); let server_contacts = fetch_action.execute(&ctx).await?; let filtered_contacts: Vec<_> = server_contacts .iter() .filter(|p| !p.name.is_empty()) .collect(); let contacts = fs::read_to_string(vcard_file).wrap_err_with(|| { format!( "Unable to read vcard file at {}", vcard_file.to_string_lossy() ) })?; 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.date_of_birth .as_ref() .map_or(false, |bdate| bday == *bdate) { debug!( "{} already has the proper date of birth, skipping", formatted_name ); } else if bday.year() > 0 { UpdatePersonDateOfBirth::new(UpdatePersonDateOfBirthArgs { person: (*c).clone(), date_of_birth: bday, }) .execute(&ctx) .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 { match src { DateAndOrTime::Date(date) => { NaiveDate::from_ymd_opt(date.year(), date.month() as u32, date.day().into()) .wrap_err("Unable to parse a date") } DateAndOrTime::DateTime(datetime) => NaiveDate::from_ymd_opt( datetime.year(), datetime.month() as u32, datetime.day().into(), ) .wrap_err("Unable to parse a datetime"), DateAndOrTime::Time(_time) => todo!(), } }