beatsaver_rs/map.rs
1//! # Map
2//!
3//! This module contains structures that correspond to the map API responses
4//!
5//! [API documentation here](https://docs.beatsaver.com/responses/beatmap.html)
6use crate::BeatSaverUser;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// This structure specifies whether or not a difficulty exists in the map
11///
12/// Note: `false` here corresponds with `None` in [MapDifficultyCharacteristics][crate::map::MapDifficultyCharacteristics]
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct MapDifficulties {
15 /// `true` if easy map is available
16 pub easy: bool,
17 /// `true` if normal map is available
18 pub normal: bool,
19 /// `true` if hard map is available
20 pub hard: bool,
21 /// `true` if expert map is available
22 pub expert: bool,
23 /// `true` if expert+ map is available
24 #[serde(alias = "expertPlus")]
25 pub expert_plus: bool,
26}
27
28/// Characteristics of a map difficulty level
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct MapDifficltyCharacteristic {
31 /// Duration of the difficulty beatmap
32 pub duration: f32,
33 /// TODO: What does this represent?
34 pub length: usize,
35 /// TODO: What does this represent?
36 pub njs: f32,
37 /// TODO: What does this represent?
38 #[serde(alias = "njsOffset")]
39 pub njs_offset: f32,
40 /// Number of bombs in the difficulty beatmap
41 pub bombs: usize,
42 /// Number of notes in the difficulty beatmap
43 pub notes: usize,
44 /// Number of walls in the difficulty beatmap
45 pub obstacles: usize,
46}
47
48/// Characteristics for each difficulty level
49///
50/// Note: A `None` characteristic will correspond with `false` in [MapDifficulties][crate::map::MapDifficulties]
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52pub struct MapDifficultyCharacteristics {
53 /// Easy difficulty beatmap characteristic
54 pub easy: Option<MapDifficltyCharacteristic>,
55 /// Normal difficulty beatmap characteristic
56 pub normal: Option<MapDifficltyCharacteristic>,
57 /// Hard difficulty beatmap characteristic
58 pub hard: Option<MapDifficltyCharacteristic>,
59 /// Expert difficulty beatmap characteristic
60 pub expert: Option<MapDifficltyCharacteristic>,
61 /// Expert+ difficulty beatmap characteristic
62 #[serde(alias = "expertPlus")]
63 pub expert_plus: Option<MapDifficltyCharacteristic>,
64}
65
66/// A named characteristic group
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub struct MapCharacteristics {
69 /// Included difficulties
70 pub difficulties: MapDifficultyCharacteristics,
71 /// Characteristic group name
72 pub name: String,
73}
74
75/// Metadata about a given map
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct MapMetadata {
78 /// Included difficulties
79 pub difficulties: MapDifficulties,
80 /// Song duration
81 pub duration: usize,
82 /// Automapper name
83 ///
84 /// If map was not autogenerated, this will be `None`
85 pub automapper: Option<String>,
86 /// Map characteristic groups
87 pub characteristics: Vec<MapCharacteristics>,
88 /// Name of the author of the beatmap
89 #[serde(alias = "levelAuthorName")]
90 pub level_author: String,
91 /// Name of the author of the song
92 #[serde(alias = "songAuthorName")]
93 pub song_author: String,
94 /// Name of the map's song
95 #[serde(alias = "songName")]
96 pub song_name: String,
97 /// Subname of the map's song
98 #[serde(alias = "songSubName")]
99 pub song_sub_name: String,
100 /// Song beats per minute
101 pub bpm: f32,
102}
103
104/// Collected BeatSaver statistics for the map
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct MapStats {
107 /// Number of times map has been downloaded
108 pub downloads: usize,
109 /// Number of times map has been played
110 pub plays: usize,
111 /// Number of times map has been downvoted
112 #[serde(alias = "downVotes")]
113 pub downvotes: usize,
114 /// Number of times map has been upvoted
115 #[serde(alias = "upVotes")]
116 pub upvotes: usize,
117 /// Rough difficulty rating of the map
118 pub heat: f32,
119 /// Average rating of the map
120 pub rating: f32,
121}
122
123/// Information about a map
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
125pub struct Map {
126 /// Map metadata
127 pub metadata: MapMetadata,
128 /// Map statistics
129 pub stats: MapStats,
130 /// Description of the map
131 pub description: String,
132 /// ID assigned to the map (e.g. `5cff620c48229f7d88fc60df`)
133 ///
134 /// Note: Maps are referenced through the `key` and `hash` fields, not this one
135 #[serde(alias = "_id")]
136 pub id: String,
137 /// Key assigned to the map (e.g. `1234`)
138 ///
139 /// Note: This is one of the values used to index maps
140 pub key: String,
141 /// Name given to the map
142 pub name: String,
143 /// User who uploaded the map
144 pub uploader: BeatSaverUser,
145 /// Hash of the map
146 /// Note: This is one of the values used to index maps
147 pub hash: String,
148 /// Timestamp of map upload
149 pub uploaded: DateTime<Utc>,
150 #[serde(alias = "directDownload")]
151 /// CDN URL to download the map from
152 ///
153 /// Note: This is a relative path, use the following code to get a full url:
154 /// ```no_run
155 /// # #[cfg(feature = "reqwest_backend")]
156 /// # use beatsaver_rs::client::BeatSaverReqwest;
157 /// # #[cfg(feature = "surf_backend")]
158 /// # use beatsaver_rs::client::BeatSaverSurf;
159 /// # #[cfg(feature = "async")]
160 /// # use beatsaver_rs::BeatSaverApiAsync;
161 /// # use std::convert::TryInto;
162 /// use beatsaver_rs::BEATSAVER_URL;
163 ///
164 /// # #[cfg(feature = "async")]
165 /// # async fn example() {
166 /// # #[cfg(feature = "reqwest_backend")]
167 /// # let client = BeatSaverReqwest::new();
168 /// # #[cfg(feature = "surf_backend")]
169 /// # let client = BeatSaverSurf::new();
170 /// # let map = client.map(&"1".try_into().unwrap()).await.unwrap();
171 /// let full_url = BEATSAVER_URL.join(map.direct_download.as_str()).unwrap();
172 /// # }
173 /// # #[cfg(all(feature = "sync", feature = "ureq_backend"))]
174 /// # mod sync {
175 /// # use beatsaver_rs::client::BeatSaverUreq;
176 /// # use beatsaver_rs::BEATSAVER_URL;
177 /// # use beatsaver_rs::BeatSaverApiSync;
178 /// # use std::convert::TryInto;
179 /// # fn info() {
180 /// # let client = BeatSaverUreq::new();
181 /// # let map = client.map(&"1".try_into().unwrap()).unwrap();
182 /// # let full_url = BEATSAVER_URL.join(map.direct_download.as_str()).unwrap();
183 /// # }
184 /// # }
185 /// ```
186 pub direct_download: String,
187 /// API URL to download the map from
188 ///
189 /// Note: This is a relative path, use the following code to get a full url:
190 /// ```no_run
191 /// # #[cfg(feature = "reqwest_backend")]
192 /// # use beatsaver_rs::client::BeatSaverReqwest;
193 /// # #[cfg(feature = "surf_backend")]
194 /// # use beatsaver_rs::client::BeatSaverSurf;
195 /// # #[cfg(feature = "async")]
196 /// # use beatsaver_rs::BeatSaverApiAsync;
197 /// # use std::convert::TryInto;
198 /// use beatsaver_rs::BEATSAVER_URL;
199 ///
200 /// # #[cfg(feature = "async")]
201 /// # async fn example() {
202 /// # #[cfg(feature = "reqwest_backend")]
203 /// # let client = BeatSaverReqwest::new();
204 /// # #[cfg(feature = "surf_backend")]
205 /// # let client = BeatSaverSurf::new();
206 /// # let map = client.map(&"1".try_into().unwrap()).await.unwrap();
207 /// let full_url = BEATSAVER_URL.join(map.download.as_str()).unwrap();
208 /// # }
209 /// # #[cfg(all(feature = "sync", feature = "ureq_backend"))]
210 /// # mod sync {
211 /// # use beatsaver_rs::client::BeatSaverUreq;
212 /// # use beatsaver_rs::BEATSAVER_URL;
213 /// # use beatsaver_rs::BeatSaverApiSync;
214 /// # use std::convert::TryInto;
215 /// # fn info() {
216 /// # let client = BeatSaverUreq::new();
217 /// # let map = client.map(&"1".try_into().unwrap()).unwrap();
218 /// # let full_url = BEATSAVER_URL.join(map.download.as_str()).unwrap();
219 /// # }
220 /// # }
221 /// ```
222 #[serde(alias = "downloadURL")]
223 pub download: String,
224 /// Cover art URL
225 ///
226 /// Note: This is a relative path, use the following code to get a full url:
227 /// ```no_run
228 /// # #[cfg(feature = "reqwest_backend")]
229 /// # use beatsaver_rs::client::BeatSaverReqwest;
230 /// # #[cfg(feature = "surf_backend")]
231 /// # use beatsaver_rs::client::BeatSaverSurf;
232 /// # #[cfg(feature = "async")]
233 /// # use beatsaver_rs::BeatSaverApiAsync;
234 /// # use std::convert::TryInto;
235 /// use beatsaver_rs::BEATSAVER_URL;
236 ///
237 /// # #[cfg(feature = "async")]
238 /// # async fn example() {
239 /// # #[cfg(feature = "reqwest_backend")]
240 /// # let client = BeatSaverReqwest::new();
241 /// # #[cfg(feature = "surf_backend")]
242 /// # let client = BeatSaverSurf::new();
243 /// # let map = client.map(&"1".try_into().unwrap()).await.unwrap();
244 /// let full_url = BEATSAVER_URL.join(map.cover.as_str()).unwrap();
245 /// # }
246 /// # #[cfg(all(feature = "sync", feature = "ureq_backend"))]
247 /// # mod sync {
248 /// # use beatsaver_rs::client::BeatSaverUreq;
249 /// # use beatsaver_rs::BEATSAVER_URL;
250 /// # use beatsaver_rs::BeatSaverApiSync;
251 /// # use std::convert::TryInto;
252 /// # fn info() {
253 /// # let client = BeatSaverUreq::new();
254 /// # let map = client.map(&"1".try_into().unwrap()).unwrap();
255 /// # let full_url = BEATSAVER_URL.join(map.cover.as_str()).unwrap();
256 /// # }
257 /// # }
258 /// ```
259 #[serde(alias = "coverURL")]
260 pub cover: String,
261}
262
263#[cfg(test)]
264mod tests {
265 use crate::map::Map;
266 use chrono::DateTime;
267 use serde_json;
268
269 #[test]
270 fn test_map() {
271 let data = r#"
272 {
273 "metadata": {
274 "difficulties": {
275 "easy": false,
276 "normal": true,
277 "hard": true,
278 "expert":true,
279 "expertPlus":true
280 },
281 "duration": 0,
282 "automapper": null,
283 "characteristics": [{
284 "name":"Standard",
285 "difficulties": {
286 "easy": null,
287 "normal": {
288 "duration": 417,
289 "length": 195,
290 "bombs": 4,
291 "notes": 301,
292 "obstacles": 24,
293 "njs": 10,
294 "njsOffset": 0
295 },
296 "hard": {
297 "duration": 417,
298 "length": 195,
299 "bombs": 4,
300 "notes": 486,
301 "obstacles": 24,
302 "njs": 10,
303 "njsOffset": 0
304 },
305 "expert": {
306 "duration": 417.5,
307 "length": 195,
308 "bombs": 4,
309 "notes": 620,
310 "obstacles": 24,
311 "njs": 10,
312 "njsOffset": 0
313 },
314 "expertPlus": {
315 "duration": 417.5,
316 "length": 195,
317 "bombs": 0,
318 "notes": 894,
319 "obstacles": 0,
320 "njs": 12,
321 "njsOffset": 0
322 }
323 }
324 }],
325 "songName": "Shut Up and Dance",
326 "songSubName": "WALK THE MOON",
327 "songAuthorName": "BennyDaBeast",
328 "levelAuthorName": "bennydabeast",
329 "bpm":128
330 },
331 "stats": {
332 "downloads": 418854,
333 "plays": 558,
334 "downVotes": 133,
335 "upVotes": 10763,
336 "heat": 395.8225333,
337 "rating": 0.9580848467461356
338 },
339 "description": "Difficulties: Expert+ (Added 11/15), Expert, Hard, Normal\r\nYouTube Preview: https://youtu.be/x9hJbTlPQUY",
340 "deletedAt": null,
341 "_id": "5cff621148229f7d88fc77c9",
342 "key": "2144",
343 "name": "Shut Up and Dance - WALK THE MOON",
344 "uploader": {
345 "_id": "5cff0b7298cc5a672c84e98d",
346 "username": "bennydabeast"
347 },
348 "uploaded": "2018-11-21T01:27:00.000Z",
349 "hash": "89cf8bb07afb3c59ae7b5ac00337d62261c36fb4",
350 "directDownload": "/cdn/2144/89cf8bb07afb3c59ae7b5ac00337d62261c36fb4.zip",
351 "downloadURL": "/api/download/key/2144",
352 "coverURL": "/cdn/2144/89cf8bb07afb3c59ae7b5ac00337d62261c36fb4.png"
353 }"#;
354
355 let v: Map = serde_json::from_str(data).unwrap();
356
357 let difficulties = v.metadata.difficulties;
358 assert_eq!(difficulties.easy, false);
359 assert_eq!(difficulties.normal, true);
360 assert_eq!(difficulties.hard, true);
361 assert_eq!(difficulties.expert, true);
362 assert_eq!(difficulties.expert_plus, true);
363
364 assert_eq!(v.metadata.duration, 0);
365 assert_eq!(v.metadata.automapper, None);
366
367 assert_eq!(v.metadata.characteristics.len(), 1);
368 let characteristics = &v.metadata.characteristics[0];
369 assert_eq!(characteristics.name, "Standard");
370
371 assert_eq!(characteristics.difficulties.easy, None);
372
373 assert!(characteristics.difficulties.normal.is_some());
374 let normal = characteristics.difficulties.normal.as_ref().unwrap();
375 assert_eq!(normal.duration, 417f32);
376 assert_eq!(normal.length, 195);
377 assert_eq!(normal.bombs, 4);
378 assert_eq!(normal.notes, 301);
379 assert_eq!(normal.obstacles, 24);
380 assert_eq!(normal.njs, 10f32);
381 assert_eq!(normal.njs_offset, 0f32);
382
383 assert!(characteristics.difficulties.hard.is_some());
384 let hard = characteristics.difficulties.hard.as_ref().unwrap();
385 assert_eq!(hard.duration, 417f32);
386 assert_eq!(hard.length, 195);
387 assert_eq!(hard.bombs, 4);
388 assert_eq!(hard.notes, 486);
389 assert_eq!(hard.obstacles, 24);
390 assert_eq!(hard.njs, 10f32);
391 assert_eq!(hard.njs_offset, 0f32);
392
393 assert!(characteristics.difficulties.expert.is_some());
394 let expert = characteristics.difficulties.expert.as_ref().unwrap();
395 assert_eq!(expert.duration, 417.5f32);
396 assert_eq!(expert.length, 195);
397 assert_eq!(expert.bombs, 4);
398 assert_eq!(expert.notes, 620);
399 assert_eq!(expert.obstacles, 24);
400 assert_eq!(expert.njs, 10f32);
401 assert_eq!(expert.njs_offset, 0f32);
402
403 assert!(characteristics.difficulties.expert_plus.is_some());
404 let expert_plus = characteristics.difficulties.expert_plus.as_ref().unwrap();
405 assert_eq!(expert_plus.duration, 417.5f32);
406 assert_eq!(expert_plus.length, 195);
407 assert_eq!(expert_plus.bombs, 0);
408 assert_eq!(expert_plus.notes, 894);
409 assert_eq!(expert_plus.obstacles, 0);
410 assert_eq!(expert_plus.njs, 12f32);
411 assert_eq!(expert_plus.njs_offset, 0f32);
412
413 assert_eq!(v.metadata.song_name, "Shut Up and Dance");
414 assert_eq!(v.metadata.song_sub_name, "WALK THE MOON");
415 assert_eq!(v.metadata.song_author, "BennyDaBeast");
416 assert_eq!(v.metadata.level_author, "bennydabeast");
417 assert_eq!(v.metadata.bpm, 128f32);
418
419 assert_eq!(v.stats.downloads, 418854);
420 assert_eq!(v.stats.plays, 558);
421 assert_eq!(v.stats.downvotes, 133);
422 assert_eq!(v.stats.upvotes, 10763);
423 assert_eq!(v.stats.heat, 395.8225333f32);
424 assert_eq!(v.stats.rating, 0.9580848467461356f32);
425
426 assert_eq!(v.description, "Difficulties: Expert+ (Added 11/15), Expert, Hard, Normal\r\nYouTube Preview: https://youtu.be/x9hJbTlPQUY");
427 assert_eq!(v.key, "2144");
428 assert_eq!(v.name, "Shut Up and Dance - WALK THE MOON");
429 assert_eq!(v.uploader.id, "5cff0b7298cc5a672c84e98d");
430 assert_eq!(v.uploader.username, "bennydabeast");
431 assert_eq!(
432 v.uploaded,
433 DateTime::parse_from_rfc3339("2018-11-21T01:27:00.000Z").unwrap()
434 );
435 assert_eq!(v.hash, "89cf8bb07afb3c59ae7b5ac00337d62261c36fb4");
436 assert_eq!(
437 v.direct_download,
438 "/cdn/2144/89cf8bb07afb3c59ae7b5ac00337d62261c36fb4.zip"
439 );
440 assert_eq!(v.download, "/api/download/key/2144");
441 assert_eq!(
442 v.cover,
443 "/cdn/2144/89cf8bb07afb3c59ae7b5ac00337d62261c36fb4.png"
444 );
445 }
446}