From 67eb270b8f02653e57afbac66eef016c1aeed679 Mon Sep 17 00:00:00 2001 From: Kasper Date: Thu, 20 Aug 2026 15:43:08 +0200 Subject: [PATCH] Add `artist:` matching for bracketsless titles For example `Supercell feat. Veela` --- CHANGELOG.md | 1 + src-native/filter.rs | 47 ++++++++++++++++++++++++++++++++---- src/components/Filter.svelte | 10 +++----- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6dca4a..5454668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Add exact filtering with double quotes, like `"the one"` - Show filter syntax guide on empty page - Add syntax highlighting for filters like `album:"mirai sekai"` +- Add `artist:` filtering for titles without brackets, like `Supercell feat. Veela` - Remove the active filter highlight - Fix undo/redo in the filter textbox only changing one character at a time - Fix "electron.app.hide is not a function" error when closing app window on Windows/Linux diff --git a/src-native/filter.rs b/src-native/filter.rs index d83eff8..3ecee89 100644 --- a/src-native/filter.rs +++ b/src-native/filter.rs @@ -79,20 +79,57 @@ fn strip_suffix_ignore_case<'a>(text: &'a str, suffix: &str) -> Option<&'a str> } } -fn feat_artists_match(track_name: &str, target: &str) -> bool { +fn find_ignore_case(haystack: &str, needle: &str) -> Option { + let needle_len = needle.len(); + if needle_len == 0 || needle_len > haystack.len() { + return None; + } + for start in 0..=(haystack.len() - needle_len) { + if !haystack.is_char_boundary(start) { + continue; + } + let end = start + needle_len; + if !haystack.is_char_boundary(end) { + continue; + } + if haystack[start..end].eq_ignore_ascii_case(needle) { + return Some(start); + } + } + None +} + +fn artists_in_title_match(track_name: &str, target: &str) -> bool { + let prefixes = ["feat.", "feat ", "ft.", "ft ", "featuring "]; + let suffixes = [" remix", " flip", " bootleg", " edit"]; + + // Look for unbracketed artists, like `Moonlight feat. Aloma Steele` + for prefix in prefixes { + if let Some(pos) = find_ignore_case(track_name, prefix) { + let after = &track_name[pos + prefix.len()..]; + // Stop at bracket + let end = after.find(['(', '[']).unwrap_or(after.len()); + let artist_text = after[..end].trim(); + if find_match(artist_text, target) { + return true; + } + } + } + + // Look inside brackets for (open, close) in &[('(', ')'), ('[', ']')] { let mut rest = track_name; while let Some(start) = rest.find(*open) { if let Some(end) = rest[start..].find(*close) { let inside = &rest[start + 1..start + end]; - for prefix in ["feat.", "feat ", "ft.", "ft ", "featuring "] { + for prefix in prefixes { let artist_text = strip_prefix_ignore_case(inside, prefix); if let Some(artist_text) = artist_text { return find_match(artist_text, target); } } - for prefix in [" remix", " flip", " bootleg", " edit"] { - let artist_text = strip_suffix_ignore_case(inside, prefix); + for suffix in suffixes { + let artist_text = strip_suffix_ignore_case(inside, suffix); if let Some(artist_text) = artist_text { return find_match(artist_text, target); } @@ -129,7 +166,7 @@ fn filter_term(ids: Vec, term: FilterTerm, library: &Library) -> Vec find_match(&track.name, &term.literal), Field::Artist => { find_match(&track.artist, &term.literal) - || feat_artists_match(&track.name, &term.literal) + || artists_in_title_match(&track.name, &term.literal) } Field::Album => find_match_opt(&track.albumName, &term.literal), Field::AlbumArtist => find_match_opt(&track.albumArtist, &term.literal), diff --git a/src/components/Filter.svelte b/src/components/Filter.svelte index 5df04f9..6d66a5c 100644 --- a/src/components/Filter.svelte +++ b/src/components/Filter.svelte @@ -33,9 +33,6 @@ skips: 10 satisfies Field.Skips, bpm: 11 satisfies Field.Bpm, } - function parse_field(text: string) { - return fields[text.toLowerCase()] - } type FilterTermDetailed = FilterTerm & { field_text?: string @@ -84,10 +81,11 @@ } const field_text = text.slice(word_start, i) - const field = parse_field(field_text) + const field = fields[field_text.toLowerCase()] + console.log(field, field_text) // field:value - if (field && text[i] === ':') { + if (field !== undefined && text[i] === ':') { i++ if (text[i] === '"') { @@ -280,7 +278,7 @@ aria-hidden="true" > {#each filter.terms as term} - {#if term.field} + {#if term.field !== undefined} {term.field_text}:{term.literal_text ?? term.literal} {:else} {term.literal_text ?? term.literal} -- 2.51.2