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 pub url: String,
72 pub version: String,
73}
74
75impl BeatmapExtended {
76 #[inline]
78 pub const fn count_objects(&self) -> u32 {
79 self.count_circles + self.count_sliders + self.count_spinners
80 }
81
82 #[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 #[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#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub converts: Option<Vec<BeatmapExtended>>,
229 pub covers: BeatmapsetCovers,
230 #[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 #[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 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#[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#[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 pub preview_url: String,
643 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#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
692#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
693pub struct BeatmapsetCovers {
694 pub cover: String,
696 #[serde(rename = "cover@2x")]
698 pub cover_2x: String,
699 pub card: String,
701 #[serde(rename = "card@2x")]
703 pub card_2x: String,
704 pub list: String,
706 #[serde(rename = "list@2x")]
708 pub list_2x: String,
709 #[serde(rename = "slimcover")]
711 pub slim_cover: String,
712 #[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, #[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 #[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 pub total: u32,
1310}
1311
1312impl BeatmapsetSearchResult {
1313 #[inline]
1316 pub const fn has_more(&self) -> bool {
1317 self.cursor.is_some()
1318 }
1319
1320 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 #[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#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
1510#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
1511pub struct FailTimes {
1512 #[serde(
1514 default,
1515 with = "hundred_items",
1516 skip_serializing_if = "Option::is_none"
1517 )]
1518 pub exit: Option<Box<[u32; 100]>>,
1519 #[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}