rosu_v2/request/
beatmap.rs

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/// Get a [`BeatmapExtended`].
29#[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    /// Specify a beatmap checksum
51    #[inline]
52    pub fn checksum(mut self, checksum: impl Into<String>) -> Self {
53        self.checksum = Some(checksum.into());
54
55        self
56    }
57
58    /// Specify a beatmap filename
59    #[inline]
60    pub fn filename(mut self, filename: impl Into<String>) -> Self {
61        self.filename = Some(filename.into());
62
63        self
64    }
65
66    /// Specify a beatmap id
67    #[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/// Get a vec of [`Beatmap`].
82#[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/// Get [`BeatmapDifficultyAttributes`] of a map.
121#[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    /// Specify the mode
140    #[inline]
141    pub const fn mode(mut self, mode: GameMode) -> Self {
142        self.mode = Some(mode);
143
144        self
145    }
146
147    /// Specify the mods
148    #[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/// Get top scores of a beatmap as [`BeatmapScores`].
203#[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    /// Specify the mode of the scores
237    #[inline]
238    pub const fn mode(mut self, mode: GameMode) -> Self {
239        self.mode = Some(mode);
240
241        self
242    }
243
244    /// Specify the mods of the scores
245    #[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    /// Specify that the global leaderboard should be requested.
256    #[inline]
257    pub const fn global(mut self) -> Self {
258        self.score_type = Some(ScoreType::Global);
259
260        self
261    }
262
263    /// Specify that the national leaderboard should be requested.
264    ///
265    /// Note that you must be authenticated through OAuth and have osu!supporter to use this.
266    #[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    /// Whether or not to exclude lazer scores.
281    #[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    /// Specify whether the scores should contain legacy data or not.
289    ///
290    /// Legacy data consists of a different grade calculation, less
291    /// populated statistics, legacy mods, and a different score kind.
292    #[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/// Get [`BeatmapUserScore`] of a user on a beatmap.
319#[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    /// Specify the mode
351    #[inline]
352    pub const fn mode(mut self, mode: GameMode) -> Self {
353        self.mode = Some(mode);
354
355        self
356    }
357
358    /// Specify the mods
359    #[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    /// Whether or not to exclude lazer scores.
370    #[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    /// Specify whether the score should contain legacy data or not.
378    ///
379    /// Legacy data consists of a different grade calculation, less
380    /// populated statistics, legacy mods, and a different score kind.
381    #[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/// Get all [`Score`]s of a user on a map.
411#[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    /// Specify the mode
440    #[inline]
441    pub const fn mode(mut self, mode: GameMode) -> Self {
442        self.mode = Some(mode);
443
444        self
445    }
446
447    /// Whether or not to exclude lazer scores.
448    #[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    /// Specify whether the scores should contain legacy data or not.
456    ///
457    /// Legacy data consists of a different grade calculation, less
458    /// populated statistics, legacy mods, and a different score kind.
459    #[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/// Get a [`BeatmapsetExtended`].
491#[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/// Get a [`BeatmapsetExtended`].
512#[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/// Get [`BeatmapsetEvents`].
534#[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/// Get a [`BeatmapsetSearchResult`] containing the maps that fit the search
552/// query.
553///
554/// The default search parameters are:
555/// - mode: any
556/// - status: has leaderboard (ranked, loved, approved, and qualified)
557/// - genre: any
558/// - language: any
559/// - extra: does neither contain "have video" nor "have storyboard"
560/// - general: recommended, converts, follows, spotlights, and featured artists are all disabled
561/// - nsfw: allowed
562/// - sort: by relevance, descending
563///
564/// The contained [`BeatmapsetExtended`]s will have the following options
565/// filled: `artist_unicode`, `legacy_thread_url`, `maps`, `ranked_date` and
566/// `submitted_date` if available, and `title_unicode`.
567///
568/// The search query allows the following options to be specified: `ar`, `artist`,
569/// `bpm`, `created`, `creator`, `cs`, `dr` (hp drain rate), `keys`, `length`,
570/// `ranked`, `stars`, and `status`.
571///
572/// ## Example
573///
574/// ```
575/// // Search for mapsets from Sotarks that have a map with no more than AR 9.
576/// let query = "creator=sotarks ar<9";
577///
578/// // Loved mapsets from Camellia including at least one map above 8 stars
579/// let query = "status=loved artist=camellia stars>8";
580/// ```
581#[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    /// Specify a search query.
604    #[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    /// Specify the mode for which the mapsets has to have at least one map.
612    #[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    /// Specify a status for the mapsets, defaults to `has_leaderboard`
620    /// i.e. ranked, loved, approved, and qualified. To allow any status,
621    /// specify `None`.
622    ///
623    /// ## Note
624    /// The API does not seem to filter for the `RankStatus::Approved` status
625    /// specifically.
626    #[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    /// Specify a genre for the mapsets, defaults to `Any`.
640    #[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    /// Specify a language for the mapsets, defaults to `Any`.
648    #[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    /// Specify whether mapsets can have a video, defaults to `false`.
656    #[inline]
657    pub const fn video(mut self, video: bool) -> Self {
658        self.data.video = video;
659
660        self
661    }
662
663    /// Specify whether mapsets can have a storyboard, defaults to `false`.
664    #[inline]
665    pub const fn storyboard(mut self, storyboard: bool) -> Self {
666        self.data.storyboard = storyboard;
667
668        self
669    }
670
671    /// Only include mapsets containing a beatmap around the authorized user's
672    /// recommended difficulty level.
673    ///
674    /// This has only an effect for oauth-clients.
675    #[inline]
676    pub const fn recommended(mut self, recommended: bool) -> Self {
677        self.data.recommended = recommended;
678
679        self
680    }
681
682    /// Specify whether converted mapsets should be included, defaults to
683    /// `false`.
684    #[inline]
685    pub const fn converts(mut self, converts: bool) -> Self {
686        self.data.converts = converts;
687
688        self
689    }
690
691    /// Only include mapsets of mappers that the authorized user follows.
692    ///
693    /// This has only an effect for oauth-clients.
694    #[inline]
695    pub const fn follows(mut self, follows: bool) -> Self {
696        self.data.follows = follows;
697
698        self
699    }
700
701    /// Specify whether only mapsets that are currently spotlighted should be
702    /// included, defaults to `false`.
703    #[inline]
704    pub const fn spotlights(mut self, spotlights: bool) -> Self {
705        self.data.spotlights = spotlights;
706
707        self
708    }
709
710    /// Specify whether only mapsets of featured artists should be included,
711    /// defaults to `false`.
712    #[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    /// Specify whether mapsets *can* be NSFW, defaults to `true`.
720    #[inline]
721    pub const fn nsfw(mut self, nsfw: bool) -> Self {
722        self.data.nsfw = nsfw;
723
724        self
725    }
726
727    /// Specify a page
728    #[inline]
729    pub const fn page(mut self, page: u32) -> Self {
730        self.page = Some(page);
731
732        self
733    }
734
735    /// Specify how the result should be sorted
736    #[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                            // SAFETY: Debug formats of RankStatus are guaranteed
858                            // to only contain ASCII chars and have a length >= 1.
859                            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}