mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 16:58:55 +00:00
Implement beatmap collecting from rosu-v2
This commit is contained in:
parent
8febf0106e
commit
7d00b95a4f
6 changed files with 171 additions and 25 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3104,6 +3104,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"serenity",
|
||||
"time",
|
||||
"youmubot-db",
|
||||
"youmubot-db-sql",
|
||||
"youmubot-prelude",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -23,7 +23,7 @@ pub struct Client {
|
|||
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());
|
||||
|
||||
for u in v.into_iter() {
|
||||
|
@ -70,8 +70,7 @@ impl Client {
|
|||
) -> Result<Vec<Beatmap>> {
|
||||
let mut r = BeatmapRequestBuilder::new(kind);
|
||||
f(&mut r);
|
||||
let res: Vec<raw::Beatmap> = r.build(self).await?.json().await?;
|
||||
Ok(vec_try_into(res)?)
|
||||
r.build(self).await
|
||||
}
|
||||
|
||||
pub async fn user(
|
||||
|
|
|
@ -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 {
|
||||
|
|
142
youmubot-osu/src/models/rosu.rs
Normal file
142
youmubot-osu/src/models/rosu.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<DateTime<Utc>>,
|
||||
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<Utc>) -> &mut Self {
|
||||
self.since = Some(since);
|
||||
self
|
||||
BeatmapRequestBuilder { kind, mode: None }
|
||||
}
|
||||
|
||||
pub fn maybe_mode(&mut self, mode: Option<Mode>) -> &mut Self {
|
||||
|
@ -121,15 +111,26 @@ pub mod builders {
|
|||
self
|
||||
}
|
||||
|
||||
pub(crate) async fn build(self, client: &Client) -> Result<Response> {
|
||||
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<Vec<models::Beatmap>> {
|
||||
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)]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue