mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-16 07:18:54 +00:00
Implement beatmap metadata caching
This commit is contained in:
parent
20571d35de
commit
00971d7d3e
6 changed files with 104 additions and 22 deletions
|
@ -1,8 +1,9 @@
|
||||||
use super::db::{OsuSavedUsers, OsuUser};
|
use super::db::{OsuSavedUsers, OsuUser};
|
||||||
use super::{embeds::score_embed, BeatmapWithMode, OsuClient};
|
use super::{embeds::score_embed, BeatmapWithMode, OsuClient};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
discord::beatmap_cache::BeatmapMetaCache,
|
||||||
models::{Mode, Score},
|
models::{Mode, Score},
|
||||||
request::{BeatmapRequestKind, UserID},
|
request::UserID,
|
||||||
Client as Osu,
|
Client as Osu,
|
||||||
};
|
};
|
||||||
use announcer::MemberToChannels;
|
use announcer::MemberToChannels;
|
||||||
|
@ -22,6 +23,7 @@ pub const ANNOUNCER_KEY: &'static str = "osu";
|
||||||
/// Announce osu! top scores.
|
/// Announce osu! top scores.
|
||||||
pub fn updates(c: Arc<CacheAndHttp>, d: AppData, channels: MemberToChannels) -> CommandResult {
|
pub fn updates(c: Arc<CacheAndHttp>, d: AppData, channels: MemberToChannels) -> CommandResult {
|
||||||
let osu = d.get_cloned::<OsuClient>();
|
let osu = d.get_cloned::<OsuClient>();
|
||||||
|
let cache = d.get_cloned::<BeatmapMetaCache>();
|
||||||
// For each user...
|
// For each user...
|
||||||
let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone();
|
let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone();
|
||||||
for (user_id, osu_user) in data.iter_mut() {
|
for (user_id, osu_user) in data.iter_mut() {
|
||||||
|
@ -31,7 +33,17 @@ pub fn updates(c: Arc<CacheAndHttp>, d: AppData, channels: MemberToChannels) ->
|
||||||
}
|
}
|
||||||
osu_user.pp = match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania])
|
osu_user.pp = match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania])
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.map(|m| handle_user_mode(c.clone(), &osu, &osu_user, *user_id, &channels[..], *m))
|
.map(|m| {
|
||||||
|
handle_user_mode(
|
||||||
|
c.clone(),
|
||||||
|
&osu,
|
||||||
|
&cache,
|
||||||
|
&osu_user,
|
||||||
|
*user_id,
|
||||||
|
&channels[..],
|
||||||
|
*m,
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect::<Result<_, _>>()
|
.collect::<Result<_, _>>()
|
||||||
{
|
{
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
|
@ -51,6 +63,7 @@ pub fn updates(c: Arc<CacheAndHttp>, d: AppData, channels: MemberToChannels) ->
|
||||||
fn handle_user_mode(
|
fn handle_user_mode(
|
||||||
c: Arc<CacheAndHttp>,
|
c: Arc<CacheAndHttp>,
|
||||||
osu: &Osu,
|
osu: &Osu,
|
||||||
|
cache: &BeatmapMetaCache,
|
||||||
osu_user: &OsuUser,
|
osu_user: &OsuUser,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
channels: &[ChannelId],
|
channels: &[ChannelId],
|
||||||
|
@ -63,9 +76,9 @@ fn handle_user_mode(
|
||||||
scores
|
scores
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.filter_map(|(rank, score)| {
|
.filter_map(|(rank, score)| {
|
||||||
let beatmap = osu
|
let beatmap = cache
|
||||||
.beatmaps(BeatmapRequestKind::Beatmap(score.beatmap_id), |f| f)
|
.get_beatmap_default(score.beatmap_id)
|
||||||
.map(|v| BeatmapWithMode(v.into_iter().next().unwrap(), mode));
|
.map(|v| BeatmapWithMode(v, mode));
|
||||||
match beatmap {
|
match beatmap {
|
||||||
Ok(v) => Some((rank, score, v)),
|
Ok(v) => Some((rank, score, v)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
70
youmubot-osu/src/discord/beatmap_cache.rs
Normal file
70
youmubot-osu/src/discord/beatmap_cache.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use crate::{
|
||||||
|
models::{ApprovalStatus, Beatmap, Mode},
|
||||||
|
Client,
|
||||||
|
};
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use serenity::framework::standard::CommandError;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use youmubot_prelude::TypeMapKey;
|
||||||
|
|
||||||
|
/// BeatmapMetaCache intercepts beatmap-by-id requests and caches them for later recalling.
|
||||||
|
/// Does not cache non-Ranked beatmaps.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BeatmapMetaCache {
|
||||||
|
client: Client,
|
||||||
|
cache: Arc<DashMap<(u64, Mode), Beatmap>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeMapKey for BeatmapMetaCache {
|
||||||
|
type Value = BeatmapMetaCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BeatmapMetaCache {
|
||||||
|
/// Create a new beatmap cache.
|
||||||
|
pub fn new(client: Client) -> Self {
|
||||||
|
BeatmapMetaCache {
|
||||||
|
client,
|
||||||
|
cache: Arc::new(DashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn insert_if_possible(&self, id: u64, mode: Option<Mode>) -> Result<Beatmap, CommandError> {
|
||||||
|
let beatmap = self
|
||||||
|
.client
|
||||||
|
.beatmaps(crate::BeatmapRequestKind::Beatmap(id), |f| {
|
||||||
|
if let Some(mode) = mode {
|
||||||
|
f.mode(mode, true);
|
||||||
|
}
|
||||||
|
f
|
||||||
|
})
|
||||||
|
.and_then(|v| {
|
||||||
|
v.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or(CommandError::from("beatmap not found"))
|
||||||
|
})?;
|
||||||
|
if let ApprovalStatus::Ranked(_) = beatmap.approval {
|
||||||
|
self.cache.insert((id, beatmap.mode), beatmap.clone());
|
||||||
|
};
|
||||||
|
Ok(beatmap)
|
||||||
|
}
|
||||||
|
/// Get the given beatmap
|
||||||
|
pub fn get_beatmap(&self, id: u64, mode: Mode) -> Result<Beatmap, CommandError> {
|
||||||
|
self.cache
|
||||||
|
.get(&(id, mode))
|
||||||
|
.map(|b| Ok(b.clone()))
|
||||||
|
.unwrap_or_else(|| self.insert_if_possible(id, Some(mode)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a beatmap without a mode...
|
||||||
|
pub fn get_beatmap_default(&self, id: u64) -> Result<Beatmap, CommandError> {
|
||||||
|
(&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania])
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&mode| {
|
||||||
|
self.cache
|
||||||
|
.get(&(id, mode))
|
||||||
|
.filter(|b| b.mode == mode)
|
||||||
|
.map(|b| Ok(b.clone()))
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.unwrap_or_else(|| self.insert_if_possible(id, None))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
|
discord::beatmap_cache::BeatmapMetaCache,
|
||||||
discord::oppai_cache::BeatmapCache,
|
discord::oppai_cache::BeatmapCache,
|
||||||
models::{Beatmap, Mode, Mods, Score, User},
|
models::{Beatmap, Mode, Mods, Score, User},
|
||||||
request::{BeatmapRequestKind, UserID},
|
request::{BeatmapRequestKind, UserID},
|
||||||
|
@ -17,6 +18,7 @@ use std::str::FromStr;
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
mod announcer;
|
mod announcer;
|
||||||
|
pub(crate) mod beatmap_cache;
|
||||||
mod cache;
|
mod cache;
|
||||||
mod db;
|
mod db;
|
||||||
pub(crate) mod embeds;
|
pub(crate) mod embeds;
|
||||||
|
@ -59,11 +61,15 @@ pub fn setup(
|
||||||
|
|
||||||
// API client
|
// API client
|
||||||
let http_client = data.get_cloned::<HTTPClient>();
|
let http_client = data.get_cloned::<HTTPClient>();
|
||||||
data.insert::<OsuClient>(OsuHttpClient::new(
|
let osu_client = OsuHttpClient::new(
|
||||||
http_client.clone(),
|
http_client.clone(),
|
||||||
std::env::var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."),
|
std::env::var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."),
|
||||||
));
|
);
|
||||||
|
data.insert::<OsuClient>(osu_client.clone());
|
||||||
data.insert::<oppai_cache::BeatmapCache>(oppai_cache::BeatmapCache::new(http_client));
|
data.insert::<oppai_cache::BeatmapCache>(oppai_cache::BeatmapCache::new(http_client));
|
||||||
|
data.insert::<beatmap_cache::BeatmapMetaCache>(beatmap_cache::BeatmapMetaCache::new(
|
||||||
|
osu_client,
|
||||||
|
));
|
||||||
|
|
||||||
// Announcer
|
// Announcer
|
||||||
announcers.add(announcer::ANNOUNCER_KEY, announcer::updates);
|
announcers.add(announcer::ANNOUNCER_KEY, announcer::updates);
|
||||||
|
@ -224,7 +230,7 @@ impl FromStr for Nth {
|
||||||
|
|
||||||
fn list_plays(plays: &[Score], mode: Mode, ctx: Context, m: &Message) -> CommandResult {
|
fn list_plays(plays: &[Score], mode: Mode, ctx: Context, m: &Message) -> CommandResult {
|
||||||
let watcher = ctx.data.get_cloned::<ReactionWatcher>();
|
let watcher = ctx.data.get_cloned::<ReactionWatcher>();
|
||||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
let osu = ctx.data.get_cloned::<BeatmapMetaCache>();
|
||||||
let beatmap_cache = ctx.data.get_cloned::<BeatmapCache>();
|
let beatmap_cache = ctx.data.get_cloned::<BeatmapCache>();
|
||||||
|
|
||||||
if plays.is_empty() {
|
if plays.is_empty() {
|
||||||
|
@ -254,11 +260,7 @@ fn list_plays(plays: &[Score], mode: Mode, ctx: Context, m: &Message) -> Command
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, v)| {
|
.map(|(i, v)| {
|
||||||
v.get_or_insert_with(|| {
|
v.get_or_insert_with(|| {
|
||||||
if let Some(b) = osu
|
if let Some(b) = osu.get_beatmap(plays[i].beatmap_id, mode).ok() {
|
||||||
.beatmaps(BeatmapRequestKind::Beatmap(plays[i].beatmap_id), |f| f)
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.into_iter().next())
|
|
||||||
{
|
|
||||||
let stars = beatmap_cache
|
let stars = beatmap_cache
|
||||||
.get_beatmap(b.beatmap_id)
|
.get_beatmap(b.beatmap_id)
|
||||||
.ok()
|
.ok()
|
||||||
|
@ -350,6 +352,7 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||||
|
|
||||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
|
let meta_cache = ctx.data.get_cloned::<BeatmapMetaCache>();
|
||||||
let user = osu
|
let user = osu
|
||||||
.user(user, |f| f.mode(mode))?
|
.user(user, |f| f.mode(mode))?
|
||||||
.ok_or(Error::from("User not found"))?;
|
.ok_or(Error::from("User not found"))?;
|
||||||
|
@ -360,12 +363,8 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.last()
|
.last()
|
||||||
.ok_or(Error::from("No such play"))?;
|
.ok_or(Error::from("No such play"))?;
|
||||||
let beatmap = osu
|
let beatmap = meta_cache
|
||||||
.beatmaps(BeatmapRequestKind::Beatmap(recent_play.beatmap_id), |f| {
|
.get_beatmap(recent_play.beatmap_id, mode)
|
||||||
f.mode(mode, true)
|
|
||||||
})?
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.map(|v| BeatmapWithMode(v, mode))
|
.map(|v| BeatmapWithMode(v, mode))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ use std::{convert::TryInto, sync::Arc};
|
||||||
|
|
||||||
/// Client is the client that will perform calls to the osu! api server.
|
/// Client is the client that will perform calls to the osu! api server.
|
||||||
/// It's cheap to clone, so do it.
|
/// It's cheap to clone, so do it.
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
key: Arc<String>,
|
key: Arc<String>,
|
||||||
client: HTTPClient,
|
client: HTTPClient,
|
||||||
|
|
|
@ -94,7 +94,7 @@ impl fmt::Display for Language {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, std::hash::Hash)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
Std,
|
Std,
|
||||||
Taiko,
|
Taiko,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
pub(crate) struct Beatmap {
|
pub(crate) struct Beatmap {
|
||||||
pub approved: String,
|
pub approved: String,
|
||||||
pub submit_date: String,
|
pub submit_date: String,
|
||||||
|
|
Loading…
Add table
Reference in a new issue