From 7a565f377203ba8e01574952014a23dfa8d94fa3 Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 30 Jul 2026 15:09:51 +0200 Subject: fix: auto completion timing out before ready + fix preexisting warns --- src/commands/ai.rs | 4 +- src/commands/autocomplete.rs | 249 +++++++++++++++++++++++++++++-------------- src/commands/music.rs | 13 ++- src/commands/trigger.rs | 12 +-- src/commands/tts.rs | 4 +- src/commands/utilities.rs | 5 +- src/copyparty/mod.rs | 18 ++-- src/events/mod.rs | 24 ++--- src/lang.rs | 2 - src/main.rs | 57 ++++++++-- 10 files changed, 259 insertions(+), 129 deletions(-) (limited to 'src') diff --git a/src/commands/ai.rs b/src/commands/ai.rs index 5ab077e..a7a9a7e 100644 --- a/src/commands/ai.rs +++ b/src/commands/ai.rs @@ -18,7 +18,7 @@ pub async fn senator( let response = client.chat(lang::SENATOR_SYSTEM_PROMPT, &text).await?; if tts_vc.unwrap_or(false) { - let guild_id = ctx.guild_id().ok_or_else(|| lang::err_not_in_guild())?; + let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; let channel_id = ctx .serenity_context() .cache @@ -28,7 +28,7 @@ pub async fn senator( .get(&ctx.author().id) .and_then(|vs| vs.channel_id) }) - .ok_or_else(|| lang::err_not_in_voice())?; + .ok_or_else(lang::err_not_in_voice)?; let tts_url = format!( "https://translate.google.com/translate_tts?ie=UTF-8&q={}&tl=pl&client=tw-ob", diff --git a/src/commands/autocomplete.rs b/src/commands/autocomplete.rs index 97a18ec..434870c 100644 --- a/src/commands/autocomplete.rs +++ b/src/commands/autocomplete.rs @@ -1,13 +1,27 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + use poise::serenity_prelude as serenity; +use tokio::sync::oneshot; +use tracing::{debug, warn}; -use crate::Data; -use crate::copyparty::{html_decode, percent_decode}; +use crate::copyparty::{self, html_decode, percent_decode}; use crate::error::Error; +use crate::Data; type Context<'a> = poise::Context<'a, Data, Error>; const MAX_CHOICE_LEN: usize = 99; +/// Maximum time we wait for a fresh search before returning a mock entry. +/// Discord gives 3 seconds; we use 2 to leave room for the API call. +const AC_SEARCH_TIMEOUT: Duration = Duration::from_secs(2); + +/// Value used for the mock "searching" placeholder — recognisable by the +/// command handler as a no-op. +const MOCK_SEARCHING: &str = "\u{231B}..."; + fn truncate_choice(s: &str) -> String { if s.chars().count() <= MAX_CHOICE_LEN { return s.to_string(); @@ -26,93 +40,173 @@ fn filename_label(raw_path: &str) -> Option { Some(truncate_choice(&decoded)) } -pub async fn audio_file<'a>( +/// Build (label, value) pairs from search hits. Sync because it doesn't stash. +fn build_choices(hits: &[copyparty::SearchHit]) -> Vec<(String, String)> { + let mut out = Vec::new(); + for hit in hits { + let rp = match &hit.rp { + Some(p) => p.clone(), + None => continue, + }; + let Some(label) = filename_label(&rp) else { + continue; + }; + let value = truncate_choice(&rp); + out.push((label, value)); + if out.len() >= 25 { + break; + } + } + out +} + +/// Stash choices into the per-user ac_cache, then wrap into AutocompleteChoice. +async fn stash_and_wrap( ctx: Context<'_>, - partial: &'a str, + hits: &[copyparty::SearchHit], + pairs: Vec<(String, String)>, ) -> Vec { - eprintln!("[DEBUG] autocomplete audio_file(partial={partial:?})"); - let results = ctx - .data() - .copyparty - .search(partial, 25, &["mp3", "wav", "ogg", "flac", "m4a", "opus"]) - .await; - match results { - Ok(hits) => { - eprintln!("[DEBUG] audio_file got {} raw hits", hits.len()); - let mut choices = Vec::new(); - for (i, hit) in hits.into_iter().enumerate() { - let rp = match &hit.rp { - Some(p) => p.clone(), - None => continue, - }; - eprintln!( - "[DEBUG] hit[{i}]: rp={rp:?}, url={:?}, name={:?}, path={:?}", - hit.url, hit.name, hit.path - ); - let Some(label) = filename_label(&rp) else { - continue; - }; - - let value = truncate_choice(&rp); - ctx.data() - .ac_stash(ctx.author().id, value.clone(), hit) - .await; - eprintln!("[DEBUG] -> cached as {value:?}"); - choices.push(serenity::AutocompleteChoice::new(label, value)); - if choices.len() >= 25 { - break; - } - } - eprintln!("[DEBUG] audio_file returning {} choices", choices.len()); - choices - } - Err(e) => { - eprintln!("[DEBUG] audio_file search failed: {e}"); - vec![] + let mut choices = Vec::with_capacity(pairs.len()); + for (label, value) in pairs { + // Re-stash into per-user cache so the command handler can resolve + let hit = hits + .iter() + .find(|h| { + h.rp + .as_deref() + .map(|rp| truncate_choice(rp) == value) + .unwrap_or(false) + }) + .cloned(); + if let Some(h) = hit { + ctx.data() + .ac_stash(ctx.author().id, value.clone(), h) + .await; } + choices.push(serenity::AutocompleteChoice::new(label, value)); } + choices } -pub async fn any_file<'a>(ctx: Context<'_>, partial: &'a str) -> Vec { - eprintln!("[DEBUG] autocomplete any_file(partial={partial:?})"); - let results = ctx.data().copyparty.search(partial, 25, &[]).await; - match results { - Ok(hits) => { - eprintln!("[DEBUG] any_file got {} raw hits", hits.len()); - let mut choices = Vec::new(); - for (i, hit) in hits.into_iter().enumerate() { - let rp = match &hit.rp { - Some(p) => p.clone(), - None => continue, - }; - eprintln!( - "[DEBUG] hit[{i}]: rp={rp:?}, url={:?}, name={:?}, path={:?}", - hit.url, hit.name, hit.path - ); - let Some(label) = filename_label(&rp) else { - continue; - }; - ctx.data() - .ac_stash(ctx.author().id, label.clone(), hit) - .await; - eprintln!("[DEBUG] -> cached as {label:?}"); - choices.push(serenity::AutocompleteChoice::new(label.clone(), label)); - if choices.len() >= 25 { - break; - } +/// Spawn a single Copyparty search task. Returns a oneshot receiver that will +/// receive the results when the search completes. +/// +/// The task always runs to completion even if the receiver is dropped, and +/// writes results into the shared `ac_search_cache` so *future* autocomplete +/// invocations can serve from cache. +type SearchCache = Arc< + tokio::sync::Mutex, std::time::Instant)>>, +>; + +fn spawn_search( + cache: SearchCache, + client: copyparty::CopypartyClient, + query: String, + extensions: Vec, +) -> oneshot::Receiver> { + let (tx, rx) = oneshot::channel(); + + tokio::spawn(async move { + let exts: Vec<&str> = extensions.iter().map(|s| s.as_str()).collect(); + let result = client.search(&query, 25, &exts).await; + + match result { + Ok(hits) => { + debug!(%query, count = hits.len(), "background search completed"); + // Always fill the shared search cache + cache + .lock() + .await + .insert(query, (hits.clone(), std::time::Instant::now())); + // Send to foreground (ignores error if receiver was dropped) + let _ = tx.send(hits); + } + Err(e) => { + debug!(%query, error = %e, "background search failed"); } - eprintln!("[DEBUG] any_file returning {} choices", choices.len()); - choices } - Err(e) => { - eprintln!("[DEBUG] any_file search failed: {e}"); + }); + + rx +} + +/// Run a Copyparty search with mock fallback and background cache fill. +/// +/// 1. Check shared search cache → return instantly if fresh and non-empty. +/// 2. Spawn a *single* search task that always runs to completion. +/// 3. Wait for it up to 2 seconds. +/// 4. On success: stash per-user + return real choices. +/// 5. On timeout: return a mock entry; the background task fills the cache, +/// so the next keystroke's autocomplete will get real results. +async fn timed_search( + ctx: Context<'_>, + partial: &str, + extensions: &[&str], +) -> Vec { + // 1. Shared search cache hit? + if let Some(hits) = ctx.data().ac_search_cache_lookup(partial).await + && !hits.is_empty() { + debug!(%partial, count = hits.len(), "autocomplete cache hit"); + let pairs = build_choices(&hits); + return stash_and_wrap(ctx, &hits, pairs).await; + } + + // 2. Spawn ONE search task (survives timeout → fills cache) + let exts: Vec = extensions.iter().map(|s| s.to_string()).collect(); + let mut rx = spawn_search( + ctx.data().ac_search_cache.clone(), + ctx.data().copyparty.clone(), + partial.to_string(), + exts, + ); + + // 3. Wait for the task with a timeout + match tokio::time::timeout(AC_SEARCH_TIMEOUT, &mut rx).await { + Ok(Ok(hits)) => { + debug!(%partial, count = hits.len(), "autocomplete got results in time"); + let pairs = build_choices(&hits); + stash_and_wrap(ctx, &hits, pairs).await + } + Ok(Err(_)) => { + warn!(%partial, "autocomplete search task finished without sending (sender dropped)"); vec![] } + Err(_elapsed) => { + warn!( + %partial, + "autocomplete search timed out after {AC_SEARCH_TIMEOUT:?}, returning mock" + ); + // The search task is still running in the background and will fill + // the cache when done. Return a mock so the user sees *something* + // and is prompted to type more → fresh autocomplete → cache hit. + vec![serenity::AutocompleteChoice::new( + format!( + "\u{231B} Szukanie \u{201E}{}\u{201D}\u{2026}", + truncate_choice(partial) + ), + MOCK_SEARCHING.to_string(), + )] + } } } -pub async fn trigger_name<'a>(ctx: Context<'_>, partial: &'a str) -> Vec { - eprintln!("[DEBUG] autocomplete trigger_name(partial={partial:?})"); +pub async fn audio_file( + ctx: Context<'_>, + partial: &str, +) -> Vec { + timed_search( + ctx, + partial, + &["mp3", "wav", "ogg", "flac", "m4a", "opus"], + ) + .await +} + +pub async fn any_file(ctx: Context<'_>, partial: &str) -> Vec { + timed_search(ctx, partial, &[]).await +} + +pub async fn trigger_name(ctx: Context<'_>, partial: &str) -> Vec { let db_pool = &ctx.data().db_pool; match crate::database::list_triggers(db_pool).await { Ok(triggers) => { @@ -122,11 +216,10 @@ pub async fn trigger_name<'a>(ctx: Context<'_>, partial: &'a str) -> Vec .filter(|name| partial.is_empty() || name.starts_with(partial)) .take(25) .collect(); - eprintln!("[DEBUG] trigger_name returning {} names", names.len()); names } Err(e) => { - eprintln!("[DEBUG] trigger_name query failed: {e}"); + warn!(%partial, error = %e, "autocomplete trigger_name failed"); vec![] } } diff --git a/src/commands/music.rs b/src/commands/music.rs index d2861e4..05abb9c 100644 --- a/src/commands/music.rs +++ b/src/commands/music.rs @@ -220,11 +220,10 @@ fn redact_url(url: &str) -> &str { /// yt-dlp only needs `?v=ID`; extra `&` params can cause failures. pub(crate) fn sanitize_yt_url(url: &str) -> String { let lower = url.to_lowercase(); - if lower.contains("youtube.com/") || lower.contains("youtu.be/") { - if let Some(pos) = url.find('&') { + if (lower.contains("youtube.com/") || lower.contains("youtu.be/")) + && let Some(pos) = url.find('&') { return url[..pos].to_string(); } - } url.to_string() } @@ -241,7 +240,7 @@ async fn play_impl( ) -> Result<()> { ctx.defer().await?; - let guild_id = ctx.guild_id().ok_or_else(|| lang::err_not_in_guild())?; + let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; let channel_id = ctx .serenity_context() @@ -252,7 +251,7 @@ async fn play_impl( .get(&ctx.author().id) .and_then(|vs| vs.channel_id) }) - .ok_or_else(|| lang::err_not_in_voice())?; + .ok_or_else(lang::err_not_in_voice)?; let src = resolve_source(ctx, melodia, file).await?; @@ -373,7 +372,7 @@ async fn play_impl( /// Stop playback and disconnect from voice #[poise::command(slash_command)] pub async fn stop(ctx: Context<'_>) -> Result<()> { - let guild_id = ctx.guild_id().ok_or_else(|| lang::err_not_in_guild())?; + let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; ctx.data().voice_manager.disconnect(guild_id).await?; ctx.data().music_tracks.lock().await.remove(&guild_id.get()); ctx.send( @@ -389,7 +388,7 @@ pub async fn stop(ctx: Context<'_>) -> Result<()> { #[poise::command(slash_command)] pub async fn join(ctx: Context<'_>) -> Result<()> { ctx.defer().await?; - let guild_id = ctx.guild_id().ok_or_else(|| lang::err_not_in_guild())?; + let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; let channel_id = ctx.serenity_context().cache.guild(guild_id).and_then(|g| { g.voice_states diff --git a/src/commands/trigger.rs b/src/commands/trigger.rs index 7453e03..2a33528 100644 --- a/src/commands/trigger.rs +++ b/src/commands/trigger.rs @@ -49,7 +49,7 @@ pub async fn add( ) -> Result<()> { let t = database::get_trigger_by_name(&ctx.data().db_pool, &name) .await? - .ok_or_else(|| lang::err_trigger_not_found())?; + .ok_or_else(lang::err_trigger_not_found)?; let mut modified = false; if let Some(kw) = trigger_keywords { @@ -90,7 +90,7 @@ pub async fn remove( ) -> Result<()> { let t = database::get_trigger_by_name(&ctx.data().db_pool, &name) .await? - .ok_or_else(|| lang::err_trigger_not_found())?; + .ok_or_else(lang::err_trigger_not_found)?; let mut modified = false; if let Some(kw) = trigger_keyword { @@ -119,7 +119,7 @@ pub async fn delete( ) -> Result<()> { let t = database::get_trigger_by_name(&ctx.data().db_pool, &name) .await? - .ok_or_else(|| lang::err_trigger_not_found())?; + .ok_or_else(lang::err_trigger_not_found)?; database::delete_trigger(&ctx.data().db_pool, t.id).await?; ctx.data().reload_triggers().await; @@ -137,7 +137,7 @@ pub async fn list( ) -> Result<()> { let t = database::get_trigger_by_name(&ctx.data().db_pool, &name) .await? - .ok_or_else(|| lang::err_trigger_not_found())?; + .ok_or_else(lang::err_trigger_not_found)?; let keywords = database::get_keywords(&ctx.data().db_pool, t.id).await?; let responses = database::get_responses(&ctx.data().db_pool, t.id).await?; @@ -153,7 +153,7 @@ pub async fn list( } else { keywords .iter() - .map(|r| format!("{r}")) + .map(|r| r.to_string()) .collect::>() .join("\n") }, @@ -166,7 +166,7 @@ pub async fn list( } else { responses .iter() - .map(|r| format!("{r}")) + .map(|r| r.to_string()) .collect::>() .join("\n") }, diff --git a/src/commands/tts.rs b/src/commands/tts.rs index ee13596..4a99539 100644 --- a/src/commands/tts.rs +++ b/src/commands/tts.rs @@ -59,7 +59,7 @@ pub async fn tts( .await?; } OutputMode::Vc => { - let guild_id = ctx.guild_id().ok_or_else(|| lang::err_not_in_guild())?; + let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; let channel_id = ctx .serenity_context() .cache @@ -69,7 +69,7 @@ pub async fn tts( .get(&ctx.author().id) .and_then(|vs| vs.channel_id) }) - .ok_or_else(|| lang::err_not_in_voice())?; + .ok_or_else(lang::err_not_in_voice)?; let call = ctx .data() diff --git a/src/commands/utilities.rs b/src/commands/utilities.rs index 10ce403..3f7e812 100644 --- a/src/commands/utilities.rs +++ b/src/commands/utilities.rs @@ -329,11 +329,10 @@ async fn find_downloaded_file(dir: &str) -> Option<(String, String)> { Ok(t) => t, Err(_) => continue, }; - if let Some((t, _)) = &newest { - if *t >= mtime { + if let Some((t, _)) = &newest + && *t >= mtime { continue; } - } newest = Some((mtime, entry.path().to_string_lossy().into_owned())); } newest.map(|(_, p)| { diff --git a/src/copyparty/mod.rs b/src/copyparty/mod.rs index 00bfd53..164c9f8 100644 --- a/src/copyparty/mod.rs +++ b/src/copyparty/mod.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use reqwest::Client; use serde::Deserialize; use tracing::warn; @@ -5,6 +7,10 @@ use tracing::warn; use crate::error::Result; use crate::lang; +/// Timeout for copyparty search requests; autocomplete has a 3s Discord budget, +/// so this must be comfortably lower. +const SEARCH_HTTP_TIMEOUT: Duration = Duration::from_secs(4); + #[derive(Clone)] pub struct CopypartyClient { pub base_url: String, @@ -20,13 +26,12 @@ pub fn percent_decode(s: &str) -> String { let mut i = 0; while i < bytes.len() { let b = bytes[i]; - if b == b'%' && i + 2 < bytes.len() { - if let (Some(hi), Some(lo)) = (hex_nibble(bytes[i + 1]), hex_nibble(bytes[i + 2])) { + if b == b'%' && i + 2 < bytes.len() + && let (Some(hi), Some(lo)) = (hex_nibble(bytes[i + 1]), hex_nibble(bytes[i + 2])) { buf.push(hi << 4 | lo); i += 3; continue; } - } if b == b'+' { buf.push(b' '); } else { @@ -56,7 +61,7 @@ pub fn html_decode(s: &str) -> String { """ => buf.extend_from_slice(b"\""), "'" => buf.extend_from_slice(b"'"), " " => buf.extend_from_slice(b" "), - _ if entity.as_bytes().len() > 3 && entity.as_bytes()[1] == b'#' => { + _ if entity.len() > 3 && entity.as_bytes()[1] == b'#' => { let (num_str, radix) = if entity.as_bytes()[2] == b'x' || entity.as_bytes()[2] == b'X' { (&entity[3..entity.len() - 1], 16u32) @@ -130,7 +135,7 @@ impl SearchHit { /// Best-effort URL path for building a full download URL. /// Tries `url`, then `rp`. pub fn url_path(&self) -> Option<&str> { - self.url.as_deref().or_else(|| self.rp.as_deref()) + self.url.as_deref().or(self.rp.as_deref()) } } @@ -203,7 +208,8 @@ impl CopypartyClient { let mut req = self .client .post(format!("{}/?srch", self.base_url)) - .json(&payload); + .json(&payload) + .timeout(SEARCH_HTTP_TIMEOUT); if let Some(pw) = &self.password { req = req.header("PW", pw); } diff --git a/src/events/mod.rs b/src/events/mod.rs index 9a193d0..d166993 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs @@ -92,11 +92,10 @@ pub async fn handle_voice_state_update( let member = guild_id .and_then(|gid| ctx.cache.guild(gid)) .and_then(|g| g.members.get(&user_id).cloned()); - if let Some(ref m) = member { - if m.user.bot { + if let Some(ref m) = member + && m.user.bot { return Ok(()); } - } let member_name = member .as_ref() @@ -269,40 +268,37 @@ pub async fn handle_voice_state_update( } // ── TTS for mute/deafen (deaf takes priority with early return) ── - if old.as_ref().map_or(false, |v| v.self_deaf) != new.self_deaf { + if old.as_ref().is_some_and(|v| v.self_deaf) != new.self_deaf { let phrase = if new.self_deaf { lang::fmt_ev_deafened(&display_name) } else { lang::fmt_ev_undeafened(&display_name) }; - if let Some(gid) = guild_id { - if let Some(cid) = tts_channel.or(new_channel) { + if let Some(gid) = guild_id + && let Some(cid) = tts_channel.or(new_channel) { play_tts_in_channel(ctx, data, gid, cid, &phrase).await; } - } return Ok(()); } - if old.as_ref().map_or(false, |v| v.self_mute) != new.self_mute { + if old.as_ref().is_some_and(|v| v.self_mute) != new.self_mute { let phrase = if new.self_mute { lang::fmt_ev_muted(&display_name) } else { lang::fmt_ev_unmuted(&display_name) }; - if let Some(gid) = guild_id { - if let Some(cid) = tts_channel.or(new_channel) { + if let Some(gid) = guild_id + && let Some(cid) = tts_channel.or(new_channel) { play_tts_in_channel(ctx, data, gid, cid, &phrase).await; } - } return Ok(()); } // ── Play join/leave TTS if no mute/deafen change ── - if let (Some(text), Some(cid)) = (tts_text, tts_channel) { - if let Some(gid) = guild_id { + if let (Some(text), Some(cid)) = (tts_text, tts_channel) + && let Some(gid) = guild_id { play_tts_in_channel(ctx, data, gid, cid, &text).await; } - } Ok(()) } diff --git a/src/lang.rs b/src/lang.rs index b1ed088..05e82fd 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -35,8 +35,6 @@ pub const L_TRIGGER_NOT_FOUND: &str = "Grupa triggerów nie znaleziona"; pub const SENATOR_SYSTEM_PROMPT: &str = "Jesteś Senatorem Steavenem Sperma Armstrongiem. Jesteś rasistą, seksistą i homofobem. Odpowiadasz w języku polskim. Odpowiedź ma maksymalnie 100 słów. Nie używaj formatowania markdown."; -pub const L_OPENROUTER_NO_KEY: &str = "Klucz API OpenRouter nie jest skonfigurowany w .env"; - // ── Cron command ────────────────────────────────────────────────────── pub const L_CRON_QUEUED: &str = "Praca cron zakolejkowana"; diff --git a/src/main.rs b/src/main.rs index 4a89315..7874c86 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,6 @@ use std::collections::HashMap; +use std::sync::Arc; +use std::time::{Duration, Instant}; use poise::serenity_prelude as serenity; use songbird::SerenityInit; @@ -17,6 +19,17 @@ mod voice; use database::DbPool; +/// How long a search cache entry stays fresh (10 seconds). +const AC_SEARCH_CACHE_TTL: Duration = Duration::from_secs(10); + +/// Per-user autocomplete stash: (UserId, filename) → SearchHit. +type AcCache = tokio::sync::Mutex>; + +/// Shared autocomplete search cache: query → (hits, cached_at). +type AcSearchCache = Arc< + tokio::sync::Mutex, Instant)>>, +>; + pub struct Data { pub db_pool: DbPool, pub config: config::Config, @@ -30,7 +43,12 @@ pub struct Data { /// Mirrors Python's per-cog `_cached_urls[user_id]` map: when two users /// search the same filename simultaneously, each invocation must resolve its /// own stashed `SearchHit`, so the cache is keyed by `(UserId, filename)`. - pub ac_cache: tokio::sync::Mutex>, + pub ac_cache: AcCache, + /// Autocomplete search results cache (query → hits). + /// + /// Populated by background tasks when the first search times out, so the + /// next keystroke's autocomplete can serve real results immediately. + pub ac_search_cache: AcSearchCache, /// Active music track per guild (only one music source at a time). pub music_tracks: tokio::sync::Mutex>, } @@ -63,13 +81,36 @@ impl Data { .get(&(user_id, key.to_string())) .cloned() } + + /// Look up search cache for an autocomplete query. Returns `None` if + /// uncached or stale. + pub async fn ac_search_cache_lookup( + &self, + query: &str, + ) -> Option> { + let cache = self.ac_search_cache.lock().await; + let (hits, cached_at) = cache.get(query)?; + if cached_at.elapsed() > AC_SEARCH_CACHE_TTL { + None + } else { + Some(hits.clone()) + } + } + + /// Store search results into the shared cache. + pub async fn ac_search_cache_store(&self, query: String, hits: Vec) { + self.ac_search_cache + .lock() + .await + .insert(query, (hits, Instant::now())); + } } #[allow(dead_code)] type Error = error::Error; #[tokio::main] -async fn main() { +async fn main() -> Result<(), Box> { let _ = rustls::crypto::ring::default_provider().install_default(); tracing_subscriber::fmt() @@ -84,10 +125,7 @@ async fn main() { config::check_env_vars(); let config = config::Config::from_env(); - let db_pool = database::init_pool(&config.database_path) - .await - .expect("Failed to initialize database"); - + let db_pool = database::init_pool(&config.database_path).await?; let http_client = reqwest::Client::new(); let copyparty = copyparty::CopypartyClient::new( @@ -132,6 +170,7 @@ async fn main() { volume: std::sync::atomic::AtomicU64::new(1.0f64.to_bits()), triggers: tokio::sync::Mutex::new(triggers), ac_cache: tokio::sync::Mutex::new(HashMap::new()), + ac_search_cache: Arc::new(tokio::sync::Mutex::new(HashMap::new())), music_tracks: tokio::sync::Mutex::new(HashMap::new()), }; @@ -164,10 +203,10 @@ async fn main() { serenity::ClientBuilder::new(&config_for_client.token, serenity::GatewayIntents::all()) .framework(framework) .register_songbird() - .await - .expect("Error creating client"); + .await?; - client.start().await.expect("Error starting client"); + client.start().await?; + Ok(()) } async fn handle_event(ctx: &serenity::Context, event: &serenity::FullEvent, user_data: &Data) { -- cgit v1.2.3