Expose exact music filters in desktop and local agent clients

This commit is contained in:
karl committed 2026-10-13 18:37:51 +02:00
1 parent 2eace15a33
commit c33047998b
6 files changed
+263 -35

No files matched your search

+75 -3
View File
@@ -52,6 +52,7 @@ enum Command {
#[arg(long)]
db: PathBuf,
/// Search text; quote phrases containing spaces in your shell.
#[arg(default_value = "")]
query: String,
/// Maximum number of returned results (1–1000).
#[arg(long, default_value_t = 20, value_parser = clap::value_parser!(u16).range(1..=1000))]
@@ -74,6 +75,12 @@ enum Command {
/// File extension, with or without the leading dot.
#[arg(long)]
extension: Option<String>,
/// Exact artist tag; unrelated semantic neighbours cannot bypass it.
#[arg(long)]
media_artist: Option<String>,
/// Exact track/media title tag.
#[arg(long)]
media_title: Option<String>,
#[arg(long)]
min_size: Option<u64>,
#[arg(long)]
@@ -171,6 +178,8 @@ fn main() -> Result<()> {
folder,
code_only,
extension,
media_artist,
media_title,
min_size,
max_size,
min_duration_ms,
@@ -181,14 +190,16 @@ fn main() -> Result<()> {
embedding_worker,
cpu_threads,
} => {
if query.trim().is_empty() {
bail!("Search text cannot be empty");
if query.trim().is_empty() && media_artist.is_none() && media_title.is_none() {
bail!("Supply search text or an exact media artist/title constraint");
}
require_existing_index(&db)?;
let options = SearchOptions {
folder,
code_only,
extension,
media_artist,
media_title,
min_size,
max_size,
min_duration_ms,
@@ -201,7 +212,10 @@ fn main() -> Result<()> {
} else {
MetricsMode::Basic
});
let results = if let (Some(model), Some(runtime)) = (model_dir, embedding_worker) {
let semantic_assets = model_dir
.zip(embedding_worker)
.filter(|_| !query.trim().is_empty());
let results = if let Some((model, runtime)) = semantic_assets {
let mut service = trace.timed("cli.session_initialize", None, |_, _| {
HybridSearch::open_query(&db, model, runtime, usize::from(cpu_threads))
})?;
@@ -393,4 +407,62 @@ mod tests {
}
));
}
#[test]
fn exact_media_cli_filters_match_real_metadata_without_query_or_model() {
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 cli = Cli::try_parse_from([
"searchi",
"search",
"--db",
"index.db",
"--media-artist",
" BJÖRK ",
"--media-title",
"JÓGA",
])
.unwrap();
let Command::Search {
query,
media_artist,
media_title,
..
} = cli.command
else {
panic!("search command");
};
assert!(query.is_empty());
let mut options = SearchOptions {
media_artist,
media_title,
..Default::default()
};
assert_eq!(
index.search_with_options(&query, 1, &options).unwrap()[0].filename,
"track.ogg"
);
options.media_artist = Some("Other Artist".into());
assert!(index
.search_with_options(&query, 1, &options)
.unwrap()
.is_empty());
options.media_artist = Some(" ".into());
assert!(index.search_with_options(&query, 1, &options).is_err());
}
}