bms_table/fetch/
reqwest.rs1#![cfg(feature = "reqwest")]
22
23use anyhow::{Context, Result, anyhow};
24use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
25use serde::de::DeserializeOwned;
26use std::time::Duration;
27use url::Url;
28
29use crate::{
30 BmsTable, BmsTableData, BmsTableHeader, BmsTableInfo, BmsTableList, BmsTableRaw,
31 fetch::{HeaderQueryContent, get_web_header_json_value, replace_control_chars},
32};
33
34pub async fn fetch_table_full(
50 client: &reqwest::Client,
51 web_url: &str,
52) -> Result<(BmsTable, BmsTableRaw)> {
53 let web_url = Url::parse(web_url).context("When parsing web url")?;
54 let web_response = client
55 .get(web_url.clone())
56 .send()
57 .await
58 .context("When fetching web")?
59 .text()
60 .await
61 .context("When parsing web response")?;
62 let (hq, web_used_raw) = header_query_with_fallback::<BmsTableHeader>(&web_response)
63 .context("When parsing header query")?;
64 let (header_url, header, header_raw) = match hq {
65 HeaderQueryContent::Url(header_url_string) => {
66 let header_url = web_url
67 .join(&header_url_string)
68 .context("When joining header url")?;
69 let header_response = client
70 .get(header_url.clone())
71 .send()
72 .await
73 .context("When fetching header")?;
74 let header_response_string = header_response
75 .text()
76 .await
77 .context("When parsing header response")?;
78 let (hq2, raw2) = header_query_with_fallback::<BmsTableHeader>(&header_response_string)
79 .context("When parsing header query")?;
80 let HeaderQueryContent::Value(v) = hq2 else {
81 return Err(anyhow!(
82 "Cycled header found. web_url: {web_url}, header_url: {header_url_string}"
83 ));
84 };
85 (header_url, v, raw2)
86 }
87 HeaderQueryContent::Value(value) => (web_url, value, web_used_raw),
88 };
89 let data_url = header_url
90 .join(&header.data_url)
91 .context("When joining data url")?;
92 let data_response = client
93 .get(data_url.clone())
94 .send()
95 .await
96 .context("When fetching web")?
97 .text()
98 .await
99 .context("When parsing web response")?;
100 let (data, data_raw_str) = parse_json_str_with_fallback::<BmsTableData>(&data_response)
101 .context("When parsing data json")?;
102 Ok((
103 BmsTable { header, data },
104 BmsTableRaw {
105 header_json_url: header_url,
106 header_raw,
107 data_json_url: data_url,
108 data_raw: data_raw_str,
109 },
110 ))
111}
112
113pub async fn fetch_table(client: &reqwest::Client, web_url: &str) -> Result<BmsTable> {
117 let (table, _raw) = fetch_table_full(client, web_url)
118 .await
119 .context("When fetching full table")?;
120 Ok(table)
121}
122
123pub async fn fetch_table_list(
128 client: &reqwest::Client,
129 web_url: &str,
130) -> Result<Vec<BmsTableInfo>> {
131 let (out, _raw) = fetch_table_list_full(client, web_url)
132 .await
133 .context("When fetching table list full")?;
134 Ok(out)
135}
136
137pub async fn fetch_table_list_full(
141 client: &reqwest::Client,
142 web_url: &str,
143) -> Result<(Vec<BmsTableInfo>, String)> {
144 let web_url = Url::parse(web_url).context("When parsing table list url")?;
145 let response_text = client
146 .get(web_url)
147 .send()
148 .await
149 .context("When fetching table list")?
150 .text()
151 .await
152 .context("When parsing table list response")?;
153 let (list, raw_used) = parse_json_str_with_fallback::<BmsTableList>(&response_text)
154 .context("When parsing table list json")?;
155 let out: Vec<BmsTableInfo> = list.listes;
156 Ok((out, raw_used))
157}
158
159pub fn make_lenient_client() -> Result<reqwest::Client> {
168 let mut headers = HeaderMap::new();
170 headers.insert(
171 HeaderName::from_static("accept"),
172 HeaderValue::from_static(
173 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
174 ),
175 );
176 headers.insert(
177 HeaderName::from_static("accept-language"),
178 HeaderValue::from_static("zh-CN,zh;q=0.9,en;q=0.8"),
179 );
180 headers.insert(
181 HeaderName::from_static("upgrade-insecure-requests"),
182 HeaderValue::from_static("1"),
183 );
184 headers.insert(
185 HeaderName::from_static("connection"),
186 HeaderValue::from_static("keep-alive"),
187 );
188
189 let client = reqwest::Client::builder()
190 .default_headers(headers)
191 .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119 Safari/537.36 bms-table-rs")
192 .timeout(Duration::from_secs(60))
193 .redirect(reqwest::redirect::Policy::limited(100))
194 .referer(true)
196 .cookie_store(true)
198 .danger_accept_invalid_certs(true)
200 .danger_accept_invalid_hostnames(true)
201 .build()
202 .context("When building client")?;
203 Ok(client)
204}
205
206fn parse_json_str_with_fallback<T: DeserializeOwned>(raw: &str) -> Result<(T, String)> {
212 match serde_json::from_str::<T>(raw) {
213 Ok(v) => Ok((v, raw.to_string())),
214 Err(_) => {
215 let cleaned = replace_control_chars(raw);
216 let v = serde_json::from_str::<T>(&cleaned).context("When parsing cleaned json")?;
217 Ok((v, cleaned))
218 }
219 }
220}
221
222fn header_query_with_fallback<T: DeserializeOwned>(
228 raw: &str,
229) -> Result<(HeaderQueryContent<T>, String)> {
230 match get_web_header_json_value::<T>(raw) {
231 Ok(v) => Ok((v, raw.to_string())),
232 Err(_) => {
233 let cleaned = replace_control_chars(raw);
234 let v = get_web_header_json_value::<T>(&cleaned)
235 .context("When extracting header from cleaned text")?;
236 Ok((v, cleaned))
237 }
238 }
239}