Collect relevant ingredients for grocery list
This commit is contained in:
parent
fd55dba23f
commit
c649fbf88c
7 changed files with 178 additions and 13 deletions
107
src/commands/groceries.rs
Normal file
107
src/commands/groceries.rs
Normal file
|
@ -0,0 +1,107 @@
|
|||
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use {
|
||||
crate::api_client::ApiClient,
|
||||
crate::constants,
|
||||
crate::recipe::{Ingredient, Recipe},
|
||||
anyhow::{anyhow, Result},
|
||||
chrono::{Duration, Local, NaiveDateTime},
|
||||
regex::Regex,
|
||||
std::collections::HashSet,
|
||||
};
|
||||
|
||||
pub async fn with(api_client: &ApiClient, calendar_name: &str, days: u32) -> Result<()> {
|
||||
let ids = map_events_to_recipe_ids(api_client, calendar_name, days)?;
|
||||
let ingredients = get_ingredients(api_client, ids).await?;
|
||||
|
||||
todo!("Recipe ingredients: {:?}", ingredients)
|
||||
|
||||
// let ingredients = merge_ingredients(ingredients);
|
||||
// let md = prepare_grocery_list(&ingredients);
|
||||
// save_grocery_list(api_client, "", md).await?;
|
||||
}
|
||||
|
||||
fn map_events_to_recipe_ids(
|
||||
api_client: &ApiClient,
|
||||
calendar_name: &str,
|
||||
days: u32,
|
||||
) -> Result<HashSet<usize>> {
|
||||
// minicaldav is quite hamfisted in retrieving events (it gets everything, every time).
|
||||
// Consider crafting an XML request and use something else for ical parsing.
|
||||
|
||||
let calendars = api_client
|
||||
.get_calendars()
|
||||
.map_err(|err| convert_caldav_err(err))?;
|
||||
let calendar = calendars
|
||||
.into_iter()
|
||||
.filter(|c| c.name() == calendar_name)
|
||||
.next()
|
||||
.ok_or(anyhow!(
|
||||
"Unable to find calendar '{}' on server",
|
||||
calendar_name
|
||||
))?;
|
||||
|
||||
let all_events = api_client
|
||||
.get_events(&calendar)
|
||||
.map_err(|err| convert_caldav_err(err))?
|
||||
.0;
|
||||
|
||||
let relevant_events = all_events.into_iter().filter(|ev| {
|
||||
ev.get("PRODID")
|
||||
.map(|p| p == constants::CALENDAR_PROVIDER)
|
||||
.unwrap_or(false)
|
||||
&& {
|
||||
let start_time = NaiveDateTime::parse_from_str(
|
||||
ev.property("DTSTART").unwrap().value(),
|
||||
"%Y%m%dT%H%M%S",
|
||||
)
|
||||
.unwrap_or(NaiveDateTime::from_timestamp(0, 0));
|
||||
let now = Local::now().naive_local();
|
||||
start_time >= now && start_time < now + Duration::days(days as i64)
|
||||
}
|
||||
});
|
||||
|
||||
let recipe_id_regex: Regex = Regex::new(r"cookbook@(\d+)").unwrap();
|
||||
let recipe_ids = relevant_events
|
||||
.flat_map(|event| {
|
||||
event
|
||||
.property("DESCRIPTION")
|
||||
.iter()
|
||||
.flat_map(|descr| recipe_id_regex.captures(descr.value()))
|
||||
.flat_map(|c| c.get(1))
|
||||
.flat_map(|m| m.as_str().parse::<usize>())
|
||||
.collect::<Vec<usize>>()
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
Ok(recipe_ids)
|
||||
}
|
||||
|
||||
async fn get_ingredients<RecipeIds>(
|
||||
api_client: &ApiClient,
|
||||
recipe_ids: RecipeIds,
|
||||
) -> Result<Vec<Ingredient>>
|
||||
where
|
||||
RecipeIds: IntoIterator<Item = usize>,
|
||||
{
|
||||
let ingredients = recipe_ids.into_iter().map(|id: usize| async move {
|
||||
// TODO code duplicated with schedule_csv::get_all_recipes
|
||||
let recipe_url = format!("apps/cookbook/api/recipes/{id}");
|
||||
let response = api_client
|
||||
.rest()
|
||||
.get(api_client.base_url().join(&recipe_url).unwrap())
|
||||
.send()
|
||||
.await
|
||||
.expect(&format!("Cannot fetch recipe with id {}", id));
|
||||
|
||||
response.json::<Recipe>().await.map(|r| r.ingredients)
|
||||
});
|
||||
|
||||
let ingredients = futures::future::try_join_all(ingredients).await?;
|
||||
Ok(ingredients.into_iter().flatten().collect())
|
||||
}
|
||||
|
||||
fn convert_caldav_err(err: minicaldav::Error) -> anyhow::Error {
|
||||
anyhow!(format!("{:?}", err))
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod groceries;
|
||||
pub mod import;
|
||||
pub mod init;
|
||||
pub mod schedule;
|
||||
|
|
|
@ -110,15 +110,8 @@ async fn publish_events<'a, EventsIter>(
|
|||
where
|
||||
EventsIter: Iterator<Item = Event>,
|
||||
{
|
||||
let calendar_prototype: ics::ICalendar = ics::ICalendar::new(
|
||||
"2.0",
|
||||
format!(
|
||||
"-//IDN {}//{} {}//EN",
|
||||
constants::VENDOR,
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
),
|
||||
);
|
||||
let calendar_prototype: ics::ICalendar =
|
||||
ics::ICalendar::new("2.0", constants::CALENDAR_PROVIDER);
|
||||
|
||||
let dav_base = api_client
|
||||
.rest()
|
||||
|
|
|
@ -2,4 +2,10 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
||||
pub const VENDOR: &str = "montecristosoftware.eu";
|
||||
pub const CALENDAR_PROVIDER: &str = concat!(
|
||||
"-//IDN montecristosoftware.eu//",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
" ",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
"//EN"
|
||||
);
|
||||
|
|
24
src/main.rs
24
src/main.rs
|
@ -41,10 +41,20 @@ fn setup_args() -> ArgMatches {
|
|||
.arg(arg!(<url> ... "One or more URLs each pointing to page with a recipe to import in NextCloud")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("schedule")
|
||||
.about("")
|
||||
Command::new("groceries")
|
||||
.about("Create a grocery list from scheduled calendar events")
|
||||
.arg(server_arg.clone())
|
||||
.arg(arg!(-d --days <days> "")
|
||||
.arg(arg!(<calendar_name> ""))
|
||||
.arg(arg!(-d --days <days> "Number of days in the future to consider")
|
||||
.value_parser(clap::builder::RangedU64ValueParser::<u32>::new().range(1..))
|
||||
.required(false)
|
||||
.default_value("7"))
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("schedule")
|
||||
.about("TODO; not implemented yet (pick automatically recipes and create calendar events for them)")
|
||||
.arg(server_arg.clone())
|
||||
.arg(arg!(-d --days <days> "Number of days to schedule")
|
||||
.value_parser(clap::builder::RangedU64ValueParser::<u32>::new().range(1..))
|
||||
.required(false)
|
||||
.default_value("7"))
|
||||
|
@ -77,6 +87,14 @@ async fn parse_args(args: &ArgMatches) -> Result<()> {
|
|||
.map(|s| s.as_str());
|
||||
commands::import::with(&api_client, urls).await
|
||||
}
|
||||
Some(("groceries", sub_matches)) => {
|
||||
let api_client = get_api_client(&sub_matches, &configuration)?;
|
||||
let calendar_name = sub_matches
|
||||
.get_one::<String>("calendar_name")
|
||||
.expect("<calendar_name> is a mandatory parameter, it cannot be missing");
|
||||
let days = sub_matches.get_one::<u32>("days").unwrap();
|
||||
commands::groceries::with(&api_client, calendar_name.as_str(), *days).await
|
||||
}
|
||||
Some(("schedule", sub_matches)) => {
|
||||
let api_client = get_api_client(&sub_matches, &configuration)?;
|
||||
commands::schedule::with(&api_client).await
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue