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(()) } // }}}