diff options
| author | salmonik <raisersoft@proton.me> | 2026-06-04 00:21:46 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-06-04 00:21:46 +0200 |
| commit | 1e48765ab39a679abcc4eac6927ad3bb58f7b438 (patch) | |
| tree | 5554f2a06bde693d4d9c39ed26f80677d61da991 /src/database.rs | |
| parent | fix no metadata songs handling (diff) | |
| download | rsplayer-1e48765ab39a679abcc4eac6927ad3bb58f7b438.tar.gz rsplayer-1e48765ab39a679abcc4eac6927ad3bb58f7b438.zip | |
dirty, broken commit
Diffstat (limited to '')
| -rw-r--r-- | src/database.rs | 136 |
1 files changed, 136 insertions, 0 deletions
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 { | |||
| 45 | 45 | ||
| 46 | #[allow(dead_code)] | 46 | #[allow(dead_code)] |
| 47 | impl Database { | 47 | impl Database { |
| 48 | pub fn conn(&self) -> &Connection { | ||
| 49 | &self.conn | ||
| 50 | } | ||
| 51 | |||
| 48 | pub fn open(_config: &crate::config::Config) -> SqlResult<Self> { | 52 | pub fn open(_config: &crate::config::Config) -> SqlResult<Self> { |
| 49 | let proj_dirs = ProjectDirs::from("com", "rsplayer", "rsplayer").ok_or_else(|| { | 53 | let proj_dirs = ProjectDirs::from("com", "rsplayer", "rsplayer").ok_or_else(|| { |
| 50 | rusqlite::Error::InvalidPath("Failed to get project directories".into()) | 54 | rusqlite::Error::InvalidPath("Failed to get project directories".into()) |
| @@ -269,6 +273,138 @@ impl Database { | |||
| 269 | self.get_album_tracks_after(album_id, track_number, &path) | 273 | self.get_album_tracks_after(album_id, track_number, &path) |
| 270 | } | 274 | } |
| 271 | 275 | ||
| 276 | pub fn complete_artists(&self, prefix: &str) -> SqlResult<Vec<String>> { | ||
| 277 | let pattern = format!("%{}%", prefix); | ||
| 278 | let mut stmt = self | ||
| 279 | .conn | ||
| 280 | .prepare("SELECT name FROM artists WHERE name LIKE ?1 ORDER BY name LIMIT 25")?; | ||
| 281 | let rows = stmt.query_map([&pattern], |row| row.get::<_, String>(0))?; | ||
| 282 | rows.collect() | ||
| 283 | } | ||
| 284 | |||
| 285 | pub fn complete_albums(&self, prefix: &str) -> SqlResult<Vec<String>> { | ||
| 286 | let (artist, album_prefix) = match prefix.split_once('/') { | ||
| 287 | Some((a, ap)) => (a, ap), | ||
| 288 | None => return Ok(Vec::new()), | ||
| 289 | }; | ||
| 290 | let pattern = format!("{}%", album_prefix); | ||
| 291 | let mut stmt = self.conn.prepare( | ||
| 292 | "SELECT a.title FROM albums a | ||
| 293 | JOIN artists ar ON a.artist_id = ar.id | ||
| 294 | WHERE ar.name = ?1 AND a.title LIKE ?2 | ||
| 295 | ORDER BY a.title LIMIT 25", | ||
| 296 | )?; | ||
| 297 | let rows = stmt.query_map(rusqlite::params![artist, pattern], |row| { | ||
| 298 | row.get::<_, String>(0) | ||
| 299 | })?; | ||
| 300 | rows.collect() | ||
| 301 | } | ||
| 302 | |||
| 303 | pub fn complete_tracks(&self, prefix: &str) -> SqlResult<Vec<String>> { | ||
| 304 | let parts: Vec<&str> = prefix.splitn(3, '/').collect(); | ||
| 305 | if parts.len() < 3 { | ||
| 306 | return Ok(Vec::new()); | ||
| 307 | } | ||
| 308 | let (artist, album, track_prefix) = (parts[0], parts[1], parts[2]); | ||
| 309 | let pattern = format!("{}%", track_prefix); | ||
| 310 | let mut stmt = self.conn.prepare( | ||
| 311 | "SELECT t.title FROM tracks t | ||
| 312 | JOIN albums a ON t.album_id = a.id | ||
| 313 | JOIN artists ar ON a.artist_id = ar.id | ||
| 314 | WHERE ar.name = ?1 AND a.title = ?2 AND t.title LIKE ?3 | ||
| 315 | ORDER BY t.track_number, t.title LIMIT 25", | ||
| 316 | )?; | ||
| 317 | let rows = stmt.query_map(rusqlite::params![artist, album, pattern], |row| { | ||
| 318 | row.get::<_, String>(0) | ||
| 319 | })?; | ||
| 320 | rows.collect() | ||
| 321 | } | ||
| 322 | |||
| 323 | #[allow(dead_code)] | ||
| 324 | pub fn open_in_memory() -> SqlResult<Database> { | ||
| 325 | let conn = Connection::open_in_memory()?; | ||
| 326 | let db = Database { conn }; | ||
| 327 | db.init_schema()?; | ||
| 328 | Ok(db) | ||
| 329 | } | ||
| 330 | |||
| 331 | /// Find all tracks in an album by exact artist name and album title. | ||
| 332 | pub fn find_tracks_by_album(&self, artist: &str, album: &str) -> SqlResult<Vec<Track>> { | ||
| 333 | let mut stmt = self.conn.prepare( | ||
| 334 | "SELECT t.id, t.album_id, t.title, t.path, t.duration, t.track_number, ar.name, a.title | ||
| 335 | FROM tracks t | ||
| 336 | JOIN albums a ON t.album_id = a.id | ||
| 337 | JOIN artists ar ON a.artist_id = ar.id | ||
| 338 | WHERE ar.name = ?1 AND a.title = ?2 | ||
| 339 | ORDER BY t.track_number", | ||
| 340 | )?; | ||
| 341 | let rows = stmt.query_map(rusqlite::params![artist, album], |row| { | ||
| 342 | Ok(Track { | ||
| 343 | id: row.get(0)?, | ||
| 344 | album_id: row.get(1)?, | ||
| 345 | title: row.get(2)?, | ||
| 346 | path: PathBuf::from(row.get::<_, String>(3)?), | ||
| 347 | duration: row.get(4)?, | ||
| 348 | track_number: row.get(5)?, | ||
| 349 | artist_name: Some(row.get(6)?), | ||
| 350 | album_title: Some(row.get(7)?), | ||
| 351 | }) | ||
| 352 | })?; | ||
| 353 | rows.collect() | ||
| 354 | } | ||
| 355 | |||
| 356 | /// Find a single track by exact artist, exact album, and track title prefix. | ||
| 357 | /// Returns None if no match found. | ||
| 358 | pub fn find_track_by_parts( | ||
| 359 | &self, | ||
| 360 | artist: &str, | ||
| 361 | album: &str, | ||
| 362 | track_prefix: &str, | ||
| 363 | ) -> SqlResult<Option<Track>> { | ||
| 364 | let pattern = format!("{}%", track_prefix); | ||
| 365 | let mut stmt = self.conn.prepare( | ||
| 366 | "SELECT t.id, t.album_id, t.title, t.path, t.duration, t.track_number, ar.name, a.title | ||
| 367 | FROM tracks t | ||
| 368 | JOIN albums a ON t.album_id = a.id | ||
| 369 | JOIN artists ar ON a.artist_id = ar.id | ||
| 370 | WHERE ar.name = ?1 AND a.title = ?2 AND t.title LIKE ?3 | ||
| 371 | ORDER BY t.track_number LIMIT 1", | ||
| 372 | )?; | ||
| 373 | let mut rows = stmt.query_map(rusqlite::params![artist, album, pattern], |row| { | ||
| 374 | Ok(Track { | ||
| 375 | id: row.get(0)?, | ||
| 376 | album_id: row.get(1)?, | ||
| 377 | title: row.get(2)?, | ||
| 378 | path: PathBuf::from(row.get::<_, String>(3)?), | ||
| 379 | duration: row.get(4)?, | ||
| 380 | track_number: row.get(5)?, | ||
| 381 | artist_name: Some(row.get(6)?), | ||
| 382 | album_title: Some(row.get(7)?), | ||
| 383 | }) | ||
| 384 | })?; | ||
| 385 | match rows.next() { | ||
| 386 | Some(Ok(t)) => Ok(Some(t)), | ||
| 387 | Some(Err(e)) => Err(e), | ||
| 388 | None => Ok(None), | ||
| 389 | } | ||
| 390 | } | ||
| 391 | |||
| 392 | /// Resolve a hierarchical path like "Artist/Album" or "Artist/Album/Track" | ||
| 393 | /// to DB tracks. Returns None if the path isn't hierarchical (single part | ||
| 394 | /// or 4+ parts) — the caller should fall back to search_tracks(). | ||
| 395 | pub fn resolve_path(&self, path: &str) -> SqlResult<Option<Vec<Track>>> { | ||
| 396 | let path = path.strip_suffix('/').unwrap_or(path); | ||
| 397 | let parts: Vec<&str> = path.splitn(3, '/').collect(); | ||
| 398 | match parts.len() { | ||
| 399 | 2 => Ok(Some(self.find_tracks_by_album(parts[0], parts[1])?)), | ||
| 400 | 3 => match self.find_track_by_parts(parts[0], parts[1], parts[2])? { | ||
| 401 | Some(t) => Ok(Some(vec![t])), | ||
| 402 | None => Ok(Some(vec![])), | ||
| 403 | }, | ||
| 404 | _ => Ok(None), | ||
| 405 | } | ||
| 406 | } | ||
| 407 | |||
| 272 | pub fn search_tracks(&self, query: &str) -> SqlResult<Vec<Track>> { | 408 | pub fn search_tracks(&self, query: &str) -> SqlResult<Vec<Track>> { |
| 273 | let mut stmt = self.conn.prepare( | 409 | let mut stmt = self.conn.prepare( |
| 274 | "SELECT t.id, t.album_id, t.title, t.path, t.duration, t.track_number, ar.name, a.title | 410 | "SELECT t.id, t.album_id, t.title, t.path, t.duration, t.track_number, ar.name, a.title |
