use serde_derive::Deserialize;
#[derive(thiserror::Error, Debug)]
#[error(transparent)]
pub struct Error (anyhow::Error);
pub struct MenuItem {
pub description: String,
pub price: String,
}
pub struct Menu {
pub date: String,
pub items: Vec<MenuItem>,
}
pub async fn get_daily_menu(city: &str, restaurant: &str) -> Result<Vec<Menu>, Error> {
get_daily_menu_internal(city, restaurant).await.map_err(Error)
}
#[derive(Deserialize, Debug)]
struct InternalMenuItem {
name: String,
#[serde(rename = "displayPrice")]
price: String,
}
#[derive(Deserialize)]
struct DailyMenu {
dishes: Vec<InternalMenuItem>,
#[serde(rename = "timeHeading")]
date: String,
}
#[derive(Deserialize)]
struct Sections {
#[serde(rename = "SECTION_DAILY_MENU")]
daily_menu: Vec<DailyMenu>,
}
#[derive(Deserialize)]
struct UnknownObject {
sections: Sections,
}
#[derive(Deserialize)]
struct Pages {
restaurant: std::collections::HashMap<String, UnknownObject>,
}
#[derive(Deserialize)]
struct Data {
pages: Pages,
}
async fn get_daily_menu_internal(city: &str, restaurant: &str) -> Result<Vec<Menu>, anyhow::Error> {
use scraper::Selector;
use anyhow::Context;
let url = format!("https://www.zomato.com/{}/{}/daily-menu", city, restaurant);
#[cfg(feature = "debug-log")]
let verbose = true;
#[cfg(not(feature = "debug-log"))]
let verbose = false;
let req_builder = reqwest::Client::builder()
.connection_verbose(verbose)
.build()?
.request(reqwest::Method::GET, &url)
.header("User-Agent", "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0")
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.header("Accept-Encoding", "identity")
.header("Connection", "keep-alive")
.header("DNT", "1")
.header("Upgrade-Insecure-Requests", "1")
.header("Cache-Control", "max-age=0")
.header("Accept-Language", "en-US,en;q=0.5");
let response = req_builder.send()
.await?
.bytes()
.await?;
let response_decoded = std::str::from_utf8(&response)?;
let html = scraper::Html::parse_document(response_decoded);
let script = html
.select(&Selector::parse("script").unwrap())
.into_iter()
.filter_map(|script| script.text().next())
.find(|script| script.contains("window.__PRELOADED_STATE__ = JSON.parse(\""))
.ok_or_else(|| anyhow::anyhow!("data not found"))?;
let mut iter = script.split("window.__PRELOADED_STATE__ = JSON.parse(\"");
iter.next().expect("empty split");
let json_with_tail = iter.next().expect("missing pattern");
let json_escaped = json_with_tail.split("\")\n").next().expect("empty split");
let mut json_unescaped = String::with_capacity(json_escaped.len());
for piece in json_escaped.split("\\\"") {
if !json_unescaped.is_empty() {
json_unescaped.push_str("\"");
}
json_unescaped.push_str(piece);
}
let data = serde_json::from_str::<Data>(&json_unescaped).context("failed to parse json")?;
let result = data
.pages
.restaurant
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("missing restaurant"))?
.1
.sections.daily_menu
.into_iter()
.map(|menu| {
let items = menu
.dishes
.into_iter()
.map(|item| MenuItem {
description: item.name,
price: item.price,
})
.collect::<Vec<_>>();
Menu {
items,
date: menu.date,
}
})
.collect::<Vec<_>>();
Ok(result)
}