Implement beatmap collecting from rosu-v2

This commit is contained in:
Natsu Kagami 2024-02-01 22:59:38 +01:00 committed by Natsu Kagami
parent 8febf0106e
commit 7d00b95a4f
6 changed files with 171 additions and 25 deletions

1
Cargo.lock generated
View file

@ -3104,6 +3104,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serenity", "serenity",
"time",
"youmubot-db", "youmubot-db",
"youmubot-db-sql", "youmubot-db-sql",
"youmubot-prelude", "youmubot-prelude",

View file

@ -17,6 +17,7 @@ regex = "1.5.6"
reqwest = "0.11.10" reqwest = "0.11.10"
rosu-pp = "0.9.1" rosu-pp = "0.9.1"
rosu-v2 = "0.8" rosu-v2 = "0.8"
time = "0.3"
serde = { version = "1.0.137", features = ["derive"] } serde = { version = "1.0.137", features = ["derive"] }
serenity = "0.11.2" serenity = "0.11.2"
zip = "0.6.2" zip = "0.6.2"

View file

@ -23,7 +23,7 @@ pub struct Client {
rosu: rosu_v2::Osu, rosu: rosu_v2::Osu,
} }
fn vec_try_into<U, T: std::convert::TryFrom<U>>(v: Vec<U>) -> Result<Vec<T>, T::Error> { pub fn vec_try_into<U, T: std::convert::TryFrom<U>>(v: Vec<U>) -> Result<Vec<T>, T::Error> {
let mut res = Vec::with_capacity(v.len()); let mut res = Vec::with_capacity(v.len());
for u in v.into_iter() { for u in v.into_iter() {
@ -70,8 +70,7 @@ impl Client {
) -> Result<Vec<Beatmap>> { ) -> Result<Vec<Beatmap>> {
let mut r = BeatmapRequestBuilder::new(kind); let mut r = BeatmapRequestBuilder::new(kind);
f(&mut r); f(&mut r);
let res: Vec<raw::Beatmap> = r.build(self).await?.json().await?; r.build(self).await
Ok(vec_try_into(res)?)
} }
pub async fn user( pub async fn user(

View file

@ -8,6 +8,7 @@ use std::time::Duration;
pub mod mods; pub mod mods;
pub mod parse; pub mod parse;
pub(crate) mod raw; pub(crate) mod raw;
pub(crate) mod rosu;
pub use mods::Mods; pub use mods::Mods;
use serenity::utils::MessageBuilder; use serenity::utils::MessageBuilder;
@ -252,6 +253,7 @@ pub enum Language {
Italian, Italian,
Russian, Russian,
Polish, Polish,
Unspecified,
} }
impl fmt::Display for Language { impl fmt::Display for Language {

View file

@ -0,0 +1,142 @@
use rosu_v2::model as rosu;
use super::*;
impl ApprovalStatus {
pub(crate) fn from_rosu(
rank_status: rosu_v2::model::beatmap::RankStatus,
ranked_date: Option<DateTime<Utc>>,
) -> Self {
use ApprovalStatus::*;
match rank_status {
rosu_v2::model::beatmap::RankStatus::Graveyard => Graveyarded,
rosu_v2::model::beatmap::RankStatus::WIP => WIP,
rosu_v2::model::beatmap::RankStatus::Pending => Pending,
rosu_v2::model::beatmap::RankStatus::Ranked => Ranked(ranked_date.unwrap()),
rosu_v2::model::beatmap::RankStatus::Approved => Approved,
rosu_v2::model::beatmap::RankStatus::Qualified => Qualified,
rosu_v2::model::beatmap::RankStatus::Loved => Loved,
}
}
}
fn time_to_utc(s: time::OffsetDateTime) -> DateTime<Utc> {
chrono::DateTime::from_timestamp(s.unix_timestamp(), 0).unwrap()
}
impl Beatmap {
pub(crate) fn from_rosu(bm: rosu::beatmap::Beatmap, set: &rosu::beatmap::Beatmapset) -> Self {
let last_updated = time_to_utc(bm.last_updated);
let difficulty = Difficulty::from_rosu(&bm);
Self {
approval: ApprovalStatus::from_rosu(bm.status, set.ranked_date.map(time_to_utc)),
submit_date: set.submitted_date.map(time_to_utc).unwrap_or(last_updated),
last_update: last_updated,
download_available: !set.availability.download_disabled, // don't think we have this stat
audio_available: !set.availability.download_disabled, // neither is this
artist: set.artist_unicode.as_ref().unwrap_or(&set.artist).clone(),
title: set.title_unicode.as_ref().unwrap_or(&set.title).clone(),
beatmapset_id: set.mapset_id as u64,
creator: set.creator_name.clone().into_string(),
creator_id: set.creator_id as u64,
source: Some(set.source.clone()).filter(|s| s != "").clone(),
genre: set.genre.map(|v| v.into()).unwrap_or(Genre::Unspecified),
language: set.language.map(|v| v.into()).unwrap_or(Language::Any),
tags: set.tags.split(", ").map(|v| v.to_owned()).collect(),
beatmap_id: bm.map_id as u64,
difficulty_name: bm.version,
difficulty,
file_hash: bm.checksum.unwrap_or_else(|| "none".to_owned()),
mode: bm.mode.into(),
favourite_count: set.favourite_count as u64,
rating: set
.ratings
.as_ref()
.map(|rs| {
(rs.iter()
.enumerate()
.map(|(r, id)| ((r + 1) as u32 * *id))
.sum::<u32>()) as f64
/ (rs.iter().sum::<u32>() as f64)
})
.unwrap_or(0.0),
play_count: bm.playcount as u64,
pass_count: bm.passcount as u64,
}
}
}
impl Difficulty {
pub(crate) fn from_rosu(bm: &rosu::beatmap::Beatmap) -> Self {
Self {
stars: bm.stars as f64,
aim: None,
speed: None,
cs: bm.cs as f64,
od: bm.od as f64,
ar: bm.ar as f64,
hp: bm.hp as f64,
count_normal: bm.count_circles as u64,
count_slider: bm.count_sliders as u64,
count_spinner: bm.count_sliders as u64,
max_combo: bm.max_combo.map(|v| v as u64),
bpm: bm.bpm as f64,
drain_length: Duration::from_secs(bm.seconds_drain as u64),
total_length: Duration::from_secs(bm.seconds_total as u64),
}
}
}
impl From<rosu::GameMode> for Mode {
fn from(value: rosu::GameMode) -> Self {
match value {
rosu::GameMode::Osu => Mode::Std,
rosu::GameMode::Taiko => Mode::Taiko,
rosu::GameMode::Catch => Mode::Catch,
rosu::GameMode::Mania => Mode::Mania,
}
}
}
impl From<rosu::beatmap::Genre> for Genre {
fn from(value: rosu::beatmap::Genre) -> Self {
match value {
rosu::beatmap::Genre::Any => Genre::Any,
rosu::beatmap::Genre::Unspecified => Genre::Unspecified,
rosu::beatmap::Genre::VideoGame => Genre::VideoGame,
rosu::beatmap::Genre::Anime => Genre::Anime,
rosu::beatmap::Genre::Rock => Genre::Rock,
rosu::beatmap::Genre::Pop => Genre::Pop,
rosu::beatmap::Genre::Other => Genre::Other,
rosu::beatmap::Genre::Novelty => Genre::Novelty,
rosu::beatmap::Genre::HipHop => Genre::HipHop,
rosu::beatmap::Genre::Electronic => Genre::Electronic,
rosu::beatmap::Genre::Metal => Genre::Metal,
rosu::beatmap::Genre::Classical => Genre::Classical,
rosu::beatmap::Genre::Folk => Genre::Folk,
rosu::beatmap::Genre::Jazz => Genre::Jazz,
}
}
}
impl From<rosu::beatmap::Language> for Language {
fn from(value: rosu::beatmap::Language) -> Self {
match value {
rosu::beatmap::Language::Any => Language::Any,
rosu::beatmap::Language::Other => Language::Other,
rosu::beatmap::Language::English => Language::English,
rosu::beatmap::Language::Japanese => Language::Japanese,
rosu::beatmap::Language::Chinese => Language::Chinese,
rosu::beatmap::Language::Instrumental => Language::Instrumental,
rosu::beatmap::Language::Korean => Language::Korean,
rosu::beatmap::Language::French => Language::French,
rosu::beatmap::Language::German => Language::German,
rosu::beatmap::Language::Swedish => Language::Swedish,
rosu::beatmap::Language::Spanish => Language::Spanish,
rosu::beatmap::Language::Italian => Language::Italian,
rosu::beatmap::Language::Russian => Language::Russian,
rosu::beatmap::Language::Polish => Language::Polish,
rosu::beatmap::Language::Unspecified => Language::Unspecified,
}
}
}

View file

@ -66,7 +66,6 @@ impl ToQuery for UserID {
} }
} }
pub enum BeatmapRequestKind { pub enum BeatmapRequestKind {
ByUser(UserID),
Beatmap(u64), Beatmap(u64),
Beatmapset(u64), Beatmapset(u64),
BeatmapHash(String), BeatmapHash(String),
@ -76,7 +75,6 @@ impl ToQuery for BeatmapRequestKind {
fn to_query(&self) -> Vec<(&'static str, String)> { fn to_query(&self) -> Vec<(&'static str, String)> {
use BeatmapRequestKind::*; use BeatmapRequestKind::*;
match self { match self {
ByUser(ref u) => u.to_query(),
Beatmap(b) => vec![("b", b.to_string())], Beatmap(b) => vec![("b", b.to_string())],
Beatmapset(s) => vec![("s", s.to_string())], Beatmapset(s) => vec![("s", s.to_string())],
BeatmapHash(ref h) => vec![("h", h.clone())], BeatmapHash(ref h) => vec![("h", h.clone())],
@ -87,25 +85,17 @@ impl ToQuery for BeatmapRequestKind {
pub mod builders { pub mod builders {
use reqwest::Response; use reqwest::Response;
use crate::models;
use super::*; use super::*;
/// A builder for a Beatmap request. /// A builder for a Beatmap request.
pub struct BeatmapRequestBuilder { pub struct BeatmapRequestBuilder {
kind: BeatmapRequestKind, kind: BeatmapRequestKind,
since: Option<DateTime<Utc>>,
mode: Option<(Mode, /* Converted */ bool)>, mode: Option<(Mode, /* Converted */ bool)>,
} }
impl BeatmapRequestBuilder { impl BeatmapRequestBuilder {
pub(crate) fn new(kind: BeatmapRequestKind) -> Self { pub(crate) fn new(kind: BeatmapRequestKind) -> Self {
BeatmapRequestBuilder { BeatmapRequestBuilder { kind, mode: None }
kind,
since: None,
mode: None,
}
}
pub fn since(&mut self, since: DateTime<Utc>) -> &mut Self {
self.since = Some(since);
self
} }
pub fn maybe_mode(&mut self, mode: Option<Mode>) -> &mut Self { pub fn maybe_mode(&mut self, mode: Option<Mode>) -> &mut Self {
@ -121,15 +111,26 @@ pub mod builders {
self self
} }
pub(crate) async fn build(self, client: &Client) -> Result<Response> { pub(crate) async fn build(self, client: &Client) -> Result<Vec<models::Beatmap>> {
Ok(client Ok(match self.kind {
.build_request("https://osu.ppy.sh/api/get_beatmaps") BeatmapRequestKind::Beatmap(id) => {
.await? let mut bm = client.rosu.beatmap().map_id(id as u32).await?;
.query(&self.kind.to_query()) let set = bm.mapset.take().unwrap();
.query(&self.since.map(|v| ("since", v)).to_query()) vec![models::Beatmap::from_rosu(bm, &set)]
.query(&self.mode.to_query()) }
.send() BeatmapRequestKind::Beatmapset(id) => {
.await?) let mut set = client.rosu.beatmapset(id as u32).await?;
let bms = set.maps.take().unwrap();
bms.into_iter()
.map(|bm| models::Beatmap::from_rosu(bm, &set))
.collect()
}
BeatmapRequestKind::BeatmapHash(hash) => {
let mut bm = client.rosu.beatmap().checksum(hash).await?;
let set = bm.mapset.take().unwrap();
vec![models::Beatmap::from_rosu(bm, &set)]
}
})
} }
} }