use crate::analysis::artist_matches;
use crate::library::TrackRow;
use crate::query::Query;
#[cfg(test)]
use crate::library::RowInput;
pub fn dst_visible(
rows: &[TrackRow],
query: &str,
auto: bool,
src: Option<&TrackRow>,
fuzzy_from_src: bool,
duration_tol_secs: i64,
) -> Vec<usize> {
let q = Query::parse(query);
let src_id = src.map(|s| s.id.as_str());
rows.iter()
.enumerate()
.filter(|(_, r)| src_id.is_none_or(|id| r.id != id))
.filter(|(_, r)| q.matches(r.fields()))
.filter(|(_, r)| !auto || r.is_unlocked_cueless_audio)
.filter(|(_, r)| match (fuzzy_from_src, src) {
(true, Some(s)) => fuzzy_match(s, r, duration_tol_secs),
_ => true,
})
.map(|(i, _)| i)
.collect()
}
pub fn src_visible(rows: &[TrackRow], query: &str) -> Vec<usize> {
let q = Query::parse(query);
rows.iter()
.enumerate()
.filter(|(_, r)| q.matches(r.fields()))
.map(|(i, _)| i)
.collect()
}
pub fn fuzzy_match(src: &TrackRow, dst: &TrackRow, tol_secs: i64) -> bool {
if src.id == dst.id {
return false;
}
if src.norm_title.is_empty() || dst.norm_title.is_empty() {
return false;
}
if src.norm_title != dst.norm_title {
return false;
}
match (src.length, dst.length) {
(Some(a), Some(b)) if (a - b).abs() <= tol_secs => {}
_ => return false,
}
artist_matches(Some(&src.artist), Some(&dst.artist))
}
#[cfg(test)]
mod tests {
use super::*;
fn input(id: &str, title: &str, artist: &str) -> RowInput {
RowInput {
id: id.to_string(),
title: Some(title.to_string()),
artist: Some(artist.to_string()),
bpm: Some(12000),
length: Some(200),
analysed: Some(UNLOCKED),
file_type: Some(AUDIO),
cue_count: 0,
}
}
fn row(id: &str, title: &str, artist: &str) -> TrackRow {
TrackRow::from_db(input(id, title, artist))
}
const LOCKED: i64 = 233;
const UNLOCKED: i64 = 105;
const AUDIO: i64 = 5;
const STREAM: i64 = 19;
#[test]
fn substring_search_is_case_insensitive_on_title_and_artist() {
let rows = vec![row("1", "Apple", "Banana"), row("2", "Orange", "Tangerine")];
assert_eq!(src_visible(&rows, "apple"), vec![0]);
assert_eq!(src_visible(&rows, "BANANA"), vec![0]);
assert_eq!(src_visible(&rows, "tang"), vec![1]);
assert_eq!(src_visible(&rows, ""), vec![0, 1]);
assert_eq!(src_visible(&rows, " "), vec![0, 1]);
}
#[test]
fn playlist_terms_filter_both_columns() {
let rows = vec![
row("1", "Apple", "Banana").with_playlists(&["Jack Night/JN4"]),
row("2", "Apple", "Cherry").with_playlists(&["Archive"]),
row("3", "Orange", "Banana"),
];
assert_eq!(src_visible(&rows, "p:jn4"), vec![0]);
assert_eq!(src_visible(&rows, "p:\"jack night\" apple"), vec![0]);
assert_eq!(src_visible(&rows, "apple"), vec![0, 1]);
assert_eq!(src_visible(&rows, "p:jn4 orange"), Vec::<usize>::new());
assert_eq!(
dst_visible(&rows, "p:archive", false, None, false, 1),
vec![1]
);
}
#[test]
fn keyword_terms_filter_the_columns_too() {
let rows = vec![
row("1", "Apple", "Banana")
.with_playlists(&["Jack Night/JN4"])
.with_tags(&["local", "present", "flac", "lossless"]),
row("2", "Apple", "Cherry")
.with_playlists(&["Jack Night/JN4"])
.with_tags(&["stream"]),
row("3", "Orange", "Banana").with_tags(&["cloud", "flac"]),
row("4", "Pear", "Damson").with_tags(&["local", "missing", "flac"]),
];
assert_eq!(src_visible(&rows, "p:jn4 is:stream"), vec![1]);
assert_eq!(src_visible(&rows, "-is:stream"), vec![0, 2, 3]);
assert_eq!(src_visible(&rows, "type:flac apple"), vec![0]);
assert_eq!(src_visible(&rows, "is:missing"), vec![3]);
assert_eq!(src_visible(&rows, "is:present"), vec![0]);
assert_eq!(src_visible(&rows, "is:local -is:present"), vec![3]);
assert_eq!(
dst_visible(&rows, "is:cloud", false, None, false, 1),
vec![2]
);
}
#[test]
fn dst_auto_mode_filters_to_unlocked_cueless_audio() {
let rows = vec![
row("1", "Track", "A"), TrackRow::from_db(RowInput {
cue_count: 3,
..input("2", "Track", "B")
}),
TrackRow::from_db(RowInput {
analysed: Some(LOCKED),
..input("3", "Track", "C")
}),
TrackRow::from_db(RowInput {
file_type: Some(STREAM),
..input("4", "Track", "D")
}),
];
assert_eq!(dst_visible(&rows, "", true, None, false, 1), vec![0]);
assert_eq!(
dst_visible(&rows, "", false, None, false, 1),
vec![0, 1, 2, 3]
);
}
#[test]
fn fuzzy_from_src_narrows_to_normalized_title_and_length() {
let at = |id, title, artist, length| RowInput {
length: Some(length),
..input(id, title, artist)
};
let src = TrackRow::from_db(RowInput {
bpm: Some(14600),
cue_count: 4,
file_type: Some(STREAM),
..at("1", "Ritual Pharmacy", "porf0d", 221)
});
let rows = vec![
TrackRow::from_db(RowInput {
bpm: Some(15000),
..at("2", "Ritual Pharmacy (Edit)", "porf0d", 221)
}),
TrackRow::from_db(at("3", "Other Song", "porf0d", 221)), TrackRow::from_db(at("4", "Ritual Pharmacy", "Different", 221)), TrackRow::from_db(at("5", "Ritual Pharmacy", "porf0d", 230)), ];
let vis = dst_visible(&rows, "", false, Some(&src), true, 1);
assert_eq!(vis, vec![0]); }
#[test]
fn src_is_excluded_from_dst_even_when_fuzzy_off() {
let src = row("1", "Same", "A");
let rows = vec![
row("1", "Same", "A"), row("2", "Other", "A"),
];
let vis = dst_visible(&rows, "", false, Some(&src), false, 1);
assert_eq!(vis, vec![1]);
}
#[test]
fn filters_compose_as_and() {
let src = TrackRow::from_db(RowInput {
cue_count: 4,
file_type: Some(STREAM),
..input("1", "Foo", "Bar")
});
let rows = vec![
row("2", "Foo", "Bar"), TrackRow::from_db(RowInput {
cue_count: 3,
..input("3", "Foo", "Bar")
}), TrackRow::from_db(RowInput {
analysed: Some(LOCKED),
..input("4", "Foo", "Bar")
}), row("5", "Baz", "Bar"), row("6", "Foo", "Bar"), ];
let vis = dst_visible(&rows, "foo", true, Some(&src), true, 1);
assert_eq!(vis, vec![0, 4]);
}
}