Skip to main content

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}