Expose exact music filters in desktop and local agent clients
This commit is contained in:
1 parent
2eace15a33
commit
c33047998b
6 files changed
+263
-35
No files matched your search
@@ -228,6 +228,8 @@ struct Filters {
|
||||
code_only: bool,
|
||||
folder: Option<PathBuf>,
|
||||
extension: Option<String>,
|
||||
media_artist: Option<String>,
|
||||
media_title: Option<String>,
|
||||
min_size: Option<u64>,
|
||||
max_size: Option<u64>,
|
||||
min_duration_ms: Option<u32>,
|
||||
@@ -242,6 +244,8 @@ impl From<Filters> for SearchOptions {
|
||||
code_only: value.code_only,
|
||||
folder: value.folder,
|
||||
extension: value.extension,
|
||||
media_artist: value.media_artist,
|
||||
media_title: value.media_title,
|
||||
min_size: value.min_size,
|
||||
max_size: value.max_size,
|
||||
min_duration_ms: value.min_duration_ms,
|
||||
@@ -253,18 +257,36 @@ impl From<Filters> for SearchOptions {
|
||||
}
|
||||
|
||||
// Each job owns its connection. Slow indexing never holds a desktop mutex.
|
||||
fn validate_search_input(query: &str, options: &SearchOptions) -> Result<(), String> {
|
||||
if query.trim().is_empty() && options.media_artist.is_none() && options.media_title.is_none() {
|
||||
return Err("Supply search text or an exact media artist/title constraint".to_owned());
|
||||
}
|
||||
for value in [
|
||||
options.media_artist.as_deref(),
|
||||
options.media_title.as_deref(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
searchi_core::normalize_media_field(value).map_err(|error| error.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn search(
|
||||
query: String,
|
||||
filters: Option<Filters>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<SearchResult>, String> {
|
||||
let options: SearchOptions = filters.unwrap_or_default().into();
|
||||
validate_search_input(&query, &options)?;
|
||||
let lexical = state.lexical.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
lexical
|
||||
.try_lock()
|
||||
.map_err(|_| "Keyword worker is busy; try the latest query again.".to_owned())?
|
||||
.search_with_options(&query, 50, &filters.unwrap_or_default().into())
|
||||
.search_with_options(&query, 50, &options)
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
.await
|
||||
@@ -290,19 +312,15 @@ async fn search_measured(
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
trace.end(queue, Default::default());
|
||||
let result = (|| {
|
||||
let options: SearchOptions = filters.unwrap_or_default().into();
|
||||
validate_search_input(&query, &options)?;
|
||||
let index = trace.timed("desktop.lexical_lock", None, |_, _| {
|
||||
lexical
|
||||
.try_lock()
|
||||
.map_err(|_| "Keyword worker is busy; try the latest query again.".to_owned())
|
||||
})?;
|
||||
index
|
||||
.search_with_options_traced(
|
||||
&query,
|
||||
50,
|
||||
&filters.unwrap_or_default().into(),
|
||||
&mut trace,
|
||||
None,
|
||||
)
|
||||
.search_with_options_traced(&query, 50, &options, &mut trace, None)
|
||||
.map_err(|error| error.to_string())
|
||||
})();
|
||||
measured_outcome(result, trace)
|
||||
@@ -841,4 +859,42 @@ mod tests {
|
||||
< preview_pixel_limit(Some(image::ImageFormat::Jpeg))
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn native_exact_media_filters_preserve_fields_and_validate_empty_queries() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("track.ogg");
|
||||
std::fs::write(&file, b"metadata storage fixture").unwrap();
|
||||
let mut index = SearchIndex::open(":memory:").unwrap();
|
||||
index.index_folder(dir.path()).unwrap();
|
||||
let path = file.canonicalize().unwrap().to_string_lossy().into_owned();
|
||||
let (bytes, modified) = index.file_signature(&path).unwrap().unwrap();
|
||||
let revision = index.media_change_revision(&path, bytes, modified).unwrap();
|
||||
let tags = std::collections::BTreeMap::from([
|
||||
("artist".to_owned(), "Björk".to_owned()),
|
||||
("title".to_owned(), "Jóga".to_owned()),
|
||||
("comment".to_owned(), "artist: Other Artist".to_owned()),
|
||||
]);
|
||||
index
|
||||
.set_structured_media_metadata_at_revision(
|
||||
&path, &tags, 1000, bytes, modified, revision,
|
||||
)
|
||||
.unwrap();
|
||||
let filters: Filters =
|
||||
serde_json::from_str(r#"{"media_artist":" BJÖRK ","media_title":"JÓGA"}"#).unwrap();
|
||||
let mut options: SearchOptions = filters.into();
|
||||
assert!(validate_search_input("", &options).is_ok());
|
||||
assert_eq!(
|
||||
index.search_with_options("", 1, &options).unwrap()[0].filename,
|
||||
"track.ogg"
|
||||
);
|
||||
options.media_artist = Some("Other Artist".into());
|
||||
assert!(index
|
||||
.search_with_options("", 1, &options)
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
options.media_artist = Some(" ".into());
|
||||
assert!(validate_search_input("", &options).is_err());
|
||||
assert!(validate_search_input("", &SearchOptions::default()).is_err());
|
||||
assert!(validate_search_input("song", &SearchOptions::default()).is_ok());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user