rosu_v2/model/
beatmap.rs

1use std::{
2    convert::TryFrom,
3    fmt::{Display, Formatter, Result as FmtResult},
4    str::FromStr,
5};
6
7use serde::{
8    de::{DeserializeSeed, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor},
9    Deserialize,
10};
11use time::OffsetDateTime;
12
13use crate::{
14    error::ParsingError,
15    prelude::{CountryCode, OsuError, UserStatisticsModes, Username},
16    request::{GetBeatmapDifficultyAttributes, GetUser},
17    Osu, OsuResult,
18};
19
20use super::{score::Score, serde_util, user::User, CacheUserFn, ContainedUsers, GameMode};
21
22#[derive(Clone, Debug, Deserialize)]
23#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
24pub struct BeatmapExtended {
25    pub ar: f32,
26    #[serde(deserialize_with = "deserialize_f32_default")]
27    pub bpm: f32,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub checksum: Option<String>,
30    pub convert: bool,
31    pub count_circles: u32,
32    pub count_sliders: u32,
33    pub count_spinners: u32,
34    #[serde(rename = "user_id")]
35    pub creator_id: u32,
36    pub cs: f32,
37    #[serde(
38        default,
39        skip_serializing_if = "Option::is_none",
40        with = "serde_util::option_datetime"
41    )]
42    pub deleted_at: Option<OffsetDateTime>,
43    #[serde(rename = "failtimes", skip_serializing_if = "Option::is_none")]
44    pub fail_times: Option<FailTimes>,
45    #[serde(rename = "drain")]
46    pub hp: f32,
47    pub is_scoreable: bool,
48    #[serde(with = "serde_util::datetime")]
49    pub last_updated: OffsetDateTime,
50    #[serde(rename = "id")]
51    pub map_id: u32,
52    #[serde(rename = "beatmapset", skip_serializing_if = "Option::is_none")]
53    pub mapset: Option<Box<BeatmapsetExtended>>,
54    #[serde(rename = "beatmapset_id")]
55    pub mapset_id: u32,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub max_combo: Option<u32>,
58    pub mode: GameMode,
59    #[serde(rename = "accuracy")]
60    pub od: f32,
61    pub passcount: u32,
62    pub playcount: u32,
63    #[serde(rename = "hit_length")]
64    pub seconds_drain: u32,
65    #[serde(rename = "total_length")]
66    pub seconds_total: u32,
67    #[serde(rename = "difficulty_rating")]
68    pub stars: f32,
69    pub status: RankStatus,
70    /// Full URL, i.e. `https://osu.ppy.sh/beatmaps/{map_id}`
71    pub url: String,
72    pub version: String,
73}
74
75impl BeatmapExtended {
76    /// Return the amount of hit objects in this map.
77    #[inline]
78    pub const fn count_objects(&self) -> u32 {
79        self.count_circles + self.count_sliders + self.count_spinners
80    }
81
82    /// Request the [`BeatmapDifficultyAttributes`] for this map.
83    #[inline]
84    pub const fn difficulty_attributes<'o>(
85        &self,
86        osu: &'o Osu,
87    ) -> GetBeatmapDifficultyAttributes<'o> {
88        GetBeatmapDifficultyAttributes::new(osu, self.map_id)
89    }
90}
91
92impl ContainedUsers for BeatmapExtended {
93    fn apply_to_users(&self, f: impl CacheUserFn) {
94        self.mapset.apply_to_users(f);
95    }
96}
97
98impl PartialEq for BeatmapExtended {
99    #[inline]
100    fn eq(&self, other: &Self) -> bool {
101        self.map_id == other.map_id && self.last_updated == other.last_updated
102    }
103}
104
105impl Eq for BeatmapExtended {}
106
107#[derive(Clone, Debug, Deserialize, PartialEq)]
108#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
109pub struct Beatmap {
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub checksum: Option<String>,
112    #[serde(rename = "user_id")]
113    pub creator_id: u32,
114    #[serde(rename = "failtimes", skip_serializing_if = "Option::is_none")]
115    pub fail_times: Option<FailTimes>,
116    #[serde(rename = "id")]
117    pub map_id: u32,
118    #[serde(rename = "beatmapset", skip_serializing_if = "Option::is_none")]
119    pub mapset: Option<Box<Beatmapset>>,
120    #[serde(rename = "beatmapset_id")]
121    pub mapset_id: u32,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub max_combo: Option<u32>,
124    pub mode: GameMode,
125    #[serde(rename = "total_length")]
126    pub seconds_total: u32,
127    #[serde(rename = "difficulty_rating")]
128    pub stars: f32,
129    pub status: RankStatus,
130    pub version: String,
131}
132
133impl Beatmap {
134    /// Request the [`BeatmapDifficultyAttributes`] for this map.
135    #[inline]
136    pub const fn difficulty_attributes<'o>(
137        &self,
138        osu: &'o Osu,
139    ) -> GetBeatmapDifficultyAttributes<'o> {
140        GetBeatmapDifficultyAttributes::new(osu, self.map_id)
141    }
142}
143
144impl ContainedUsers for Beatmap {
145    fn apply_to_users(&self, f: impl CacheUserFn) {
146        self.mapset.apply_to_users(f);
147    }
148}
149
150impl From<BeatmapExtended> for Beatmap {
151    #[inline]
152    fn from(map: BeatmapExtended) -> Self {
153        Self {
154            checksum: map.checksum,
155            creator_id: map.creator_id,
156            fail_times: map.fail_times,
157            map_id: map.map_id,
158            mapset: map.mapset.map(|ms| Box::new((*ms).into())),
159            mapset_id: map.mapset_id,
160            max_combo: map.max_combo,
161            mode: map.mode,
162            seconds_total: map.seconds_total,
163            stars: map.stars,
164            status: map.status,
165            version: map.version,
166        }
167    }
168}
169
170#[derive(Deserialize)]
171#[doc(hidden)]
172pub struct BeatmapDifficultyAttributesWrapper {
173    pub(crate) attributes: BeatmapDifficultyAttributes,
174}
175
176#[derive(Clone, Debug, PartialEq, Deserialize)]
177#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
178pub struct BeatmapDifficultyAttributes {
179    pub max_combo: u32,
180    #[serde(rename = "star_rating")]
181    pub stars: f64,
182    #[serde(flatten, skip_serializing_if = "Option::is_none")]
183    pub attrs: Option<GameModeAttributes>,
184}
185
186impl ContainedUsers for BeatmapDifficultyAttributes {
187    fn apply_to_users(&self, _: impl CacheUserFn) {}
188}
189
190#[derive(Clone, Debug, PartialEq, Deserialize)]
191#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
192#[serde(untagged)]
193pub enum GameModeAttributes {
194    Osu {
195        aim_difficulty: f64,
196        slider_factor: f64,
197        speed_difficulty: f64,
198        speed_note_count: f64,
199        aim_difficult_slider_count: f64,
200        aim_difficult_strain_count: f64,
201        speed_difficult_strain_count: f64,
202    },
203    Taiko {
204        stamina_difficulty: f64,
205        rhythm_difficulty: f64,
206        reading_difficulty: f64,
207        colour_difficulty: f64,
208        mono_stamina_factor: f64,
209        stamina_difficult_strains: f64,
210        rhythm_difficult_strains: f64,
211        colour_difficult_strains: f64,
212    },
213}
214
215/// Represents a beatmapset. This extends [`Beatmapset`] with additional attributes.
216#[derive(Clone, Debug, Deserialize)]
217#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
218pub struct BeatmapsetExtended {
219    pub artist: String,
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    pub artist_unicode: Option<String>,
222    pub availability: BeatmapsetAvailability,
223    #[serde(deserialize_with = "deserialize_f32_default")]
224    pub bpm: f32,
225    pub can_be_hyped: bool,
226    /// Each difficulty's converted map for each mode
227    #[serde(default, skip_serializing_if = "Option::is_none")]
228    pub converts: Option<Vec<BeatmapExtended>>,
229    pub covers: BeatmapsetCovers,
230    /// Username of the mapper at the time of beatmapset creation
231    #[serde(
232        default,
233        rename = "user",
234        deserialize_with = "deser_mapset_user",
235        skip_serializing_if = "Option::is_none"
236    )]
237    pub creator: Option<Box<User>>,
238    #[serde(rename = "creator")]
239    pub creator_name: Username,
240    #[serde(rename = "user_id")]
241    pub creator_id: u32,
242    #[serde(
243        default,
244        deserialize_with = "flatten_description",
245        skip_serializing_if = "Option::is_none"
246    )]
247    pub description: Option<String>,
248    pub discussion_enabled: bool,
249    pub discussion_locked: bool,
250    pub favourite_count: u32,
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub genre: Option<Genre>,
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub hype: Option<BeatmapsetHype>,
255    pub is_scoreable: bool,
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub language: Option<Language>,
258    #[serde(with = "serde_util::datetime")]
259    pub last_updated: OffsetDateTime,
260    /// Full URL, i.e. `https://osu.ppy.sh/community/forums/topics/{thread_id}`
261    #[serde(default, skip_serializing_if = "Option::is_none")]
262    pub legacy_thread_url: Option<String>,
263    #[serde(default, rename = "beatmaps", skip_serializing_if = "Option::is_none")]
264    pub maps: Option<Vec<BeatmapExtended>>,
265    #[serde(rename = "id")]
266    pub mapset_id: u32,
267    pub nominations_summary: BeatmapsetNominations,
268    pub nsfw: bool,
269    #[serde(rename = "play_count")]
270    pub playcount: u32,
271    /// Full URL, i.e. `b.ppy.sh/preview/{mapset_id}.mp3`
272    pub preview_url: String,
273    #[serde(default, skip_serializing_if = "Option::is_none")]
274    pub ratings: Option<Vec<u32>>,
275    #[serde(
276        default,
277        skip_serializing_if = "Option::is_none",
278        with = "serde_util::option_datetime"
279    )]
280    pub ranked_date: Option<OffsetDateTime>,
281    #[serde(default, skip_serializing_if = "Option::is_none")]
282    pub recent_favourites: Option<Vec<User>>,
283    pub source: String,
284    pub status: RankStatus,
285    pub storyboard: bool,
286    #[serde(
287        default,
288        skip_serializing_if = "Option::is_none",
289        with = "serde_util::option_datetime"
290    )]
291    pub submitted_date: Option<OffsetDateTime>,
292    pub tags: String,
293    pub title: String,
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub title_unicode: Option<String>,
296    pub video: bool,
297}
298
299impl ContainedUsers for BeatmapsetExtended {
300    fn apply_to_users(&self, f: impl CacheUserFn) {
301        f(self.creator_id, &self.creator_name);
302        self.maps.apply_to_users(f);
303        self.converts.apply_to_users(f);
304    }
305}
306
307// Deserialize the creator's `UserCompact` manually for edge cases
308// like mapset /s/3 where the user was deleted
309#[allow(clippy::too_many_lines)]
310fn deser_mapset_user<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Box<User>>, D::Error> {
311    struct MapsetUserVisitor;
312
313    impl<'de> Visitor<'de> for MapsetUserVisitor {
314        type Value = Option<Box<User>>;
315
316        fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
317            f.write_str("an optional UserCompact")
318        }
319
320        #[allow(clippy::too_many_lines)]
321        fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
322            struct DateSeed;
323
324            impl<'de> DeserializeSeed<'de> for DateSeed {
325                type Value = Option<OffsetDateTime>;
326
327                #[inline]
328                fn deserialize<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
329                    serde_util::option_datetime::deserialize(d)
330                }
331            }
332
333            struct DefaultGroupSeed;
334
335            impl<'de> DeserializeSeed<'de> for DefaultGroupSeed {
336                type Value = String;
337
338                #[inline]
339                fn deserialize<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
340                    serde_util::from_option::deserialize(d)
341                }
342            }
343
344            let mut avatar_url: Option<Option<String>> = None;
345            let mut country_code: Option<Option<CountryCode>> = None;
346            let mut default_group = None;
347            let mut is_active = None;
348            let mut is_bot = None;
349            let mut is_deleted = None;
350            let mut is_online = None;
351            let mut is_supporter = None;
352            let mut last_visit = None;
353            let mut pm_friends_only = None;
354            let mut profile_color = None;
355            let mut user_id: Option<Option<u32>> = None;
356            let mut username = None;
357            let mut team = None;
358
359            while let Some(key) = map.next_key()? {
360                match key {
361                    "avatar_url" => avatar_url = Some(map.next_value()?),
362                    "country_code" => country_code = Some(map.next_value()?),
363                    "default_group" => default_group = Some(map.next_value_seed(DefaultGroupSeed)?),
364                    "id" => user_id = Some(map.next_value()?),
365                    "is_active" => is_active = Some(map.next_value()?),
366                    "is_bot" => is_bot = Some(map.next_value()?),
367                    "is_deleted" => is_deleted = Some(map.next_value()?),
368                    "is_online" => is_online = Some(map.next_value()?),
369                    "is_supporter" => is_supporter = Some(map.next_value()?),
370                    "last_visit" => last_visit = Some(map.next_value_seed(DateSeed)?),
371                    "pm_friends_only" => pm_friends_only = Some(map.next_value()?),
372                    "profile_colour" => profile_color = Some(map.next_value()?),
373                    "team" => team = Some(map.next_value()?),
374                    "username" => username = Some(map.next_value()?),
375                    _ => {
376                        let _: IgnoredAny = map.next_value()?;
377                    }
378                }
379            }
380
381            let avatar_url = avatar_url
382                .ok_or_else(|| Error::missing_field("avatar_url"))?
383                .unwrap_or_default();
384
385            let country_code = country_code
386                .ok_or_else(|| Error::missing_field("country_code"))?
387                .unwrap_or_else(|| "??".into());
388
389            let default_group =
390                default_group.ok_or_else(|| Error::missing_field("default_group"))?;
391
392            let user_id = user_id
393                .ok_or_else(|| Error::missing_field("user_id"))?
394                .unwrap_or(0);
395
396            let is_active = is_active.ok_or_else(|| Error::missing_field("is_active"))?;
397            let is_bot = is_bot.ok_or_else(|| Error::missing_field("is_bot"))?;
398            let is_deleted = is_deleted.ok_or_else(|| Error::missing_field("is_deleted"))?;
399            let is_online = is_online.ok_or_else(|| Error::missing_field("is_online"))?;
400            let is_supporter = is_supporter.ok_or_else(|| Error::missing_field("is_supporter"))?;
401            let last_visit = last_visit.ok_or_else(|| Error::missing_field("last_visit"))?;
402            let pm_friends_only =
403                pm_friends_only.ok_or_else(|| Error::missing_field("pm_friends_only"))?;
404            let profile_color =
405                profile_color.ok_or_else(|| Error::missing_field("profile_color"))?;
406            let username = username.ok_or_else(|| Error::missing_field("username"))?;
407
408            Ok(Some(Box::new(User {
409                avatar_url,
410                country_code,
411                default_group,
412                is_active,
413                is_bot,
414                is_deleted,
415                is_online,
416                is_supporter,
417                last_visit,
418                pm_friends_only,
419                profile_color,
420                team,
421                user_id,
422                username,
423                account_history: None,
424                badges: None,
425                beatmap_playcounts_count: None,
426                country: None,
427                cover: None,
428                favourite_mapset_count: None,
429                follower_count: None,
430                graveyard_mapset_count: None,
431                groups: None,
432                guest_mapset_count: None,
433                highest_rank: None,
434                is_admin: None,
435                is_bng: None,
436                is_full_bn: None,
437                is_gmt: None,
438                is_limited_bn: None,
439                is_moderator: None,
440                is_nat: None,
441                is_silenced: None,
442                loved_mapset_count: None,
443                medals: None,
444                monthly_playcounts: None,
445                page: None,
446                previous_usernames: None,
447                rank_history: None,
448                ranked_mapset_count: None,
449                replays_watched_counts: None,
450                scores_best_count: None,
451                scores_first_count: None,
452                scores_recent_count: None,
453                statistics: None,
454                statistics_modes: UserStatisticsModes::default(),
455                support_level: None,
456                pending_mapset_count: None,
457            })))
458        }
459
460        #[inline]
461        fn visit_some<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
462            d.deserialize_map(self)
463        }
464
465        #[inline]
466        fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
467            Ok(None)
468        }
469    }
470
471    d.deserialize_option(MapsetUserVisitor)
472}
473
474#[inline]
475fn deserialize_f32_default<'de, D: Deserializer<'de>>(d: D) -> Result<f32, D::Error> {
476    <Option<f32> as Deserialize>::deserialize(d).map(Option::unwrap_or_default)
477}
478
479impl BeatmapsetExtended {
480    #[inline]
481    pub fn get_creator<'o>(&self, osu: &'o Osu) -> GetUser<'o> {
482        osu.user(self.creator_id)
483    }
484}
485
486impl PartialEq for BeatmapsetExtended {
487    #[inline]
488    fn eq(&self, other: &Self) -> bool {
489        self.mapset_id == other.mapset_id && self.last_updated == other.last_updated
490    }
491}
492
493impl Eq for BeatmapsetExtended {}
494
495#[derive(Clone, Debug, Deserialize)]
496#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
497pub struct BeatmapsetAvailability {
498    pub download_disabled: bool,
499    #[serde(default, skip_serializing_if = "Option::is_none")]
500    pub more_information: Option<String>,
501}
502
503#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
504#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
505pub struct BeatmapsetCommentEdit<T> {
506    #[serde(flatten)]
507    pub comment_id: BeatmapsetCommentId,
508    pub old: T,
509    pub new: T,
510}
511
512#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
513#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
514pub struct BeatmapsetCommentId {
515    #[serde(
516        default,
517        rename = "beatmap_discussion_id",
518        skip_serializing_if = "Option::is_none"
519    )]
520    pub map_discussion_id: Option<u64>,
521    #[serde(
522        default,
523        rename = "beatmap_discussion_post_id",
524        skip_serializing_if = "Option::is_none"
525    )]
526    pub map_discussion_post_id: Option<u64>,
527    #[serde(
528        default,
529        rename = "beatmapset_discussion_id",
530        skip_serializing_if = "Option::is_none"
531    )]
532    pub mapset_discussion_id: Option<u64>,
533    #[serde(
534        default,
535        rename = "beatmapset_discussion_post_id",
536        skip_serializing_if = "Option::is_none"
537    )]
538    pub mapset_discussion_post_id: Option<u64>,
539}
540
541#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
542#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
543pub struct BeatmapsetCommentKudosuGain {
544    #[serde(flatten)]
545    pub comment_id: BeatmapsetCommentId,
546    pub new_vote: BeatmapsetVote,
547    pub votes: Vec<BeatmapsetVote>,
548}
549
550#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
551#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
552pub struct BeatmapsetCommentNominate {
553    pub modes: Vec<GameMode>,
554}
555
556#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
557#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
558pub struct BeatmapsetCommentNominationReset {
559    #[serde(
560        default,
561        rename = "beatmap_discussion_id",
562        skip_serializing_if = "Option::is_none"
563    )]
564    pub map_discussion_id: Option<u64>,
565    #[serde(
566        default,
567        rename = "beatmap_discussion_post_id",
568        skip_serializing_if = "Option::is_none"
569    )]
570    pub map_discussion_post_id: Option<u64>,
571    pub nominator_ids: Vec<u32>,
572}
573
574#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
575#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
576pub struct BeatmapsetCommentNominationResetReceived {
577    #[serde(
578        default,
579        rename = "beatmap_discussion_id",
580        skip_serializing_if = "Option::is_none"
581    )]
582    pub map_discussion_id: Option<u64>,
583    #[serde(
584        default,
585        rename = "beatmap_discussion_post_id",
586        skip_serializing_if = "Option::is_none"
587    )]
588    pub map_discussion_post_id: Option<u64>,
589    pub source_user_id: u32,
590    pub source_user_username: Username,
591}
592
593#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
594#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
595pub struct BeatmapsetCommentOwnerChange {
596    #[serde(
597        default,
598        rename = "beatmap_discussion_id",
599        skip_serializing_if = "Option::is_none"
600    )]
601    pub map_discussion_id: Option<u64>,
602    #[serde(
603        default,
604        rename = "beatmap_discussion_post_id",
605        skip_serializing_if = "Option::is_none"
606    )]
607    pub map_discussion_post_id: Option<u64>,
608    #[serde(rename = "beatmap_id")]
609    pub map_id: u32,
610    #[serde(rename = "beatmap_version")]
611    pub version: String,
612    pub new_user_id: u32,
613    #[serde(rename = "new_user_username")]
614    pub new_username: Username,
615}
616
617/// Represents a beatmapset.
618#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
619#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
620pub struct Beatmapset {
621    pub artist: String,
622    #[serde(default, skip_serializing_if = "Option::is_none")]
623    pub artist_unicode: Option<String>,
624    pub covers: BeatmapsetCovers,
625    #[serde(rename = "creator")]
626    pub creator_name: Username,
627    #[serde(rename = "user_id")]
628    pub creator_id: u32,
629    pub favourite_count: u32,
630    #[serde(default, skip_serializing_if = "Option::is_none")]
631    pub genre: Option<Genre>,
632    #[serde(default, skip_serializing_if = "Option::is_none")]
633    pub hype: Option<BeatmapsetHype>,
634    #[serde(default, skip_serializing_if = "Option::is_none")]
635    pub language: Option<Language>,
636    #[serde(rename = "id")]
637    pub mapset_id: u32,
638    pub nsfw: bool,
639    #[serde(rename = "play_count")]
640    pub playcount: u32,
641    /// Full URL, i.e. `b.ppy.sh/preview/{mapset_id}.mp3`
642    pub preview_url: String,
643    // TODO: Add ratings
644    pub source: String,
645    pub status: RankStatus,
646    pub title: String,
647    #[serde(default, skip_serializing_if = "Option::is_none")]
648    pub title_unicode: Option<String>,
649    pub video: bool,
650}
651
652impl Beatmapset {
653    #[inline]
654    pub fn get_creator<'o>(&self, osu: &'o Osu) -> GetUser<'o> {
655        osu.user(self.creator_id)
656    }
657}
658
659impl ContainedUsers for Beatmapset {
660    fn apply_to_users(&self, f: impl CacheUserFn) {
661        f(self.creator_id, &self.creator_name);
662    }
663}
664
665impl From<BeatmapsetExtended> for Beatmapset {
666    fn from(mapset: BeatmapsetExtended) -> Self {
667        Self {
668            artist: mapset.artist,
669            artist_unicode: mapset.artist_unicode,
670            covers: mapset.covers,
671            creator_name: mapset.creator_name,
672            creator_id: mapset.creator_id,
673            favourite_count: mapset.favourite_count,
674            genre: mapset.genre,
675            hype: mapset.hype,
676            language: mapset.language,
677            mapset_id: mapset.mapset_id,
678            nsfw: mapset.nsfw,
679            playcount: mapset.playcount,
680            preview_url: mapset.preview_url,
681            source: mapset.source,
682            status: mapset.status,
683            title: mapset.title,
684            title_unicode: mapset.title_unicode,
685            video: mapset.video,
686        }
687    }
688}
689
690/// URLs to various sizes of (parts of) the background picture
691#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
692#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
693pub struct BeatmapsetCovers {
694    /// Lengthy part of the background
695    pub cover: String,
696    /// Same as `cover` but larger
697    #[serde(rename = "cover@2x")]
698    pub cover_2x: String,
699    /// Same as `cover` but much smaller
700    pub card: String,
701    /// Same as `card` but larger
702    #[serde(rename = "card@2x")]
703    pub card_2x: String,
704    /// Tiny preview of the full background
705    pub list: String,
706    /// Small preview of the full background
707    #[serde(rename = "list@2x")]
708    pub list_2x: String,
709    /// Same as `cover` but much larger
710    #[serde(rename = "slimcover")]
711    pub slim_cover: String,
712    /// Same as `cover` but huge
713    #[serde(rename = "slimcover@2x")]
714    pub slim_cover_2x: String,
715}
716
717#[derive(Clone, Debug, Deserialize)]
718#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
719pub struct BeatmapsetDiscussion {
720    #[serde(rename = "id")]
721    pub discussion_id: u64,
722    #[serde(rename = "beatmapset_id")]
723    pub mapset_id: u32,
724    #[serde(
725        default,
726        rename = "beatmap_id",
727        skip_serializing_if = "Option::is_none"
728    )]
729    pub map_id: Option<u32>,
730    pub user_id: u32,
731    #[serde(default, skip_serializing_if = "Option::is_none")]
732    pub deleted_by_id: Option<u32>,
733    pub message_type: String, // TODO
734    #[serde(default, skip_serializing_if = "Option::is_none")]
735    pub parent_id: Option<u64>,
736    #[serde(default, skip_serializing_if = "Option::is_none")]
737    pub timestamp: Option<u64>,
738    pub resolved: bool,
739    pub can_be_resolved: bool,
740    pub can_grant_kudosu: bool,
741    #[serde(with = "serde_util::datetime")]
742    pub created_at: OffsetDateTime,
743    #[serde(
744        default,
745        skip_serializing_if = "Option::is_none",
746        with = "serde_util::option_datetime"
747    )]
748    pub updated_at: Option<OffsetDateTime>,
749    #[serde(
750        default,
751        skip_serializing_if = "Option::is_none",
752        with = "serde_util::option_datetime"
753    )]
754    pub deleted_at: Option<OffsetDateTime>,
755    #[serde(with = "serde_util::datetime")]
756    pub last_post_at: OffsetDateTime,
757    pub kudosu_denied: bool,
758    pub starting_post: BeatmapsetPost,
759}
760
761impl PartialEq for BeatmapsetDiscussion {
762    #[inline]
763    fn eq(&self, other: &Self) -> bool {
764        self.discussion_id == other.discussion_id && self.updated_at == other.updated_at
765    }
766}
767
768impl Eq for BeatmapsetDiscussion {}
769
770#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
771#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
772#[serde(rename_all = "snake_case", tag = "type")]
773#[non_exhaustive]
774pub enum BeatmapsetEvent {
775    Disqualify {
776        #[serde(rename = "id")]
777        event_id: u64,
778        comment: BeatmapsetCommentId,
779        #[serde(with = "serde_util::datetime")]
780        created_at: OffsetDateTime,
781        user_id: u32,
782        #[serde(rename = "beatmapset")]
783        mapset: Box<Beatmapset>,
784        discussion: BeatmapsetDiscussion,
785    },
786    GenreEdit {
787        #[serde(rename = "id")]
788        event_id: u64,
789        comment: BeatmapsetCommentEdit<Genre>,
790        #[serde(with = "serde_util::datetime")]
791        created_at: OffsetDateTime,
792        user_id: u32,
793        #[serde(rename = "beatmapset")]
794        mapset: Box<Beatmapset>,
795    },
796    IssueReopen {
797        #[serde(rename = "id")]
798        event_id: u64,
799        comment: BeatmapsetCommentId,
800        #[serde(with = "serde_util::datetime")]
801        created_at: OffsetDateTime,
802        user_id: u32,
803        #[serde(rename = "beatmapset")]
804        mapset: Box<Beatmapset>,
805        discussion: BeatmapsetDiscussion,
806    },
807    IssueResolve {
808        #[serde(rename = "id")]
809        event_id: u64,
810        comment: BeatmapsetCommentId,
811        #[serde(with = "serde_util::datetime")]
812        created_at: OffsetDateTime,
813        user_id: u32,
814        #[serde(rename = "beatmapset")]
815        mapset: Box<Beatmapset>,
816        discussion: BeatmapsetDiscussion,
817    },
818    KudosuDeny {
819        #[serde(rename = "id")]
820        event_id: u64,
821        comment: BeatmapsetCommentId,
822        #[serde(with = "serde_util::datetime")]
823        created_at: OffsetDateTime,
824        #[serde(rename = "beatmapset")]
825        mapset: Box<Beatmapset>,
826        discussion: BeatmapsetDiscussion,
827    },
828    KudosuGain {
829        #[serde(rename = "id")]
830        event_id: u64,
831        comment: BeatmapsetCommentKudosuGain,
832        #[serde(with = "serde_util::datetime")]
833        created_at: OffsetDateTime,
834        user_id: u32,
835        #[serde(rename = "beatmapset")]
836        mapset: Box<Beatmapset>,
837        discussion: BeatmapsetDiscussion,
838    },
839    KudosuLost {
840        #[serde(rename = "id")]
841        event_id: u64,
842        comment: BeatmapsetCommentKudosuGain,
843        #[serde(with = "serde_util::datetime")]
844        created_at: OffsetDateTime,
845        user_id: u32,
846        #[serde(rename = "beatmapset")]
847        mapset: Box<Beatmapset>,
848        discussion: BeatmapsetDiscussion,
849    },
850    LanguageEdit {
851        #[serde(rename = "id")]
852        event_id: u64,
853        comment: BeatmapsetCommentEdit<Language>,
854        #[serde(with = "serde_util::datetime")]
855        created_at: OffsetDateTime,
856        user_id: u32,
857        #[serde(rename = "beatmapset")]
858        mapset: Box<Beatmapset>,
859    },
860    Love {
861        #[serde(rename = "id")]
862        event_id: u64,
863        #[serde(with = "serde_util::datetime")]
864        created_at: OffsetDateTime,
865        user_id: u32,
866        #[serde(rename = "beatmapset")]
867        mapset: Box<Beatmapset>,
868    },
869    Nominate {
870        #[serde(rename = "id")]
871        event_id: u64,
872        comment: BeatmapsetCommentNominate,
873        #[serde(with = "serde_util::datetime")]
874        created_at: OffsetDateTime,
875        user_id: u32,
876        #[serde(rename = "beatmapset")]
877        mapset: Box<Beatmapset>,
878    },
879    NominationReset {
880        #[serde(rename = "id")]
881        event_id: u64,
882        comment: BeatmapsetCommentNominationReset,
883        #[serde(with = "serde_util::datetime")]
884        created_at: OffsetDateTime,
885        user_id: u32,
886        #[serde(rename = "beatmapset")]
887        mapset: Box<Beatmapset>,
888        discussion: BeatmapsetDiscussion,
889    },
890    NominationResetReceived {
891        #[serde(rename = "id")]
892        event_id: u64,
893        comment: BeatmapsetCommentNominationResetReceived,
894        #[serde(with = "serde_util::datetime")]
895        created_at: OffsetDateTime,
896        user_id: u32,
897        #[serde(rename = "beatmapset")]
898        mapset: Box<Beatmapset>,
899        discussion: BeatmapsetDiscussion,
900    },
901    NsfwToggle {
902        #[serde(rename = "id")]
903        event_id: u64,
904        comment: BeatmapsetCommentEdit<bool>,
905        #[serde(with = "serde_util::datetime")]
906        created_at: OffsetDateTime,
907        user_id: u32,
908        #[serde(rename = "beatmapset")]
909        mapset: Box<Beatmapset>,
910    },
911    #[serde(rename = "beatmap_owner_change")]
912    OwnerChange {
913        #[serde(rename = "id")]
914        event_id: u64,
915        comment: BeatmapsetCommentOwnerChange,
916        #[serde(with = "serde_util::datetime")]
917        created_at: OffsetDateTime,
918        user_id: u32,
919        #[serde(rename = "beatmapset")]
920        mapset: Box<Beatmapset>,
921    },
922    Rank {
923        #[serde(rename = "id")]
924        event_id: u64,
925        #[serde(with = "serde_util::datetime")]
926        created_at: OffsetDateTime,
927        #[serde(rename = "beatmapset")]
928        mapset: Box<Beatmapset>,
929    },
930    Qualify {
931        #[serde(rename = "id")]
932        event_id: u64,
933        #[serde(with = "serde_util::datetime")]
934        created_at: OffsetDateTime,
935        #[serde(rename = "beatmapset")]
936        mapset: Box<Beatmapset>,
937    },
938    TagsEdit {
939        #[serde(rename = "id")]
940        event_id: u64,
941        comment: BeatmapsetCommentEdit<String>,
942        #[serde(with = "serde_util::datetime")]
943        created_at: OffsetDateTime,
944        #[serde(rename = "beatmapset")]
945        mapset: Box<Beatmapset>,
946    },
947}
948
949impl ContainedUsers for BeatmapsetEvent {
950    fn apply_to_users(&self, f: impl CacheUserFn) {
951        let mapset = match self {
952            BeatmapsetEvent::Disqualify { mapset, .. } => mapset,
953            BeatmapsetEvent::GenreEdit { mapset, .. } => mapset,
954            BeatmapsetEvent::IssueReopen { mapset, .. } => mapset,
955            BeatmapsetEvent::IssueResolve { mapset, .. } => mapset,
956            BeatmapsetEvent::KudosuDeny { mapset, .. } => mapset,
957            BeatmapsetEvent::KudosuGain { mapset, .. } => mapset,
958            BeatmapsetEvent::KudosuLost { mapset, .. } => mapset,
959            BeatmapsetEvent::LanguageEdit { mapset, .. } => mapset,
960            BeatmapsetEvent::Love { mapset, .. } => mapset,
961            BeatmapsetEvent::Nominate { mapset, .. } => mapset,
962            BeatmapsetEvent::NominationReset { mapset, .. } => mapset,
963            BeatmapsetEvent::NominationResetReceived { mapset, .. } => mapset,
964            BeatmapsetEvent::NsfwToggle { mapset, .. } => mapset,
965            BeatmapsetEvent::OwnerChange { mapset, .. } => mapset,
966            BeatmapsetEvent::Rank { mapset, .. } => mapset,
967            BeatmapsetEvent::Qualify { mapset, .. } => mapset,
968            BeatmapsetEvent::TagsEdit { mapset, .. } => mapset,
969        };
970
971        mapset.apply_to_users(f);
972    }
973}
974
975#[derive(Clone, Debug, Deserialize, PartialEq)]
976#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
977pub struct BeatmapsetEvents {
978    pub events: Vec<BeatmapsetEvent>,
979    #[serde(rename = "reviewsConfig")]
980    pub reviews_config: BeatmapsetReviewsConfig,
981    pub users: Vec<User>,
982}
983
984impl ContainedUsers for BeatmapsetEvents {
985    fn apply_to_users(&self, f: impl CacheUserFn) {
986        self.events.apply_to_users(f);
987        self.users.apply_to_users(f);
988    }
989}
990
991#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
992#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
993pub struct BeatmapsetHype {
994    pub current: u32,
995    pub required: u32,
996}
997
998#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
999#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
1000pub struct BeatmapsetNominations {
1001    pub current: u32,
1002    pub eligible_main_rulesets: Option<Vec<GameMode>>,
1003    pub required_meta: BeatmapsetNominationsRequiredMeta,
1004}
1005
1006#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
1007#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
1008pub struct BeatmapsetNominationsRequiredMeta {
1009    #[serde(rename = "main_ruleset")]
1010    pub main_mode: GameMode,
1011    #[serde(rename = "non_main_ruleset")]
1012    pub non_main_mode: GameMode,
1013}
1014
1015#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
1016#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
1017pub struct BeatmapsetPost {
1018    #[serde(rename = "id")]
1019    pub post_id: u64,
1020    #[serde(rename = "beatmapset_discussion_id")]
1021    pub discussion_id: u64,
1022    pub user_id: u32,
1023    #[serde(default, skip_serializing_if = "Option::is_none")]
1024    pub last_editor_id: Option<u32>,
1025    #[serde(default, skip_serializing_if = "Option::is_none")]
1026    pub deleted_by_id: Option<u32>,
1027    pub system: bool,
1028    pub message: String,
1029    #[serde(with = "serde_util::datetime")]
1030    pub created_at: OffsetDateTime,
1031    #[serde(
1032        default,
1033        skip_serializing_if = "Option::is_none",
1034        with = "serde_util::option_datetime"
1035    )]
1036    pub updated_at: Option<OffsetDateTime>,
1037    #[serde(
1038        default,
1039        skip_serializing_if = "Option::is_none",
1040        with = "serde_util::option_datetime"
1041    )]
1042    pub deleted_at: Option<OffsetDateTime>,
1043}
1044
1045#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
1046#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
1047pub struct BeatmapsetReviewsConfig {
1048    pub max_blocks: u32,
1049}
1050
1051#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
1052pub struct BeatmapScores {
1053    pub score_count: usize,
1054    pub scores: Vec<Score>,
1055}
1056
1057impl ContainedUsers for BeatmapScores {
1058    fn apply_to_users(&self, f: impl CacheUserFn) {
1059        self.scores.apply_to_users(f);
1060    }
1061}
1062
1063#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1064pub(crate) enum SearchRankStatus {
1065    Any,
1066    Specific(RankStatus),
1067}
1068
1069const SEARCH_RANK_STATUS_ANY: i8 = 9;
1070
1071struct SearchRankStatusVisitor;
1072
1073impl Visitor<'_> for SearchRankStatusVisitor {
1074    type Value = SearchRankStatus;
1075
1076    fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
1077        f.write_str("a rank status, \"any\", or `9`")
1078    }
1079
1080    fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E> {
1081        let visitor = super::EnumVisitor::<RankStatus>::new();
1082
1083        if let Ok(status) = visitor.visit_str::<E>(s) {
1084            Ok(SearchRankStatus::Specific(status))
1085        } else if s == "any" {
1086            Ok(SearchRankStatus::Any)
1087        } else {
1088            Err(Error::invalid_value(
1089                Unexpected::Str(s),
1090                &"a rank status or \"any\"",
1091            ))
1092        }
1093    }
1094
1095    fn visit_u64<E: Error>(self, u: u64) -> Result<Self::Value, E> {
1096        let visitor = super::EnumVisitor::<RankStatus>::new();
1097
1098        if let Ok(status) = visitor.visit_u64::<E>(u) {
1099            Ok(SearchRankStatus::Specific(status))
1100        } else if u as i8 == SEARCH_RANK_STATUS_ANY {
1101            Ok(SearchRankStatus::Any)
1102        } else {
1103            Err(Error::invalid_value(
1104                Unexpected::Unsigned(u),
1105                &"a RankStatus i8 or `9`",
1106            ))
1107        }
1108    }
1109
1110    fn visit_i64<E: Error>(self, i: i64) -> Result<Self::Value, E> {
1111        let visitor = super::EnumVisitor::<RankStatus>::new();
1112
1113        if let Ok(status) = visitor.visit_i64::<E>(i) {
1114            Ok(SearchRankStatus::Specific(status))
1115        } else if i as i8 == SEARCH_RANK_STATUS_ANY {
1116            Ok(SearchRankStatus::Any)
1117        } else {
1118            Err(Error::invalid_value(
1119                Unexpected::Signed(i),
1120                &"a RankStatus i8 or `9`",
1121            ))
1122        }
1123    }
1124}
1125
1126impl<'de> Deserialize<'de> for SearchRankStatus {
1127    #[inline]
1128    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1129        d.deserialize_any(SearchRankStatusVisitor)
1130    }
1131}
1132
1133#[cfg(feature = "serialize")]
1134impl serde::Serialize for SearchRankStatus {
1135    #[inline]
1136    fn serialize<S: serde::ser::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1137        match self {
1138            Self::Any => s.serialize_i8(SEARCH_RANK_STATUS_ANY),
1139            Self::Specific(status) => s.serialize_i8(*status as i8),
1140        }
1141    }
1142}
1143
1144#[derive(Clone, Debug, Eq, PartialEq)]
1145#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
1146pub(crate) struct BeatmapsetSearchParameters {
1147    #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))]
1148    pub(crate) query: Option<String>,
1149    #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))]
1150    pub(crate) mode: Option<u8>,
1151    #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))]
1152    pub(crate) status: Option<SearchRankStatus>,
1153    #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))]
1154    pub(crate) genre: Option<u8>,
1155    #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))]
1156    pub(crate) language: Option<u8>,
1157    pub(crate) video: bool,
1158    pub(crate) storyboard: bool,
1159    pub(crate) recommended: bool,
1160    pub(crate) converts: bool,
1161    pub(crate) follows: bool,
1162    pub(crate) spotlights: bool,
1163    pub(crate) featured_artists: bool,
1164    pub(crate) nsfw: bool,
1165    #[cfg_attr(feature = "serialize", serde(rename(serialize = "_sort")))]
1166    sort: BeatmapsetSearchSort,
1167    descending: bool,
1168}
1169
1170impl Default for BeatmapsetSearchParameters {
1171    #[inline]
1172    fn default() -> Self {
1173        Self {
1174            query: None,
1175            mode: None,
1176            status: None,
1177            genre: None,
1178            language: None,
1179            video: false,
1180            storyboard: false,
1181            recommended: false,
1182            converts: false,
1183            follows: false,
1184            spotlights: false,
1185            featured_artists: false,
1186            nsfw: true,
1187            sort: BeatmapsetSearchSort::default(),
1188            descending: true,
1189        }
1190    }
1191}
1192
1193struct BeatmapsetSearchParametersVisitor;
1194
1195impl<'de> Visitor<'de> for BeatmapsetSearchParametersVisitor {
1196    type Value = BeatmapsetSearchParameters;
1197
1198    fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
1199        f.write_str("a search struct")
1200    }
1201
1202    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
1203        let mut params = None;
1204
1205        let mut query = None;
1206        let mut mode = None;
1207        let mut status = None;
1208        let mut genre = None;
1209        let mut language = None;
1210        let mut video = None;
1211        let mut storyboard = None;
1212        let mut recommended = None;
1213        let mut converts = None;
1214        let mut follows = None;
1215        let mut spotlights = None;
1216        let mut featured_artists = None;
1217        let mut nsfw = None;
1218        let mut sort = None;
1219        let mut descending = None;
1220
1221        while let Some(key) = map.next_key()? {
1222            match key {
1223                "sort" => {
1224                    let SubSort { sort, descending } = map.next_value()?;
1225
1226                    params = Some(BeatmapsetSearchParameters {
1227                        sort,
1228                        descending,
1229                        ..Default::default()
1230                    });
1231                }
1232                "query" => query = map.next_value()?,
1233                "mode" => mode = Some(map.next_value()?),
1234                "status" => status = Some(map.next_value()?),
1235                "genre" => genre = Some(map.next_value()?),
1236                "language" => language = Some(map.next_value()?),
1237                "video" => video = Some(map.next_value()?),
1238                "storyboard" => storyboard = Some(map.next_value()?),
1239                "recommended" => recommended = Some(map.next_value()?),
1240                "converts" => converts = Some(map.next_value()?),
1241                "follows" => follows = Some(map.next_value()?),
1242                "spotlights" => spotlights = Some(map.next_value()?),
1243                "featured_artists" => featured_artists = Some(map.next_value()?),
1244                "nsfw" => nsfw = Some(map.next_value()?),
1245                "_sort" => sort = Some(map.next_value()?),
1246                "descending" => descending = Some(map.next_value()?),
1247                _ => {
1248                    let _: IgnoredAny = map.next_value()?;
1249                }
1250            }
1251        }
1252
1253        if let Some(params) = params {
1254            return Ok(params);
1255        }
1256
1257        let sort = sort.ok_or_else(|| Error::missing_field("sort or _sort"))?;
1258        let video = video.ok_or_else(|| Error::missing_field("sort or video"))?;
1259        let storyboard = storyboard.ok_or_else(|| Error::missing_field("sort or storyboard"))?;
1260        let recommended = recommended.ok_or_else(|| Error::missing_field("sort or recommended"))?;
1261        let converts = converts.ok_or_else(|| Error::missing_field("sort or converts"))?;
1262        let follows = follows.ok_or_else(|| Error::missing_field("sort or follows"))?;
1263        let spotlights = spotlights.ok_or_else(|| Error::missing_field("sort or spotlights"))?;
1264        let featured_artists =
1265            featured_artists.ok_or_else(|| Error::missing_field("sort or featured_artists"))?;
1266        let nsfw = nsfw.ok_or_else(|| Error::missing_field("sort or nsfw"))?;
1267        let descending = descending.ok_or_else(|| Error::missing_field("sort or descending"))?;
1268
1269        let params = BeatmapsetSearchParameters {
1270            query,
1271            mode,
1272            status,
1273            genre,
1274            language,
1275            video,
1276            storyboard,
1277            recommended,
1278            converts,
1279            follows,
1280            spotlights,
1281            featured_artists,
1282            nsfw,
1283            sort,
1284            descending,
1285        };
1286
1287        Ok(params)
1288    }
1289}
1290
1291impl<'de> Deserialize<'de> for BeatmapsetSearchParameters {
1292    #[inline]
1293    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1294        d.deserialize_map(BeatmapsetSearchParametersVisitor)
1295    }
1296}
1297
1298#[derive(Clone, Debug, Eq, PartialEq)]
1299#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
1300pub struct BeatmapsetSearchResult {
1301    #[cfg_attr(feature = "serialize", serde(rename = "cursor_string"))]
1302    cursor: Option<Box<str>>,
1303    /// All mapsets of the current page
1304    #[cfg_attr(feature = "serialize", serde(rename(serialize = "beatmapsets")))]
1305    pub mapsets: Vec<BeatmapsetExtended>,
1306    #[cfg_attr(feature = "serialize", serde(rename(serialize = "search")))]
1307    pub(crate) params: BeatmapsetSearchParameters,
1308    /// Total amount of mapsets that fit the search query
1309    pub total: u32,
1310}
1311
1312impl BeatmapsetSearchResult {
1313    /// Returns whether there is a next page of search results,
1314    /// retrievable via [`get_next`](BeatmapsetSearchResult::get_next).
1315    #[inline]
1316    pub const fn has_more(&self) -> bool {
1317        self.cursor.is_some()
1318    }
1319
1320    /// If [`has_more`](BeatmapsetSearchResult::has_more) is true, the API can provide
1321    /// the next set of search results and this method will request them.
1322    /// Otherwise, this method returns `None`.
1323    pub async fn get_next(&self, osu: &Osu) -> Option<OsuResult<BeatmapsetSearchResult>> {
1324        let cursor = self.cursor.as_deref()?;
1325        let params = &self.params;
1326
1327        let mut fut = osu
1328            .beatmapset_search()
1329            .cursor(cursor)
1330            .video(params.video)
1331            .storyboard(params.storyboard)
1332            .recommended(params.recommended)
1333            .converts(params.converts)
1334            .follows(params.follows)
1335            .spotlights(params.spotlights)
1336            .featured_artists(params.featured_artists)
1337            .nsfw(params.nsfw)
1338            .sort(params.sort, params.descending);
1339
1340        if let Some(ref query) = params.query {
1341            fut = fut.query(query);
1342        }
1343
1344        if let Some(mode) = params.mode.map(GameMode::from) {
1345            fut = fut.mode(mode);
1346        }
1347
1348        match params.status {
1349            None => {}
1350            Some(SearchRankStatus::Specific(status)) => fut = fut.status(Some(status)),
1351            Some(SearchRankStatus::Any) => fut = fut.status(None),
1352        }
1353
1354        if let Some(Ok(genre)) = params.genre.map(Genre::try_from) {
1355            fut = fut.genre(genre);
1356        }
1357
1358        if let Some(Ok(language)) = params.language.map(Language::try_from) {
1359            fut = fut.language(language);
1360        }
1361
1362        Some(fut.await)
1363    }
1364}
1365
1366impl ContainedUsers for BeatmapsetSearchResult {
1367    fn apply_to_users(&self, f: impl CacheUserFn) {
1368        self.mapsets.apply_to_users(f);
1369    }
1370}
1371
1372struct BeatmapsetSearchResultVisitor;
1373
1374impl<'de> Visitor<'de> for BeatmapsetSearchResultVisitor {
1375    type Value = BeatmapsetSearchResult;
1376
1377    fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
1378        f.write_str("a BeatmapsetSearchResult struct")
1379    }
1380
1381    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
1382        let mut mapsets = None;
1383        let mut cursor = None;
1384        let mut params = None;
1385        let mut total = None;
1386
1387        while let Some(key) = map.next_key()? {
1388            match key {
1389                "beatmapsets" => mapsets = Some(map.next_value()?),
1390                "cursor_string" => cursor = map.next_value()?,
1391                "search" => params = Some(map.next_value()?),
1392                "total" => total = Some(map.next_value()?),
1393                _ => {
1394                    let _: IgnoredAny = map.next_value()?;
1395                }
1396            }
1397        }
1398
1399        let mapsets = mapsets.ok_or_else(|| Error::missing_field("beatmapsets"))?;
1400        let params = params.unwrap_or_default();
1401        let total = total.ok_or_else(|| Error::missing_field("total"))?;
1402
1403        Ok(BeatmapsetSearchResult {
1404            cursor,
1405            mapsets,
1406            params,
1407            total,
1408        })
1409    }
1410}
1411
1412impl<'de> Deserialize<'de> for BeatmapsetSearchResult {
1413    #[inline]
1414    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1415        d.deserialize_map(BeatmapsetSearchResultVisitor)
1416    }
1417}
1418
1419macro_rules! search_sort_enum {
1420    ( $( $( #[$meta:meta] )? $variant:ident => $name:literal ,)+ ) => {
1421        /// Provides an option to specify a mapset order in a mapset search,
1422        /// see [`Osu::beatmapset_search`](crate::client::Osu::beatmapset_search).
1423        #[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq)]
1424        #[cfg_attr(feature = "serialize", derive(serde::Serialize))]
1425        pub enum BeatmapsetSearchSort {
1426            $(
1427                #[serde(rename = $name)]
1428                $( #[$meta] )?
1429                $variant,
1430            )+
1431        }
1432
1433        impl Display for BeatmapsetSearchSort {
1434            #[inline]
1435            fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
1436                match self {
1437                    $(Self::$variant => f.write_str($name),)+
1438                }
1439            }
1440        }
1441
1442        impl FromStr for BeatmapsetSearchSort {
1443            type Err = ();
1444
1445            #[inline]
1446            fn from_str(s: &str) -> Result<Self, Self::Err> {
1447                match s {
1448                    $($name => Ok(Self::$variant),)+
1449                    _ => Err(()),
1450                }
1451            }
1452        }
1453
1454        impl<'de> Deserialize<'de> for SubSort {
1455            fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1456                let s: &str = Deserialize::deserialize(d)?;
1457
1458                let underscore = s.find('_').ok_or_else(|| {
1459                    Error::invalid_value(Unexpected::Str(s), &"a string containing an underscore")
1460                })?;
1461
1462                let sort = s[..underscore].parse().map_err(|_| {
1463                    Error::invalid_value(
1464                        Unexpected::Str(&s[..underscore]),
1465                        &stringify!($($name),+),
1466                    )
1467                })?;
1468
1469                let descending = match s.get(underscore + 1..) {
1470                    Some("desc") => true,
1471                    Some("asc") => false,
1472                    _ => return Err(Error::invalid_value(Unexpected::Str(s), &"*_desc or *_asc")),
1473                };
1474
1475                Ok(SubSort { sort, descending })
1476            }
1477        }
1478    }
1479}
1480
1481search_sort_enum! {
1482    Artist => "artist",
1483    Creator => "creator",
1484    Favourites => "favourites",
1485    Nominations => "nominations",
1486    Playcount => "plays",
1487    ApprovedDate => "ranked",
1488    Rating => "rating",
1489    #[default]
1490    Relevance => "relevance",
1491    Stars => "difficulty",
1492    Title => "title",
1493    LastUpdate => "updated",
1494}
1495
1496struct SubSort {
1497    sort: BeatmapsetSearchSort,
1498    descending: bool,
1499}
1500
1501#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
1502#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
1503pub struct BeatmapsetVote {
1504    pub user_id: u32,
1505    pub score: i32,
1506}
1507
1508/// All fields are optional but there's always at least one field returned.
1509#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
1510#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
1511pub struct FailTimes {
1512    /// List of length 100
1513    #[serde(
1514        default,
1515        with = "hundred_items",
1516        skip_serializing_if = "Option::is_none"
1517    )]
1518    pub exit: Option<Box<[u32; 100]>>,
1519    /// List of length 100
1520    #[serde(
1521        default,
1522        with = "hundred_items",
1523        skip_serializing_if = "Option::is_none"
1524    )]
1525    pub fail: Option<Box<[u32; 100]>>,
1526}
1527
1528mod hundred_items {
1529    use serde::de::{Deserializer, Error as DeError, SeqAccess, Visitor};
1530    use std::fmt::{Formatter, Result as FmtResult};
1531
1532    pub(super) fn deserialize<'de, D: Deserializer<'de>>(
1533        d: D,
1534    ) -> Result<Option<Box<[u32; 100]>>, D::Error> {
1535        d.deserialize_option(VecOptionVisitor)
1536    }
1537
1538    #[cfg(feature = "serialize")]
1539    #[allow(clippy::ref_option, reason = "required by serde")]
1540    pub(super) fn serialize<S: serde::Serializer>(
1541        opt: &Option<Box<[u32; 100]>>,
1542        s: S,
1543    ) -> Result<S::Ok, S::Error> {
1544        use serde::ser::SerializeSeq;
1545
1546        let Some(seq) = opt else {
1547            return s.serialize_none();
1548        };
1549
1550        let mut state = s.serialize_seq(Some(seq.len()))?;
1551
1552        for item in seq.iter() {
1553            state.serialize_element(item)?;
1554        }
1555
1556        state.end()
1557    }
1558
1559    struct VecOptionVisitor;
1560
1561    impl<'de> Visitor<'de> for VecOptionVisitor {
1562        type Value = Option<Box<[u32; 100]>>;
1563
1564        fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
1565            f.write_str("null or a sequence of u32")
1566        }
1567
1568        #[inline]
1569        fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
1570            let mut ids = Vec::with_capacity(100);
1571
1572            while let Some(n) = seq.next_element()? {
1573                ids.push(n);
1574            }
1575
1576            ids.try_into()
1577                .map(Some)
1578                .map_err(|ids: Vec<u32>| DeError::invalid_length(ids.len(), &"100"))
1579        }
1580
1581        #[inline]
1582        fn visit_some<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
1583            d.deserialize_seq(self)
1584        }
1585
1586        #[inline]
1587        fn visit_none<E: DeError>(self) -> Result<Self::Value, E> {
1588            self.visit_unit()
1589        }
1590
1591        #[inline]
1592        fn visit_unit<E: DeError>(self) -> Result<Self::Value, E> {
1593            Ok(None)
1594        }
1595    }
1596}
1597
1598#[derive(Clone, Debug, Deserialize)]
1599#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
1600pub struct MostPlayedMap {
1601    pub count: usize,
1602    #[serde(rename = "beatmap")]
1603    pub map: Box<Beatmap>,
1604    #[serde(rename = "beatmap_id")]
1605    pub map_id: u32,
1606    #[serde(rename = "beatmapset")]
1607    pub mapset: Box<Beatmapset>,
1608}
1609
1610impl ContainedUsers for MostPlayedMap {
1611    fn apply_to_users(&self, f: impl CacheUserFn) {
1612        self.map.apply_to_users(f);
1613        self.mapset.apply_to_users(f);
1614    }
1615}
1616
1617impl PartialEq for MostPlayedMap {
1618    #[inline]
1619    fn eq(&self, other: &Self) -> bool {
1620        self.map_id == other.map_id && self.count == other.count
1621    }
1622}
1623
1624impl Eq for MostPlayedMap {}
1625
1626#[allow(clippy::upper_case_acronyms, missing_docs)]
1627#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
1628pub enum RankStatus {
1629    Graveyard = -2,
1630    WIP = -1,
1631    Pending = 0,
1632    Ranked = 1,
1633    Approved = 2,
1634    Qualified = 3,
1635    Loved = 4,
1636}
1637
1638impl<'de> serde::Deserialize<'de> for RankStatus {
1639    #[inline]
1640    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1641        d.deserialize_option(super::EnumVisitor::<RankStatus>::new())
1642    }
1643}
1644impl From<RankStatus> for i8 {
1645    #[inline]
1646    fn from(v: RankStatus) -> Self {
1647        v as i8
1648    }
1649}
1650
1651impl TryFrom<i8> for RankStatus {
1652    type Error = OsuError;
1653
1654    #[inline]
1655    fn try_from(value: i8) -> Result<Self, Self::Error> {
1656        match value {
1657            -2 => Ok(Self::Graveyard),
1658            -1 => Ok(Self::WIP),
1659            0 => Ok(Self::Pending),
1660            1 => Ok(Self::Ranked),
1661            2 => Ok(Self::Approved),
1662            3 => Ok(Self::Qualified),
1663            4 => Ok(Self::Loved),
1664            _ => Err(ParsingError::RankStatus(value).into()),
1665        }
1666    }
1667}
1668
1669#[cfg(feature = "serialize")]
1670impl serde::Serialize for RankStatus {
1671    #[inline]
1672    fn serialize<S: serde::ser::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1673        s.serialize_i8(*self as i8)
1674    }
1675}
1676
1677impl<'de> Visitor<'de> for super::EnumVisitor<RankStatus> {
1678    type Value = RankStatus;
1679
1680    fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
1681        f.write_str("an optional RankStatus i8")
1682    }
1683
1684    fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E> {
1685        match s {
1686            "graveyard" => Ok(RankStatus::Graveyard),
1687            "wip" => Ok(RankStatus::WIP),
1688            "pending" => Ok(RankStatus::Pending),
1689            "ranked" => Ok(RankStatus::Ranked),
1690            "approved" => Ok(RankStatus::Approved),
1691            "qualified" => Ok(RankStatus::Qualified),
1692            "loved" => Ok(RankStatus::Loved),
1693            _ => Err(Error::unknown_variant(
1694                s,
1695                &[
1696                    "graveyard",
1697                    "wip",
1698                    "pending",
1699                    "ranked",
1700                    "approved",
1701                    "qualified",
1702                    "loved",
1703                ],
1704            )),
1705        }
1706    }
1707
1708    fn visit_i64<E: Error>(self, v: i64) -> Result<Self::Value, E> {
1709        match v {
1710            -2 => Ok(RankStatus::Graveyard),
1711            -1 => Ok(RankStatus::WIP),
1712            0 => Ok(RankStatus::Pending),
1713            1 => Ok(RankStatus::Ranked),
1714            2 => Ok(RankStatus::Approved),
1715            3 => Ok(RankStatus::Qualified),
1716            4 => Ok(RankStatus::Loved),
1717            _ => Err(Error::invalid_value(
1718                Unexpected::Signed(v),
1719                &"-2, -1, 0, 1, 2, 3 or 4",
1720            )),
1721        }
1722    }
1723
1724    fn visit_u64<E: Error>(self, v: u64) -> Result<Self::Value, E> {
1725        match v {
1726            0 => Ok(RankStatus::Pending),
1727            1 => Ok(RankStatus::Ranked),
1728            2 => Ok(RankStatus::Approved),
1729            3 => Ok(RankStatus::Qualified),
1730            4 => Ok(RankStatus::Loved),
1731            _ => Err(Error::invalid_value(
1732                Unexpected::Unsigned(v),
1733                &"-2, -1, 0, 1, 2, 3 or 4",
1734            )),
1735        }
1736    }
1737
1738    #[inline]
1739    fn visit_some<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
1740        d.deserialize_any(self)
1741    }
1742}
1743
1744def_enum!(Genre {
1745    Any = 0 ("Any"),
1746    Unspecified = 1 ("Unspecified"),
1747    VideoGame = 2 ("Video Game"),
1748    Anime = 3 ("Anime"),
1749    Rock = 4 ("Rock"),
1750    Pop = 5 ("Pop"),
1751    Other = 6 ("Other"),
1752    Novelty = 7 ("Novelty"),
1753    HipHop = 9 ("Hip Hop"),
1754    Electronic = 10 ("Electronic"),
1755    Metal = 11 ("Metal"),
1756    Classical = 12 ("Classical"),
1757    Folk = 13 ("Folk"),
1758    Jazz = 14 ("Jazz"),
1759});
1760
1761impl Default for Genre {
1762    #[inline]
1763    fn default() -> Self {
1764        Self::Any
1765    }
1766}
1767
1768def_enum!(Language {
1769    Any = 0,
1770    Other = 1,
1771    English = 2,
1772    Japanese = 3,
1773    Chinese = 4,
1774    Instrumental = 5,
1775    Korean = 6,
1776    French = 7,
1777    German = 8,
1778    Swedish = 9,
1779    Spanish = 10,
1780    Italian = 11,
1781    Russian = 12,
1782    Polish = 13,
1783    Unspecified = 14,
1784});
1785
1786impl Default for Language {
1787    #[inline]
1788    fn default() -> Self {
1789        Self::Any
1790    }
1791}
1792
1793struct DescriptionVisitor;
1794
1795impl<'de> Visitor<'de> for DescriptionVisitor {
1796    type Value = Option<String>;
1797
1798    fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
1799        f.write_str("a string or a map containing a 'description' field")
1800    }
1801
1802    #[inline]
1803    fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
1804        Ok(Some(v.to_owned()))
1805    }
1806
1807    #[inline]
1808    fn visit_string<E: Error>(self, v: String) -> Result<Self::Value, E> {
1809        Ok(Some(v))
1810    }
1811
1812    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
1813        let mut description = None;
1814
1815        while let Some(key) = map.next_key()? {
1816            match key {
1817                "description" => description = map.next_value()?,
1818                _ => {
1819                    let _: IgnoredAny = map.next_value()?;
1820                }
1821            }
1822        }
1823
1824        Ok(description)
1825    }
1826
1827    #[inline]
1828    fn visit_some<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
1829        d.deserialize_any(self)
1830    }
1831
1832    #[inline]
1833    fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
1834        self.visit_unit()
1835    }
1836
1837    #[inline]
1838    fn visit_unit<E: Error>(self) -> Result<Self::Value, E> {
1839        Ok(None)
1840    }
1841}
1842
1843fn flatten_description<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
1844    d.deserialize_option(DescriptionVisitor)
1845}
1846
1847#[cfg(test)]
1848#[cfg(feature = "serialize")]
1849mod serde_tests {
1850    use super::*;
1851    use serde::de::DeserializeOwned;
1852    use std::fmt::Debug;
1853
1854    fn ser_de<T: DeserializeOwned + serde::Serialize + PartialEq + Debug>(val: &T) {
1855        let serialized =
1856            serde_json::to_string(val).unwrap_or_else(|e| panic!("Failed to serialize: {e}"));
1857
1858        let deserialized: T = serde_json::from_str(&serialized)
1859            .unwrap_or_else(|e| panic!("Failed to deserialize: {e}"));
1860
1861        assert_eq!(val, &deserialized);
1862    }
1863
1864    #[test]
1865    fn ser_de_search_result_any_status() {
1866        let search_result = BeatmapsetSearchResult {
1867            cursor: None,
1868            mapsets: Vec::new(),
1869            params: BeatmapsetSearchParameters {
1870                query: Some("my query".to_owned()),
1871                mode: Some(1),
1872                status: Some(SearchRankStatus::Any),
1873                genre: Some(4),
1874                language: Some(5),
1875                video: true,
1876                storyboard: false,
1877                recommended: true,
1878                converts: true,
1879                follows: true,
1880                spotlights: false,
1881                featured_artists: false,
1882                nsfw: false,
1883                sort: BeatmapsetSearchSort::ApprovedDate,
1884                descending: false,
1885            },
1886            total: 42,
1887        };
1888
1889        ser_de(&search_result);
1890    }
1891
1892    #[test]
1893    fn ser_de_search_result_specific_status() {
1894        let search_result = BeatmapsetSearchResult {
1895            cursor: None,
1896            mapsets: Vec::new(),
1897            params: BeatmapsetSearchParameters {
1898                query: None,
1899                mode: Some(1),
1900                status: Some(SearchRankStatus::Specific(RankStatus::Pending)),
1901                genre: None,
1902                language: Some(5),
1903                video: true,
1904                storyboard: false,
1905                recommended: false,
1906                converts: false,
1907                follows: true,
1908                spotlights: true,
1909                featured_artists: true,
1910                nsfw: true,
1911                sort: BeatmapsetSearchSort::Playcount,
1912                descending: true,
1913            },
1914            total: 42,
1915        };
1916
1917        ser_de(&search_result);
1918    }
1919}