269 lines
6.9 KiB
Rust
269 lines
6.9 KiB
Rust
use std::time::Instant;
|
|
|
|
use crate::arcaea::play::{CreatePlay, Play};
|
|
use crate::arcaea::score::Score;
|
|
use crate::context::{Context, Error};
|
|
use crate::recognition::recognize::{ImageAnalyzer, ScoreKind};
|
|
use crate::user::{discord_it_to_discord_user, User};
|
|
use crate::{edit_reply, get_user, timed};
|
|
use image::DynamicImage;
|
|
use poise::serenity_prelude::futures::future::join_all;
|
|
use poise::serenity_prelude::CreateMessage;
|
|
use poise::{serenity_prelude as serenity, CreateReply};
|
|
use sqlx::query;
|
|
|
|
// {{{ Score
|
|
/// Score management
|
|
#[poise::command(
|
|
prefix_command,
|
|
slash_command,
|
|
subcommands("magic", "delete", "show"),
|
|
subcommand_required
|
|
)]
|
|
pub async fn score(_ctx: Context<'_>) -> Result<(), Error> {
|
|
Ok(())
|
|
}
|
|
// }}}
|
|
// {{{ Score magic
|
|
/// Identify scores from attached images.
|
|
#[poise::command(prefix_command, slash_command)]
|
|
pub async fn magic(
|
|
ctx: Context<'_>,
|
|
#[description = "Images containing scores"] files: Vec<serenity::Attachment>,
|
|
) -> Result<(), Error> {
|
|
let user = get_user!(&ctx);
|
|
|
|
if files.len() == 0 {
|
|
ctx.reply("No images found attached to message").await?;
|
|
return Ok(());
|
|
}
|
|
|
|
let mut embeds = Vec::with_capacity(files.len());
|
|
let mut attachments = Vec::with_capacity(files.len());
|
|
let handle = ctx
|
|
.reply(format!("Processed 0/{} scores", files.len()))
|
|
.await?;
|
|
|
|
let mut analyzer = ImageAnalyzer::default();
|
|
|
|
// {{{ Download files
|
|
let download_tasks = files
|
|
.iter()
|
|
.filter(|file| file.dimensions().is_some())
|
|
.map(|file| async move { (file, file.download().await) });
|
|
|
|
let downloaded = timed!("dowload_files", { join_all(download_tasks).await });
|
|
|
|
if downloaded.len() < files.len() {
|
|
ctx.reply("One or more of the attached files are not images!")
|
|
.await?;
|
|
}
|
|
// }}}
|
|
|
|
for (i, (file, bytes)) in downloaded.into_iter().enumerate() {
|
|
let bytes = bytes?;
|
|
|
|
let start = Instant::now();
|
|
// {{{ Preapare image
|
|
let mut image = timed!("decode image", { image::load_from_memory(&bytes)? });
|
|
let mut grayscale_image = timed!("grayscale image", {
|
|
DynamicImage::ImageLuma8(image.to_luma8())
|
|
});
|
|
// image = image.resize(1024, 1024, FilterType::Nearest);
|
|
// }}}
|
|
|
|
let result: Result<(), Error> = try {
|
|
// {{{ Detection
|
|
|
|
// edit_reply!(ctx, handle, "Image {}: reading kind", i + 1).await?;
|
|
let kind = timed!("read_score_kind", {
|
|
analyzer.read_score_kind(ctx.data(), &grayscale_image)?
|
|
});
|
|
|
|
// edit_reply!(ctx, handle, "Image {}: reading difficulty", i + 1).await?;
|
|
// Do not use `ocr_image` because this reads the colors
|
|
let difficulty = timed!("read_difficulty", {
|
|
analyzer.read_difficulty(ctx.data(), &image, &grayscale_image, kind)?
|
|
});
|
|
|
|
// edit_reply!(ctx, handle, "Image {}: reading jacket", i + 1).await?;
|
|
let (song, chart) = timed!("read_jacket", {
|
|
analyzer.read_jacket(ctx.data(), &mut image, kind, difficulty)?
|
|
});
|
|
|
|
let max_recall = match kind {
|
|
ScoreKind::ScoreScreen => {
|
|
// edit_reply!(ctx, handle, "Image {}: reading max recall", i + 1).await?;
|
|
Some(analyzer.read_max_recall(ctx.data(), &grayscale_image)?)
|
|
}
|
|
ScoreKind::SongSelect => None,
|
|
};
|
|
|
|
grayscale_image.invert();
|
|
let note_distribution = match kind {
|
|
ScoreKind::ScoreScreen => {
|
|
// edit_reply!(ctx, handle, "Image {}: reading distribution", i + 1).await?;
|
|
Some(analyzer.read_distribution(ctx.data(), &grayscale_image)?)
|
|
}
|
|
ScoreKind::SongSelect => None,
|
|
};
|
|
|
|
// edit_reply!(ctx, handle, "Image {}: reading score", i + 1).await?;
|
|
let score = timed!("read_score", {
|
|
analyzer.read_score(ctx.data(), Some(chart.note_count), &grayscale_image, kind)?
|
|
});
|
|
|
|
// {{{ Build play
|
|
let maybe_fars =
|
|
Score::resolve_distibution_ambiguities(score, note_distribution, chart.note_count);
|
|
|
|
let play = CreatePlay::new(score, &chart, &user)
|
|
.with_attachment(file)
|
|
.with_fars(maybe_fars)
|
|
.with_max_recall(max_recall)
|
|
.save(&ctx.data())
|
|
.await?;
|
|
// }}}
|
|
// }}}
|
|
// {{{ Deliver embed
|
|
|
|
let (embed, attachment) = timed!("to embed", {
|
|
play.to_embed(&ctx.data().db, &user, &song, &chart, i, None)
|
|
.await?
|
|
});
|
|
|
|
embeds.push(embed);
|
|
attachments.extend(attachment);
|
|
// }}}
|
|
};
|
|
|
|
if let Err(err) = result {
|
|
analyzer
|
|
.send_discord_error(ctx, &image, &file.filename, err)
|
|
.await?;
|
|
}
|
|
|
|
let took = start.elapsed();
|
|
|
|
edit_reply!(
|
|
ctx,
|
|
handle,
|
|
"Processed {}/{} scores. Last score took {took:?} to process.",
|
|
i + 1,
|
|
files.len()
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
handle.delete(ctx).await?;
|
|
|
|
if embeds.len() > 0 {
|
|
ctx.channel_id()
|
|
.send_files(ctx.http(), attachments, CreateMessage::new().embeds(embeds))
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
// }}}
|
|
// {{{ Score delete
|
|
/// Delete scores, given their IDs.
|
|
#[poise::command(prefix_command, slash_command)]
|
|
pub async fn delete(
|
|
ctx: Context<'_>,
|
|
#[description = "Id of score to delete"] ids: Vec<u32>,
|
|
) -> Result<(), Error> {
|
|
let user = get_user!(&ctx);
|
|
|
|
if ids.len() == 0 {
|
|
ctx.reply("Empty ID list provided").await?;
|
|
return Ok(());
|
|
}
|
|
|
|
let mut count = 0;
|
|
|
|
for id in ids {
|
|
let res = query!("DELETE FROM plays WHERE id=? AND user_id=?", id, user.id)
|
|
.execute(&ctx.data().db)
|
|
.await?;
|
|
|
|
if res.rows_affected() == 0 {
|
|
ctx.reply(format!("No play with id {} found", id)).await?;
|
|
} else {
|
|
count += 1;
|
|
}
|
|
}
|
|
|
|
if count > 0 {
|
|
ctx.reply(format!("Deleted {} play(s) successfully!", count))
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
// }}}
|
|
// {{{ Score show
|
|
/// Show scores given their ides
|
|
#[poise::command(prefix_command, slash_command)]
|
|
pub async fn show(
|
|
ctx: Context<'_>,
|
|
#[description = "Ids of score to show"] ids: Vec<u32>,
|
|
) -> Result<(), Error> {
|
|
if ids.len() == 0 {
|
|
ctx.reply("Empty ID list provided").await?;
|
|
return Ok(());
|
|
}
|
|
|
|
let mut embeds = Vec::with_capacity(ids.len());
|
|
let mut attachments = Vec::with_capacity(ids.len());
|
|
for (i, id) in ids.iter().enumerate() {
|
|
let res = query!(
|
|
"
|
|
SELECT
|
|
p.id,p.chart_id,p.user_id,p.score,p.zeta_score,
|
|
p.max_recall,p.created_at,p.far_notes,
|
|
u.discord_id
|
|
FROM plays p
|
|
JOIN users u ON p.user_id = u.id
|
|
WHERE p.id=?
|
|
",
|
|
id
|
|
)
|
|
.fetch_one(&ctx.data().db)
|
|
.await
|
|
.map_err(|_| format!("Could not find play with id {}", id))?;
|
|
|
|
let play = Play {
|
|
id: res.id as u32,
|
|
chart_id: res.chart_id as u32,
|
|
user_id: res.user_id as u32,
|
|
score: Score(res.score as u32),
|
|
zeta_score: Score(res.zeta_score as u32),
|
|
max_recall: res.max_recall.map(|r| r as u32),
|
|
far_notes: res.far_notes.map(|r| r as u32),
|
|
created_at: res.created_at,
|
|
discord_attachment_id: None,
|
|
creation_ptt: None,
|
|
creation_zeta_ptt: None,
|
|
};
|
|
|
|
let author = discord_it_to_discord_user(&ctx, &res.discord_id).await?;
|
|
let user = User::by_id(&ctx.data().db, play.user_id).await?;
|
|
|
|
let (song, chart) = ctx.data().song_cache.lookup_chart(play.chart_id)?;
|
|
let (embed, attachment) = play
|
|
.to_embed(&ctx.data().db, &user, song, chart, i, Some(&author))
|
|
.await?;
|
|
|
|
embeds.push(embed);
|
|
attachments.extend(attachment);
|
|
}
|
|
|
|
ctx.channel_id()
|
|
.send_files(ctx.http(), attachments, CreateMessage::new().embeds(embeds))
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
// }}}
|