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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
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]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
@ -171,7 +180,9 @@ dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"ics",
|
"ics",
|
||||||
|
"markdown-gen",
|
||||||
"minicaldav",
|
"minicaldav",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rusty-hook",
|
"rusty-hook",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -704,6 +715,12 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown-gen"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8034621d7f1258317ca1dfb9205e3925d27ee4aa2a46620a09c567daf0310562"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matches"
|
name = "matches"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
|
@ -1015,12 +1032,29 @@ dependencies = [
|
||||||
"thiserror",
|
"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]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.1.10"
|
version = "0.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "remove_dir_all"
|
name = "remove_dir_all"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|
|
@ -45,9 +45,15 @@ version = "0.3"
|
||||||
[dependencies.ics]
|
[dependencies.ics]
|
||||||
version = "0.5"
|
version = "0.5"
|
||||||
|
|
||||||
|
[dependencies.markdown-gen]
|
||||||
|
version = "1.2"
|
||||||
|
|
||||||
[dependencies.minicaldav]
|
[dependencies.minicaldav]
|
||||||
version = "0.2"
|
version = "0.2"
|
||||||
|
|
||||||
|
[dependencies.regex]
|
||||||
|
version = "1.6"
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
version = "0.11"
|
version = "0.11"
|
||||||
features = ["json", "blocking"]
|
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-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
pub mod groceries;
|
||||||
pub mod import;
|
pub mod import;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod schedule;
|
pub mod schedule;
|
||||||
|
|
|
@ -110,15 +110,8 @@ async fn publish_events<'a, EventsIter>(
|
||||||
where
|
where
|
||||||
EventsIter: Iterator<Item = Event>,
|
EventsIter: Iterator<Item = Event>,
|
||||||
{
|
{
|
||||||
let calendar_prototype: ics::ICalendar = ics::ICalendar::new(
|
let calendar_prototype: ics::ICalendar =
|
||||||
"2.0",
|
ics::ICalendar::new("2.0", constants::CALENDAR_PROVIDER);
|
||||||
format!(
|
|
||||||
"-//IDN {}//{} {}//EN",
|
|
||||||
constants::VENDOR,
|
|
||||||
env!("CARGO_PKG_NAME"),
|
|
||||||
env!("CARGO_PKG_VERSION")
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let dav_base = api_client
|
let dav_base = api_client
|
||||||
.rest()
|
.rest()
|
||||||
|
|
|
@ -2,4 +2,10 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
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")),
|
.arg(arg!(<url> ... "One or more URLs each pointing to page with a recipe to import in NextCloud")),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("schedule")
|
Command::new("groceries")
|
||||||
.about("")
|
.about("Create a grocery list from scheduled calendar events")
|
||||||
.arg(server_arg.clone())
|
.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..))
|
.value_parser(clap::builder::RangedU64ValueParser::<u32>::new().range(1..))
|
||||||
.required(false)
|
.required(false)
|
||||||
.default_value("7"))
|
.default_value("7"))
|
||||||
|
@ -77,6 +87,14 @@ async fn parse_args(args: &ArgMatches) -> Result<()> {
|
||||||
.map(|s| s.as_str());
|
.map(|s| s.as_str());
|
||||||
commands::import::with(&api_client, urls).await
|
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)) => {
|
Some(("schedule", sub_matches)) => {
|
||||||
let api_client = get_api_client(&sub_matches, &configuration)?;
|
let api_client = get_api_client(&sub_matches, &configuration)?;
|
||||||
commands::schedule::with(&api_client).await
|
commands::schedule::with(&api_client).await
|
||||||
|
|
Loading…
Reference in New Issue