Merge branch 'actions-refactor' into 'main'
PoC of refactoring actions See merge request kernald/immich-tools!19
This commit is contained in:
commit
bc46df634d
11 changed files with 158 additions and 56 deletions
11
src/actions/action.rs
Normal file
11
src/actions/action.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
use crate::context::Context;
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
|
||||||
|
pub trait Action {
|
||||||
|
type Input;
|
||||||
|
type Output;
|
||||||
|
|
||||||
|
fn new(input: Self::Input) -> Self;
|
||||||
|
fn describe(&self) -> String;
|
||||||
|
async fn execute(&self, ctx: &Context) -> Result<Self::Output>;
|
||||||
|
}
|
38
src/actions/fetch_all_persons.rs
Normal file
38
src/actions/fetch_all_persons.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
|
||||||
|
use crate::{context::Context, models::person::Person};
|
||||||
|
|
||||||
|
use super::action::Action;
|
||||||
|
|
||||||
|
pub struct FetchAllPersons {}
|
||||||
|
|
||||||
|
impl Action for FetchAllPersons {
|
||||||
|
type Input = ();
|
||||||
|
type Output = Vec<Person>;
|
||||||
|
|
||||||
|
fn new(_input: Self::Input) -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn describe(&self) -> String {
|
||||||
|
String::from("Fetching all persons")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, ctx: &Context) -> Result<Self::Output> {
|
||||||
|
let mut all_people = Vec::new();
|
||||||
|
let mut page_number = 1;
|
||||||
|
let mut has_next_page = true;
|
||||||
|
|
||||||
|
while has_next_page {
|
||||||
|
let fetched = ctx
|
||||||
|
.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.into_iter().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
}
|
3
src/actions/mod.rs
Normal file
3
src/actions/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod action;
|
||||||
|
pub mod fetch_all_persons;
|
||||||
|
pub mod update_person_date_of_birth;
|
51
src/actions/update_person_date_of_birth.rs
Normal file
51
src/actions/update_person_date_of_birth.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{context::Context, models::person::Person, types::PersonUpdateDto};
|
||||||
|
|
||||||
|
use super::action::Action;
|
||||||
|
|
||||||
|
pub struct UpdatePersonDateOfBirth {
|
||||||
|
args: UpdatePersonDateOfBirthArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UpdatePersonDateOfBirthArgs {
|
||||||
|
pub person: Person,
|
||||||
|
pub date_of_birth: NaiveDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action for UpdatePersonDateOfBirth {
|
||||||
|
type Input = UpdatePersonDateOfBirthArgs;
|
||||||
|
type Output = ();
|
||||||
|
|
||||||
|
fn new(input: Self::Input) -> Self {
|
||||||
|
Self { args: input }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn describe(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"Updating {}'s date of birth to {}",
|
||||||
|
self.args.person.name, self.args.date_of_birth,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, ctx: &Context) -> Result<Self::Output> {
|
||||||
|
let update = PersonUpdateDto {
|
||||||
|
feature_face_asset_id: None,
|
||||||
|
is_hidden: None,
|
||||||
|
name: None,
|
||||||
|
birth_date: Some(self.args.date_of_birth),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("{}", self.describe());
|
||||||
|
|
||||||
|
if !ctx.dry_run {
|
||||||
|
ctx.client
|
||||||
|
.update_person(&self.args.person.id, &update)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,16 @@
|
||||||
use crate::{context::Context, utils::people::fetch_all_contacts};
|
use crate::{
|
||||||
|
actions::{action::Action, fetch_all_persons::FetchAllPersons},
|
||||||
|
context::Context,
|
||||||
|
};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use log::*;
|
use log::*;
|
||||||
|
|
||||||
pub async fn missing_date_of_birth(ctx: Context) -> Result<()> {
|
pub async fn missing_date_of_birth(ctx: Context) -> Result<()> {
|
||||||
let contacts = fetch_all_contacts(&ctx.client).await?;
|
let fetch_action = FetchAllPersons::new(());
|
||||||
|
let contacts = fetch_action.execute(&ctx).await?;
|
||||||
let mut filtered_contacts = contacts
|
let mut filtered_contacts = contacts
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| c.birth_date.is_none() && !c.name.is_empty())
|
.filter(|c| c.date_of_birth.is_none() && !c.name.is_empty())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
filtered_contacts.sort_by_key(|c| c.name.clone());
|
filtered_contacts.sort_by_key(|c| c.name.clone());
|
||||||
|
|
|
@ -1,30 +1,37 @@
|
||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
use chrono::{Datelike, NaiveDate};
|
use chrono::{Datelike, NaiveDate};
|
||||||
use color_eyre::eyre::{ContextCompat, Result};
|
use color_eyre::eyre::{Context as _, ContextCompat, Result};
|
||||||
use log::*;
|
use log::*;
|
||||||
use uuid::Uuid;
|
|
||||||
use vcard4::{
|
use vcard4::{
|
||||||
parse_loose,
|
parse_loose,
|
||||||
property::{DateAndOrTime, DateTimeOrTextProperty},
|
property::{DateAndOrTime, DateTimeOrTextProperty},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
actions::{
|
||||||
|
action::Action,
|
||||||
|
fetch_all_persons::FetchAllPersons,
|
||||||
|
update_person_date_of_birth::{UpdatePersonDateOfBirth, UpdatePersonDateOfBirthArgs},
|
||||||
|
},
|
||||||
context::Context,
|
context::Context,
|
||||||
types::{PersonResponseDto, PersonUpdateDto},
|
|
||||||
utils::people::fetch_all_contacts,
|
|
||||||
Client,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn sync_date_of_birth(ctx: Context, vcard_file: &PathBuf) -> Result<()> {
|
pub async fn sync_date_of_birth(ctx: Context, vcard_file: &PathBuf) -> Result<()> {
|
||||||
let server_contacts = fetch_all_contacts(&ctx.client).await?;
|
let fetch_action = FetchAllPersons::new(());
|
||||||
|
let server_contacts = fetch_action.execute(&ctx).await?;
|
||||||
|
|
||||||
let filtered_contacts: Vec<_> = server_contacts
|
let filtered_contacts: Vec<_> = server_contacts
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|p| !p.name.is_empty())
|
.filter(|p| !p.name.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let contacts = fs::read_to_string(vcard_file)?;
|
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 = parse_loose(contacts)?;
|
||||||
|
|
||||||
let cards_with_bday: Vec<_> = cards.iter().filter(|c| c.bday.is_some()).collect();
|
let cards_with_bday: Vec<_> = cards.iter().filter(|c| c.bday.is_some()).collect();
|
||||||
|
@ -46,13 +53,21 @@ pub async fn sync_date_of_birth(ctx: Context, vcard_file: &PathBuf) -> Result<()
|
||||||
Some(c) => match card.bday.as_ref().unwrap() {
|
Some(c) => match card.bday.as_ref().unwrap() {
|
||||||
DateTimeOrTextProperty::DateTime(dt) => {
|
DateTimeOrTextProperty::DateTime(dt) => {
|
||||||
let bday = vcard_date_to_naive_date(dt.value.first().unwrap().clone())?;
|
let bday = vcard_date_to_naive_date(dt.value.first().unwrap().clone())?;
|
||||||
if c.birth_date.as_ref().map_or(false, |bdate| bday == *bdate) {
|
if c.date_of_birth
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |bdate| bday == *bdate)
|
||||||
|
{
|
||||||
debug!(
|
debug!(
|
||||||
"{} already has the proper date of birth, skipping",
|
"{} already has the proper date of birth, skipping",
|
||||||
formatted_name
|
formatted_name
|
||||||
);
|
);
|
||||||
} else if bday.year() > 0 {
|
} else if bday.year() > 0 {
|
||||||
update_person_bday(c, bday, &ctx.client, ctx.dry_run).await?;
|
UpdatePersonDateOfBirth::new(UpdatePersonDateOfBirthArgs {
|
||||||
|
person: (*c).clone(),
|
||||||
|
date_of_birth: bday,
|
||||||
|
})
|
||||||
|
.execute(&ctx)
|
||||||
|
.await?;
|
||||||
updated_dobs += 1;
|
updated_dobs += 1;
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
|
@ -90,27 +105,3 @@ fn vcard_date_to_naive_date(src: DateAndOrTime) -> Result<NaiveDate> {
|
||||||
DateAndOrTime::Time(_time) => todo!(),
|
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(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -20,10 +20,12 @@ use figment_file_provider_adapter::FileAdapter;
|
||||||
use log::*;
|
use log::*;
|
||||||
use reqwest::header;
|
use reqwest::header;
|
||||||
|
|
||||||
|
mod actions;
|
||||||
mod args;
|
mod args;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod context;
|
mod context;
|
||||||
|
mod models;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
1
src/models/mod.rs
Normal file
1
src/models/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod person;
|
21
src/models/person.rs
Normal file
21
src/models/person.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::types::PersonResponseDto;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Person {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub date_of_birth: Option<NaiveDate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PersonResponseDto> for Person {
|
||||||
|
fn from(value: PersonResponseDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Uuid::parse_str(&value.id).expect("Unable to parse a person's UUID"),
|
||||||
|
name: value.name,
|
||||||
|
date_of_birth: value.birth_date,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,2 @@
|
||||||
pub mod albums;
|
pub mod albums;
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
pub mod people;
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
use crate::{types::PersonResponseDto, Client};
|
|
||||||
use color_eyre::eyre::Result;
|
|
||||||
|
|
||||||
pub 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)
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue