fix: improve CLI behavior from testing feedback
Stop a running daemon during uninstall, fuzzy-match cached app names, and pace remote sequences with a configurable delay so Roku secret screen sequences behave more reliably.
This commit is contained in:
+95
-7
@@ -1,4 +1,4 @@
|
||||
use std::{collections::BTreeMap, path::PathBuf};
|
||||
use std::{cmp::Reverse, collections::BTreeMap, path::PathBuf};
|
||||
|
||||
use tokio::fs;
|
||||
|
||||
@@ -72,12 +72,20 @@ impl AppCacheStore {
|
||||
/// Resolve an app by case-insensitive name or exact ID from the persisted platform cache.
|
||||
pub async fn find_app(&self, platform: &str, query: &str) -> anyhow::Result<Option<AppInfo>> {
|
||||
let cache = self.load_platform(platform).await?;
|
||||
let normalized = query.to_ascii_lowercase();
|
||||
Ok(cache.apps.into_iter().find(|app| {
|
||||
app.platform_id == query
|
||||
|| app.id == query
|
||||
|| app.name.to_ascii_lowercase() == normalized
|
||||
}))
|
||||
let normalized = normalize_query(query);
|
||||
|
||||
if let Some(app) = cache.apps.iter().find(|app| {
|
||||
app.platform_id == query || app.id == query || normalize_query(&app.name) == normalized
|
||||
}) {
|
||||
return Ok(Some(app.clone()));
|
||||
}
|
||||
|
||||
Ok(cache
|
||||
.apps
|
||||
.into_iter()
|
||||
.filter_map(|app| fuzzy_match_score(&normalized, &app).map(|score| (score, app)))
|
||||
.max_by_key(|(score, app)| (*score, Reverse(app.name.len())))
|
||||
.map(|(_, app)| app))
|
||||
}
|
||||
|
||||
/// Remove the persisted app cache for a platform.
|
||||
@@ -94,6 +102,52 @@ impl AppCacheStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_query(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.filter(|character| character.is_ascii_alphanumeric())
|
||||
.flat_map(char::to_lowercase)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn fuzzy_match_score(query: &str, app: &AppInfo) -> Option<u8> {
|
||||
if query.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = normalize_query(&app.name);
|
||||
let id = normalize_query(&app.id);
|
||||
let platform_id = normalize_query(&app.platform_id);
|
||||
|
||||
if name.starts_with(query) || id.starts_with(query) || platform_id.starts_with(query) {
|
||||
return Some(3);
|
||||
}
|
||||
if name.contains(query) || id.contains(query) || platform_id.contains(query) {
|
||||
return Some(2);
|
||||
}
|
||||
if is_subsequence(query, &name) {
|
||||
return Some(1);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn is_subsequence(needle: &str, haystack: &str) -> bool {
|
||||
let mut needle_chars = needle.chars();
|
||||
let mut current = needle_chars.next();
|
||||
|
||||
for hay in haystack.chars() {
|
||||
if current == Some(hay) {
|
||||
current = needle_chars.next();
|
||||
if current.is_none() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -192,4 +246,38 @@ mod tests {
|
||||
let loaded = store.load_platform("roku").await.expect("apps should load");
|
||||
assert!(loaded.apps.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_app_supports_fuzzy_name_matches() {
|
||||
let temp_dir = tempfile::tempdir().expect("temp dir should exist");
|
||||
let store = AppCacheStore::new(temp_dir.path().join("cache"));
|
||||
|
||||
store
|
||||
.record_platform_apps(
|
||||
"roku",
|
||||
vec![
|
||||
AppInfo {
|
||||
id: "592369".to_string(),
|
||||
name: "Jellyfin".to_string(),
|
||||
version: None,
|
||||
platform_id: "592369".to_string(),
|
||||
},
|
||||
AppInfo {
|
||||
id: "12".to_string(),
|
||||
name: "Netflix".to_string(),
|
||||
version: None,
|
||||
platform_id: "12".to_string(),
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.expect("apps should save");
|
||||
|
||||
let resolved = store
|
||||
.find_app("roku", "jelly")
|
||||
.await
|
||||
.expect("lookup should work")
|
||||
.expect("jellyfin should resolve");
|
||||
assert_eq!(resolved.name, "Jellyfin");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user