mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 08:48:54 +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",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serenity",
|
"serenity",
|
||||||
|
"time",
|
||||||
"youmubot-db",
|
"youmubot-db",
|
||||||
"youmubot-db-sql",
|
"youmubot-db-sql",
|
||||||
"youmubot-prelude",
|
"youmubot-prelude",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
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 {
|
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)]
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue