add fuzzy search
This commit is contained in:
Generated
+7
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ pub enum Command {
|
|||||||
Add(String),
|
Add(String),
|
||||||
Del(String),
|
Del(String),
|
||||||
Init(String),
|
Init(String),
|
||||||
|
Search(String),
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-8
@@ -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
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user