diff --git a/Cargo.lock b/Cargo.lock index cbadde2..cb73d97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3104,6 +3104,7 @@ dependencies = [ "serde", "serde_json", "serenity", + "time", "youmubot-db", "youmubot-db-sql", "youmubot-prelude", diff --git a/youmubot-osu/Cargo.toml b/youmubot-osu/Cargo.toml index 08c00a9..b400532 100644 --- a/youmubot-osu/Cargo.toml +++ b/youmubot-osu/Cargo.toml @@ -17,6 +17,7 @@ regex = "1.5.6" reqwest = "0.11.10" rosu-pp = "0.9.1" rosu-v2 = "0.8" +time = "0.3" serde = { version = "1.0.137", features = ["derive"] } serenity = "0.11.2" zip = "0.6.2" diff --git a/youmubot-osu/src/lib.rs b/youmubot-osu/src/lib.rs index eb451b0..64d6af5 100644 --- a/youmubot-osu/src/lib.rs +++ b/youmubot-osu/src/lib.rs @@ -23,7 +23,7 @@ pub struct Client { rosu: rosu_v2::Osu, } -fn vec_try_into>(v: Vec) -> Result, T::Error> { +pub fn vec_try_into>(v: Vec) -> Result, T::Error> { let mut res = Vec::with_capacity(v.len()); for u in v.into_iter() { @@ -70,8 +70,7 @@ impl Client { ) -> Result> { let mut r = BeatmapRequestBuilder::new(kind); f(&mut r); - let res: Vec = r.build(self).await?.json().await?; - Ok(vec_try_into(res)?) + r.build(self).await } pub async fn user( diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index 932eee4..c905782 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -8,6 +8,7 @@ use std::time::Duration; pub mod mods; pub mod parse; pub(crate) mod raw; +pub(crate) mod rosu; pub use mods::Mods; use serenity::utils::MessageBuilder; @@ -252,6 +253,7 @@ pub enum Language { Italian, Russian, Polish, + Unspecified, } impl fmt::Display for Language { diff --git a/youmubot-osu/src/models/rosu.rs b/youmubot-osu/src/models/rosu.rs new file mode 100644 index 0000000..ebeb4e7 --- /dev/null +++ b/youmubot-osu/src/models/rosu.rs @@ -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>, + ) -> 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 { + 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::()) as f64 + / (rs.iter().sum::() 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 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 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 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, + } + } +} diff --git a/youmubot-osu/src/request.rs b/youmubot-osu/src/request.rs index 5da2a54..516376a 100644 --- a/youmubot-osu/src/request.rs +++ b/youmubot-osu/src/request.rs @@ -66,7 +66,6 @@ impl ToQuery for UserID { } } pub enum BeatmapRequestKind { - ByUser(UserID), Beatmap(u64), Beatmapset(u64), BeatmapHash(String), @@ -76,7 +75,6 @@ impl ToQuery for BeatmapRequestKind { fn to_query(&self) -> Vec<(&'static str, String)> { use BeatmapRequestKind::*; match self { - ByUser(ref u) => u.to_query(), Beatmap(b) => vec![("b", b.to_string())], Beatmapset(s) => vec![("s", s.to_string())], BeatmapHash(ref h) => vec![("h", h.clone())], @@ -87,25 +85,17 @@ impl ToQuery for BeatmapRequestKind { pub mod builders { use reqwest::Response; + use crate::models; + use super::*; /// A builder for a Beatmap request. pub struct BeatmapRequestBuilder { kind: BeatmapRequestKind, - since: Option>, mode: Option<(Mode, /* Converted */ bool)>, } impl BeatmapRequestBuilder { pub(crate) fn new(kind: BeatmapRequestKind) -> Self { - BeatmapRequestBuilder { - kind, - since: None, - mode: None, - } - } - - pub fn since(&mut self, since: DateTime) -> &mut Self { - self.since = Some(since); - self + BeatmapRequestBuilder { kind, mode: None } } pub fn maybe_mode(&mut self, mode: Option) -> &mut Self { @@ -121,15 +111,26 @@ pub mod builders { self } - pub(crate) async fn build(self, client: &Client) -> Result { - Ok(client - .build_request("https://osu.ppy.sh/api/get_beatmaps") - .await? - .query(&self.kind.to_query()) - .query(&self.since.map(|v| ("since", v)).to_query()) - .query(&self.mode.to_query()) - .send() - .await?) + pub(crate) async fn build(self, client: &Client) -> Result> { + Ok(match self.kind { + BeatmapRequestKind::Beatmap(id) => { + let mut bm = client.rosu.beatmap().map_id(id as u32).await?; + let set = bm.mapset.take().unwrap(); + vec![models::Beatmap::from_rosu(bm, &set)] + } + BeatmapRequestKind::Beatmapset(id) => { + 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)] + } + }) } }