From 7495682e41a94905e671a9b05549e146358c917f Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Fri, 5 Aug 2022 15:53:57 +0200 Subject: [PATCH] Cover use case of manually inserted recipes; allow recipe names instead of URLs in CSV --- examples/example-schedule.csv | 20 +++++----- src/api_client.rs | 24 ++++++------ src/commands/groceries.rs | 59 ++++++++++++++-------------- src/commands/purge.rs | 4 +- src/commands/schedule_csv.rs | 72 ++++++++++++++++------------------- src/recipe.rs | 6 +-- src/scheduling.rs | 14 ++++--- 7 files changed, 96 insertions(+), 103 deletions(-) diff --git a/examples/example-schedule.csv b/examples/example-schedule.csv index bd8e0b4..8400fb3 100644 --- a/examples/example-schedule.csv +++ b/examples/example-schedule.csv @@ -128,7 +128,7 @@ "2022-05-07","sabato",,"https://ricette.giallozafferano.it/Moussaka.html" "2022-05-08","domenica",, "2022-05-09","lunedì",, -"2022-05-10","martedì",, +"2022-05-10","martedì",,"https://ricette.giallozafferano.it/Torta-salata-di-melanzane.html" "2022-05-11","mercoledì",,"https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html" "2022-05-12","giovedì",, "2022-05-13","venerdì",,"https://ricette.giallozafferano.it/Spiedini-di-pollo.html" @@ -193,7 +193,7 @@ "2022-07-11","lunedì",,"https://ricette.giallozafferano.it/Insalata-con-uova-strapazzate.html" "2022-07-12","martedì",, "2022-07-13","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" -"2022-07-14","giovedì",, +"2022-07-14","giovedì",,"Tomatenpita" "2022-07-15","venerdì",,"https://ricette.giallozafferano.it/Salmorejo.html" "2022-07-16","sabato",, "2022-07-17","domenica",,"https://ricette.giallozafferano.it/Insalata-di-quinoa-alla-greca.html" @@ -213,17 +213,17 @@ "2022-07-31","domenica",, "2022-08-01","lunedì",,"https://ricette.giallozafferano.it/Riso-freddo-con-tonno-zucchine-e-limone.html" "2022-08-02","martedì",,"https://ricette.giallozafferano.it/Insalata-Shirazi.html" -"2022-08-03","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" +"2022-08-03","mercoledì",,"https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html" "2022-08-04","giovedì",,"https://ricette.giallozafferano.it/Petto-di-pollo-ai-peperoni.html" -"2022-08-05","venerdì",,"https://ricette.giallozafferano.it/Pasta-con-pomodorini-e-stracchino.html" -"2022-08-06","sabato","https://ricette.giallozafferano.it/Spaghetti-di-riso-con-carne-e-verdure.html", -"2022-08-07","domenica",, -"2022-08-08","lunedì",, -"2022-08-09","martedì",,"https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html" +"2022-08-05","venerdì",, +"2022-08-06","sabato","https://ricette.giallozafferano.it/Spaghetti-di-riso-con-carne-e-verdure.html","https://ricette.giallozafferano.it/Pasta-con-pomodorini-e-stracchino.html" +"2022-08-07","domenica",,"https://ricette.giallozafferano.it/Insalata-di-riso-vegetariana.html" +"2022-08-08","lunedì",,"https://ricette.giallozafferano.it/Torta-salata-di-melanzane.html" +"2022-08-09","martedì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" "2022-08-10","mercoledì",,"https://ricette.giallozafferano.it/Insalata-con-uova-strapazzate.html" "2022-08-11","giovedì",,"https://ricette.giallozafferano.it/Verdure-gratinate-al-forno.html" "2022-08-12","venerdì",,"https://ricette.giallozafferano.it/Garganelli-con-pesto-di-zucchine-e-gamberetti.html" -"2022-08-13","sabato","https://ricette.giallozafferano.it/Insalata-di-bulgur-vegana.html", +"2022-08-13","sabato","https://ricette.giallozafferano.it/Insalata-di-bulgur-vegana.html","Tomatenpita" "2022-08-14","domenica",, "2022-08-15","lunedì",,"https://ricette.giallozafferano.it/Pasta-con-le-melanzane.html" "2022-08-16","martedì",,"https://ricette.giallozafferano.it/Tempeh-alle-verdure.html" @@ -254,7 +254,7 @@ "2022-09-10","sabato","https://ricette.giallozafferano.it/Maccheroncini-al-fume.html", "2022-09-11","domenica","https://ricette.giallozafferano.it/Polpo-alla-Luciana.html", "2022-09-12","lunedì",,"https://ricette.giallozafferano.it/Pennette-con-speck-e-zucchine.html" -"2022-09-13","martedì",, +"2022-09-13","martedì",,"https://ricette.giallozafferano.it/Torta-salata-di-melanzane.html" "2022-09-14","mercoledì",,"https://ricette.giallozafferano.it/Scaloppine-ai-funghi.html" "2022-09-15","giovedì",, "2022-09-16","venerdì",,"https://ricette.giallozafferano.it/Cordon-bleu-di-melanzane.html" diff --git a/src/api_client.rs b/src/api_client.rs index b17d0a3..229169b 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -9,10 +9,10 @@ use { pub struct ApiClient { rest: reqwest::Client, - pub(crate) rest_semaphore: Arc, // TODO: wrap in dereferentiable struct + rest_semaphore: Arc, base_url: Url, caldav_base_url: Url, - username: String, + webdav_base_url: Url, } impl ApiClient { @@ -30,6 +30,8 @@ impl ApiClient { ) })?; + let username = server.login_name.clone(); + use reqwest::header; let mut default_headers = header::HeaderMap::new(); let mut auth_header = b"Basic ".to_vec(); @@ -53,12 +55,14 @@ impl ApiClient { .send(), )? .url() - .clone(); + .join(&format!("calendars/{}/", &username))?; + + let webdav_base_url = base_url.join(&format!("remote.php/dav/files/{}/", username))?; Ok(ApiClient { base_url, caldav_base_url, - username: server.login_name.clone(), + webdav_base_url, rest: rest_client, rest_semaphore: Arc::new(Semaphore::new(5)), }) @@ -94,10 +98,6 @@ impl ApiClient { &self.caldav_base_url } - pub fn username(&self) -> &str { - &self.username - } - pub async fn get_events( &self, calendar_name: &str, @@ -113,10 +113,8 @@ impl ApiClient { let response = client .request( report_method.clone(), - // TODO extract into helper method self.caldav_base_url.join(&format!( - "calendars/{}/{}", - self.username(), + "{}/", calendar_name.to_lowercase().replace(" ", "-") ))?, ) @@ -195,4 +193,8 @@ impl ApiClient { Ok(events.collect::>()) } + + pub fn webdav_base_url(&self) -> &Url { + &self.webdav_base_url + } } diff --git a/src/commands/groceries.rs b/src/commands/groceries.rs index ba7643c..18a5c2c 100644 --- a/src/commands/groceries.rs +++ b/src/commands/groceries.rs @@ -8,7 +8,7 @@ use { chrono::{Duration, Local}, icalendar::Component, regex::Regex, - reqwest::{Method, StatusCode}, + reqwest::{Method, StatusCode, Url}, std::collections::HashSet, std::ops::Range, }; @@ -109,43 +109,16 @@ fn prepare_grocery_list(ingredients: &Vec) -> Result { } async fn save_grocery_list(api_client: &ApiClient, filename: &str, contents: &str) -> Result<()> { - let dav_base_url = api_client - .base_url() - .join(&format!("remote.php/dav/files/{}/", api_client.username()))?; - let filename_components = filename.split('/').collect::>(); filename_components .iter() .take(filename_components.len() - 1) - .fold(Ok(dav_base_url.clone()), |url, dir| { + .fold(Ok(api_client.webdav_base_url().clone()), |url, dir| { url.map(|u| u.join(&format!("{dir}/")).unwrap()) - .and_then(|url| { - futures::executor::block_on(async { - let response = api_client - .rest(|client| async { - let r = client - .request(Method::from_bytes(b"MKCOL").unwrap(), url.clone()) - .send() - .await; - Ok(r?) - }) - .await; - - match response.map(|r| r.status()) { - Ok(StatusCode::OK) - | Ok(StatusCode::METHOD_NOT_ALLOWED /* already exists */) => Ok(url), - Ok(status) => Err(anyhow!( - "Could not create WebDAV collection {}, server responded with {}", - &url, - status - )), - Err(e) => Err(anyhow!(e)), - } - }) - }) + .and_then(|url| ensure_collection_exist(api_client, url)) })?; - let file_url = dav_base_url.join(filename).unwrap(); + let file_url = api_client.webdav_base_url().join(filename).unwrap(); log::info!("Saving grocery list to {}", &file_url); let response = api_client .rest(|client| async { @@ -168,3 +141,27 @@ async fn save_grocery_list(api_client: &ApiClient, filename: &str, contents: &st )), } } + +fn ensure_collection_exist(api_client: &ApiClient, url: Url) -> Result { + futures::executor::block_on(async { + let response = api_client + .rest(|client| async { + let r = client + .request(Method::from_bytes(b"MKCOL").unwrap(), url.clone()) + .send() + .await; + Ok(r?) + }) + .await; + + match response.map(|r| r.status()) { + Ok(StatusCode::OK) | Ok(StatusCode::METHOD_NOT_ALLOWED /* already exists */) => Ok(url), + Ok(status) => Err(anyhow!( + "Could not create WebDAV collection {}, server responded with {}", + &url, + status + )), + Err(e) => Err(anyhow!(e)), + } + }) +} diff --git a/src/commands/purge.rs b/src/commands/purge.rs index 6c859e5..c9d95d5 100644 --- a/src/commands/purge.rs +++ b/src/commands/purge.rs @@ -10,10 +10,8 @@ pub async fn with(api_client: &ApiClient, calendar_name: &str) -> Result<()> { let response = client .request( report_method.clone(), - // TODO extract into helper method api_client.caldav_base_url().join(&format!( - "calendars/{}/{}/", - api_client.username(), + "{}/", calendar_name.to_lowercase().replace(" ", "-") ))?, ) diff --git a/src/commands/schedule_csv.rs b/src/commands/schedule_csv.rs index d86c76b..0e40118 100644 --- a/src/commands/schedule_csv.rs +++ b/src/commands/schedule_csv.rs @@ -11,7 +11,7 @@ use { chrono::naive::NaiveDate, futures::future::try_join_all, icalendar::Event, - reqwest::StatusCode, + reqwest::{StatusCode, Url}, std::collections::{HashMap, HashSet}, std::fmt::Write, std::iter::Iterator, @@ -61,17 +61,17 @@ pub async fn with( Ok(()) } -fn urls_from_csv<'a, RecordsIter>(records: RecordsIter) -> Result> +fn urls_from_csv<'a, RecordsIter>(records: RecordsIter) -> Result> where RecordsIter: Iterator, { Ok( records.fold(std::collections::HashSet::new(), |mut set, r| { - if !r.lunch.is_empty() { - set.insert(r.lunch.clone()); + if let Ok(lunch) = Url::try_from(r.lunch.as_str()) { + set.insert(lunch); } - if !r.dinner.is_empty() { - set.insert(r.dinner.clone()); + if let Ok(dinner) = Url::try_from(r.dinner.as_str()) { + set.insert(dinner); } set }), @@ -93,35 +93,20 @@ async fn get_all_recipes(api_client: &ApiClient) -> Result break response, - Err(_) if retries < 10 => { - retries += 1; - std::thread::sleep(retries * std::time::Duration::from_millis(500)); - continue; - } - _ => bail!("Cannot fetch recipe {} with id {}", rm.name, rm.id), - } - }; + let response = api_client + .rest(|client| async { + let r = client + .get( + api_client + .base_url() + .join(&format!("apps/cookbook/api/recipes/{id}", id = rm.id)) + .unwrap(), + ) + .send() + .await; + Ok(r?) + }) + .await?; response .json::() @@ -131,9 +116,17 @@ async fn get_all_recipes(api_client: &ApiClient) -> Result u, + _ => &r.name, + } + .clone(); + + (id, r) + }))) } async fn publish_events<'a, SchedulingsIter>( @@ -146,8 +139,7 @@ where SchedulingsIter: Iterator, { let calendar_url = api_client.caldav_base_url().join(&format!( - "calendars/{}/{}/", - &api_client.username(), + "{}/", calendar.to_lowercase().as_str().replace(" ", "-") ))?; diff --git a/src/recipe.rs b/src/recipe.rs index ba7a504..6197233 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -12,7 +12,7 @@ pub struct Metadata { #[serde(rename = "recipe_id")] pub id: u32, pub name: String, - pub keywords: String, + pub keywords: Option, pub date_created: DateTime, pub date_modified: DateTime, } @@ -24,8 +24,8 @@ pub struct Recipe { pub id: isize, pub name: String, pub description: String, - pub url: String, - pub keywords: String, + pub url: Option, + pub keywords: Option, #[serde(rename = "dateCreated")] pub created: DateTime, diff --git a/src/scheduling.rs b/src/scheduling.rs index 4510d64..7b820b2 100644 --- a/src/scheduling.rs +++ b/src/scheduling.rs @@ -49,16 +49,20 @@ impl From for Event { use icalendar::Component; let start_time = ev.ends_at - ev.recipe.total_time(); - let cal_event = Event::new() + let mut cal_event = Event::new(); + + cal_event .uid(&ev.uid) .summary(&ev.recipe.name) .description(&format!("cookbook@{}", ev.recipe.id)) - .location(&ev.recipe.url) .timestamp(Utc::now()) .starts(start_time) - .ends(ev.ends_at) - .done(); + .ends(ev.ends_at); - cal_event + if let Some(ref location) = ev.recipe.url.clone() { + cal_event.location(&location); + } + + cal_event.done() } }