From 1e48765ab39a679abcc4eac6927ad3bb58f7b438 Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 4 Jun 2026 00:21:46 +0200 Subject: dirty, broken commit --- src/database.rs | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) (limited to 'src/database.rs') diff --git a/src/database.rs b/src/database.rs index b80ad4a..00d970a 100644 --- a/src/database.rs +++ b/src/database.rs @@ -45,6 +45,10 @@ pub struct Database { #[allow(dead_code)] impl Database { + pub fn conn(&self) -> &Connection { + &self.conn + } + pub fn open(_config: &crate::config::Config) -> SqlResult { let proj_dirs = ProjectDirs::from("com", "rsplayer", "rsplayer").ok_or_else(|| { rusqlite::Error::InvalidPath("Failed to get project directories".into()) @@ -269,6 +273,138 @@ impl Database { self.get_album_tracks_after(album_id, track_number, &path) } + pub fn complete_artists(&self, prefix: &str) -> SqlResult> { + let pattern = format!("%{}%", prefix); + let mut stmt = self + .conn + .prepare("SELECT name FROM artists WHERE name LIKE ?1 ORDER BY name LIMIT 25")?; + let rows = stmt.query_map([&pattern], |row| row.get::<_, String>(0))?; + rows.collect() + } + + pub fn complete_albums(&self, prefix: &str) -> SqlResult> { + let (artist, album_prefix) = match prefix.split_once('/') { + Some((a, ap)) => (a, ap), + None => return Ok(Vec::new()), + }; + let pattern = format!("{}%", album_prefix); + let mut stmt = self.conn.prepare( + "SELECT a.title FROM albums a + JOIN artists ar ON a.artist_id = ar.id + WHERE ar.name = ?1 AND a.title LIKE ?2 + ORDER BY a.title LIMIT 25", + )?; + let rows = stmt.query_map(rusqlite::params![artist, pattern], |row| { + row.get::<_, String>(0) + })?; + rows.collect() + } + + pub fn complete_tracks(&self, prefix: &str) -> SqlResult> { + let parts: Vec<&str> = prefix.splitn(3, '/').collect(); + if parts.len() < 3 { + return Ok(Vec::new()); + } + let (artist, album, track_prefix) = (parts[0], parts[1], parts[2]); + let pattern = format!("{}%", track_prefix); + let mut stmt = self.conn.prepare( + "SELECT t.title FROM tracks t + JOIN albums a ON t.album_id = a.id + JOIN artists ar ON a.artist_id = ar.id + WHERE ar.name = ?1 AND a.title = ?2 AND t.title LIKE ?3 + ORDER BY t.track_number, t.title LIMIT 25", + )?; + let rows = stmt.query_map(rusqlite::params![artist, album, pattern], |row| { + row.get::<_, String>(0) + })?; + rows.collect() + } + + #[allow(dead_code)] + pub fn open_in_memory() -> SqlResult { + let conn = Connection::open_in_memory()?; + let db = Database { conn }; + db.init_schema()?; + Ok(db) + } + + /// Find all tracks in an album by exact artist name and album title. + pub fn find_tracks_by_album(&self, artist: &str, album: &str) -> SqlResult> { + let mut stmt = self.conn.prepare( + "SELECT t.id, t.album_id, t.title, t.path, t.duration, t.track_number, ar.name, a.title + FROM tracks t + JOIN albums a ON t.album_id = a.id + JOIN artists ar ON a.artist_id = ar.id + WHERE ar.name = ?1 AND a.title = ?2 + ORDER BY t.track_number", + )?; + let rows = stmt.query_map(rusqlite::params![artist, album], |row| { + Ok(Track { + id: row.get(0)?, + album_id: row.get(1)?, + title: row.get(2)?, + path: PathBuf::from(row.get::<_, String>(3)?), + duration: row.get(4)?, + track_number: row.get(5)?, + artist_name: Some(row.get(6)?), + album_title: Some(row.get(7)?), + }) + })?; + rows.collect() + } + + /// Find a single track by exact artist, exact album, and track title prefix. + /// Returns None if no match found. + pub fn find_track_by_parts( + &self, + artist: &str, + album: &str, + track_prefix: &str, + ) -> SqlResult> { + let pattern = format!("{}%", track_prefix); + let mut stmt = self.conn.prepare( + "SELECT t.id, t.album_id, t.title, t.path, t.duration, t.track_number, ar.name, a.title + FROM tracks t + JOIN albums a ON t.album_id = a.id + JOIN artists ar ON a.artist_id = ar.id + WHERE ar.name = ?1 AND a.title = ?2 AND t.title LIKE ?3 + ORDER BY t.track_number LIMIT 1", + )?; + let mut rows = stmt.query_map(rusqlite::params![artist, album, pattern], |row| { + Ok(Track { + id: row.get(0)?, + album_id: row.get(1)?, + title: row.get(2)?, + path: PathBuf::from(row.get::<_, String>(3)?), + duration: row.get(4)?, + track_number: row.get(5)?, + artist_name: Some(row.get(6)?), + album_title: Some(row.get(7)?), + }) + })?; + match rows.next() { + Some(Ok(t)) => Ok(Some(t)), + Some(Err(e)) => Err(e), + None => Ok(None), + } + } + + /// Resolve a hierarchical path like "Artist/Album" or "Artist/Album/Track" + /// to DB tracks. Returns None if the path isn't hierarchical (single part + /// or 4+ parts) — the caller should fall back to search_tracks(). + pub fn resolve_path(&self, path: &str) -> SqlResult>> { + let path = path.strip_suffix('/').unwrap_or(path); + let parts: Vec<&str> = path.splitn(3, '/').collect(); + match parts.len() { + 2 => Ok(Some(self.find_tracks_by_album(parts[0], parts[1])?)), + 3 => match self.find_track_by_parts(parts[0], parts[1], parts[2])? { + Some(t) => Ok(Some(vec![t])), + None => Ok(Some(vec![])), + }, + _ => Ok(None), + } + } + pub fn search_tracks(&self, query: &str) -> SqlResult> { let mut stmt = self.conn.prepare( "SELECT t.id, t.album_id, t.title, t.path, t.duration, t.track_number, ar.name, a.title -- cgit v1.2.3