Merge branch 'grocery-list' into 'master'

Collect relevant ingredients for grocery list

Closes #3

See merge request nextcloud/cooking-schedule!2
This commit is contained in:
Matteo Settenvini 2022-08-02 13:54:18 +00:00
commit e3d99c89e4
19 changed files with 1198 additions and 627 deletions

View File

@ -3,6 +3,7 @@
stages:
- build
- check
- test
- deploy
@ -12,25 +13,6 @@ stages:
- job: docker:build
optional: true
.cargo_test_template:
extends: .with_rust_image
stage: test
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: cobertura.xml
junit: junit.xml
variables:
RUST_BACKTRACE: 1
CARGO_COMMON_ARGS: --workspace --no-default-features
script:
- mkdir -p .git/hooks # for cargo-husky
- cargo tarpaulin ${CARGO_COMMON_ARGS} --locked -o Xml
- cargo test ${CARGO_COMMON_ARGS} -- -Z unstable-options --format json | tee test-results.json
- cargo2junit < test-results.json > junit.xml
# - cargo bench ${CARGO_COMMON_ARGS} # DISABLED UNTIL WE HAVE BENCH TESTS
docker:build:
stage: build
variables:
@ -49,8 +31,34 @@ docker:build:
- Dockerfile
allow_failure: true
cargo:check:
extends: .with_rust_image
stage: check
variables:
CARGO_COMMON_ARGS: --workspace --no-default-features
script:
- reuse lint
- cargo fmt -- --check
- cargo deny check
cargo:test:
extends: .cargo_test_template
extends: .with_rust_image
stage: test
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: cobertura.xml
junit: junit.xml
variables:
RUST_BACKTRACE: 1
CARGO_COMMON_ARGS: --workspace --no-default-features
script:
- mkdir -p .git/hooks # for rusty-hook
- cargo tarpaulin ${CARGO_COMMON_ARGS} --locked -o Xml
- cargo test ${CARGO_COMMON_ARGS} -- -Z unstable-options --format json | tee test-results.json
- cargo2junit < test-results.json > junit.xml
# - cargo bench ${CARGO_COMMON_ARGS} # DISABLED UNTIL WE HAVE BENCH TESTS
cargo:doc: # pages: in the future
extends: .with_rust_image

View File

@ -2,5 +2,5 @@
# SPDX-License-Identifier: CC0-1.0
[hooks]
pre-commit = "reuse lint && cargo fmt -- --check && cargo test"
pre-commit = "reuse lint && cargo deny check && cargo fmt -- --check && cargo test"
pre-push = "cargo check"

50
.vscode/launch.json vendored
View File

@ -6,7 +6,28 @@
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'cook'",
"name": "Debug executable 'cook' -> groceries",
"cargo": {
"args": [
"build",
"--bin=cook",
"--package=cooking-schedule"
],
"filter": {
"name": "cook",
"kind": "bin"
}
},
"args": ["groceries", "Cucina"],
"env": {
"RUST_LOG": "debug",
},
"cwd": "${workspaceFolder}",
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'cook' -> schedule-csv",
"cargo": {
"args": [
"build",
@ -19,6 +40,30 @@
}
},
"args": ["schedule-csv", "Cucina", "examples/example-schedule.csv"],
"env": {
"RUST_LOG": "debug",
},
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'cook' -> purge",
"cargo": {
"args": [
"build",
"--bin=cook",
"--package=cooking-schedule"
],
"filter": {
"name": "cook",
"kind": "bin"
}
},
"args": ["purge", "Cucina"],
"env": {
"RUST_LOG": "debug",
},
"cwd": "${workspaceFolder}"
},
{
@ -38,6 +83,9 @@
}
},
"args": [],
"env": {
"RUST_LOG": "debug",
},
"cwd": "${workspaceFolder}"
}
]

369
Cargo.lock generated
View File

@ -3,18 +3,12 @@
version = 3
[[package]]
name = "adler"
version = "1.0.2"
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "android_system_properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"libc",
"memchr",
]
[[package]]
@ -72,9 +66,9 @@ checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
[[package]]
name = "bytes"
version = "1.1.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]]
name = "cc"
@ -108,12 +102,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "chunked_transfer"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "ci_info"
version = "0.10.2"
@ -125,9 +113,9 @@ dependencies = [
[[package]]
name = "clap"
version = "3.2.8"
version = "3.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83"
checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9"
dependencies = [
"atty",
"bitflags",
@ -168,10 +156,12 @@ dependencies = [
"clap",
"csv",
"directories",
"env_logger",
"futures",
"iana-time-zone",
"ics",
"minicaldav",
"icalendar",
"libxml",
"log",
"regex",
"reqwest",
"rusty-hook",
"serde",
@ -180,7 +170,6 @@ dependencies = [
"strum_macros",
"tokio",
"toml",
"ureq",
"webbrowser",
]
@ -200,15 +189,6 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "csv"
version = "1.1.6"
@ -295,6 +275,19 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "env_logger"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]]
name = "envmnt"
version = "0.8.4"
@ -307,23 +300,13 @@ dependencies = [
[[package]]
name = "fastrand"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
dependencies = [
"instant",
]
[[package]]
name = "flate2"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -491,9 +474,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.12.1"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heck"
@ -545,10 +528,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "hyper"
version = "0.14.19"
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
dependencies = [
"bytes",
"futures-channel",
@ -582,24 +571,16 @@ dependencies = [
]
[[package]]
name = "iana-time-zone"
version = "0.1.40"
name = "icalendar"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00c0d80ad9ca8d30ca648bf6cb1e3e3326d75071b76dbe143dd4a9cedcd58975"
checksum = "bf4c8ac7e9b7eefe850f6380c68ef5fd1300858c1ef65d121d602a9bab63c2f4"
dependencies = [
"android_system_properties",
"core-foundation",
"js-sys",
"wasm-bindgen",
"winapi",
"chrono",
"nom",
"uuid",
]
[[package]]
name = "ics"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b891481ef6353e3b97118d4650469e379a39e4373a66908c12f99763182826b1"
[[package]]
name = "ident_case"
version = "1.0.1"
@ -676,9 +657,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "js-sys"
version = "0.3.58"
version = "0.3.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27"
checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2"
dependencies = [
"wasm-bindgen",
]
@ -695,6 +676,17 @@ version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "libxml"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "687f5a78939052c5d02865c0fe3ea2ce2acdca875f7f81db82f7aef256dd97ac"
dependencies = [
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "log"
version = "0.4.17"
@ -723,26 +715,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "minicaldav"
version = "0.2.0"
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb263a7d12c40d5f200dda93b3665b9ae714d4fe64a6467938c92d974a579edb"
dependencies = [
"base64",
"log",
"ureq",
"url",
"xmltree",
]
[[package]]
name = "miniz_oxide"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
dependencies = [
"adler",
]
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "mio"
@ -836,6 +812,16 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab250442c86f1850815b5d268639dff018c0627022bc1940eb2d642ca1ce12f0"
[[package]]
name = "nom"
version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "num-integer"
version = "0.1.45"
@ -888,15 +874,15 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.12.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
[[package]]
name = "openssl"
version = "0.10.40"
version = "0.10.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0"
dependencies = [
"bitflags",
"cfg-if",
@ -926,9 +912,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.74"
version = "0.9.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1"
checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f"
dependencies = [
"autocfg",
"cc",
@ -939,9 +925,9 @@ dependencies = [
[[package]]
name = "os_str_bytes"
version = "6.1.0"
version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4"
[[package]]
name = "percent-encoding"
@ -979,9 +965,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.40"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b"
dependencies = [
"unicode-ident",
]
@ -997,9 +983,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.2.13"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
@ -1015,12 +1001,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"
@ -1067,33 +1070,6 @@ dependencies = [
"winreg",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi",
]
[[package]]
name = "rustls"
version = "0.20.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
dependencies = [
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "rustversion"
version = "1.0.8"
@ -1137,16 +1113,6 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "security-framework"
version = "2.6.1"
@ -1172,18 +1138,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.137"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.137"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da"
dependencies = [
"proc-macro2",
"quote",
@ -1215,9 +1181,12 @@ dependencies = [
[[package]]
name = "slab"
version = "0.4.6"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
dependencies = [
"autocfg",
]
[[package]]
name = "socket2"
@ -1239,12 +1208,6 @@ dependencies = [
"strum_macros",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "strsim"
version = "0.10.0"
@ -1361,10 +1324,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.19.2"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581"
dependencies = [
"autocfg",
"bytes",
"libc",
"memchr",
@ -1429,9 +1393,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]]
name = "tracing"
version = "0.1.35"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160"
checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307"
dependencies = [
"cfg-if",
"pin-project-lite",
@ -1440,9 +1404,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.28"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7"
checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7"
dependencies = [
"once_cell",
]
@ -1461,9 +1425,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
[[package]]
name = "unicode-normalization"
@ -1480,30 +1444,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f"
dependencies = [
"base64",
"chunked_transfer",
"encoding_rs",
"flate2",
"log",
"once_cell",
"rustls",
"url",
"webpki",
"webpki-roots",
]
[[package]]
name = "url"
version = "2.2.2"
@ -1516,6 +1456,15 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
@ -1557,9 +1506,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.81"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994"
checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@ -1567,13 +1516,13 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.81"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a"
checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
@ -1582,9 +1531,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.31"
version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f"
checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad"
dependencies = [
"cfg-if",
"js-sys",
@ -1594,9 +1543,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.81"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa"
checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -1604,9 +1553,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.81"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048"
checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
dependencies = [
"proc-macro2",
"quote",
@ -1617,15 +1566,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.81"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be"
checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a"
[[package]]
name = "web-sys"
version = "0.3.58"
version = "0.3.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90"
checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1"
dependencies = [
"js-sys",
"wasm-bindgen",
@ -1645,25 +1594,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf"
dependencies = [
"webpki",
]
[[package]]
name = "widestring"
version = "0.5.1"
@ -1752,18 +1682,3 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "xml-rs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "xmltree"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb"
dependencies = [
"xml-rs",
]

View File

@ -24,10 +24,8 @@ version = "0.13"
[dependencies.chrono]
version = "0.4"
features = ["serde"]
[dependencies.iana-time-zone]
version = "0.1"
default-features = false
features = ["alloc", "serde"]
[dependencies.csv]
version = "1.1"
@ -39,14 +37,24 @@ features = ["cargo"]
[dependencies.directories]
version = "4.0"
[dependencies.env_logger]
version = "0.9"
[dependencies.futures]
version = "0.3"
[dependencies.ics]
version = "0.5"
[dependencies.icalendar]
version = "0.13.0"
features = ["parser"]
[dependencies.minicaldav]
version = "0.2"
[dependencies.libxml]
version = "0.3"
[dependencies.log]
version = "0.4"
[dependencies.regex]
version = "1.6"
[dependencies.reqwest]
version = "0.11"
@ -72,8 +80,5 @@ version = "0.5"
version = "1"
features = ["rt-multi-thread", "net", "macros"]
[dependencies.ureq]
version = "2.5"
[dependencies.webbrowser]
version = "0.7"

View File

@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2021 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
# SPDX-License-Identifier: CC0-1.0
FROM docker.io/rust:latest@sha256:490d4448c29f5407ee6eaf62f081a82b2741b4416c8e20aef7c01b68776802c2
FROM docker.io/rust:latest@sha256:4f1d43c216d995c2f734d7c682bc3e2abe3e110961cda4ae0743ce3944e673a2
ENV DEBIAN_FRONTEND noninteractive
@ -9,11 +9,17 @@ RUN apt update && \
apt install -y \
libssl-dev \
pkg-config \
python3-pip \
&& \
rm -rf /var/lib/apt/lists/*
RUN cargo install -f -- \
RUN pip3 install reuse
RUN rustup component add rustfmt
RUN cargo install --locked -f -- \
cargo-tarpaulin \
cargo-deny \
cargo2junit \
&& \
rm -rf "${HOME}/.cargo/registry" "${HOME}/.cargo/git"

214
deny.toml Normal file
View File

@ -0,0 +1,214 @@
# SPDX-FileCopyrightText: 2021 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
# SPDX-License-Identifier: CC0-1.0
# This template contains all of the possible sections and their default values
# Note that all fields that take a lint level have these possible values:
# * deny - An error will be produced and the check will fail
# * warn - A warning will be produced, but the check will not fail
# * allow - No warning or error will be produced, though in some cases a note
# will be
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
# dependency, such as, for example, the `nix` crate only being used via the
# `target_family = "unix"` configuration, that only having windows targets in
# this list would mean the nix crate, as well as any of its exclusive
# dependencies not shared by any other crates, would be ignored, as the target
# list here is effectively saying which targets you are building for.
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#{ triple = "x86_64-unknown-linux-musl" },
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory database is cloned/fetched into
db-path = "~/.cargo/advisory-db"
# The url(s) of the advisory databases to use
db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for security vulnerabilities
vulnerability = "deny"
# The lint level for unmaintained crates
unmaintained = "warn"
# The lint level for crates that have been yanked from their source registry
yanked = "warn"
# The lint level for crates with security notices.
notice = "warn"
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
"RUSTSEC-2020-0071",
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
# will still output a note when they are encountered.
# * None - CVSS Score 0.0
# * Low - CVSS Score 0.1 - 3.9
# * Medium - CVSS Score 4.0 - 6.9
# * High - CVSS Score 7.0 - 8.9
# * Critical - CVSS Score 9.0 - 10.0
#severity-threshold =
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "deny"
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
#"MIT",
#"Apache-2.0",
#"Apache-2.0 WITH LLVM-exception",
]
# List of explicitly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
deny = [
#"Nokia",
]
# Lint level for licenses considered copyleft
copyleft = "allow"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
# * both - The license will be approved if it is both OSI-approved *AND* FSF
# * either - The license will be approved if it is either OSI-approved *OR* FSF
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
# * neither - This predicate is ignored and the default lint level is used
allow-osi-fsf-free = "both"
# Lint level used when no other predicates are matched
# 1. License isn't in the allow or deny lists
# 2. License isn't copyleft
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
default = "deny"
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
# [possible values: any between 0.0 and 1.0].
confidence-threshold = 0.8
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
# aren't accepted for every possible crate as with the normal allow list
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], name = "adler32", version = "*" },
{ allow = ["Unicode-DFS-2016"], name = "unicode-ident", version = "*" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
#[[licenses.clarify]]
# The name of the crate the clarification applies to
#name = "ring"
# The optional version constraint for the crate
#version = "*"
# The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
#license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
# not have its license(s) checked
registries = [
#"https://sekretz.com/registry
]
# This section is considered when running `cargo deny check bans`.
# More documentation about the 'bans' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
# with multiple versions
# * lowest-version - The path to the lowest versioned duplicate is highlighted
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# List of crates that are allowed. Use with care!
allow = [
#{ name = "ansi_term", version = "=0.11.0" },
]
# List of crates to deny
deny = [
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#{ name = "ansi_term", version = "=0.11.0" },
#
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
]
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#{ name = "ansi_term", version = "=0.11.0" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite
skip-tree = [
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
# Lint level for what to happen when a crate from a crate registry that is not
# in the allow list is encountered
unknown-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not
# in the allow list is encountered
unknown-git = "warn"
# List of URLs for allowed crate registries. Defaults to the crates.io index
# if not specified. If it is specified but empty, no registries are allowed.
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# List of URLs for allowed Git repositories
allow-git = []
[sources.allow-org]
# 1 or more github.com organizations to allow git sources for
#github = [""]
# 1 or more gitlab.com organizations to allow git sources for
#gitlab = [""]
# 1 or more bitbucket.org organizations to allow git sources for
#bitbucket = [""]

View File

@ -1,366 +1,366 @@
"day","wday","lunch","dinner"
2022-01-01,"sabato",,
2022-01-02,"domenica",,
2022-01-03,"lunedì",,
2022-01-04,"martedì",,
2022-01-05,"mercoledì",,
2022-01-06,"giovedì",,
2022-01-07,"venerdì",,
2022-01-08,"sabato",,
2022-01-09,"domenica",,
2022-01-10,"lunedì",,
2022-01-11,"martedì",,
2022-01-12,"mercoledì",,
2022-01-13,"giovedì",,
2022-01-14,"venerdì",,
2022-01-15,"sabato",,
2022-01-16,"domenica",,
2022-01-17,"lunedì",,
2022-01-18,"martedì",,
2022-01-19,"mercoledì",,
2022-01-20,"giovedì",,
2022-01-21,"venerdì",,
2022-01-22,"sabato",,
2022-01-23,"domenica",,
2022-01-24,"lunedì",,
2022-01-25,"martedì",,
2022-01-26,"mercoledì",,
2022-01-27,"giovedì",,
2022-01-28,"venerdì",,
2022-01-29,"sabato",,
2022-01-30,"domenica",,
2022-01-31,"lunedì",,
2022-02-01,"martedì",,
2022-02-02,"mercoledì",,
2022-02-03,"giovedì",,
2022-02-04,"venerdì",,
2022-02-05,"sabato",,
2022-02-06,"domenica",,
2022-02-07,"lunedì",,
2022-02-08,"martedì",,
2022-02-09,"mercoledì",,
2022-02-10,"giovedì",,
2022-02-11,"venerdì",,
2022-02-12,"sabato",,
2022-02-13,"domenica",,
2022-02-14,"lunedì",,
2022-02-15,"martedì",,
2022-02-16,"mercoledì",,
2022-02-17,"giovedì",,
2022-02-18,"venerdì",,
2022-02-19,"sabato",,
2022-02-20,"domenica",,
2022-02-21,"lunedì",,
2022-02-22,"martedì",,
2022-02-23,"mercoledì",,
2022-02-24,"giovedì",,
2022-02-25,"venerdì",,
2022-02-26,"sabato",,
2022-02-27,"domenica",,
2022-02-28,"lunedì",,
2022-03-01,"martedì",,
2022-03-02,"mercoledì",,
2022-03-03,"giovedì",,
2022-03-04,"venerdì",,
2022-03-05,"sabato",,
2022-03-06,"domenica",,
2022-03-07,"lunedì",,
2022-03-08,"martedì",,
2022-03-09,"mercoledì",,
2022-03-10,"giovedì",,
2022-03-11,"venerdì",,
2022-03-12,"sabato",,
2022-03-13,"domenica",,
2022-03-14,"lunedì",,
2022-03-15,"martedì",,
2022-03-16,"mercoledì",,
2022-03-17,"giovedì",,
2022-03-18,"venerdì",,
2022-03-19,"sabato",,
2022-03-20,"domenica",,
2022-03-21,"lunedì",,
2022-03-22,"martedì",,
2022-03-23,"mercoledì",,
2022-03-24,"giovedì",,
2022-03-25,"venerdì",,
2022-03-26,"sabato",,
2022-03-27,"domenica",,
2022-03-28,"lunedì",,
2022-03-29,"martedì",,
2022-03-30,"mercoledì",,
2022-03-31,"giovedì",,
2022-04-01,"venerdì",,
2022-04-02,"sabato",,
2022-04-03,"domenica",,
2022-04-04,"lunedì",,
2022-04-05,"martedì",,
2022-04-06,"mercoledì",,
2022-04-07,"giovedì",,
2022-04-08,"venerdì",,
2022-04-09,"sabato",,
2022-04-10,"domenica",,
2022-04-11,"lunedì",,
2022-04-12,"martedì",,
2022-04-13,"mercoledì",,
2022-04-14,"giovedì",,
2022-04-15,"venerdì",,
2022-04-16,"sabato",,
2022-04-17,"domenica",,
2022-04-18,"lunedì",,
2022-04-19,"martedì",,
2022-04-20,"mercoledì",,
2022-04-21,"giovedì",,
2022-04-22,"venerdì",,
2022-04-23,"sabato",,
2022-04-24,"domenica",,
2022-04-25,"lunedì",,
2022-04-26,"martedì",,
2022-04-27,"mercoledì",,
2022-04-28,"giovedì",,
2022-04-29,"venerdì",,
2022-04-30,"sabato",,
2022-05-01,"domenica",,
2022-05-02,"lunedì",,
2022-05-03,"martedì",,
2022-05-04,"mercoledì",,
2022-05-05,"giovedì",,
2022-05-06,"venerdì",,
2022-05-07,"sabato",,
2022-05-08,"domenica",,
2022-05-09,"lunedì",,
2022-05-10,"martedì",,
2022-05-11,"mercoledì",,
2022-05-12,"giovedì",,
2022-05-13,"venerdì",,
2022-05-14,"sabato",,
2022-05-15,"domenica",,
2022-05-16,"lunedì",,
2022-05-17,"martedì",,
2022-05-18,"mercoledì",,
2022-05-19,"giovedì",,
2022-05-20,"venerdì",,
2022-05-21,"sabato",,
2022-05-22,"domenica",,
2022-05-23,"lunedì",,
2022-05-24,"martedì",,
2022-05-25,"mercoledì",,
2022-05-26,"giovedì",,
2022-05-27,"venerdì",,
2022-05-28,"sabato",,
2022-05-29,"domenica",,
2022-05-30,"lunedì",,
2022-05-31,"martedì",,
2022-06-01,"mercoledì",,
2022-06-02,"giovedì",,
2022-06-03,"venerdì",,
2022-06-04,"sabato",,
2022-06-05,"domenica",,
2022-06-06,"lunedì",,
2022-06-07,"martedì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html"
2022-06-08,"mercoledì",,
2022-06-09,"giovedì",,
2022-06-10,"venerdì",,
2022-06-11,"sabato",,
2022-06-12,"domenica",,
2022-06-13,"lunedì",,
2022-06-14,"martedì",,
2022-06-15,"mercoledì",,
2022-06-16,"giovedì",,
2022-06-17,"venerdì",,"https://ricette.giallozafferano.it/Salmorejo.html"
2022-06-18,"sabato",,
2022-06-19,"domenica",,
2022-06-20,"lunedì",,
2022-06-21,"martedì",,
2022-06-22,"mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html"
2022-06-23,"giovedì",,
2022-06-24,"venerdì",,
2022-06-25,"sabato",,
2022-06-26,"domenica",,
2022-06-27,"lunedì",,
2022-06-28,"martedì",,
2022-06-29,"mercoledì",,
2022-06-30,"giovedì",,
2022-07-01,"venerdì",,
2022-07-02,"sabato",,
2022-07-03,"domenica",,
2022-07-04,"lunedì",,
2022-07-05,"martedì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html"
2022-07-06,"mercoledì",,
2022-07-07,"giovedì",,
2022-07-08,"venerdì",,
2022-07-09,"sabato",,
2022-07-10,"domenica",,
2022-07-11,"lunedì",,
2022-07-12,"martedì",,
2022-07-13,"mercoledì",,
2022-07-14,"giovedì",,
2022-07-15,"venerdì",,"https://ricette.giallozafferano.it/Salmorejo.html"
2022-07-16,"sabato",,
2022-07-17,"domenica",,
2022-07-18,"lunedì",,
2022-07-19,"martedì",,
2022-07-20,"mercoledì",,
2022-07-21,"giovedì",,
2022-07-22,"venerdì",,
2022-07-23,"sabato",,
2022-07-24,"domenica",,
2022-07-25,"lunedì",,
2022-07-26,"martedì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html"
2022-07-27,"mercoledì",,
2022-07-28,"giovedì",,
2022-07-29,"venerdì",,
2022-07-30,"sabato",,
2022-07-31,"domenica",,
"2022-01-01","sabato",,
"2022-01-02","domenica",,
"2022-01-03","lunedì",,
"2022-01-04","martedì",,
"2022-01-05","mercoledì",,
"2022-01-06","giovedì",,
"2022-01-07","venerdì",,
"2022-01-08","sabato",,
"2022-01-09","domenica","https://ricette.giallozafferano.it/Pollo-alla-cacciatora.html","https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html"
"2022-01-10","lunedì",,
"2022-01-11","martedì",,
"2022-01-12","mercoledì",,
"2022-01-13","giovedì",,
"2022-01-14","venerdì",,"https://ricette.giallozafferano.it/Polpette-al-sugo.html"
"2022-01-15","sabato","https://ricette.giallozafferano.it/Orata-al-forno.html","https://ricette.giallozafferano.it/Risotto-radicchio-e-pancetta.html"
"2022-01-16","domenica",,
"2022-01-17","lunedì",,"https://ricette.giallozafferano.it/Spatzle-di-spinaci.html"
"2022-01-18","martedì",,
"2022-01-19","mercoledì",,
"2022-01-20","giovedì",,
"2022-01-21","venerdì",,
"2022-01-22","sabato","https://ricette.giallozafferano.it/Gnocchi-alla-sorrentina.html",
"2022-01-23","domenica",,
"2022-01-24","lunedì",,
"2022-01-25","martedì",,
"2022-01-26","mercoledì",,
"2022-01-27","giovedì",,
"2022-01-28","venerdì",,
"2022-01-29","sabato",,"https://ricette.giallozafferano.it/Costine-al-forno.html"
"2022-01-30","domenica",,
"2022-01-31","lunedì",,
"2022-02-01","martedì",,
"2022-02-02","mercoledì",,
"2022-02-03","giovedì",,
"2022-02-04","venerdì",,
"2022-02-05","sabato",,"https://ricette.giallozafferano.it/Chili-con-carne.html"
"2022-02-06","domenica",,
"2022-02-07","lunedì",,
"2022-02-08","martedì",,
"2022-02-09","mercoledì",,"https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html"
"2022-02-10","giovedì",,
"2022-02-11","venerdì",,
"2022-02-12","sabato",,
"2022-02-13","domenica",,"https://ricette.giallozafferano.it/Spatzle-di-spinaci.html"
"2022-02-14","lunedì",,
"2022-02-15","martedì",,
"2022-02-16","mercoledì",,"https://ricette.giallozafferano.it/Risotto-radicchio-e-pancetta.html"
"2022-02-17","giovedì",,
"2022-02-18","venerdì",,
"2022-02-19","sabato",,"https://ricette.giallozafferano.it/Polpette-al-sugo.html"
"2022-02-20","domenica",,
"2022-02-21","lunedì",,
"2022-02-22","martedì",,
"2022-02-23","mercoledì",,
"2022-02-24","giovedì",,"https://ricette.giallozafferano.it/Orata-al-forno.html"
"2022-02-25","venerdì",,"https://ricette.giallozafferano.it/Torta-rustica.html"
"2022-02-26","sabato","https://ricette.giallozafferano.it/Gnudi.html",
"2022-02-27","domenica",,
"2022-02-28","lunedì",,
"2022-03-01","martedì",,
"2022-03-02","mercoledì",,"https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html"
"2022-03-03","giovedì",,
"2022-03-04","venerdì",,
"2022-03-05","sabato",,
"2022-03-06","domenica","https://ricette.giallozafferano.it/Torta-Pasqualina.html",
"2022-03-07","lunedì",,
"2022-03-08","martedì",,"https://ricette.giallozafferano.it/Strozzapreti-cacio-cozze-e-pepe.html"
"2022-03-09","mercoledì",,"https://ricette.giallozafferano.it/Saltimbocca-alla-Romana.html"
"2022-03-10","giovedì",,
"2022-03-11","venerdì",,
"2022-03-12","sabato",,"https://ricette.giallozafferano.it/Costine-al-forno.html"
"2022-03-13","domenica",,
"2022-03-14","lunedì",,
"2022-03-15","martedì",,
"2022-03-16","mercoledì",,
"2022-03-17","giovedì",,"https://ricette.giallozafferano.it/Filetti-di-salmone-su-julienne-di-cavoletti-di-bruxelles-porri-e-carote.html"
"2022-03-18","venerdì",,"https://ricette.giallozafferano.it/Torta-rustica.html"
"2022-03-19","sabato",,
"2022-03-20","domenica",,
"2022-03-21","lunedì",,
"2022-03-22","martedì",,
"2022-03-23","mercoledì",,
"2022-03-24","giovedì",,
"2022-03-25","venerdì",,
"2022-03-26","sabato","https://ricette.giallozafferano.it/Orata-al-forno.html",
"2022-03-27","domenica",,
"2022-03-28","lunedì",,
"2022-03-29","martedì",,
"2022-03-30","mercoledì",,
"2022-03-31","giovedì",,
"2022-04-01","venerdì",,
"2022-04-02","sabato",,
"2022-04-03","domenica",,
"2022-04-04","lunedì",,
"2022-04-05","martedì",,
"2022-04-06","mercoledì",,
"2022-04-07","giovedì",,
"2022-04-08","venerdì",,
"2022-04-09","sabato",,
"2022-04-10","domenica",,
"2022-04-11","lunedì",,
"2022-04-12","martedì",,
"2022-04-13","mercoledì",,"https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html"
"2022-04-14","giovedì",,
"2022-04-15","venerdì",,
"2022-04-16","sabato",,"https://ricette.giallozafferano.it/Couscous-alla-marocchina.html"
"2022-04-17","domenica",,
"2022-04-18","lunedì",,
"2022-04-19","martedì",,
"2022-04-20","mercoledì",,
"2022-04-21","giovedì",,
"2022-04-22","venerdì",,"https://ricette.giallozafferano.it/Risotto-agli-asparagi-e-scampi.html"
"2022-04-23","sabato",,
"2022-04-24","domenica","https://ricette.giallozafferano.it/Torta-Pasqualina.html",
"2022-04-25","lunedì",,
"2022-04-26","martedì",,
"2022-04-27","mercoledì",,"https://ricette.giallozafferano.it/Orata-al-forno.html"
"2022-04-28","giovedì",,
"2022-04-29","venerdì",,"https://ricette.giallozafferano.it/Cordon-bleu-di-melanzane.html"
"2022-04-30","sabato",,
"2022-05-01","domenica",,
"2022-05-02","lunedì",,
"2022-05-03","martedì",,
"2022-05-04","mercoledì",,
"2022-05-05","giovedì",,
"2022-05-06","venerdì",,
"2022-05-07","sabato",,
"2022-05-08","domenica",,
"2022-05-09","lunedì",,
"2022-05-10","martedì",,
"2022-05-11","mercoledì",,"https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html"
"2022-05-12","giovedì",,
"2022-05-13","venerdì",,
"2022-05-14","sabato",,
"2022-05-15","domenica",,
"2022-05-16","lunedì",,
"2022-05-17","martedì",,
"2022-05-18","mercoledì",,"https://ricette.giallozafferano.it/Risotto-agli-asparagi-e-scampi.html"
"2022-05-19","giovedì",,
"2022-05-20","venerdì",,
"2022-05-21","sabato",,"https://ricette.giallozafferano.it/Couscous-alla-marocchina.html"
"2022-05-22","domenica","https://ricette.giallozafferano.it/Orata-al-forno.html",
"2022-05-23","lunedì",,
"2022-05-24","martedì",,
"2022-05-25","mercoledì",,
"2022-05-26","giovedì",,"https://ricette.giallozafferano.it/Saltimbocca-alla-Romana.html"
"2022-05-27","venerdì",,
"2022-05-28","sabato",,
"2022-05-29","domenica","https://ricette.giallozafferano.it/Polpo-alla-Luciana.html",
"2022-05-30","lunedì",,
"2022-05-31","martedì",,
"2022-06-01","mercoledì",,"https://ricette.giallozafferano.it/Riso-alla-cantonese.html"
"2022-06-02","giovedì",,
"2022-06-03","venerdì",,
"2022-06-04","sabato",,
"2022-06-05","domenica",,
"2022-06-06","lunedì",,
"2022-06-07","martedì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html"
"2022-06-08","mercoledì",,
"2022-06-09","giovedì",,
"2022-06-10","venerdì",,"https://ricette.giallozafferano.it/Verdure-gratinate-al-forno.html"
"2022-06-11","sabato",,
"2022-06-12","domenica",,"https://ricette.giallozafferano.it/Riso-alla-cantonese.html"
"2022-06-13","lunedì",,"https://ricette.giallozafferano.it/Petto-di-pollo-ai-peperoni.html"
"2022-06-14","martedì",,"https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html"
"2022-06-15","mercoledì",,"https://ricette.giallozafferano.it/Cocotte-di-quinoa.html"
"2022-06-16","giovedì",,
"2022-06-17","venerdì",,"https://ricette.giallozafferano.it/Salmorejo.html"
"2022-06-18","sabato","https://ricette.giallozafferano.it/Insalata-di-ceci-estiva.html",
"2022-06-19","domenica",,
"2022-06-20","lunedì",,
"2022-06-21","martedì",,
"2022-06-22","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html"
"2022-06-23","giovedì",,
"2022-06-24","venerdì",,
"2022-06-25","sabato","https://ricette.giallozafferano.it/Orata-al-forno.html",
"2022-06-26","domenica",,
"2022-06-27","lunedì",,
"2022-06-28","martedì",,
"2022-06-29","mercoledì",,
"2022-06-30","giovedì",,
"2022-07-01","venerdì",,
"2022-07-02","sabato",,
"2022-07-03","domenica",,
"2022-07-04","lunedì",,
"2022-07-05","martedì",,"https://ricette.giallozafferano.it/Insalata-di-ceci-estiva.html"
"2022-07-06","mercoledì",,"https://ricette.giallozafferano.it/Pasta-con-le-melanzane.html"
"2022-07-07","giovedì",,
"2022-07-08","venerdì",,
"2022-07-09","sabato",,
"2022-07-10","domenica",,
"2022-07-11","lunedì",,
"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-15","venerdì",,"https://ricette.giallozafferano.it/Salmorejo.html"
"2022-07-16","sabato",,
"2022-07-17","domenica",,
"2022-07-18","lunedì",,
"2022-07-19","martedì",,"https://ricette.giallozafferano.it/Verdure-gratinate-al-forno.html"
"2022-07-20","mercoledì",,
"2022-07-21","giovedì",,"https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html"
"2022-07-22","venerdì",,
"2022-07-23","sabato",,"https://ricette.giallozafferano.it/Cocotte-di-quinoa.html"
"2022-07-24","domenica",,
"2022-07-25","lunedì",,
"2022-07-26","martedì",,
"2022-07-27","mercoledì",,
"2022-07-28","giovedì",,
"2022-07-29","venerdì",,"https://ricette.giallozafferano.it/Orata-al-forno.html"
"2022-07-30","sabato",,
"2022-07-31","domenica",,
"2022-08-01","lunedì",,"https://ricette.giallozafferano.it/Riso-freddo-con-tonno-zucchine-e-limone.html"
"2022-08-02","martedì",,
"2022-08-03","mercoledì",,
"2022-08-04","giovedì",,
"2022-08-03","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html"
"2022-08-04","giovedì",,"https://ricette.giallozafferano.it/Petto-di-pollo-ai-peperoni.html"
"2022-08-05","venerdì",,
"2022-08-06","sabato",,
"2022-08-07","domenica",,
"2022-08-08","lunedì",,
"2022-08-09","martedì",,
"2022-08-09","martedì",,"https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html"
"2022-08-10","mercoledì",,
"2022-08-11","giovedì",,
"2022-08-11","giovedì",,"https://ricette.giallozafferano.it/Verdure-gratinate-al-forno.html"
"2022-08-12","venerdì",,
"2022-08-13","sabato",,
"2022-08-14","domenica",,
"2022-08-15","lunedì",,
"2022-08-15","lunedì",,"https://ricette.giallozafferano.it/Pasta-con-le-melanzane.html"
"2022-08-16","martedì",,
"2022-08-17","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html"
"2022-08-17","mercoledì",,"https://ricette.giallozafferano.it/Cocotte-di-quinoa.html"
"2022-08-18","giovedì",,
"2022-08-19","venerdì",,
"2022-08-20","sabato",,"https://ricette.giallozafferano.it/Salmorejo.html"
"2022-08-21","domenica",,
"2022-08-22","lunedì",,
"2022-08-23","martedì",,
"2022-08-24","mercoledì",,
"2022-08-24","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html"
"2022-08-25","giovedì",,
"2022-08-26","venerdì",,
"2022-08-27","sabato",,
"2022-08-27","sabato","https://ricette.giallozafferano.it/Orata-al-forno.html",
"2022-08-28","domenica",,
"2022-08-29","lunedì",,
"2022-08-30","martedì",,
"2022-08-31","mercoledì",,
"2022-09-01","giovedì",,
"2022-09-02","venerdì",,
"2022-09-02","venerdì",,"https://ricette.giallozafferano.it/Petto-di-pollo-ai-peperoni.html"
"2022-09-03","sabato",,
"2022-09-04","domenica",,
"2022-09-05","lunedì",,
"2022-09-06","martedì",,
"2022-09-07","mercoledì",,
"2022-09-08","giovedì",,
"2022-09-08","giovedì",,"https://ricette.giallozafferano.it/Pasta-con-le-melanzane.html"
"2022-09-09","venerdì",,
"2022-09-10","sabato",,
"2022-09-11","domenica",,
"2022-09-12","lunedì",,
"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-14","mercoledì",,
"2022-09-14","mercoledì",,"https://ricette.giallozafferano.it/Scaloppine-ai-funghi.html"
"2022-09-15","giovedì",,
"2022-09-16","venerdì",,
"2022-09-16","venerdì",,"https://ricette.giallozafferano.it/Cordon-bleu-di-melanzane.html"
"2022-09-17","sabato",,
"2022-09-18","domenica",,
"2022-09-19","lunedì",,
"2022-09-20","martedì",,
"2022-09-21","mercoledì",,
"2022-09-22","giovedì",,
"2022-09-21","mercoledì",,"https://ricette.giallozafferano.it/Strozzapreti-cacio-cozze-e-pepe.html"
"2022-09-22","giovedì",,"https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html"
"2022-09-23","venerdì",,
"2022-09-24","sabato",,
"2022-09-25","domenica",,
"2022-09-26","lunedì",,
"2022-09-24","sabato","https://ricette.giallozafferano.it/Gnudi.html",
"2022-09-25","domenica","https://ricette.giallozafferano.it/Pollo-alla-cacciatora.html",
"2022-09-26","lunedì",,"https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html"
"2022-09-27","martedì",,
"2022-09-28","mercoledì",,
"2022-09-28","mercoledì",,"https://ricette.giallozafferano.it/Orata-al-forno.html"
"2022-09-29","giovedì",,
"2022-09-30","venerdì",,
"2022-10-01","sabato",,
"2022-10-02","domenica",,
"2022-10-02","domenica","https://ricette.giallozafferano.it/Gnocchi-alla-sorrentina.html",
"2022-10-03","lunedì",,
"2022-10-04","martedì",,
"2022-10-04","martedì",,"https://ricette.giallozafferano.it/Spatzle-di-spinaci.html"
"2022-10-05","mercoledì",,
"2022-10-06","giovedì",,
"2022-10-07","venerdì",,
"2022-10-08","sabato",,
"2022-10-06","giovedì",,"https://ricette.giallozafferano.it/Saltimbocca-alla-Romana.html"
"2022-10-07","venerdì",,"https://ricette.giallozafferano.it/Risotto-ai-funghi.html"
"2022-10-08","sabato","https://ricette.giallozafferano.it/Torta-Pasqualina.html",
"2022-10-09","domenica",,
"2022-10-10","lunedì",,
"2022-10-10","lunedì",,"https://ricette.giallozafferano.it/Pennette-con-speck-e-zucchine.html"
"2022-10-11","martedì",,
"2022-10-12","mercoledì",,
"2022-10-13","giovedì",,
"2022-10-14","venerdì",,
"2022-10-14","venerdì",,"https://ricette.giallozafferano.it/Polpette-al-sugo.html"
"2022-10-15","sabato",,
"2022-10-16","domenica",,
"2022-10-17","lunedì",,
"2022-10-18","martedì",,
"2022-10-19","mercoledì",,
"2022-10-19","mercoledì",,"https://ricette.giallozafferano.it/Scaloppine-ai-funghi.html"
"2022-10-20","giovedì",,
"2022-10-21","venerdì",,
"2022-10-22","sabato",,
"2022-10-22","sabato","https://ricette.giallozafferano.it/Gnudi.html",
"2022-10-23","domenica",,
"2022-10-24","lunedì",,
"2022-10-25","martedì",,
"2022-10-26","mercoledì",,
"2022-10-27","giovedì",,
"2022-10-28","venerdì",,
"2022-10-29","sabato",,
"2022-10-28","venerdì",,"https://ricette.giallozafferano.it/Vellutata-di-zucca-e-carote.html"
"2022-10-29","sabato","https://ricette.giallozafferano.it/Orata-al-forno.html",
"2022-10-30","domenica",,
"2022-10-31","lunedì",,
"2022-11-01","martedì",,
"2022-11-02","mercoledì",,
"2022-11-03","giovedì",,
"2022-11-04","venerdì",,
"2022-11-05","sabato",,
"2022-11-05","sabato","https://ricette.giallozafferano.it/Spatzle-di-spinaci.html",
"2022-11-06","domenica",,
"2022-11-07","lunedì",,
"2022-11-08","martedì",,
"2022-11-09","mercoledì",,
"2022-11-10","giovedì",,
"2022-11-11","venerdì",,
"2022-11-12","sabato",,
"2022-11-11","venerdì",,"https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html"
"2022-11-12","sabato",,"https://ricette.giallozafferano.it/Chili-con-carne.html"
"2022-11-13","domenica",,
"2022-11-14","lunedì",,
"2022-11-15","martedì",,
"2022-11-16","mercoledì",,
"2022-11-17","giovedì",,
"2022-11-18","venerdì",,
"2022-11-18","venerdì",,"https://ricette.giallozafferano.it/Torta-rustica.html"
"2022-11-19","sabato",,
"2022-11-20","domenica",,
"2022-11-21","lunedì",,
"2022-11-22","martedì",,
"2022-11-23","mercoledì",,
"2022-11-24","giovedì",,
"2022-11-25","venerdì",,
"2022-11-26","sabato",,
"2022-11-25","venerdì",,"https://ricette.giallozafferano.it/Vellutata-di-zucca-e-carote.html"
"2022-11-26","sabato","https://ricette.giallozafferano.it/Orata-al-forno.html",
"2022-11-27","domenica",,
"2022-11-28","lunedì",,
"2022-11-29","martedì",,
"2022-11-29","martedì",,"https://ricette.giallozafferano.it/Scaloppine-ai-funghi.html"
"2022-11-30","mercoledì",,
"2022-12-01","giovedì",,
"2022-12-02","venerdì",,
"2022-12-03","sabato",,
"2022-12-04","domenica",,
"2022-12-05","lunedì",,
"2022-12-05","lunedì",,"https://ricette.giallozafferano.it/Spatzle-di-spinaci.html"
"2022-12-06","martedì",,
"2022-12-07","mercoledì",,
"2022-12-08","giovedì",,
"2022-12-09","venerdì",,
"2022-12-10","sabato",,
"2022-12-10","sabato","https://ricette.giallozafferano.it/Gnudi.html",
"2022-12-11","domenica",,
"2022-12-12","lunedì",,
"2022-12-13","martedì",,
"2022-12-14","mercoledì",,
"2022-12-15","giovedì",,
"2022-12-14","mercoledì",,"https://ricette.giallozafferano.it/Risotto-ai-funghi.html"
"2022-12-15","giovedì",,"https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html"
"2022-12-16","venerdì",,
"2022-12-17","sabato",,
"2022-12-18","domenica",,
"2022-12-18","domenica","https://ricette.giallozafferano.it/Polpette-al-sugo.html",
"2022-12-19","lunedì",,
"2022-12-20","martedì",,
"2022-12-21","mercoledì",,
"2022-12-22","giovedì",,
"2022-12-23","venerdì",,
"2022-12-24","sabato",,
"2022-12-25","domenica",,
"2022-12-25","domenica","https://ricette.giallozafferano.it/Filetto-in-crosta.html",
"2022-12-26","lunedì",,
"2022-12-27","martedì",,
"2022-12-28","mercoledì",,
"2022-12-28","mercoledì",,"https://ricette.giallozafferano.it/Orata-al-forno.html"
"2022-12-29","giovedì",,
"2022-12-30","venerdì",,
"2022-12-31","sabato",,
1 day wday lunch dinner
2 2022-01-01 sabato
3 2022-01-02 domenica
4 2022-01-03 lunedì
5 2022-01-04 martedì
6 2022-01-05 mercoledì
7 2022-01-06 giovedì
8 2022-01-07 venerdì
9 2022-01-08 sabato
10 2022-01-09 domenica https://ricette.giallozafferano.it/Pollo-alla-cacciatora.html https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html
11 2022-01-10 lunedì
12 2022-01-11 martedì
13 2022-01-12 mercoledì
14 2022-01-13 giovedì
15 2022-01-14 venerdì https://ricette.giallozafferano.it/Polpette-al-sugo.html
16 2022-01-15 sabato https://ricette.giallozafferano.it/Orata-al-forno.html https://ricette.giallozafferano.it/Risotto-radicchio-e-pancetta.html
17 2022-01-16 domenica
18 2022-01-17 lunedì https://ricette.giallozafferano.it/Spatzle-di-spinaci.html
19 2022-01-18 martedì
20 2022-01-19 mercoledì
21 2022-01-20 giovedì
22 2022-01-21 venerdì
23 2022-01-22 sabato https://ricette.giallozafferano.it/Gnocchi-alla-sorrentina.html
24 2022-01-23 domenica
25 2022-01-24 lunedì
26 2022-01-25 martedì
27 2022-01-26 mercoledì
28 2022-01-27 giovedì
29 2022-01-28 venerdì
30 2022-01-29 sabato https://ricette.giallozafferano.it/Costine-al-forno.html
31 2022-01-30 domenica
32 2022-01-31 lunedì
33 2022-02-01 martedì
34 2022-02-02 mercoledì
35 2022-02-03 giovedì
36 2022-02-04 venerdì
37 2022-02-05 sabato https://ricette.giallozafferano.it/Chili-con-carne.html
38 2022-02-06 domenica
39 2022-02-07 lunedì
40 2022-02-08 martedì
41 2022-02-09 mercoledì https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html
42 2022-02-10 giovedì
43 2022-02-11 venerdì
44 2022-02-12 sabato
45 2022-02-13 domenica https://ricette.giallozafferano.it/Spatzle-di-spinaci.html
46 2022-02-14 lunedì
47 2022-02-15 martedì
48 2022-02-16 mercoledì https://ricette.giallozafferano.it/Risotto-radicchio-e-pancetta.html
49 2022-02-17 giovedì
50 2022-02-18 venerdì
51 2022-02-19 sabato https://ricette.giallozafferano.it/Polpette-al-sugo.html
52 2022-02-20 domenica
53 2022-02-21 lunedì
54 2022-02-22 martedì
55 2022-02-23 mercoledì
56 2022-02-24 giovedì https://ricette.giallozafferano.it/Orata-al-forno.html
57 2022-02-25 venerdì https://ricette.giallozafferano.it/Torta-rustica.html
58 2022-02-26 sabato https://ricette.giallozafferano.it/Gnudi.html
59 2022-02-27 domenica
60 2022-02-28 lunedì
61 2022-03-01 martedì
62 2022-03-02 mercoledì https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html
63 2022-03-03 giovedì
64 2022-03-04 venerdì
65 2022-03-05 sabato
66 2022-03-06 domenica https://ricette.giallozafferano.it/Torta-Pasqualina.html
67 2022-03-07 lunedì
68 2022-03-08 martedì https://ricette.giallozafferano.it/Strozzapreti-cacio-cozze-e-pepe.html
69 2022-03-09 mercoledì https://ricette.giallozafferano.it/Saltimbocca-alla-Romana.html
70 2022-03-10 giovedì
71 2022-03-11 venerdì
72 2022-03-12 sabato https://ricette.giallozafferano.it/Costine-al-forno.html
73 2022-03-13 domenica
74 2022-03-14 lunedì
75 2022-03-15 martedì
76 2022-03-16 mercoledì
77 2022-03-17 giovedì https://ricette.giallozafferano.it/Filetti-di-salmone-su-julienne-di-cavoletti-di-bruxelles-porri-e-carote.html
78 2022-03-18 venerdì https://ricette.giallozafferano.it/Torta-rustica.html
79 2022-03-19 sabato
80 2022-03-20 domenica
81 2022-03-21 lunedì
82 2022-03-22 martedì
83 2022-03-23 mercoledì
84 2022-03-24 giovedì
85 2022-03-25 venerdì
86 2022-03-26 sabato https://ricette.giallozafferano.it/Orata-al-forno.html
87 2022-03-27 domenica
88 2022-03-28 lunedì
89 2022-03-29 martedì
90 2022-03-30 mercoledì
91 2022-03-31 giovedì
92 2022-04-01 venerdì
93 2022-04-02 sabato
94 2022-04-03 domenica
95 2022-04-04 lunedì
96 2022-04-05 martedì
97 2022-04-06 mercoledì
98 2022-04-07 giovedì
99 2022-04-08 venerdì
100 2022-04-09 sabato
101 2022-04-10 domenica
102 2022-04-11 lunedì
103 2022-04-12 martedì
104 2022-04-13 mercoledì https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html
105 2022-04-14 giovedì
106 2022-04-15 venerdì
107 2022-04-16 sabato https://ricette.giallozafferano.it/Couscous-alla-marocchina.html
108 2022-04-17 domenica
109 2022-04-18 lunedì
110 2022-04-19 martedì
111 2022-04-20 mercoledì
112 2022-04-21 giovedì
113 2022-04-22 venerdì https://ricette.giallozafferano.it/Risotto-agli-asparagi-e-scampi.html
114 2022-04-23 sabato
115 2022-04-24 domenica https://ricette.giallozafferano.it/Torta-Pasqualina.html
116 2022-04-25 lunedì
117 2022-04-26 martedì
118 2022-04-27 mercoledì https://ricette.giallozafferano.it/Orata-al-forno.html
119 2022-04-28 giovedì
120 2022-04-29 venerdì https://ricette.giallozafferano.it/Cordon-bleu-di-melanzane.html
121 2022-04-30 sabato
122 2022-05-01 domenica
123 2022-05-02 lunedì
124 2022-05-03 martedì
125 2022-05-04 mercoledì
126 2022-05-05 giovedì
127 2022-05-06 venerdì
128 2022-05-07 sabato
129 2022-05-08 domenica
130 2022-05-09 lunedì
131 2022-05-10 martedì
132 2022-05-11 mercoledì https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html
133 2022-05-12 giovedì
134 2022-05-13 venerdì
135 2022-05-14 sabato
136 2022-05-15 domenica
137 2022-05-16 lunedì
138 2022-05-17 martedì
139 2022-05-18 mercoledì https://ricette.giallozafferano.it/Risotto-agli-asparagi-e-scampi.html
140 2022-05-19 giovedì
141 2022-05-20 venerdì
142 2022-05-21 sabato https://ricette.giallozafferano.it/Couscous-alla-marocchina.html
143 2022-05-22 domenica https://ricette.giallozafferano.it/Orata-al-forno.html
144 2022-05-23 lunedì
145 2022-05-24 martedì
146 2022-05-25 mercoledì
147 2022-05-26 giovedì https://ricette.giallozafferano.it/Saltimbocca-alla-Romana.html
148 2022-05-27 venerdì
149 2022-05-28 sabato
150 2022-05-29 domenica https://ricette.giallozafferano.it/Polpo-alla-Luciana.html
151 2022-05-30 lunedì
152 2022-05-31 martedì
153 2022-06-01 mercoledì https://ricette.giallozafferano.it/Riso-alla-cantonese.html
154 2022-06-02 giovedì
155 2022-06-03 venerdì
156 2022-06-04 sabato
157 2022-06-05 domenica
158 2022-06-06 lunedì
159 2022-06-07 martedì https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html
160 2022-06-08 mercoledì
161 2022-06-09 giovedì
162 2022-06-10 venerdì https://ricette.giallozafferano.it/Verdure-gratinate-al-forno.html
163 2022-06-11 sabato
164 2022-06-12 domenica https://ricette.giallozafferano.it/Riso-alla-cantonese.html
165 2022-06-13 lunedì https://ricette.giallozafferano.it/Petto-di-pollo-ai-peperoni.html
166 2022-06-14 martedì https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html
167 2022-06-15 mercoledì https://ricette.giallozafferano.it/Cocotte-di-quinoa.html
168 2022-06-16 giovedì
169 2022-06-17 venerdì https://ricette.giallozafferano.it/Salmorejo.html
170 2022-06-18 sabato https://ricette.giallozafferano.it/Insalata-di-ceci-estiva.html
171 2022-06-19 domenica
172 2022-06-20 lunedì
173 2022-06-21 martedì
174 2022-06-22 mercoledì https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html
175 2022-06-23 giovedì
176 2022-06-24 venerdì
177 2022-06-25 sabato https://ricette.giallozafferano.it/Orata-al-forno.html
178 2022-06-26 domenica
179 2022-06-27 lunedì
180 2022-06-28 martedì
181 2022-06-29 mercoledì
182 2022-06-30 giovedì
183 2022-07-01 venerdì
184 2022-07-02 sabato
185 2022-07-03 domenica
186 2022-07-04 lunedì
187 2022-07-05 martedì https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html https://ricette.giallozafferano.it/Insalata-di-ceci-estiva.html
188 2022-07-06 mercoledì https://ricette.giallozafferano.it/Pasta-con-le-melanzane.html
189 2022-07-07 giovedì
190 2022-07-08 venerdì
191 2022-07-09 sabato
192 2022-07-10 domenica
193 2022-07-11 lunedì
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ì
197 2022-07-15 venerdì https://ricette.giallozafferano.it/Salmorejo.html
198 2022-07-16 sabato
199 2022-07-17 domenica
200 2022-07-18 lunedì
201 2022-07-19 martedì https://ricette.giallozafferano.it/Verdure-gratinate-al-forno.html
202 2022-07-20 mercoledì
203 2022-07-21 giovedì https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html
204 2022-07-22 venerdì
205 2022-07-23 sabato https://ricette.giallozafferano.it/Cocotte-di-quinoa.html
206 2022-07-24 domenica
207 2022-07-25 lunedì
208 2022-07-26 martedì https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html
209 2022-07-27 mercoledì
210 2022-07-28 giovedì
211 2022-07-29 venerdì https://ricette.giallozafferano.it/Orata-al-forno.html
212 2022-07-30 sabato
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ì
216 2022-08-03 mercoledì https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html
217 2022-08-04 giovedì https://ricette.giallozafferano.it/Petto-di-pollo-ai-peperoni.html
218 2022-08-05 venerdì
219 2022-08-06 sabato
220 2022-08-07 domenica
221 2022-08-08 lunedì
222 2022-08-09 martedì https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html
223 2022-08-10 mercoledì
224 2022-08-11 giovedì https://ricette.giallozafferano.it/Verdure-gratinate-al-forno.html
225 2022-08-12 venerdì
226 2022-08-13 sabato
227 2022-08-14 domenica
228 2022-08-15 lunedì https://ricette.giallozafferano.it/Pasta-con-le-melanzane.html
229 2022-08-16 martedì
230 2022-08-17 mercoledì https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html https://ricette.giallozafferano.it/Cocotte-di-quinoa.html
231 2022-08-18 giovedì
232 2022-08-19 venerdì
233 2022-08-20 sabato https://ricette.giallozafferano.it/Salmorejo.html
234 2022-08-21 domenica
235 2022-08-22 lunedì
236 2022-08-23 martedì
237 2022-08-24 mercoledì https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html
238 2022-08-25 giovedì
239 2022-08-26 venerdì
240 2022-08-27 sabato https://ricette.giallozafferano.it/Orata-al-forno.html
241 2022-08-28 domenica
242 2022-08-29 lunedì
243 2022-08-30 martedì
244 2022-08-31 mercoledì
245 2022-09-01 giovedì
246 2022-09-02 venerdì https://ricette.giallozafferano.it/Petto-di-pollo-ai-peperoni.html
247 2022-09-03 sabato
248 2022-09-04 domenica
249 2022-09-05 lunedì
250 2022-09-06 martedì
251 2022-09-07 mercoledì
252 2022-09-08 giovedì https://ricette.giallozafferano.it/Pasta-con-le-melanzane.html
253 2022-09-09 venerdì
254 2022-09-10 sabato
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ì
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
261 2022-09-17 sabato
262 2022-09-18 domenica
263 2022-09-19 lunedì
264 2022-09-20 martedì
265 2022-09-21 mercoledì https://ricette.giallozafferano.it/Strozzapreti-cacio-cozze-e-pepe.html
266 2022-09-22 giovedì https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html
267 2022-09-23 venerdì
268 2022-09-24 sabato https://ricette.giallozafferano.it/Gnudi.html
269 2022-09-25 domenica https://ricette.giallozafferano.it/Pollo-alla-cacciatora.html
270 2022-09-26 lunedì https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html
271 2022-09-27 martedì
272 2022-09-28 mercoledì https://ricette.giallozafferano.it/Orata-al-forno.html
273 2022-09-29 giovedì
274 2022-09-30 venerdì
275 2022-10-01 sabato
276 2022-10-02 domenica https://ricette.giallozafferano.it/Gnocchi-alla-sorrentina.html
277 2022-10-03 lunedì
278 2022-10-04 martedì https://ricette.giallozafferano.it/Spatzle-di-spinaci.html
279 2022-10-05 mercoledì
280 2022-10-06 giovedì https://ricette.giallozafferano.it/Saltimbocca-alla-Romana.html
281 2022-10-07 venerdì https://ricette.giallozafferano.it/Risotto-ai-funghi.html
282 2022-10-08 sabato https://ricette.giallozafferano.it/Torta-Pasqualina.html
283 2022-10-09 domenica
284 2022-10-10 lunedì https://ricette.giallozafferano.it/Pennette-con-speck-e-zucchine.html
285 2022-10-11 martedì
286 2022-10-12 mercoledì
287 2022-10-13 giovedì
288 2022-10-14 venerdì https://ricette.giallozafferano.it/Polpette-al-sugo.html
289 2022-10-15 sabato
290 2022-10-16 domenica
291 2022-10-17 lunedì
292 2022-10-18 martedì
293 2022-10-19 mercoledì https://ricette.giallozafferano.it/Scaloppine-ai-funghi.html
294 2022-10-20 giovedì
295 2022-10-21 venerdì
296 2022-10-22 sabato https://ricette.giallozafferano.it/Gnudi.html
297 2022-10-23 domenica
298 2022-10-24 lunedì
299 2022-10-25 martedì
300 2022-10-26 mercoledì
301 2022-10-27 giovedì
302 2022-10-28 venerdì https://ricette.giallozafferano.it/Vellutata-di-zucca-e-carote.html
303 2022-10-29 sabato https://ricette.giallozafferano.it/Orata-al-forno.html
304 2022-10-30 domenica
305 2022-10-31 lunedì
306 2022-11-01 martedì
307 2022-11-02 mercoledì
308 2022-11-03 giovedì
309 2022-11-04 venerdì
310 2022-11-05 sabato https://ricette.giallozafferano.it/Spatzle-di-spinaci.html
311 2022-11-06 domenica
312 2022-11-07 lunedì
313 2022-11-08 martedì
314 2022-11-09 mercoledì
315 2022-11-10 giovedì
316 2022-11-11 venerdì https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html
317 2022-11-12 sabato https://ricette.giallozafferano.it/Chili-con-carne.html
318 2022-11-13 domenica
319 2022-11-14 lunedì
320 2022-11-15 martedì
321 2022-11-16 mercoledì
322 2022-11-17 giovedì
323 2022-11-18 venerdì https://ricette.giallozafferano.it/Torta-rustica.html
324 2022-11-19 sabato
325 2022-11-20 domenica
326 2022-11-21 lunedì
327 2022-11-22 martedì
328 2022-11-23 mercoledì
329 2022-11-24 giovedì
330 2022-11-25 venerdì https://ricette.giallozafferano.it/Vellutata-di-zucca-e-carote.html
331 2022-11-26 sabato https://ricette.giallozafferano.it/Orata-al-forno.html
332 2022-11-27 domenica
333 2022-11-28 lunedì
334 2022-11-29 martedì https://ricette.giallozafferano.it/Scaloppine-ai-funghi.html
335 2022-11-30 mercoledì
336 2022-12-01 giovedì
337 2022-12-02 venerdì
338 2022-12-03 sabato
339 2022-12-04 domenica
340 2022-12-05 lunedì https://ricette.giallozafferano.it/Spatzle-di-spinaci.html
341 2022-12-06 martedì
342 2022-12-07 mercoledì
343 2022-12-08 giovedì
344 2022-12-09 venerdì
345 2022-12-10 sabato https://ricette.giallozafferano.it/Gnudi.html
346 2022-12-11 domenica
347 2022-12-12 lunedì
348 2022-12-13 martedì
349 2022-12-14 mercoledì https://ricette.giallozafferano.it/Risotto-ai-funghi.html
350 2022-12-15 giovedì https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html
351 2022-12-16 venerdì
352 2022-12-17 sabato
353 2022-12-18 domenica https://ricette.giallozafferano.it/Polpette-al-sugo.html
354 2022-12-19 lunedì
355 2022-12-20 martedì
356 2022-12-21 mercoledì
357 2022-12-22 giovedì
358 2022-12-23 venerdì
359 2022-12-24 sabato
360 2022-12-25 domenica https://ricette.giallozafferano.it/Filetto-in-crosta.html
361 2022-12-26 lunedì
362 2022-12-27 martedì
363 2022-12-28 mercoledì https://ricette.giallozafferano.it/Orata-al-forno.html
364 2022-12-29 giovedì
365 2022-12-30 venerdì
366 2022-12-31 sabato

View File

@ -3,16 +3,16 @@
use {
crate::config::Config, crate::constants, anyhow::anyhow, anyhow::Result,
base64::write::EncoderWriter as Base64Encoder, reqwest::Url, std::io::Write,
base64::write::EncoderWriter as Base64Encoder, chrono::DateTime, icalendar::CalendarComponent,
reqwest::Url, std::io::Write, std::ops::Range, std::sync::Arc, tokio::sync::Semaphore,
};
pub struct ApiClient {
rest: reqwest::Client,
agent: ureq::Agent,
pub(crate) rest_semaphore: Arc<Semaphore>, // TODO: wrap in dereferentiable struct
base_url: Url,
caldav_base_url: Url,
username: String,
password: String,
}
impl ApiClient {
@ -59,9 +59,8 @@ impl ApiClient {
base_url,
caldav_base_url,
username: server.login_name.clone(),
password: server.password.clone(),
rest: rest_client,
agent: ureq::Agent::new(),
rest_semaphore: Arc::new(Semaphore::new(5)),
})
}
@ -73,31 +72,105 @@ impl ApiClient {
&self.base_url
}
pub fn caldav_base_url(&self) -> &Url {
&self.caldav_base_url
}
pub fn username(&self) -> &str {
&self.username
}
pub fn get_calendars(
pub async fn get_events<Tz>(
&self,
) -> core::result::Result<Vec<minicaldav::Calendar>, minicaldav::Error> {
minicaldav::get_calendars(
self.agent.clone(),
self.username(),
&self.password,
&self.caldav_base_url,
)
}
pub fn get_events(
&self,
calendar: &minicaldav::Calendar,
) -> core::result::Result<(Vec<minicaldav::Event>, Vec<minicaldav::Error>), minicaldav::Error>
calendar_name: &str,
date_range: Range<DateTime<Tz>>,
) -> Result<Vec<icalendar::Event>>
where
Tz: chrono::TimeZone,
Tz::Offset: std::fmt::Display,
{
minicaldav::get_events(
self.agent.clone(),
self.username(),
&self.password,
calendar,
)
let report_method = reqwest::Method::from_bytes(b"REPORT").unwrap();
let events_xml = self
.rest()
.request(
report_method,
// TODO extract into helper method
self.caldav_base_url.join(&format!(
"calendars/{}/{}",
self.username(),
calendar_name.to_lowercase().replace(" ", "-")
))?,
)
.header("Prefer", "return-minimal")
.header("Content-Type", "application/xml; charset=utf-8")
.header("Depth", 1)
.body(format!(
"<c:calendar-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">
<d:prop>
<c:calendar-data />
</d:prop>
<c:filter>
<c:comp-filter name=\"VCALENDAR\">
<c:prop-filter name=\"PRODID\">
<c:text-match>{}</c:text-match>
</c:prop-filter>
<c:comp-filter name=\"VEVENT\">
<c:prop-filter name=\"DTSTART\">
<time-range start=\"{}\" end=\"{}\" />
</c:prop-filter>
</c:comp-filter>
</c:comp-filter>
</c:filter>
</c:calendar-query>
",
constants::CALENDAR_PROVIDER,
date_range
.start
.naive_utc()
.format(constants::ICAL_UTCTIME_FMT)
.to_string(),
date_range
.end
.naive_utc()
.format(constants::ICAL_UTCTIME_FMT)
.to_string()
))
.send()
.await?
.text()
.await?;
let xml_doc = libxml::parser::Parser::default().parse_string(events_xml.as_bytes())?;
let mut xpath_ctx = libxml::xpath::Context::new(&xml_doc).unwrap();
xpath_ctx
.register_namespace("cal", "urn:ietf:params:xml:ns:caldav")
.unwrap();
let calendars_txt = xpath_ctx
.findnodes("//cal:calendar-data/text()", None)
.unwrap()
.into_iter()
.map(|n| icalendar::parser::unfold(&n.get_content()))
.collect::<Vec<_>>();
let calendars = calendars_txt
.iter()
.map(|cal| icalendar::parser::read_calendar(cal).unwrap())
.collect::<Vec<_>>();
let events = calendars
.into_iter()
.flat_map(|cal| {
cal.components
.into_iter()
.filter(|comp| comp.name == "VEVENT")
})
.map(|comp| {
let event: CalendarComponent = comp.into();
match event {
CalendarComponent::Event(e) => e,
_ => unreachable!(),
}
});
Ok(events.collect::<Vec<_>>())
}
}

159
src/commands/groceries.rs Normal file
View File

@ -0,0 +1,159 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
use {
crate::api_client::ApiClient,
crate::recipe::{Ingredient, Recipe},
anyhow::{anyhow, Result},
chrono::{Duration, Local},
icalendar::Component,
regex::Regex,
reqwest::{Method, StatusCode},
std::collections::HashSet,
std::ops::Range,
};
pub async fn with(
api_client: &ApiClient,
calendar_name: &str,
location: &str,
days: u32,
) -> Result<()> {
let ids = map_events_to_recipe_ids(api_client, calendar_name, days).await?;
let ingredients = get_ingredients(api_client, ids).await?;
let ingredients = merge_ingredients(ingredients);
let md = prepare_grocery_list(&ingredients)?;
log::debug!("Saving the following grocery list:\n\n{}", &md);
save_grocery_list(api_client, location, &md).await?;
Ok(())
}
async fn map_events_to_recipe_ids(
api_client: &ApiClient,
calendar_name: &str,
days: u32,
) -> Result<HashSet<usize>> {
let date_range = Range {
start: Local::now(),
end: Local::now() + Duration::days(days as i64),
};
let all_events = api_client.get_events(calendar_name, date_range).await?;
let recipe_id_regex: Regex = Regex::new(r"cookbook@(\d+)").unwrap();
let recipe_ids = all_events
.iter()
.flat_map(|event| event.property_value("DESCRIPTION"))
.flat_map(|descr| recipe_id_regex.captures(descr))
.flat_map(|c| c.get(1))
.flat_map(|m| m.as_str().parse::<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| {
log::info!("Retrieved ingredients for '{}'", r.name);
r.ingredients
})
});
let ingredients = futures::future::try_join_all(ingredients).await?;
Ok(ingredients.into_iter().flatten().collect())
}
fn merge_ingredients(mut ingredients: Vec<Ingredient>) -> Vec<Ingredient> {
ingredients.sort();
// TODO actual merging
ingredients
}
fn prepare_grocery_list(ingredients: &Vec<Ingredient>) -> Result<String> {
let mut out = String::new();
use std::fmt::Write;
writeln!(
out,
"# Grocery list - {}",
chrono::Local::now().format("%Y-%m-%d").to_string()
)?;
writeln!(out)?; // leave an empty line
for ingredient in ingredients {
let ingredient = ingredient.0.as_str();
writeln!(out, "- [ ] {}", ingredient)?;
}
Ok(out)
}
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<_>>();
filename_components
.iter()
.take(filename_components.len() - 1)
.fold(Ok(dav_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()
.request(Method::from_bytes(b"MKCOL").unwrap(), url.clone())
.send()
.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();
log::info!("Saving grocery list to {}", &file_url);
let response = api_client
.rest()
.put(file_url.clone())
.header("Content-Type", "text/markdown; charset=utf-8")
.body(contents.to_owned())
.send()
.await?;
match response.status() {
StatusCode::CREATED | StatusCode::NO_CONTENT => Ok(()),
status => Err(anyhow!(
"Cannot save grocery list at {}, server responded with status {}",
file_url,
status
)),
}
}

View File

@ -24,6 +24,7 @@ where
response.status()
);
}
log::info!("Imported recipe into cookbook: {}", url.as_ref());
}
Ok(())

View File

@ -1,7 +1,9 @@
// 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 purge;
pub mod schedule;
pub mod schedule_csv;

63
src/commands/purge.rs Normal file
View File

@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
use {crate::api_client::ApiClient, crate::constants, anyhow::Result, reqwest::Method};
pub async fn with(api_client: &ApiClient, calendar_name: &str) -> Result<()> {
let report_method = Method::from_bytes(b"REPORT")?;
let events_xml = api_client
.rest()
.request(
report_method,
// TODO extract into helper method
api_client.caldav_base_url().join(&format!(
"calendars/{}/{}/",
api_client.username(),
calendar_name.to_lowercase().replace(" ", "-")
))?,
)
.header("Prefer", "return-minimal")
.header("Content-Type", "application/xml; charset=utf-8")
.header("Depth", 1)
.body(format!(
"<c:calendar-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">
<d:prop>
<d:getetag/>
</d:prop>
<c:filter>
<c:comp-filter name=\"VCALENDAR\">
<c:prop-filter name=\"PRODID\">
<c:text-match>{}</c:text-match>
</c:prop-filter>
</c:comp-filter>
</c:filter>
</c:calendar-query>
",
constants::CALENDAR_PROVIDER,
))
.send()
.await?
.text()
.await?;
let xml_doc = libxml::parser::Parser::default().parse_string(events_xml.as_bytes())?;
let mut xpath_ctx = libxml::xpath::Context::new(&xml_doc).unwrap();
xpath_ctx.register_namespace("d", "DAV:").unwrap();
let events_to_purge = xpath_ctx
.findnodes("//d:response/d:href/text()", None)
.unwrap()
.into_iter()
.map(|n| icalendar::parser::unfold(&n.get_content()))
.collect::<Vec<_>>();
for url in events_to_purge {
api_client
.rest()
.delete(api_client.base_url().join(&url)?)
.send()
.await?;
log::debug!("Purged {}", &url);
}
Ok(())
}

View File

@ -4,9 +4,9 @@
use {
crate::api_client::ApiClient,
crate::commands::import,
crate::constants,
crate::event::{Event, Meal},
crate::recipe,
crate::{constants, helpers},
anyhow::{bail, Result},
chrono::naive::NaiveDate,
futures::future::try_join_all,
@ -31,6 +31,9 @@ pub async fn with(api_client: &ApiClient, calendar: &str, csv_file: &Path) -> Re
let recipe_urls = urls_from_csv(records.iter())?;
import::with(&api_client, recipe_urls.into_iter()).await?;
// Unfortunately, Nextcloud Cookbook doesn't return an id for imported recipes,
// so we have to resort to fetch all of them to match them
let recipes = get_all_recipes(&api_client).await?;
let events = records
@ -70,6 +73,7 @@ where
}
async fn get_all_recipes(api_client: &ApiClient) -> Result<HashMap<String, Rc<recipe::Recipe>>> {
log::info!("Getting list of all recipes");
let metadata = api_client
.rest()
.get(api_client.base_url().join("apps/cookbook/api/recipes")?)
@ -93,6 +97,7 @@ async fn get_all_recipes(api_client: &ApiClient) -> Result<HashMap<String, Rc<re
"Cannot fetch recipe {} with id {}",
rm.name, rm.id
));
response.json::<recipe::Recipe>().await.map(|r| Rc::new(r))
});
@ -110,42 +115,58 @@ 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 dav_base = api_client
.rest()
.head(api_client.base_url().join("/.well-known/caldav")?)
.send()
.await?;
let calendar_url = dav_base.url().join(&format!(
let calendar_url = api_client.caldav_base_url().join(&format!(
"calendars/{}/{}/",
&api_client.username(),
calendar.to_lowercase().as_str().replace(" ", "-")
))?;
let calendar_prototype = &calendar_prototype;
let calendar_url = &calendar_url;
let update_requests = events.map(|ev| async move {
let url = calendar_url.join(&format!("{}.ics", ev.uid)).unwrap();
let mut cal = calendar_prototype.clone();
cal.add_event(ev.into());
let alarm_text_repr = format!(
"BEGIN:VALARM\nACTION:DISPLAY\nTRIGGER:-PT15M\nDESCRIPTION:{}\nEND:VALARM",
&helpers::ical_escape_text(&ev.recipe.name)
);
api_client
let info_message = format!(
"Saving event at {} for '{}'",
&ev.ends_at.date(),
&ev.recipe.name
);
let cal = icalendar::Calendar::new()
.push::<icalendar::Event>(ev.into())
.done();
let cal_as_string = (&cal.to_string())
.replacen(
// need to hack around inability to set PRODID in icalendar::Calendar
"PRODID:ICALENDAR-RS",
&format!("PRODID:{}", constants::CALENDAR_PROVIDER),
1,
)
.replacen(
// need to hack around inability to set VALARM in icalendar::Event
"END:VEVENT",
&format!("{}\nEND:VEVENT", alarm_text_repr),
1,
);
// TODO: wrap this
let _ = api_client.rest_semaphore.acquire().await.unwrap();
let response = api_client
.rest()
.put(url)
.header("Content-Type", "text/calendar; charset=utf-8")
.body(cal.to_string())
.body(cal_as_string)
.send()
.await
.await;
log::info!("{}", info_message);
// TODO: magic numbers are bad...
std::thread::sleep(std::time::Duration::from_millis(300));
response
});
let responses = try_join_all(update_requests).await?;

View File

@ -2,4 +2,9 @@
// 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"),
"//EN"
);
pub const ICAL_UTCTIME_FMT: &str = "%Y%m%dT%H%M%SZ";

View File

@ -3,10 +3,8 @@
use {
crate::recipe::Recipe,
chrono::{DateTime, Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc},
ics::escape_text,
ics::properties as calprop,
ics::Event as CalEvent,
chrono::{/*Datelike, */ NaiveDate, NaiveDateTime, NaiveTime, Utc},
icalendar::Event as CalEvent,
std::rc::Rc,
};
@ -46,55 +44,40 @@ impl Event {
}
}
impl<'a> From<Event> for CalEvent<'a> {
impl From<Event> for CalEvent {
fn from(ev: Event) -> Self {
let start_time = Local
.from_local_datetime(&(*&ev.ends_at - ev.recipe.total_time()))
.unwrap();
let end_time = Local.from_local_datetime(&ev.ends_at).unwrap();
let timezone = iana_time_zone::get_timezone().unwrap();
use icalendar::Component;
let mut event = ics::Event::new(ev.uid.clone(), dt_utc_fmt(&Utc::now()));
event.push(calprop::Summary::new(escape_text(ev.recipe.name.clone())));
event.push(calprop::Description::new(format!(
"cookbook@{}",
ev.recipe.id
)));
event.push(calprop::Location::new(escape_text(ev.recipe.url.clone())));
// const DAY_NAMES: [&str; 7] = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
let start_time = ev.ends_at - ev.recipe.total_time();
let mut dtstart = calprop::DtStart::new(dt_fmt(&start_time));
dtstart.append(ics::parameters!("TZID" => timezone.clone()));
event.push(dtstart);
let cal_event = CalEvent::new()
.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)
/* FIXME
.add_property(
// TODO make configurable yearly repetition, for now this suits my personal uses
"RRULE",
&format!(
"FREQ=YEARLY;BYDAY={weekday};BYWEEKNO={weekno}",
weekday = DAY_NAMES
.get(start_time.weekday().num_days_from_monday() as usize)
.unwrap(),
weekno = start_time.iso_week().week(),
),
)*/
.add_property(
// TODO make configurable yearly repetition, for now this suits my personal uses
"RRULE",
"FREQ=YEARLY",
)
.done();
let mut dtend = calprop::DtEnd::new(dt_fmt(&end_time));
dtend.append(ics::parameters!("TZID" => timezone));
event.push(dtend);
// TODO make configurable yearly repetition, for now this suits my personal uses
const DAY_NAMES: [&str; 7] = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
event.push(calprop::RRule::new(format!(
"FREQ=YEARLY;BYDAY={weekday};BYWEEKNO={weekno}",
weekday = DAY_NAMES
.get(start_time.weekday().num_days_from_monday() as usize)
.unwrap(),
weekno = start_time.iso_week().week(),
)));
let mut trigger = calprop::Trigger::new("-PT15M");
trigger.append(ics::parameters!("RELATED" => "START"));
let alarm = ics::Alarm::display(
trigger,
calprop::Description::new(escape_text(ev.recipe.name.clone())),
);
event.add_alarm(alarm);
event
cal_event
}
}
fn dt_fmt(datetime: &DateTime<Local>) -> String {
datetime.format("%Y%m%dT%H%M%S").to_string()
}
fn dt_utc_fmt(datetime: &DateTime<Utc>) -> String {
datetime.format("%Y%m%dT%H%M%SZ").to_string()
}

32
src/helpers.rs Normal file
View File

@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
pub fn ical_escape_text(text: &str) -> String {
/* https://www.kanzaki.com/docs/ical/text.html
The "TEXT" property values may also contain special characters that
are used to signify delimiters, such as a COMMA character for lists of
values or a SEMICOLON character for structured values. In order to
support the inclusion of these special characters in "TEXT" property
values, they MUST be escaped with a BACKSLASH character. A BACKSLASH
character (US-ASCII decimal 92) in a "TEXT" property value MUST be
escaped with another BACKSLASH character. A COMMA character in a "TEXT"
property value MUST be escaped with a BACKSLASH character
(US-ASCII decimal 92). A SEMICOLON character in a "TEXT" property
value MUST be escaped with a BACKSLASH character (US-ASCII decimal
92). However, a COLON character in a "TEXT" property value SHALL
NOT be escaped with a BACKSLASH character.
*/
let mut out = Vec::<u8>::with_capacity(text.len());
for c in text.as_bytes() {
match c {
b'\\' => out.extend_from_slice(&[b'\\', b'\\']),
b',' => out.extend_from_slice(&[b'\\', b',']),
b';' => out.extend_from_slice(&[b'\\', b';']),
_ => out.push(*c),
}
}
unsafe { String::from_utf8_unchecked(out) }
}

View File

@ -6,6 +6,7 @@ mod commands;
mod config;
mod constants;
mod event;
mod helpers;
mod recipe;
use {
@ -18,6 +19,8 @@ use {
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let args = setup_args();
parse_args(&args).await
}
@ -41,10 +44,21 @@ 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> "The name of the calendar to read from for scheduled recipes"))
.arg(arg!(<filename> "The relative path for the Markdown file that will be saved on the server. Recommended ending it in '.md'."))
.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"))
@ -56,6 +70,12 @@ fn setup_args() -> ArgMatches {
.arg(arg!(<calendar_name> ""))
.arg(arg!(<csv_file> "").value_parser(clap::value_parser!(PathBuf)))
)
.subcommand(
Command::new("purge")
.about("Removes all events created by this application from a given calendar")
.arg(server_arg.clone())
.arg(arg!(<calendar_name> ""))
)
.get_matches()
}
@ -77,6 +97,15 @@ 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();
let filename = sub_matches.get_one::<String>("filename").unwrap();
commands::groceries::with(&api_client, calendar_name.as_str(), filename, *days).await
}
Some(("schedule", sub_matches)) => {
let api_client = get_api_client(&sub_matches, &configuration)?;
commands::schedule::with(&api_client).await
@ -91,6 +120,13 @@ async fn parse_args(args: &ArgMatches) -> Result<()> {
.expect("<calendar_name> is a mandatory parameter, it cannot be missing");
commands::schedule_csv::with(&api_client, calendar_name.as_str(), &csv_file).await
}
Some(("purge", 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");
commands::purge::with(&api_client, calendar_name.as_str()).await
}
_ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"),
}
}

View File

@ -60,17 +60,17 @@ pub struct Recipe {
//pub nutrition: Nutrition,
}
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, Ord, Eq, PartialEq, PartialOrd)]
#[serde(rename_all = "camelCase")]
pub struct Ingredient(String);
pub struct Ingredient(pub String);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Tool(String);
pub struct Tool(pub String);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Instruction(String);
pub struct Instruction(pub String);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]