1#![warn(missing_docs)]
5#![warn(clippy::must_use_candidate)]
6#![deny(rustdoc::broken_intra_doc_links)]
7#![cfg_attr(docsrs, feature(doc_cfg))]
8
9pub mod fetch;
10
11use anyhow::Result;
12use serde::Serialize;
13use serde_json::Value;
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct BmsTable {
18 pub header: BmsTableHeader,
20 pub data: BmsTableData,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize)]
26pub struct BmsTableHeader {
27 pub name: String,
29 pub symbol: String,
31 pub data_url: String,
33 pub course: Vec<Vec<CourseInfo>>,
35 pub level_order: Vec<String>,
37 pub extra: serde_json::Value,
39}
40
41impl<'de> serde::Deserialize<'de> for BmsTableHeader {
42 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
43 where
44 D: serde::Deserializer<'de>,
45 {
46 let mut value: Value = Value::deserialize(deserializer)?;
48
49 let name = value
51 .get("name")
52 .and_then(|v| v.as_str())
53 .ok_or_else(|| serde::de::Error::missing_field("name"))?
54 .to_string();
55
56 let symbol = value
58 .get("symbol")
59 .and_then(|v| v.as_str())
60 .ok_or_else(|| serde::de::Error::missing_field("symbol"))?
61 .to_string();
62
63 let data_url = value
65 .get("data_url")
66 .and_then(|v| v.as_str())
67 .ok_or_else(|| serde::de::Error::missing_field("data_url"))?
68 .to_string();
69
70 let course = match value.get("course") {
72 Some(Value::Array(arr)) if !arr.is_empty() => {
73 if matches!(arr.first(), Some(Value::Array(_))) {
74 serde_json::from_value::<Vec<Vec<CourseInfo>>>(value["course"].clone())
76 .map_err(serde::de::Error::custom)?
77 } else {
78 let inner: Vec<CourseInfo> = serde_json::from_value(value["course"].clone())
80 .map_err(serde::de::Error::custom)?;
81 vec![inner]
82 }
83 }
84 _ => Vec::new(),
85 };
86
87 let level_order = match value.get("level_order") {
89 Some(Value::Array(arr)) => arr
90 .iter()
91 .map(|v| match v {
92 Value::Number(n) => n.to_string(),
93 Value::String(s) => s.clone(),
94 _ => v.to_string(),
95 })
96 .collect::<Vec<String>>(),
97 _ => Vec::new(),
98 };
99
100 if let Some(obj) = value.as_object_mut() {
102 obj.remove("name");
103 obj.remove("symbol");
104 obj.remove("data_url");
105 obj.remove("course");
106 obj.remove("level_order");
107 }
108
109 Ok(Self {
110 name,
111 symbol,
112 data_url,
113 course,
114 level_order,
115 extra: value,
116 })
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct BmsTableData {
123 pub charts: Vec<ChartItem>,
125}
126
127impl<'de> serde::Deserialize<'de> for BmsTableData {
128 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
129 where
130 D: serde::Deserializer<'de>,
131 {
132 let value: Value = Value::deserialize(deserializer)?;
136 match value {
137 Value::Array(arr) => {
138 let charts: Vec<ChartItem> =
139 serde_json::from_value(Value::Array(arr)).map_err(serde::de::Error::custom)?;
140 Ok(Self { charts })
141 }
142 Value::Object(mut obj) => {
143 let charts_value = obj.remove("charts").unwrap_or(Value::Array(vec![]));
144 let charts: Vec<ChartItem> =
145 serde_json::from_value(charts_value).map_err(serde::de::Error::custom)?;
146 Ok(Self { charts })
147 }
148 _ => Err(serde::de::Error::custom(
149 "BmsTableData expects array or object with charts",
150 )),
151 }
152 }
153}
154
155#[derive(Debug, Clone, Serialize, PartialEq)]
159pub struct CourseInfo {
160 pub name: String,
162 #[serde(default)]
164 pub constraint: Vec<String>,
165 #[serde(default)]
167 pub trophy: Vec<Trophy>,
168 #[serde(default)]
170 pub charts: Vec<ChartItem>,
171}
172
173impl<'de> serde::Deserialize<'de> for CourseInfo {
174 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
175 where
176 D: serde::Deserializer<'de>,
177 {
178 #[derive(serde::Deserialize)]
179 struct CourseInfoHelper {
180 name: String,
181 #[serde(default)]
182 constraint: Vec<String>,
183 #[serde(default)]
184 trophy: Vec<Trophy>,
185 #[serde(default, rename = "md5")]
186 md5list: Vec<String>,
187 #[serde(default, rename = "sha256")]
188 sha256list: Vec<String>,
189 #[serde(default)]
190 charts: Vec<Value>,
191 }
192
193 let helper = CourseInfoHelper::deserialize(deserializer)?;
194
195 let mut charts = helper
197 .charts
198 .into_iter()
199 .map(|mut chart_value| {
200 if chart_value.get("level").is_none() {
201 let Some(chart_obj) = chart_value.as_object() else {
203 return Err(serde::de::Error::custom("chart_value is not an object"));
204 };
205 let mut chart_obj = chart_obj.clone();
206 chart_obj.insert(
207 "level".to_string(),
208 serde_json::Value::String("0".to_string()),
209 );
210 chart_value = serde_json::Value::Object(chart_obj);
211 }
212 serde_json::from_value(chart_value)
213 })
214 .collect::<Result<Vec<ChartItem>, serde_json::Error>>()
215 .map_err(serde::de::Error::custom)?;
216
217 for md5 in &helper.md5list {
219 charts.push(ChartItem {
220 level: "0".to_string(),
221 md5: Some(md5.clone()),
222 sha256: None,
223 title: None,
224 subtitle: None,
225 artist: None,
226 subartist: None,
227 url: None,
228 url_diff: None,
229 extra: serde_json::Value::Object(serde_json::Map::new()),
230 });
231 }
232
233 for sha256 in &helper.sha256list {
235 charts.push(ChartItem {
236 level: "0".to_string(),
237 md5: None,
238 sha256: Some(sha256.clone()),
239 title: None,
240 subtitle: None,
241 artist: None,
242 subartist: None,
243 url: None,
244 url_diff: None,
245 extra: serde_json::Value::Object(serde_json::Map::new()),
246 });
247 }
248
249 Ok(Self {
250 name: helper.name,
251 constraint: helper.constraint,
252 trophy: helper.trophy,
253 charts,
254 })
255 }
256}
257
258#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
263pub struct ChartItem {
264 pub level: String,
266 pub md5: Option<String>,
268 pub sha256: Option<String>,
270 pub title: Option<String>,
272 pub subtitle: Option<String>,
274 pub artist: Option<String>,
276 pub subartist: Option<String>,
278 pub url: Option<String>,
280 pub url_diff: Option<String>,
282 pub extra: Value,
284}
285
286impl<'de> serde::Deserialize<'de> for ChartItem {
287 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
288 where
289 D: serde::Deserializer<'de>,
290 {
291 let value: Value = Value::deserialize(deserializer)?;
293
294 let level = value
296 .get("level")
297 .and_then(|v| v.as_str())
298 .ok_or_else(|| serde::de::Error::missing_field("level"))?
299 .to_string();
300
301 let md5 = value
302 .get("md5")
303 .and_then(|v| v.as_str())
304 .filter(|s| !s.is_empty())
305 .map(|s| s.to_string());
306 let sha256 = value
307 .get("sha256")
308 .and_then(|v| v.as_str())
309 .filter(|s| !s.is_empty())
310 .map(|s| s.to_string());
311 let title = value
312 .get("title")
313 .and_then(|v| v.as_str())
314 .filter(|s| !s.is_empty())
315 .map(|s| s.to_string());
316 let subtitle = value
317 .get("subtitle")
318 .and_then(|v| v.as_str())
319 .filter(|s| !s.is_empty())
320 .map(|s| s.to_string());
321 let artist = value
322 .get("artist")
323 .and_then(|v| v.as_str())
324 .filter(|s| !s.is_empty())
325 .map(|s| s.to_string());
326 let subartist = value
327 .get("subartist")
328 .and_then(|v| v.as_str())
329 .filter(|s| !s.is_empty())
330 .map(|s| s.to_string());
331 let url = value
332 .get("url")
333 .and_then(|v| v.as_str())
334 .filter(|s| !s.is_empty())
335 .map(|s| s.to_string());
336 let url_diff = value
337 .get("url_diff")
338 .and_then(|v| v.as_str())
339 .filter(|s| !s.is_empty())
340 .map(|s| s.to_string());
341
342 let mut extra_data = value;
344 if let Some(obj) = extra_data.as_object_mut() {
345 obj.remove("level");
347 obj.remove("md5");
348 obj.remove("sha256");
349 obj.remove("title");
350 obj.remove("subtitle");
351 obj.remove("artist");
352 obj.remove("subartist");
353 obj.remove("url");
354 obj.remove("url_diff");
355 }
356
357 Ok(Self {
358 level,
359 md5,
360 sha256,
361 title,
362 subtitle,
363 artist,
364 subartist,
365 url,
366 url_diff,
367 extra: extra_data,
368 })
369 }
370}
371#[derive(Debug, Clone, serde::Deserialize, Serialize, PartialEq)]
375pub struct Trophy {
376 pub name: String,
378 pub missrate: f64,
380 pub scorerate: f64,
382}