1
Fork 0

Refactor a huge amount of code!

Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
prescientmoon 2024-08-08 23:26:13 +02:00
parent 8298bdf7cb
commit eec8d4f964
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
25 changed files with 1627 additions and 1786 deletions

View file

@ -2,9 +2,9 @@ use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage};
use sqlx::query;
use crate::{
chart::Side,
arcaea::chart::Side,
context::{Context, Error},
score::guess_song_and_chart,
recognition::fuzzy_song_name::guess_song_and_chart,
};
// {{{ Chart

View file

@ -3,6 +3,7 @@ use crate::context::{Context, Error};
pub mod chart;
pub mod score;
pub mod stats;
mod utils;
// {{{ Help
/// Show this help menu

View file

@ -1,10 +1,10 @@
use std::fmt::Display;
use crate::arcaea::play::{CreatePlay, Play};
use crate::arcaea::score::Score;
use crate::context::{Context, Error};
use crate::score::{CreatePlay, ImageCropper, Play, Score, ScoreKind};
use crate::recognition::recognize::{ImageAnalyzer, ScoreKind};
use crate::user::{discord_it_to_discord_user, User};
use image::imageops::FilterType;
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage};
use crate::{edit_reply, get_user};
use poise::serenity_prelude::CreateMessage;
use poise::{serenity_prelude as serenity, CreateReply};
use sqlx::query;
@ -21,46 +21,13 @@ pub async fn score(_ctx: Context<'_>) -> Result<(), Error> {
}
// }}}
// {{{ Score magic
// {{{ Send error embed with image
async fn error_with_image(
ctx: Context<'_>,
bytes: &[u8],
filename: &str,
message: &str,
err: impl Display,
) -> Result<(), Error> {
let error_attachement = CreateAttachment::bytes(bytes, filename);
let msg = CreateMessage::default().embed(
CreateEmbed::default()
.title(message)
.attachment(filename)
.description(format!("{}", err)),
);
ctx.channel_id()
.send_files(ctx.http(), [error_attachement], msg)
.await?;
Ok(())
}
// }}}
/// 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 = match User::from_context(&ctx).await {
Ok(user) => user,
Err(_) => {
ctx.say("You are not an user in my database, sorry!")
.await?;
return Ok(());
}
};
println!("Handling command from user {:?}", user.discord_id);
let user = get_user!(&ctx);
if files.len() == 0 {
ctx.reply("No images found attached to message").await?;
@ -71,246 +38,103 @@ pub async fn magic(
.reply(format!("Processed 0/{} scores", files.len()))
.await?;
let mut analyzer = ImageAnalyzer::default();
for (i, file) in files.iter().enumerate() {
if let Some(_) = file.dimensions() {
// {{{ Image pre-processing
let bytes = file.download().await?;
let mut image = image::load_from_memory(&bytes)?;
// image = image.resize(1024, 1024, FilterType::Nearest);
// }}}
// {{{ Detection
// Create cropper and run OCR
let mut cropper = ImageCropper::default();
let edited = CreateReply::default()
.reply(true)
.content(format!("Image {}: reading jacket", i + 1));
handle.edit(ctx, edited).await?;
let result: Result<(), Error> = try {
// {{{ Detection
// This makes OCR more likely to work
let mut ocr_image = image.grayscale().blur(1.);
// This makes OCR more likely to work
let mut ocr_image = image.grayscale().blur(1.);
edit_reply!(ctx, handle, "Image {}: reading kind", i + 1).await?;
let kind = analyzer.read_score_kind(ctx.data(), &ocr_image)?;
// {{{ Kind
let edited = CreateReply::default()
.reply(true)
.content(format!("Image {}: reading kind", i + 1));
handle.edit(ctx, edited).await?;
edit_reply!(ctx, handle, "Image {}: reading difficulty", i + 1).await?;
// Do not use `ocr_image` because this reads the colors
let difficulty = analyzer.read_difficulty(ctx.data(), &image, kind)?;
let kind = match cropper.read_score_kind(ctx.data(), &ocr_image) {
// {{{ OCR error handling
Err(err) => {
error_with_image(
ctx,
&cropper.bytes,
&file.filename,
"Could not read kind from picture",
&err,
)
edit_reply!(ctx, handle, "Image {}: reading jacket", i + 1).await?;
let (song, chart) = analyzer
.read_jacket(ctx.data(), &mut image, kind, difficulty)
.await?;
continue;
}
// }}}
Ok(k) => k,
};
// }}}
// {{{ Difficulty
let edited = CreateReply::default()
.reply(true)
.content(format!("Image {}: reading difficulty", i + 1));
handle.edit(ctx, edited).await?;
ocr_image.invert();
// Do not use `ocr_image` because this reads the colors
let difficulty = match cropper.read_difficulty(ctx.data(), &image, kind) {
// {{{ OCR error handling
Err(err) => {
error_with_image(
ctx,
&cropper.bytes,
&file.filename,
"Could not read difficulty from picture",
&err,
)
.await?;
let (note_distribution, max_recall) = match kind {
ScoreKind::ScoreScreen => {
edit_reply!(ctx, handle, "Image {}: reading distribution", i + 1)
.await?;
let note_distribution =
Some(analyzer.read_distribution(ctx.data(), &image)?);
continue;
}
// }}}
Ok(d) => d,
};
edit_reply!(ctx, handle, "Image {}: reading max recall", i + 1).await?;
let max_recall = Some(analyzer.read_max_recall(ctx.data(), &image)?);
println!("{difficulty:?}");
// }}}
// {{{ Jacket & distribution
let mut jacket_rect = None;
let song_by_jacket = cropper
.read_jacket(ctx.data(), &mut image, kind, difficulty, &mut jacket_rect)
.await;
// image.invert();
ocr_image.invert();
let note_distribution = match kind {
ScoreKind::ScoreScreen => Some(cropper.read_distribution(ctx.data(), &image)?),
ScoreKind::SongSelect => None,
};
// }}}
// {{{ Title
let edited = CreateReply::default()
.reply(true)
.content(format!("Image {}: reading title", i + 1));
handle.edit(ctx, edited).await?;
let song_by_name = match kind {
ScoreKind::SongSelect => None,
ScoreKind::ScoreScreen => {
Some(cropper.read_song(ctx.data(), &ocr_image, difficulty))
}
};
let (song, chart) = match (song_by_jacket, song_by_name) {
// {{{ Only name succeeded
(Err(err_jacket), Some(Ok(by_name))) => {
println!("Could not recognise jacket with error: {}", err_jacket);
by_name
}
// }}}
// {{{ Both succeeded
(Ok(by_jacket), Some(Ok(by_name))) => {
if by_name.0.id != by_jacket.0.id {
println!(
"Got diverging choices between '{}' and '{}'",
by_jacket.0.title, by_name.0.title
);
};
by_jacket
} // }}}
// {{{ Only jacket succeeded
(Ok(by_jacket), err_name) => {
if let Some(err) = err_name {
println!("Could not read name with error: {:?}", err.unwrap_err());
(note_distribution, max_recall)
}
ScoreKind::SongSelect => (None, None),
};
by_jacket
}
// }}}
// {{{ Both errors
(Err(err_jacket), err_name) => {
if let Some(rect) = jacket_rect {
cropper.crop_image_to_bytes(&image, rect)?;
error_with_image(
ctx,
&cropper.bytes,
&file.filename,
"Hey! I could not read the score in the provided picture.",
&format!(
"This can mean one of three things:
1. The image you provided is *not that of an Arcaea score
2. The image you provided contains a newly added chart that is not in my database yet
3. The image you provided contains character art that covers the chart name. When this happens, I try to make use of the jacket art in order to determine the chart. Contact `@prescientmoon` on discord to try and resolve the issue!
edit_reply!(ctx, handle, "Image {}: reading score", i + 1).await?;
let score_possibilities = analyzer.read_score(
ctx.data(),
Some(chart.note_count),
&ocr_image,
kind,
)?;
Nerdy info:
```
Jacket error: {}
Title error: {:?}
```" ,
err_jacket, err_name
),
)
.await?;
} else {
ctx.reply(format!(
"This is a weird error that should never happen...
Nerdy info:
```
Jacket error: {}
Title error: {:?}
```",
err_jacket, err_name
))
.await?;
}
continue;
} // }}}
};
println!("{}", song.title);
// }}}
// {{{ Score
let edited = CreateReply::default()
.reply(true)
.content(format!("Image {}: reading score", i + 1));
handle.edit(ctx, edited).await?;
let score_possibilities = match cropper.read_score(
ctx.data(),
Some(chart.note_count),
&ocr_image,
kind,
) {
// {{{ OCR error handling
Err(err) => {
error_with_image(
ctx,
&cropper.bytes,
&file.filename,
"Could not read score from picture",
&err,
)
.await?;
continue;
}
// }}}
Ok(scores) => scores,
};
// }}}
// {{{ Build play
let (score, maybe_fars, score_warning) = Score::resolve_ambiguities(
score_possibilities,
note_distribution,
chart.note_count,
)
.map_err(|err| {
format!(
"Error occurred when disambiguating scores for '{}' [{:?}] by {}: {}",
song.title, difficulty, song.artist, err
// {{{ Build play
let (score, maybe_fars, score_warning) = Score::resolve_ambiguities(
score_possibilities,
note_distribution,
chart.note_count,
)
})?;
println!(
"Maybe fars {:?}, distribution {:?}",
maybe_fars, note_distribution
);
let play = CreatePlay::new(score, &chart, &user)
.with_attachment(file)
.with_fars(maybe_fars)
.save(&ctx.data())
.await?;
// }}}
// }}}
// {{{ Deliver embed
let (mut embed, attachment) = play
.to_embed(&ctx.data().db, &user, &song, &chart, i, None)
.await?;
if let Some(warning) = score_warning {
embed = embed.description(warning);
}
.map_err(|err| {
format!(
"Error occurred when disambiguating scores for '{}' [{:?}] by {}: {}",
song.title, difficulty, song.artist, err
)
})?;
embeds.push(embed);
attachments.extend(attachment);
// }}}
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 (mut embed, attachment) = play
.to_embed(&ctx.data().db, &user, &song, &chart, i, None)
.await?;
if let Some(warning) = score_warning {
embed = embed.description(warning);
}
embeds.push(embed);
attachments.extend(attachment);
// }}}
};
if let Err(err) = result {
analyzer
.send_discord_error(ctx, &image, &file.filename, err)
.await?;
}
} else {
ctx.reply("One of the attached files is not an image!")
.await?;
continue;
}
let edited = CreateReply::default().reply(true).content(format!(
"Processed {}/{} scores",
i + 1,
files.len()
));
handle.edit(ctx, edited).await?;
edit_reply!(ctx, handle, "Processed {}/{} scores", i + 1, files.len()).await?;
}
handle.delete(ctx).await?;
@ -330,14 +154,7 @@ pub async fn delete(
ctx: Context<'_>,
#[description = "Id of score to delete"] ids: Vec<u32>,
) -> Result<(), Error> {
let user = match User::from_context(&ctx).await {
Ok(user) => user,
Err(_) => {
ctx.say("You are not an user in my database, sorry!")
.await?;
return Ok(());
}
};
let user = get_user!(&ctx);
if ids.len() == 0 {
ctx.reply("Empty ID list provided").await?;
@ -383,14 +200,14 @@ pub async fn show(
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=?
",
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)

View file

@ -17,17 +17,20 @@ use poise::{
use sqlx::query_as;
use crate::{
arcaea::chart::{Chart, Song},
arcaea::jacket::BITMAP_IMAGE_SIZE,
arcaea::play::{DbPlay, Play},
arcaea::score::Score,
assets::{
get_b30_background, get_count_background, get_difficulty_background, get_grade_background,
get_name_backgound, get_ptt_emblem, get_score_background, get_status_background,
get_top_backgound, EXO_FONT,
},
bitmap::{Align, BitmapCanvas, Color, LayoutDrawer, LayoutManager, Rect},
chart::{Chart, Song},
context::{Context, Error},
jacket::BITMAP_IMAGE_SIZE,
score::{guess_song_and_chart, DbPlay, Play, Score},
user::{discord_it_to_discord_user, User},
get_user,
recognition::fuzzy_song_name::guess_song_and_chart,
user::discord_it_to_discord_user,
};
// {{{ Stats
@ -63,14 +66,7 @@ pub async fn best(
#[description = "Name of chart to show (difficulty at the end)"]
name: String,
) -> Result<(), Error> {
let user = match User::from_context(&ctx).await {
Ok(user) => user,
Err(_) => {
ctx.say("You are not an user in my database, sorry!")
.await?;
return Ok(());
}
};
let user = get_user!(&ctx);
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
let play = query_as!(
@ -121,14 +117,7 @@ pub async fn plot(
#[description = "Name of chart to show (difficulty at the end)"]
name: String,
) -> Result<(), Error> {
let user = match User::from_context(&ctx).await {
Ok(user) => user,
Err(_) => {
ctx.say("You are not an user in my database, sorry!")
.await?;
return Ok(());
}
};
let user = get_user!(&ctx);
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
@ -240,14 +229,7 @@ pub async fn plot(
/// Show the 30 best scores
#[poise::command(prefix_command, slash_command)]
pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
let user = match User::from_context(&ctx).await {
Ok(user) => user,
Err(_) => {
ctx.say("You are not an user in my database, sorry!")
.await?;
return Ok(());
}
};
let user = get_user!(&ctx);
let plays: Vec<DbPlay> = query_as(
"

24
src/commands/utils.rs Normal file
View file

@ -0,0 +1,24 @@
#[macro_export]
macro_rules! edit_reply {
($ctx:expr, $handle:expr, $($arg:tt)*) => {{
let content = format!($($arg)*);
let edited = CreateReply::default()
.reply(true)
.content(content);
$handle.edit($ctx, edited)
}};
}
#[macro_export]
macro_rules! get_user {
($ctx:expr) => {
match crate::user::User::from_context($ctx).await {
Ok(user) => user,
Err(_) => {
$ctx.say("You are not an user in my database, sorry!")
.await?;
return Ok(());
}
}
};
}