Cover use case of manually inserted recipes; allow recipe names instead of URLs in CSV

This commit is contained in:
Matteo Settenvini 2022-08-05 15:53:57 +02:00
parent fe363c0e26
commit 7495682e41
Signed by: matteo
GPG Key ID: 8576CC1AD97D42DF
7 changed files with 96 additions and 103 deletions

View File

@ -128,7 +128,7 @@
"2022-05-07","sabato",,"https://ricette.giallozafferano.it/Moussaka.html" "2022-05-07","sabato",,"https://ricette.giallozafferano.it/Moussaka.html"
"2022-05-08","domenica",, "2022-05-08","domenica",,
"2022-05-09","lunedì",, "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-11","mercoledì",,"https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html"
"2022-05-12","giovedì",, "2022-05-12","giovedì",,
"2022-05-13","venerdì",,"https://ricette.giallozafferano.it/Spiedini-di-pollo.html" "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-11","lunedì",,"https://ricette.giallozafferano.it/Insalata-con-uova-strapazzate.html"
"2022-07-12","martedì",, "2022-07-12","martedì",,
"2022-07-13","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" "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-15","venerdì",,"https://ricette.giallozafferano.it/Salmorejo.html"
"2022-07-16","sabato",, "2022-07-16","sabato",,
"2022-07-17","domenica",,"https://ricette.giallozafferano.it/Insalata-di-quinoa-alla-greca.html" "2022-07-17","domenica",,"https://ricette.giallozafferano.it/Insalata-di-quinoa-alla-greca.html"
@ -213,17 +213,17 @@
"2022-07-31","domenica",, "2022-07-31","domenica",,
"2022-08-01","lunedì",,"https://ricette.giallozafferano.it/Riso-freddo-con-tonno-zucchine-e-limone.html" "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-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-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-05","venerdì",,
"2022-08-06","sabato","https://ricette.giallozafferano.it/Spaghetti-di-riso-con-carne-e-verdure.html", "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",, "2022-08-07","domenica",,"https://ricette.giallozafferano.it/Insalata-di-riso-vegetariana.html"
"2022-08-08","lunedì",, "2022-08-08","lunedì",,"https://ricette.giallozafferano.it/Torta-salata-di-melanzane.html"
"2022-08-09","martedì",,"https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.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-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-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-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-14","domenica",,
"2022-08-15","lunedì",,"https://ricette.giallozafferano.it/Pasta-con-le-melanzane.html" "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" "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-10","sabato","https://ricette.giallozafferano.it/Maccheroncini-al-fume.html",
"2022-09-11","domenica","https://ricette.giallozafferano.it/Polpo-alla-Luciana.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-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-14","mercoledì",,"https://ricette.giallozafferano.it/Scaloppine-ai-funghi.html"
"2022-09-15","giovedì",, "2022-09-15","giovedì",,
"2022-09-16","venerdì",,"https://ricette.giallozafferano.it/Cordon-bleu-di-melanzane.html" "2022-09-16","venerdì",,"https://ricette.giallozafferano.it/Cordon-bleu-di-melanzane.html"

1 day wday lunch dinner
128 2022-05-07 sabato https://ricette.giallozafferano.it/Moussaka.html
129 2022-05-08 domenica
130 2022-05-09 lunedì
131 2022-05-10 martedì https://ricette.giallozafferano.it/Torta-salata-di-melanzane.html
132 2022-05-11 mercoledì https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html
133 2022-05-12 giovedì
134 2022-05-13 venerdì https://ricette.giallozafferano.it/Spiedini-di-pollo.html
193 2022-07-11 lunedì https://ricette.giallozafferano.it/Insalata-con-uova-strapazzate.html
194 2022-07-12 martedì
195 2022-07-13 mercoledì https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html
196 2022-07-14 giovedì Tomatenpita
197 2022-07-15 venerdì https://ricette.giallozafferano.it/Salmorejo.html
198 2022-07-16 sabato
199 2022-07-17 domenica https://ricette.giallozafferano.it/Insalata-di-quinoa-alla-greca.html
213 2022-07-31 domenica
214 2022-08-01 lunedì https://ricette.giallozafferano.it/Riso-freddo-con-tonno-zucchine-e-limone.html
215 2022-08-02 martedì https://ricette.giallozafferano.it/Insalata-Shirazi.html
216 2022-08-03 mercoledì https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html
217 2022-08-04 giovedì https://ricette.giallozafferano.it/Petto-di-pollo-ai-peperoni.html
218 2022-08-05 venerdì https://ricette.giallozafferano.it/Pasta-con-pomodorini-e-stracchino.html
219 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
220 2022-08-07 domenica https://ricette.giallozafferano.it/Insalata-di-riso-vegetariana.html
221 2022-08-08 lunedì https://ricette.giallozafferano.it/Torta-salata-di-melanzane.html
222 2022-08-09 martedì https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html
223 2022-08-10 mercoledì https://ricette.giallozafferano.it/Insalata-con-uova-strapazzate.html
224 2022-08-11 giovedì https://ricette.giallozafferano.it/Verdure-gratinate-al-forno.html
225 2022-08-12 venerdì https://ricette.giallozafferano.it/Garganelli-con-pesto-di-zucchine-e-gamberetti.html
226 2022-08-13 sabato https://ricette.giallozafferano.it/Insalata-di-bulgur-vegana.html Tomatenpita
227 2022-08-14 domenica
228 2022-08-15 lunedì https://ricette.giallozafferano.it/Pasta-con-le-melanzane.html
229 2022-08-16 martedì https://ricette.giallozafferano.it/Tempeh-alle-verdure.html
254 2022-09-10 sabato https://ricette.giallozafferano.it/Maccheroncini-al-fume.html
255 2022-09-11 domenica https://ricette.giallozafferano.it/Polpo-alla-Luciana.html
256 2022-09-12 lunedì https://ricette.giallozafferano.it/Pennette-con-speck-e-zucchine.html
257 2022-09-13 martedì https://ricette.giallozafferano.it/Torta-salata-di-melanzane.html
258 2022-09-14 mercoledì https://ricette.giallozafferano.it/Scaloppine-ai-funghi.html
259 2022-09-15 giovedì
260 2022-09-16 venerdì https://ricette.giallozafferano.it/Cordon-bleu-di-melanzane.html

View File

@ -9,10 +9,10 @@ use {
pub struct ApiClient { pub struct ApiClient {
rest: reqwest::Client, rest: reqwest::Client,
pub(crate) rest_semaphore: Arc<Semaphore>, // TODO: wrap in dereferentiable struct rest_semaphore: Arc<Semaphore>,
base_url: Url, base_url: Url,
caldav_base_url: Url, caldav_base_url: Url,
username: String, webdav_base_url: Url,
} }
impl ApiClient { impl ApiClient {
@ -30,6 +30,8 @@ impl ApiClient {
) )
})?; })?;
let username = server.login_name.clone();
use reqwest::header; use reqwest::header;
let mut default_headers = header::HeaderMap::new(); let mut default_headers = header::HeaderMap::new();
let mut auth_header = b"Basic ".to_vec(); let mut auth_header = b"Basic ".to_vec();
@ -53,12 +55,14 @@ impl ApiClient {
.send(), .send(),
)? )?
.url() .url()
.clone(); .join(&format!("calendars/{}/", &username))?;
let webdav_base_url = base_url.join(&format!("remote.php/dav/files/{}/", username))?;
Ok(ApiClient { Ok(ApiClient {
base_url, base_url,
caldav_base_url, caldav_base_url,
username: server.login_name.clone(), webdav_base_url,
rest: rest_client, rest: rest_client,
rest_semaphore: Arc::new(Semaphore::new(5)), rest_semaphore: Arc::new(Semaphore::new(5)),
}) })
@ -94,10 +98,6 @@ impl ApiClient {
&self.caldav_base_url &self.caldav_base_url
} }
pub fn username(&self) -> &str {
&self.username
}
pub async fn get_events<Tz>( pub async fn get_events<Tz>(
&self, &self,
calendar_name: &str, calendar_name: &str,
@ -113,10 +113,8 @@ impl ApiClient {
let response = client let response = client
.request( .request(
report_method.clone(), report_method.clone(),
// TODO extract into helper method
self.caldav_base_url.join(&format!( self.caldav_base_url.join(&format!(
"calendars/{}/{}", "{}/",
self.username(),
calendar_name.to_lowercase().replace(" ", "-") calendar_name.to_lowercase().replace(" ", "-")
))?, ))?,
) )
@ -195,4 +193,8 @@ impl ApiClient {
Ok(events.collect::<Vec<_>>()) Ok(events.collect::<Vec<_>>())
} }
pub fn webdav_base_url(&self) -> &Url {
&self.webdav_base_url
}
} }

View File

@ -8,7 +8,7 @@ use {
chrono::{Duration, Local}, chrono::{Duration, Local},
icalendar::Component, icalendar::Component,
regex::Regex, regex::Regex,
reqwest::{Method, StatusCode}, reqwest::{Method, StatusCode, Url},
std::collections::HashSet, std::collections::HashSet,
std::ops::Range, std::ops::Range,
}; };
@ -109,43 +109,16 @@ fn prepare_grocery_list(ingredients: &Vec<Ingredient>) -> Result<String> {
} }
async fn save_grocery_list(api_client: &ApiClient, filename: &str, contents: &str) -> 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::<Vec<_>>(); let filename_components = filename.split('/').collect::<Vec<_>>();
filename_components filename_components
.iter() .iter()
.take(filename_components.len() - 1) .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()) url.map(|u| u.join(&format!("{dir}/")).unwrap())
.and_then(|url| { .and_then(|url| ensure_collection_exist(api_client, 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)),
}
})
})
})?; })?;
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); log::info!("Saving grocery list to {}", &file_url);
let response = api_client let response = api_client
.rest(|client| async { .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<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)),
}
})
}

View File

@ -10,10 +10,8 @@ pub async fn with(api_client: &ApiClient, calendar_name: &str) -> Result<()> {
let response = client let response = client
.request( .request(
report_method.clone(), report_method.clone(),
// TODO extract into helper method
api_client.caldav_base_url().join(&format!( api_client.caldav_base_url().join(&format!(
"calendars/{}/{}/", "{}/",
api_client.username(),
calendar_name.to_lowercase().replace(" ", "-") calendar_name.to_lowercase().replace(" ", "-")
))?, ))?,
) )

View File

@ -11,7 +11,7 @@ use {
chrono::naive::NaiveDate, chrono::naive::NaiveDate,
futures::future::try_join_all, futures::future::try_join_all,
icalendar::Event, icalendar::Event,
reqwest::StatusCode, reqwest::{StatusCode, Url},
std::collections::{HashMap, HashSet}, std::collections::{HashMap, HashSet},
std::fmt::Write, std::fmt::Write,
std::iter::Iterator, std::iter::Iterator,
@ -61,17 +61,17 @@ pub async fn with(
Ok(()) Ok(())
} }
fn urls_from_csv<'a, RecordsIter>(records: RecordsIter) -> Result<HashSet<String>> fn urls_from_csv<'a, RecordsIter>(records: RecordsIter) -> Result<HashSet<Url>>
where where
RecordsIter: Iterator<Item = &'a CsvRecord>, RecordsIter: Iterator<Item = &'a CsvRecord>,
{ {
Ok( Ok(
records.fold(std::collections::HashSet::new(), |mut set, r| { records.fold(std::collections::HashSet::new(), |mut set, r| {
if !r.lunch.is_empty() { if let Ok(lunch) = Url::try_from(r.lunch.as_str()) {
set.insert(r.lunch.clone()); set.insert(lunch);
} }
if !r.dinner.is_empty() { if let Ok(dinner) = Url::try_from(r.dinner.as_str()) {
set.insert(r.dinner.clone()); set.insert(dinner);
} }
set set
}), }),
@ -93,13 +93,9 @@ async fn get_all_recipes(api_client: &ApiClient) -> Result<HashMap<String, Rc<re
.await?; .await?;
let recipes = metadata.iter().map(|rm| async { let recipes = metadata.iter().map(|rm| async {
// TODO: wrap this, heavily refactor this mess let response = api_client
let _ = api_client.rest_semaphore.acquire().await.unwrap();
let mut retries = 0;
let response = loop {
let result = api_client
.rest(|client| async { .rest(|client| async {
let response = client let r = client
.get( .get(
api_client api_client
.base_url() .base_url()
@ -108,20 +104,9 @@ async fn get_all_recipes(api_client: &ApiClient) -> Result<HashMap<String, Rc<re
) )
.send() .send()
.await; .await;
Ok(response?) Ok(r?)
}) })
.await; .await?;
match result {
Ok(response) => 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),
}
};
response response
.json::<recipe::Recipe>() .json::<recipe::Recipe>()
@ -131,9 +116,17 @@ async fn get_all_recipes(api_client: &ApiClient) -> Result<HashMap<String, Rc<re
}); });
let recipes = try_join_all(recipes).await?; let recipes = try_join_all(recipes).await?;
Ok(HashMap::from_iter(
recipes.into_iter().map(|r| (r.url.clone(), r)), Ok(HashMap::from_iter(recipes.into_iter().map(|r| {
)) // Fallback to recipe name as ID if URL is empty
let id = match &r.url {
Some(u) if !u.is_empty() => u,
_ => &r.name,
}
.clone();
(id, r)
})))
} }
async fn publish_events<'a, SchedulingsIter>( async fn publish_events<'a, SchedulingsIter>(
@ -146,8 +139,7 @@ where
SchedulingsIter: Iterator<Item = Scheduling>, SchedulingsIter: Iterator<Item = Scheduling>,
{ {
let calendar_url = api_client.caldav_base_url().join(&format!( let calendar_url = api_client.caldav_base_url().join(&format!(
"calendars/{}/{}/", "{}/",
&api_client.username(),
calendar.to_lowercase().as_str().replace(" ", "-") calendar.to_lowercase().as_str().replace(" ", "-")
))?; ))?;

View File

@ -12,7 +12,7 @@ pub struct Metadata {
#[serde(rename = "recipe_id")] #[serde(rename = "recipe_id")]
pub id: u32, pub id: u32,
pub name: String, pub name: String,
pub keywords: String, pub keywords: Option<String>,
pub date_created: DateTime, pub date_created: DateTime,
pub date_modified: DateTime, pub date_modified: DateTime,
} }
@ -24,8 +24,8 @@ pub struct Recipe {
pub id: isize, pub id: isize,
pub name: String, pub name: String,
pub description: String, pub description: String,
pub url: String, pub url: Option<String>,
pub keywords: String, pub keywords: Option<String>,
#[serde(rename = "dateCreated")] #[serde(rename = "dateCreated")]
pub created: DateTime, pub created: DateTime,

View File

@ -49,16 +49,20 @@ impl From<Scheduling> for Event {
use icalendar::Component; use icalendar::Component;
let start_time = ev.ends_at - ev.recipe.total_time(); 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) .uid(&ev.uid)
.summary(&ev.recipe.name) .summary(&ev.recipe.name)
.description(&format!("cookbook@{}", ev.recipe.id)) .description(&format!("cookbook@{}", ev.recipe.id))
.location(&ev.recipe.url)
.timestamp(Utc::now()) .timestamp(Utc::now())
.starts(start_time) .starts(start_time)
.ends(ev.ends_at) .ends(ev.ends_at);
.done();
cal_event if let Some(ref location) = ev.recipe.url.clone() {
cal_event.location(&location);
}
cal_event.done()
} }
} }