From 382bb2af15e7076f8dcbbe6cc4a6b135709ef7b9 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 01:32:06 +0100 Subject: autocomplete: key results by extension filter and reject the placeholder --- src/commands/autocomplete.rs | 24 ++++++++++++++++++------ src/commands/cron.rs | 5 +++++ src/commands/music.rs | 5 ++++- 3 files changed, 27 insertions(+), 7 deletions(-) (limited to 'src/commands') diff --git a/src/commands/autocomplete.rs b/src/commands/autocomplete.rs index d05a48b..4cefd67 100644 --- a/src/commands/autocomplete.rs +++ b/src/commands/autocomplete.rs @@ -20,7 +20,7 @@ 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}..."; +pub(crate) const MOCK_SEARCHING: &str = "\u{231B}..."; fn truncate_choice(s: &str) -> String { if s.chars().count() <= MAX_CHOICE_LEN { @@ -98,6 +98,12 @@ type SearchCache = Arc< tokio::sync::Mutex, std::time::Instant)>>, >; +fn search_cache_key(partial: &str, extensions: &[&str]) -> String { + // The extension filter changes the result set, so it must be part of the + // key; otherwise `/music` and `/upload` poison each other's cache. + format!("{partial}\u{1f}{}", extensions.join(",")) +} + fn spawn_search( cache: SearchCache, client: copyparty::CopypartyClient, @@ -114,10 +120,10 @@ fn spawn_search( 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())); + cache.lock().await.insert( + search_cache_key(&query, &exts), + (hits.clone(), std::time::Instant::now()), + ); // Send to foreground (ignores error if receiver was dropped) let _ = tx.send(hits); } @@ -143,8 +149,14 @@ async fn timed_search( partial: &str, extensions: &[&str], ) -> Vec { + // The placeholder is a UI hint, not a real query. + if partial == MOCK_SEARCHING { + return vec![]; + } + // 1. Shared search cache hit? - if let Some(hits) = ctx.data().ac_search_cache_lookup(partial).await + let cache_key = search_cache_key(partial, extensions); + if let Some(hits) = ctx.data().ac_search_cache_lookup(&cache_key).await && !hits.is_empty() { debug!(%partial, count = hits.len(), "autocomplete cache hit"); let pairs = build_choices(&hits); diff --git a/src/commands/cron.rs b/src/commands/cron.rs index 94cf955..ec5b702 100644 --- a/src/commands/cron.rs +++ b/src/commands/cron.rs @@ -34,6 +34,11 @@ pub async fn dodaj_audio( _ => return Err(lang::err_bad_channel_type()), }; + if audio_filename == crate::commands::autocomplete::MOCK_SEARCHING { + ctx.say(lang::L_UTIL_PICK_FROM_AUTOCOMPLETE).await?; + return Ok(()); + } + // Resolve full path from autocomplete cache (value is truncated to 99 chars by Discord) let full_url = if let Some(hit) = ctx.data().ac_lookup(ctx.author().id, &audio_filename).await { let path = hit.url_path().unwrap_or(&audio_filename); diff --git a/src/commands/music.rs b/src/commands/music.rs index baa91b1..51bd619 100644 --- a/src/commands/music.rs +++ b/src/commands/music.rs @@ -3,7 +3,7 @@ use poise::serenity_prelude as serenity; use tracing::{error, info}; -use super::autocomplete::audio_file; +use super::autocomplete::{MOCK_SEARCHING, audio_file}; use crate::Data; use crate::copyparty::{html_decode, percent_decode}; use crate::error::{Error, Result}; @@ -87,6 +87,9 @@ async fn resolve_source( } let name = melodia.ok_or_else(|| Error::Custom(lang::L_MUSIC_NEED_NAME.into()))?; + if name == MOCK_SEARCHING { + return Err(Error::Custom(lang::L_UTIL_PICK_FROM_AUTOCOMPLETE.into())); + } // ── Autocomplete cache (per-user: (UserId, filename) → SearchHit) ─ if let Some(hit) = ctx.data().ac_lookup(ctx.author().id, &name).await { -- cgit v1.2.3