Collect relevant ingredients for grocery list
This commit is contained in:
parent
fd55dba23f
commit
c649fbf88c
|
@ -8,6 +8,15 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.4"
|
||||
|
@ -171,7 +180,9 @@ dependencies = [
|
|||
"futures",
|
||||
"iana-time-zone",
|
||||
"ics",
|
||||
"markdown-gen",
|
||||
"minicaldav",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rusty-hook",
|
||||
"serde",
|
||||
|
@ -704,6 +715,12 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-gen"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8034621d7f1258317ca1dfb9205e3925d27ee4aa2a46620a09c567daf0310562"
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.9"
|
||||
|
@ -1015,12 +1032,29 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
|
|
|
@ -45,9 +45,15 @@ version = "0.3"
|
|||
[dependencies.ics]
|
||||
version = "0.5"
|
||||
|
||||
[dependencies.markdown-gen]
|
||||
version = "1.2"
|
||||
|
||||
[dependencies.minicaldav]
|
||||
version = "0.2"
|
||||
|
||||
[dependencies.regex]
|
||||
version = "1.6"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.11"
|
||||
features = ["json", "blocking"]
|
||||
|
|
|
@ -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…
Reference in New Issue