add fuzzy search

This commit is contained in:
2026-06-09 14:37:51 +02:00
parent 6551d063e6
commit c18ca38a08
5 changed files with 129 additions and 23 deletions
+7
View File
@@ -862,6 +862,7 @@ dependencies = [
"rusqlite", "rusqlite",
"serde", "serde",
"serde_json", "serde_json",
"strsim",
] ]
[[package]] [[package]]
@@ -876,6 +877,12 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "symphonia" name = "symphonia"
version = "0.5.5" version = "0.5.5"
+1
View File
@@ -9,3 +9,4 @@ rodio = "0.19"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
dirs = "5" dirs = "5"
strsim = "0.11"
+1
View File
@@ -6,4 +6,5 @@ pub enum Command {
Add(String), Add(String),
Del(String), Del(String),
Init(String), Init(String),
Search(String),
} }
+28 -8
View File
@@ -22,21 +22,41 @@ pub fn start_socket(state: Arc<Mutex<DaemonState>>) {
let msg = String::from_utf8_lossy(&buf[..size]); let msg = String::from_utf8_lossy(&buf[..size]);
if let Ok(cmd) = serde_json::from_str::<Command>(&msg) { if let Ok(cmd) = serde_json::from_str::<Command>(&msg) {
handle(cmd, state); let response = handle(cmd, state);
let _ = stream.write_all(response.as_bytes());
} else {
let _ = stream.write_all(b"ERROR: Invalid JSON");
} }
let _ = stream.write_all(b"OK");
}); });
} }
} }
fn handle(cmd: Command, state: Arc<Mutex<DaemonState>>) { fn handle(cmd: Command, state: Arc<Mutex<DaemonState>>) -> String {
let mut state = state.lock().unwrap(); let mut state = state.lock().unwrap();
match cmd { match cmd {
Command::Play(song) => state.play(song), Command::Play(query) => {
Command::Add(song) => state.add(song), state.play(&query);
Command::Del(song) => state.delete(song), format!("OK: Playing {}", query)
Command::Init(song) => state.init(song), }
Command::Add(query) => {
state.add(&query);
format!("OK: Added {}", query)
}
Command::Del(query) => {
state.delete(&query);
format!("OK: Deleted {}", query)
}
Command::Init(song) => {
state.init(song.clone());
format!("OK: Downloading {}", song)
}
Command::Search(query) => {
if let Some(song) = state.search(&query) {
serde_json::to_string(&song).unwrap_or_else(|_| "ERROR: Serialization failed".to_string())
} else {
"ERROR: Song not found".to_string()
}
}
} }
} }
+92 -15
View File
@@ -1,6 +1,8 @@
use std::process::Command as SysCommand; use std::process::Command as SysCommand;
use strsim::jaro_winkler;
use serde::{Serialize, Deserialize};
#[derive(Clone)] #[derive(Clone, Serialize, Deserialize)]
pub struct Song { pub struct Song {
pub name: String, pub name: String,
pub path: String, pub path: String,
@@ -19,24 +21,29 @@ impl DaemonState {
} }
} }
pub fn play(&mut self, name: String) { pub fn play(&mut self, query: &str) {
let path = crate::paths::song_path(&name); if let Some(song) = self.search(query) {
self.queue.insert(0, song);
self.queue.insert(0, Song { name, path }); } else {
eprintln!("Song nicht gefunden: {}", query);
}
} }
pub fn add(&mut self, name: String) { pub fn add(&mut self, query: &str) {
let path = crate::paths::song_path(&name); if let Some(song) = self.search(query) {
self.queue.push(song);
self.queue.push(Song { name, path }); } else {
eprintln!("Song nicht gefunden: {}", query);
}
} }
pub fn delete(&mut self, name: String) { pub fn delete(&mut self, query: &str) {
let path = crate::paths::song_path(&name); if let Some(song) = self.search(query) {
let _ = std::fs::remove_file(&song.path);
let _ = std::fs::remove_file(&path); self.queue.retain(|s| s.name != song.name);
} else {
self.queue.retain(|s| s.name != name); eprintln!("Song nicht gefunden: {}", query);
}
} }
pub fn init(&mut self, name: String) { pub fn init(&mut self, name: String) {
@@ -63,4 +70,74 @@ impl DaemonState {
path, path,
}); });
} }
pub fn search(&self, query: &str) -> Option<Song> {
use std::fs;
let songs_dir = crate::paths::songs_dir();
let query_lower = query.to_lowercase();
// Alle .mp3 Dateien aus songs_dir lesen
let entries = match fs::read_dir(&songs_dir) {
Ok(entries) => entries,
Err(_) => return None,
};
let mut best_match: Option<(String, f64)> = None;
const MIN_SCORE: f64 = 0.65; // Threshold - nicht zu aggressiv
for entry in entries {
if let Ok(entry) = entry {
if let Ok(metadata) = entry.metadata() {
if metadata.is_file() {
if let Some(filename) = entry.file_name().to_str() {
// .mp3 entfernen
let song_name = filename.strip_suffix(".mp3")
.or_else(|| filename.strip_suffix(".webm"))
.unwrap_or(filename);
let song_name_lower = song_name.to_lowercase();
// Exakte Übereinstimmung (case-insensitive)
if song_name_lower == query_lower {
return Some(Song {
name: song_name.to_string(),
path: entry.path().to_string_lossy().to_string(),
});
}
// Fuzzy match mit Jaro-Winkler
let score = jaro_winkler(&song_name_lower, &query_lower);
if score > MIN_SCORE {
if best_match.is_none() || score > best_match.as_ref().unwrap().1 {
best_match = Some((
entry.path().to_string_lossy().to_string(),
score
));
}
}
}
}
}
}
}
// Wenn fuzzy match gefunden, returne es
if let Some((path, _)) = best_match {
if let Some(filename) = std::path::Path::new(&path)
.file_name()
.and_then(|f| f.to_str()) {
let song_name = filename.strip_suffix(".mp3")
.or_else(|| filename.strip_suffix(".webm"))
.unwrap_or(filename);
return Some(Song {
name: song_name.to_string(),
path,
});
}
}
None
}
} }