1use crate::{
2 model::{
3 beatmap::{
4 Beatmap, BeatmapDifficultyAttributes, BeatmapDifficultyAttributesWrapper,
5 BeatmapExtended, BeatmapScores, BeatmapsetEvents, BeatmapsetExtended,
6 BeatmapsetSearchParameters, BeatmapsetSearchResult, BeatmapsetSearchSort, Genre,
7 Language, RankStatus, SearchRankStatus,
8 },
9 score::{BeatmapUserScore, Score},
10 DeserializedList, GameMode,
11 },
12 prelude::GameModsIntermode,
13 request::{
14 serialize::{maybe_mode_as_str, maybe_mods_as_list},
15 Query, Request,
16 },
17 routing::Route,
18 Osu,
19};
20
21use itoa::Buffer;
22use serde::ser::SerializeMap;
23use serde::{Serialize, Serializer};
24use std::fmt::Write;
25
26use super::{JsonBody, UserId};
27
28#[must_use = "requests must be configured and executed"]
30#[derive(Serialize)]
31pub struct GetBeatmap<'a> {
32 #[serde(skip)]
33 osu: &'a Osu,
34 checksum: Option<String>,
35 filename: Option<String>,
36 #[serde(rename(serialize = "id"))]
37 map_id: Option<u32>,
38}
39
40impl<'a> GetBeatmap<'a> {
41 pub(crate) const fn new(osu: &'a Osu) -> Self {
42 Self {
43 osu,
44 checksum: None,
45 filename: None,
46 map_id: None,
47 }
48 }
49
50 #[inline]
52 pub fn checksum(mut self, checksum: impl Into<String>) -> Self {
53 self.checksum = Some(checksum.into());
54
55 self
56 }
57
58 #[inline]
60 pub fn filename(mut self, filename: impl Into<String>) -> Self {
61 self.filename = Some(filename.into());
62
63 self
64 }
65
66 #[inline]
68 pub const fn map_id(mut self, map_id: u32) -> Self {
69 self.map_id = Some(map_id);
70
71 self
72 }
73}
74
75into_future! {
76 |self: GetBeatmap<'_>| -> BeatmapExtended {
77 Request::with_query(Route::GetBeatmap, Query::encode(&self))
78 }
79}
80
81#[must_use = "requests must be configured and executed"]
83pub struct GetBeatmaps<'a> {
84 osu: &'a Osu,
85 query: String,
86}
87
88impl<'a> GetBeatmaps<'a> {
89 pub(crate) fn new<I>(osu: &'a Osu, map_ids: I) -> Self
90 where
91 I: IntoIterator<Item = u32>,
92 {
93 let mut query = String::new();
94 let mut buf = Buffer::new();
95
96 let mut iter = map_ids.into_iter().take(50);
97
98 if let Some(map_id) = iter.next() {
99 query.push_str("ids[]=");
100 query.push_str(buf.format(map_id));
101
102 for map_id in iter {
103 query.push_str("&ids[]=");
104 query.push_str(buf.format(map_id));
105 }
106 }
107
108 Self { osu, query }
109 }
110}
111
112into_future! {
113 |self: GetBeatmaps<'_>| -> DeserializedList<Beatmap> {
114 Request::with_query(Route::GetBeatmaps, self.query)
115 } => |maps, _| -> Vec<Beatmap> {
116 Ok(maps.0)
117 }
118}
119
120#[must_use = "requests must be configured and executed"]
122pub struct GetBeatmapDifficultyAttributes<'a> {
123 osu: &'a Osu,
124 map_id: u32,
125 mode: Option<GameMode>,
126 mods: Option<u32>,
127}
128
129impl<'a> GetBeatmapDifficultyAttributes<'a> {
130 pub(crate) const fn new(osu: &'a Osu, map_id: u32) -> Self {
131 Self {
132 osu,
133 map_id,
134 mode: None,
135 mods: None,
136 }
137 }
138
139 #[inline]
141 pub const fn mode(mut self, mode: GameMode) -> Self {
142 self.mode = Some(mode);
143
144 self
145 }
146
147 #[inline]
149 pub fn mods<M>(mut self, mods: M) -> Self
150 where
151 GameModsIntermode: From<M>,
152 {
153 self.mods = Some(GameModsIntermode::from(mods).bits());
154
155 self
156 }
157}
158
159into_future! {
160 |self: GetBeatmapDifficultyAttributes<'_>| -> BeatmapDifficultyAttributesWrapper {
161 let route = Route::PostBeatmapDifficultyAttributes {
162 map_id: self.map_id,
163 };
164
165 let mut body = JsonBody::new();
166
167 if let Some(mods) = self.mods {
168 body.push_int("mods", mods);
169 }
170
171 if let Some(mode) = self.mode {
172 body.push_int("ruleset_id", mode as u32);
173 }
174
175 Request::with_body(route, body)
176 } => |attrs, _| -> BeatmapDifficultyAttributes {
177 Ok(attrs.attributes)
178 }
179}
180
181#[derive(Copy, Clone, Debug)]
182enum ScoreType {
183 Country,
184 Global,
185}
186
187impl ScoreType {
188 const fn as_str(self) -> &'static str {
189 match self {
190 Self::Country => "country",
191 Self::Global => "global",
192 }
193 }
194}
195
196impl Serialize for ScoreType {
197 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
198 serializer.serialize_str(self.as_str())
199 }
200}
201
202#[must_use = "requests must be configured and executed"]
204#[derive(Serialize)]
205pub struct GetBeatmapScores<'a> {
206 #[serde(skip)]
207 osu: &'a Osu,
208 #[serde(skip)]
209 map_id: u32,
210 #[serde(rename(serialize = "type"))]
211 score_type: Option<ScoreType>,
212 #[serde(serialize_with = "maybe_mode_as_str")]
213 mode: Option<GameMode>,
214 #[serde(flatten, serialize_with = "maybe_mods_as_list")]
215 mods: Option<GameModsIntermode>,
216 limit: Option<u32>,
217 legacy_only: bool,
218 #[serde(skip)]
219 legacy_scores: bool,
220}
221
222impl<'a> GetBeatmapScores<'a> {
223 pub(crate) const fn new(osu: &'a Osu, map_id: u32) -> Self {
224 Self {
225 osu,
226 map_id,
227 score_type: None,
228 mode: None,
229 mods: None,
230 limit: None,
231 legacy_only: false,
232 legacy_scores: false,
233 }
234 }
235
236 #[inline]
238 pub const fn mode(mut self, mode: GameMode) -> Self {
239 self.mode = Some(mode);
240
241 self
242 }
243
244 #[inline]
246 pub fn mods<M>(mut self, mods: M) -> Self
247 where
248 GameModsIntermode: From<M>,
249 {
250 self.mods = Some(GameModsIntermode::from(mods));
251
252 self
253 }
254
255 #[inline]
257 pub const fn global(mut self) -> Self {
258 self.score_type = Some(ScoreType::Global);
259
260 self
261 }
262
263 #[inline]
267 pub const fn country(mut self) -> Self {
268 self.score_type = Some(ScoreType::Country);
269
270 self
271 }
272
273 #[inline]
274 pub const fn limit(mut self, limit: u32) -> Self {
275 self.limit = Some(limit);
276
277 self
278 }
279
280 #[inline]
282 pub const fn legacy_only(mut self, legacy_only: bool) -> Self {
283 self.legacy_only = legacy_only;
284
285 self
286 }
287
288 #[inline]
293 pub const fn legacy_scores(mut self, legacy_scores: bool) -> Self {
294 self.legacy_scores = legacy_scores;
295
296 self
297 }
298}
299
300into_future! {
301 |self: GetBeatmapScores<'_>| -> BeatmapScores {
302 let query = Query::encode(&self);
303
304 let route = Route::GetBeatmapScores {
305 map_id: self.map_id,
306 };
307
308 let mut req = Request::with_query(route, query);
309
310 if self.legacy_scores {
311 req.api_version(0);
312 }
313
314 req
315 }
316}
317
318#[must_use = "requests must be configured and executed"]
320#[derive(Serialize)]
321pub struct GetBeatmapUserScore<'a> {
322 #[serde(skip)]
323 osu: &'a Osu,
324 #[serde(skip)]
325 map_id: u32,
326 #[serde(serialize_with = "maybe_mode_as_str")]
327 mode: Option<GameMode>,
328 #[serde(flatten, serialize_with = "maybe_mods_as_list")]
329 mods: Option<GameModsIntermode>,
330 legacy_only: bool,
331 #[serde(skip)]
332 legacy_scores: bool,
333 #[serde(skip)]
334 user_id: UserId,
335}
336
337impl<'a> GetBeatmapUserScore<'a> {
338 pub(crate) const fn new(osu: &'a Osu, map_id: u32, user_id: UserId) -> Self {
339 Self {
340 osu,
341 map_id,
342 user_id,
343 mode: None,
344 mods: None,
345 legacy_only: false,
346 legacy_scores: false,
347 }
348 }
349
350 #[inline]
352 pub const fn mode(mut self, mode: GameMode) -> Self {
353 self.mode = Some(mode);
354
355 self
356 }
357
358 #[inline]
360 pub fn mods<M>(mut self, mods: M) -> Self
361 where
362 GameModsIntermode: From<M>,
363 {
364 self.mods = Some(GameModsIntermode::from(mods));
365
366 self
367 }
368
369 #[inline]
371 pub const fn legacy_only(mut self, legacy_only: bool) -> Self {
372 self.legacy_only = legacy_only;
373
374 self
375 }
376
377 #[inline]
382 pub const fn legacy_scores(mut self, legacy_scores: bool) -> Self {
383 self.legacy_scores = legacy_scores;
384
385 self
386 }
387}
388
389into_future! {
390 |self: GetBeatmapUserScore<'_>| -> BeatmapUserScore {
391 BeatmapUserScoreData {
392 query: String = Query::encode(&self),
393 map_id: u32 = self.map_id,
394 legacy_scores: bool = self.legacy_scores,
395 }
396 } => |user_id, data| {
397 let mut req = Request::with_query(
398 Route::GetBeatmapUserScore { user_id, map_id: data.map_id },
399 data.query,
400 );
401
402 if data.legacy_scores {
403 req.api_version(0);
404 }
405
406 req
407 }
408}
409
410#[must_use = "requests must be configured and executed"]
412#[derive(Serialize)]
413pub struct GetBeatmapUserScores<'a> {
414 #[serde(skip)]
415 osu: &'a Osu,
416 #[serde(skip)]
417 map_id: u32,
418 #[serde(serialize_with = "maybe_mode_as_str")]
419 mode: Option<GameMode>,
420 legacy_only: bool,
421 #[serde(skip)]
422 legacy_scores: bool,
423 #[serde(skip)]
424 user_id: UserId,
425}
426
427impl<'a> GetBeatmapUserScores<'a> {
428 pub(crate) const fn new(osu: &'a Osu, map_id: u32, user_id: UserId) -> Self {
429 Self {
430 osu,
431 map_id,
432 user_id,
433 mode: None,
434 legacy_only: false,
435 legacy_scores: false,
436 }
437 }
438
439 #[inline]
441 pub const fn mode(mut self, mode: GameMode) -> Self {
442 self.mode = Some(mode);
443
444 self
445 }
446
447 #[inline]
449 pub const fn legacy_only(mut self, legacy_only: bool) -> Self {
450 self.legacy_only = legacy_only;
451
452 self
453 }
454
455 #[inline]
460 pub const fn legacy_scores(mut self, legacy_scores: bool) -> Self {
461 self.legacy_scores = legacy_scores;
462
463 self
464 }
465}
466
467into_future! {
468 |self: GetBeatmapUserScores<'_>| -> DeserializedList<Score> {
469 GetBeatmapUserScoresData {
470 query: String = Query::encode(&self),
471 map_id: u32 = self.map_id,
472 legacy_scores: bool = self.legacy_scores,
473 }
474 } => |user_id, data| {
475 let mut req = Request::with_query(
476 Route::GetBeatmapUserScores { user_id, map_id: data.map_id },
477 data.query,
478 );
479
480 if data.legacy_scores {
481 req.api_version(0);
482 }
483
484 req
485 } => |scores, _| -> Vec<Score> {
486 Ok(scores.0)
487 }
488}
489
490#[must_use = "requests must be configured and executed"]
492pub struct GetBeatmapset<'a> {
493 osu: &'a Osu,
494 mapset_id: u32,
495}
496
497impl<'a> GetBeatmapset<'a> {
498 pub(crate) const fn new(osu: &'a Osu, mapset_id: u32) -> Self {
499 Self { osu, mapset_id }
500 }
501}
502
503into_future! {
504 |self: GetBeatmapset<'_>| -> BeatmapsetExtended {
505 Request::new(Route::GetBeatmapset {
506 mapset_id: self.mapset_id,
507 })
508 }
509}
510
511#[must_use = "requests must be configured and executed"]
513#[derive(Serialize)]
514pub struct GetBeatmapsetFromMapId<'a> {
515 #[serde(skip)]
516 osu: &'a Osu,
517 #[serde(rename(serialize = "beatmap_id"))]
518 map_id: u32,
519}
520
521impl<'a> GetBeatmapsetFromMapId<'a> {
522 pub(crate) const fn new(osu: &'a Osu, map_id: u32) -> Self {
523 Self { osu, map_id }
524 }
525}
526
527into_future! {
528 |self: GetBeatmapsetFromMapId<'_>| -> BeatmapsetExtended {
529 Request::with_query(Route::GetBeatmapsetFromMapId, Query::encode(&self))
530 }
531}
532
533#[must_use = "requests must be configured and executed"]
535pub struct GetBeatmapsetEvents<'a> {
536 osu: &'a Osu,
537}
538
539impl<'a> GetBeatmapsetEvents<'a> {
540 pub(crate) const fn new(osu: &'a Osu) -> Self {
541 Self { osu }
542 }
543}
544
545into_future! {
546 |self: GetBeatmapsetEvents<'_>| -> BeatmapsetEvents {
547 Request::new(Route::GetBeatmapsetEvents)
548 }
549}
550
551#[must_use = "requests must be configured and executed"]
582pub struct GetBeatmapsetSearch<'a> {
583 osu: &'a Osu,
584 data: GetBeatmapsetSearchData,
585 sort: Option<BeatmapsetSearchSort>,
586 descending: bool,
587 page: Option<u32>,
588 cursor: Option<&'a str>,
589}
590
591impl<'a> GetBeatmapsetSearch<'a> {
592 pub(crate) const fn new(osu: &'a Osu) -> Self {
593 Self {
594 osu,
595 data: GetBeatmapsetSearchData::DEFAULT,
596 sort: None,
597 descending: true,
598 page: None,
599 cursor: None,
600 }
601 }
602
603 #[inline]
605 pub fn query(mut self, query: impl Into<String>) -> Self {
606 self.data.query = Some(query.into());
607
608 self
609 }
610
611 #[inline]
613 pub const fn mode(mut self, mode: GameMode) -> Self {
614 self.data.mode = Some(mode as u8);
615
616 self
617 }
618
619 #[inline]
627 pub const fn status(mut self, status: Option<RankStatus>) -> Self {
628 let status = match status {
629 Some(RankStatus::WIP) => SearchRankStatus::Specific(RankStatus::Pending),
630 Some(status) => SearchRankStatus::Specific(status),
631 None => SearchRankStatus::Any,
632 };
633
634 self.data.status = Some(status);
635
636 self
637 }
638
639 #[inline]
641 pub const fn genre(mut self, genre: Genre) -> Self {
642 self.data.genre = Some(genre as u8);
643
644 self
645 }
646
647 #[inline]
649 pub const fn language(mut self, language: Language) -> Self {
650 self.data.language = Some(language as u8);
651
652 self
653 }
654
655 #[inline]
657 pub const fn video(mut self, video: bool) -> Self {
658 self.data.video = video;
659
660 self
661 }
662
663 #[inline]
665 pub const fn storyboard(mut self, storyboard: bool) -> Self {
666 self.data.storyboard = storyboard;
667
668 self
669 }
670
671 #[inline]
676 pub const fn recommended(mut self, recommended: bool) -> Self {
677 self.data.recommended = recommended;
678
679 self
680 }
681
682 #[inline]
685 pub const fn converts(mut self, converts: bool) -> Self {
686 self.data.converts = converts;
687
688 self
689 }
690
691 #[inline]
695 pub const fn follows(mut self, follows: bool) -> Self {
696 self.data.follows = follows;
697
698 self
699 }
700
701 #[inline]
704 pub const fn spotlights(mut self, spotlights: bool) -> Self {
705 self.data.spotlights = spotlights;
706
707 self
708 }
709
710 #[inline]
713 pub const fn featured_artists(mut self, featured_artists: bool) -> Self {
714 self.data.featured_artists = featured_artists;
715
716 self
717 }
718
719 #[inline]
721 pub const fn nsfw(mut self, nsfw: bool) -> Self {
722 self.data.nsfw = nsfw;
723
724 self
725 }
726
727 #[inline]
729 pub const fn page(mut self, page: u32) -> Self {
730 self.page = Some(page);
731
732 self
733 }
734
735 #[inline]
737 pub const fn sort(mut self, sort: BeatmapsetSearchSort, descending: bool) -> Self {
738 self.sort = Some(sort);
739 self.descending = descending;
740
741 self
742 }
743
744 #[inline]
745 pub(crate) const fn cursor(mut self, cursor: &'a str) -> Self {
746 self.cursor = Some(cursor);
747
748 self
749 }
750}
751
752#[doc(hidden)]
753pub struct GetBeatmapsetSearchData {
754 query: Option<String>,
755 mode: Option<u8>,
756 status: Option<SearchRankStatus>,
757 genre: Option<u8>,
758 language: Option<u8>,
759 video: bool,
760 storyboard: bool,
761 recommended: bool,
762 converts: bool,
763 follows: bool,
764 spotlights: bool,
765 featured_artists: bool,
766 nsfw: bool,
767}
768
769impl GetBeatmapsetSearchData {
770 const DEFAULT: Self = Self {
771 query: None,
772 mode: None,
773 status: None,
774 genre: None,
775 language: None,
776 video: false,
777 storyboard: false,
778 recommended: false,
779 converts: false,
780 follows: false,
781 spotlights: false,
782 featured_artists: false,
783 nsfw: true,
784 };
785
786 fn apply(self, params: &mut BeatmapsetSearchParameters) {
787 let GetBeatmapsetSearchData {
788 query,
789 mode,
790 status,
791 genre,
792 language,
793 video,
794 storyboard,
795 recommended,
796 converts,
797 follows,
798 spotlights,
799 featured_artists,
800 nsfw,
801 } = self;
802
803 params.query = query;
804 params.mode = mode;
805 params.status = status;
806 params.genre = genre;
807 params.language = language;
808 params.video = video;
809 params.storyboard = storyboard;
810 params.recommended = recommended;
811 params.converts = converts;
812 params.follows = follows;
813 params.spotlights = spotlights;
814 params.featured_artists = featured_artists;
815 params.nsfw = nsfw;
816 }
817}
818
819into_future! {
820 |self: GetBeatmapsetSearch<'_>| -> BeatmapsetSearchResult {
821 (
822 Request::with_query(Route::GetBeatmapsetSearch, Query::encode(&self)),
823 self.data,
824 )
825 } => |result, data: GetBeatmapsetSearchData| -> BeatmapsetSearchResult {
826 data.apply(&mut result.params);
827
828 Ok(result)
829 }
830}
831
832impl Serialize for GetBeatmapsetSearch<'_> {
833 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
834 let mut map = serializer.serialize_map(None)?;
835
836 if let Some(ref query) = self.data.query {
837 map.serialize_entry("q", query)?;
838 }
839
840 if let Some(ref mode) = self.data.mode {
841 map.serialize_entry("m", mode)?;
842 }
843
844 if let Some(status) = self.data.status {
845 struct Status(SearchRankStatus);
846
847 impl Serialize for Status {
848 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
849 where
850 S: Serializer,
851 {
852 match self.0 {
853 SearchRankStatus::Specific(status) => {
854 let mut buf = String::new();
855 let _ = write!(buf, "{status:?}");
856
857 unsafe { buf.as_bytes_mut()[0].make_ascii_lowercase() }
860
861 serializer.serialize_str(&buf)
862 }
863 SearchRankStatus::Any => serializer.serialize_str("any"),
864 }
865 }
866 }
867
868 map.serialize_entry("s", &Status(status))?;
869 }
870
871 if let Some(ref genre) = self.data.genre {
872 map.serialize_entry("g", genre)?;
873 }
874
875 if let Some(ref language) = self.data.language {
876 map.serialize_entry("l", language)?;
877 }
878
879 let extra = match (self.data.video, self.data.storyboard) {
880 (false, false) => None,
881 (false, true) => Some("storyboard"),
882 (true, false) => Some("video"),
883 (true, true) => Some("storyboard.video"),
884 };
885
886 if let Some(ref extra) = extra {
887 map.serialize_entry("e", extra)?;
888 }
889
890 let mut general = None::<String>;
891
892 let mut add_general = |should_add: bool, value: &str| {
893 if !should_add {
894 return;
895 }
896
897 let is_some = general.is_some();
898 let general = general.get_or_insert_with(String::new);
899
900 if is_some {
901 general.push('.');
902 }
903
904 general.push_str(value);
905 };
906
907 add_general(self.data.recommended, "recommended");
908 add_general(self.data.converts, "converts");
909 add_general(self.data.follows, "follows");
910 add_general(self.data.spotlights, "spotlights");
911 add_general(self.data.featured_artists, "featured_artists");
912
913 if let Some(ref general) = general {
914 map.serialize_entry("c", general)?;
915 }
916
917 map.serialize_entry("nsfw", &self.data.nsfw)?;
918
919 if let Some(ref page) = self.page {
920 map.serialize_entry("page", page)?;
921 }
922
923 if let Some(cursor) = self.cursor {
924 map.serialize_entry("cursor_string", cursor)?;
925 }
926
927 if let Some(ref sort) = self.sort {
928 let mut buf = String::with_capacity(16);
929 let _ = write!(buf, "{sort}_");
930 let order = if self.descending { "desc" } else { "asc" };
931 buf.push_str(order);
932
933 map.serialize_entry("sort", &buf)?;
934 }
935
936 map.end()
937 }
938}