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}, utils::people::fetch_all_contacts, 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 { 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(()) }