1use std::{
2 borrow::Cow,
3 fmt::{Display, Formatter, Result as FmtResult},
4};
5
6use hyper::{body::Bytes, StatusCode};
7use serde::{
8 de::{Error as DeError, IgnoredAny, MapAccess, Unexpected, Visitor},
9 Deserialize, Deserializer, Serialize,
10};
11use time::OffsetDateTime;
12
13use crate::{request::Requestable, util::datetime::deserialize_datetime, ClientError};
14
15#[derive(Clone, Debug, Deserialize)]
17pub struct RenderList {
18 pub renders: Vec<Render>,
20 #[serde(rename = "maxRenders")]
23 pub max_renders: u32,
24}
25
26impl Requestable for RenderList {
27 fn response_error(status: StatusCode, bytes: Bytes) -> ClientError {
28 ClientError::response_error(bytes, status.as_u16())
29 }
30}
31
32#[derive(Clone, Debug, Deserialize)]
33pub struct Render {
34 #[serde(rename = "renderID")]
35 pub id: u32,
36 #[serde(deserialize_with = "deserialize_datetime")]
37 pub date: OffsetDateTime,
38 pub username: Box<str>,
39 pub progress: Box<str>,
40 pub renderer: Box<str>,
41 pub description: Box<str>,
42 pub title: Box<str>,
43 #[serde(rename = "isBot")]
44 pub is_bot: bool,
45 #[serde(rename = "isVerified")]
46 pub is_verified: bool,
47 #[serde(rename = "videoUrl")]
48 pub video_url: Box<str>,
49 #[serde(rename = "mapLink")]
50 pub map_link: Box<str>,
51 #[serde(rename = "mapTitle")]
52 pub map_title: Box<str>,
53 #[serde(rename = "replayDifficulty")]
54 pub replay_difficulty: Box<str>,
55 #[serde(rename = "replayUsername")]
56 pub replay_username: Box<str>,
57 #[serde(rename = "mapID")]
58 pub map_id: u32,
59 #[serde(rename = "needToRedownload")]
60 pub need_to_redownload: bool,
61 #[serde(rename = "motionBlur960fps")]
62 pub motion_blur: bool,
63 #[serde(rename = "renderStartTime", deserialize_with = "deserialize_datetime")]
64 pub render_start_time: OffsetDateTime,
65 #[serde(rename = "renderEndTime", deserialize_with = "deserialize_datetime")]
66 pub render_end_time: OffsetDateTime,
67 #[serde(rename = "uploadEndTime", deserialize_with = "deserialize_datetime")]
68 pub upload_end_time: OffsetDateTime,
69 #[serde(rename = "renderTotalTime")]
70 pub render_total_time: u32,
71 #[serde(rename = "uploadTotalTime")]
72 pub upload_total_time: u32,
73 #[serde(rename = "mapLength")]
74 pub map_length: u32,
75 #[serde(rename = "replayMods")]
76 pub replay_mods: Box<str>,
77 pub removed: bool,
78 #[serde(flatten)]
79 pub options: RenderOptions,
80 #[serde(flatten)]
81 pub skin: RenderSkinOption<'static>,
82}
83
84#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
85pub enum RenderResolution {
86 #[serde(rename = "720x480")]
88 SD480,
89 #[serde(rename = "960x540")]
91 SD960,
92 #[serde(rename = "1280x720")]
94 HD720,
95 #[serde(rename = "1920x1080")]
97 HD1080,
98}
99
100impl RenderResolution {
101 #[must_use]
102 pub fn as_str(self) -> &'static str {
103 match self {
104 Self::SD480 => "720x480",
105 Self::SD960 => "960x540",
106 Self::HD720 => "1280x720",
107 Self::HD1080 => "1920x1080",
108 }
109 }
110}
111
112impl Display for RenderResolution {
113 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
114 f.write_str(self.as_str())
115 }
116}
117
118#[derive(Clone, Debug, Deserialize, Serialize)]
120pub struct RenderOptions {
121 pub resolution: RenderResolution,
122 #[serde(rename = "globalVolume")]
124 pub global_volume: u8,
125 #[serde(rename = "musicVolume")]
127 pub music_volume: u8,
128 #[serde(rename = "hitsoundVolume")]
130 pub hitsound_volume: u8,
131 #[serde(rename = "showHitErrorMeter")]
133 pub show_hit_error_meter: bool,
134 #[serde(rename = "showUnstableRate")]
136 pub show_unstable_rate: bool,
137 #[serde(rename = "showScore")]
139 pub show_score: bool,
140 #[serde(rename = "showHPBar")]
142 pub show_hp_bar: bool,
143 #[serde(rename = "showComboCounter")]
145 pub show_combo_counter: bool,
146 #[serde(rename = "showPPCounter")]
148 pub show_pp_counter: bool,
149 #[serde(rename = "showScoreboard")]
151 pub show_scoreboard: bool,
152 #[serde(rename = "showBorders")]
154 pub show_borders: bool,
155 #[serde(rename = "showMods")]
157 pub show_mods: bool,
158 #[serde(rename = "showResultScreen")]
160 pub show_result_screen: bool,
161 #[serde(rename = "useSkinCursor")]
163 pub use_skin_cursor: bool,
164 #[serde(rename = "useSkinColors")]
166 pub use_skin_colors: bool,
167 #[serde(rename = "useSkinHitsounds")]
169 pub use_skin_hitsounds: bool,
170 #[serde(rename = "useBeatmapColors")]
172 pub use_beatmap_colors: bool,
173 #[serde(rename = "cursorScaleToCS")]
175 pub cursor_scale_to_cs: bool,
176 #[serde(rename = "cursorRainbow")]
178 pub cursor_rainbow: bool,
179 #[serde(rename = "cursorTrailGlow")]
181 pub cursor_trail_glow: bool,
182 #[serde(rename = "drawFollowPoints")]
184 pub draw_follow_points: bool,
185 #[serde(rename = "scaleToTheBeat")]
187 pub beat_scaling: bool,
188 #[serde(rename = "sliderMerge")]
190 pub slider_merge: bool,
191 #[serde(rename = "objectsRainbow")]
193 pub objects_rainbow: bool,
194 #[serde(rename = "objectsFlashToTheBeat")]
196 pub flash_objects: bool,
197 #[serde(rename = "useHitCircleColor")]
199 pub use_slider_hitcircle_color: bool,
200 #[serde(rename = "seizureWarning")]
202 pub seizure_warning: bool,
203 #[serde(rename = "loadStoryboard")]
205 pub load_storyboard: bool,
206 #[serde(rename = "loadVideo")]
208 pub load_video: bool,
209 #[serde(rename = "introBGDim")]
211 pub intro_bg_dim: u8,
212 #[serde(rename = "inGameBGDim")]
214 pub ingame_bg_dim: u8,
215 #[serde(rename = "breakBGDim")]
217 pub break_bg_dim: u8,
218 #[serde(rename = "BGParallax")]
220 pub bg_parallax: bool,
221 #[serde(rename = "showDanserLogo")]
223 pub show_danser_logo: bool,
224 #[serde(rename = "skip")]
226 pub skip_intro: bool,
227 #[serde(rename = "cursorRipples")]
229 pub cursor_ripples: bool,
230 #[serde(rename = "cursorSize")]
232 pub cursor_size: f32,
233 #[serde(rename = "cursorTrail")]
235 pub cursor_trail: bool,
236 #[serde(rename = "drawComboNumbers")]
238 pub draw_combo_numbers: bool,
239 #[serde(rename = "sliderSnakingIn")]
241 pub slider_snaking_in: bool,
242 #[serde(rename = "sliderSnakingOut")]
244 pub slider_snaking_out: bool,
245 #[serde(rename = "showHitCounter")]
247 pub show_hit_counter: bool,
248 #[serde(rename = "showKeyOverlay")]
250 pub show_key_overlay: bool,
251 #[serde(rename = "showAvatarsOnScoreboard")]
254 pub show_avatars_on_scoreboard: bool,
255 #[serde(rename = "showAimErrorMeter")]
257 pub show_aim_error_meter: bool,
258 #[serde(rename = "playNightcoreSamples")]
260 pub play_nightcore_samples: bool,
261 #[serde(rename = "showStrainGraph")]
263 pub show_strain_graph: bool,
264 #[serde(rename = "showSliderBreaks")]
266 pub show_slider_breaks: bool,
267 #[serde(rename = "ignoreFail")]
269 pub ignore_fail: bool,
270 #[serde(
274 rename = "discordUserId",
275 skip_serializing_if = "Option::is_none",
276 with = "maybe_u64_as_str"
277 )]
278 pub discord_user_id: Option<u64>,
279}
280
281mod maybe_u64_as_str {
282 use serde::{
283 de::{Deserialize, Deserializer, Error},
284 Serializer,
285 };
286
287 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<u64>, D::Error> {
288 match Option::<&str>::deserialize(d)? {
289 Some(s) => s.parse().map(Some).map_err(Error::custom),
290 None => Ok(None),
291 }
292 }
293
294 #[expect(clippy::ref_option, reason = "required by serde")]
295 pub fn serialize<S: Serializer>(opt: &Option<u64>, s: S) -> Result<S::Ok, S::Error> {
296 let Some(n) = opt else { unreachable!() };
298
299 s.serialize_str(&n.to_string())
300 }
301}
302
303impl RenderOptions {
304 pub const DEFAULT_RESOLUTION: RenderResolution = RenderResolution::HD720;
305}
306
307impl Default for RenderOptions {
308 fn default() -> Self {
309 Self {
310 resolution: Self::DEFAULT_RESOLUTION,
311 global_volume: 50,
312 music_volume: 50,
313 hitsound_volume: 50,
314 show_hit_error_meter: true,
315 show_unstable_rate: true,
316 show_score: true,
317 show_hp_bar: true,
318 show_combo_counter: true,
319 show_pp_counter: true,
320 show_key_overlay: true,
321 show_scoreboard: true,
322 show_borders: true,
323 show_mods: true,
324 show_result_screen: true,
325 use_skin_cursor: true,
326 use_skin_colors: false,
327 use_skin_hitsounds: true,
328 use_beatmap_colors: true,
329 cursor_scale_to_cs: false,
330 cursor_rainbow: false,
331 cursor_trail_glow: false,
332 draw_follow_points: true,
333 draw_combo_numbers: true,
334 cursor_size: 1.0,
335 cursor_trail: true,
336 beat_scaling: false,
337 slider_merge: false,
338 objects_rainbow: false,
339 flash_objects: false,
340 use_slider_hitcircle_color: false,
341 seizure_warning: false,
342 load_storyboard: false,
343 load_video: false,
344 intro_bg_dim: 0,
345 ingame_bg_dim: 80,
346 break_bg_dim: 30,
347 bg_parallax: false,
348 show_danser_logo: true,
349 skip_intro: true,
350 cursor_ripples: false,
351 slider_snaking_in: true,
352 slider_snaking_out: true,
353 show_hit_counter: true,
354 show_avatars_on_scoreboard: false,
355 show_aim_error_meter: false,
356 play_nightcore_samples: true,
357 show_strain_graph: false,
358 show_slider_breaks: false,
359 ignore_fail: false,
360 discord_user_id: None,
361 }
362 }
363}
364
365#[derive(Clone, Debug, PartialEq, Eq)]
366pub enum RenderSkinOption<'a> {
367 Official { name: Cow<'a, str> },
368 Custom { id: u32 },
369}
370
371impl Default for RenderSkinOption<'_> {
372 fn default() -> Self {
373 Self::Official {
374 name: "default".into(),
375 }
376 }
377}
378
379impl From<u32> for RenderSkinOption<'_> {
380 fn from(id: u32) -> Self {
381 Self::Custom { id }
382 }
383}
384
385macro_rules! impl_from_name {
386 ( $( $ty:ty ),* ) => {
387 $(
388 impl<'a> From<$ty> for RenderSkinOption<'a> {
389 fn from(name: $ty) -> Self {
390 Self::Official { name: name.into() }
391 }
392 }
393 )*
394 };
395}
396
397impl_from_name!(&'a str, &'a String, String, Cow<'a, str>);
398
399impl<'de> Deserialize<'de> for RenderSkinOption<'static> {
400 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
401 struct SkinVisitor;
402
403 impl<'de> Visitor<'de> for SkinVisitor {
404 type Value = RenderSkinOption<'static>;
405
406 fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
407 f.write_str("`skin` and `customSkin` fields")
408 }
409
410 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
411 let mut custom_skin: Option<bool> = None;
412 let mut skin: Option<String> = None;
413
414 while let Some(key) = map.next_key()? {
415 match key {
416 "customSkin" => custom_skin = Some(map.next_value()?),
417 "skin" => skin = Some(map.next_value()?),
418 _ => {
419 let _: IgnoredAny = map.next_value()?;
420 }
421 }
422 }
423
424 let custom_skin =
425 custom_skin.ok_or_else(|| DeError::missing_field("customSkin"))?;
426 let skin = skin.ok_or_else(|| DeError::missing_field("skin"))?;
427
428 let skin = if custom_skin {
429 let id = skin
430 .parse()
431 .map_err(|_| DeError::invalid_value(Unexpected::Str(&skin), &"a u32"))?;
432
433 RenderSkinOption::Custom { id }
434 } else {
435 RenderSkinOption::Official {
436 name: Cow::Owned(skin),
437 }
438 };
439
440 Ok(skin)
441 }
442 }
443
444 d.deserialize_map(SkinVisitor)
445 }
446}
447
448#[derive(Clone, Debug, Deserialize, PartialEq)]
449pub struct RenderServers {
450 pub servers: Vec<RenderServer>,
451}
452
453impl Requestable for RenderServers {
454 fn response_error(status: StatusCode, bytes: Bytes) -> ClientError {
455 ClientError::response_error(bytes, status.as_u16())
456 }
457}
458
459#[derive(Clone, Debug, Deserialize, PartialEq)]
460pub struct RenderServer {
461 pub enabled: bool,
462 #[serde(rename = "lastSeen", deserialize_with = "deserialize_datetime")]
463 pub last_seen: OffsetDateTime,
464 pub name: Box<str>,
465 pub priority: f32,
466 #[serde(rename = "oldScore")]
467 pub old_score: f32,
468 #[serde(rename = "avgFPS")]
469 pub avg_fps: u32,
470 pub power: Box<str>,
471 pub status: Box<str>,
472 #[serde(rename = "totalRendered")]
473 pub total_rendered: u32,
474 #[serde(rename = "renderingType")]
475 pub rendering_type: Box<str>,
476 pub cpu: Box<str>,
477 pub gpu: Box<str>,
478 #[serde(rename = "motionBlurCapable")]
479 pub motion_blur_capable: bool,
480 #[serde(rename = "usingOsuApi")]
481 pub using_osu_api: bool,
482 #[serde(rename = "uhdCapable")]
483 pub uhd_capable: bool,
484 #[serde(rename = "avgRenderTime")]
485 pub avg_render_time: f32,
486 #[serde(rename = "avgUploadTime")]
487 pub avg_upload_time: f32,
488 #[serde(rename = "totalAvgTime")]
489 pub total_avg_time: f32,
490 #[serde(rename = "totalUploadedVideosSize")]
491 pub total_uploaded_videos_size: u32,
492 #[serde(rename = "ownerUserId")]
493 pub owner_user_id: u32,
494 #[serde(rename = "ownerUsername")]
495 pub owner_username: Box<str>,
496 pub customization: RenderServerOptions,
497}
498
499#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
500pub struct RenderServerOptions {
501 #[serde(rename = "textColor")]
502 pub text_color: Box<str>,
503 #[serde(rename = "backgroundType")]
504 pub background_type: i32,
505}
506
507#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
508pub struct ServerOnlineCount(pub u32);
509
510impl Requestable for ServerOnlineCount {
511 fn response_error(status: StatusCode, bytes: Bytes) -> ClientError {
512 ClientError::response_error(bytes, status.as_u16())
513 }
514}