aboutsummaryrefslogtreecommitdiffstats
path: root/src/database.rs
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-06-06 19:48:22 +0300
committersalmonik <raisersoft@proton.me>2026-06-06 19:48:22 +0300
commitd347d89dfeef54d648ffdea054897617c0016509 (patch)
tree0ea0e25e2a8e9bcdd8c06ffadbe29721a7f07f77 /src/database.rs
parentdirty, broken commit (diff)
downloadrsplayer-d347d89dfeef54d648ffdea054897617c0016509.tar.gz
rsplayer-d347d89dfeef54d648ffdea054897617c0016509.zip
correctly working auto completion for db
Diffstat (limited to '')
-rw-r--r--src/database.rs259
1 files changed, 206 insertions, 53 deletions
diff --git a/src/database.rs b/src/database.rs
index 00d970a..01bee92 100644
--- a/src/database.rs
+++ b/src/database.rs
@@ -63,7 +63,29 @@ impl Database {
63 fs::create_dir_all(&data_dir).ok(); 63 fs::create_dir_all(&data_dir).ok();
64 let db_path = data_dir.join("library.db"); 64 let db_path = data_dir.join("library.db");
65 65
66 let conn = Connection::open(&db_path)?; 66 let debug_log = std::env::temp_dir().join("rsp-completion-debug.log");
67 macro_rules! debug_log {
68 ($($arg:tt)*) => {
69 let _ = std::fs::OpenOptions::new()
70 .create(true)
71 .append(true)
72 .open(&debug_log)
73 .and_then(|mut f| {
74 use std::io::Write;
75 writeln!(f, $($arg)*)
76 });
77 };
78 }
79 debug_log!("[Database::open] db_path: {:?}", db_path);
80 debug_log!("[Database::open] db exists: {}", db_path.exists());
81
82 let conn = match Connection::open(&db_path) {
83 Ok(c) => c,
84 Err(e) => {
85 debug_log!("[Database::open] Connection::open failed: {e}");
86 return Err(e);
87 }
88 };
67 let db = Database { conn }; 89 let db = Database { conn };
68 db.init_schema()?; 90 db.init_schema()?;
69 Ok(db) 91 Ok(db)
@@ -273,51 +295,174 @@ impl Database {
273 self.get_album_tracks_after(album_id, track_number, &path) 295 self.get_album_tracks_after(album_id, track_number, &path)
274 } 296 }
275 297
276 pub fn complete_artists(&self, prefix: &str) -> SqlResult<Vec<String>> { 298 pub fn complete(&self, query: &str) -> SqlResult<Vec<String>> {
277 let pattern = format!("%{}%", prefix); 299 let mut results: Vec<String> = Vec::new();
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 300
285 pub fn complete_albums(&self, prefix: &str) -> SqlResult<Vec<String>> { 301 if query.is_empty() {
286 let (artist, album_prefix) = match prefix.split_once('/') { 302 let mut stmt = self
287 Some((a, ap)) => (a, ap), 303 .conn
288 None => return Ok(Vec::new()), 304 .prepare("SELECT name FROM artists ORDER BY name LIMIT 25")?;
289 }; 305 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
290 let pattern = format!("{}%", album_prefix); 306 for name in rows.flatten() {
291 let mut stmt = self.conn.prepare( 307 results.push(format!("{name}/"));
292 "SELECT a.title FROM albums a 308 }
293 JOIN artists ar ON a.artist_id = ar.id 309 return Ok(results);
294 WHERE ar.name = ?1 AND a.title LIKE ?2 310 }
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 311
303 pub fn complete_tracks(&self, prefix: &str) -> SqlResult<Vec<String>> { 312 let has_trailing_slash = query.ends_with('/');
304 let parts: Vec<&str> = prefix.splitn(3, '/').collect(); 313
305 if parts.len() < 3 { 314 if has_trailing_slash {
306 return Ok(Vec::new()); 315 let prefix = query.trim_end_matches('/');
316 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
317
318 match segments.len() {
319 1 => {
320 let artist = segments[0];
321 let mut stmt = self.conn.prepare(
322 "SELECT a.title FROM albums a
323 JOIN artists ar ON a.artist_id = ar.id
324 WHERE ar.name LIKE ?1
325 ORDER BY a.title LIMIT 25",
326 )?;
327 let rows = stmt.query_map([artist], |row| row.get::<_, String>(0))?;
328 for album in rows.flatten() {
329 results.push(format!("{artist}/{album}/"));
330 }
331 }
332 2 => {
333 let artist = segments[0];
334 let album = segments[1];
335 let mut stmt = self.conn.prepare(
336 "SELECT t.title FROM tracks t
337 JOIN albums a ON t.album_id = a.id
338 JOIN artists ar ON a.artist_id = ar.id
339 WHERE ar.name LIKE ?1 AND a.title LIKE ?2
340 ORDER BY t.track_number, t.title LIMIT 25",
341 )?;
342 let rows = stmt.query_map(rusqlite::params![artist, album], |row| {
343 row.get::<_, String>(0)
344 })?;
345 for track in rows.flatten() {
346 results.push(format!("{artist}/{album}/{track}"));
347 }
348 }
349 _ => {}
350 }
351 } else {
352 let segments: Vec<&str> = query.split('/').collect();
353
354 match segments.len() {
355 1 => {
356 let prefix = segments[0];
357 let like_pattern = format!("%{prefix}%");
358 let prefix_pattern = format!("{prefix}%");
359
360 let mut stmt = self.conn.prepare(
361 "SELECT name FROM artists
362 WHERE name LIKE ?1
363 ORDER BY
364 CASE WHEN name LIKE ?2 THEN 0 ELSE 1 END,
365 name
366 LIMIT 25",
367 )?;
368 let rows = stmt
369 .query_map(rusqlite::params![&like_pattern, &prefix_pattern], |row| {
370 row.get::<_, String>(0)
371 })?;
372 for name in rows.flatten() {
373 results.push(format!("{name}/"));
374 }
375 }
376 2 => {
377 let artist = segments[0];
378 let album_prefix = segments[1];
379 if artist.is_empty() {
380 return Ok(results);
381 }
382
383 let artist_pattern = format!("%{artist}%");
384 let artist_prefix = format!("{artist}%");
385 let album_pattern = format!("%{album_prefix}%");
386 let album_prefix_pattern = format!("{album_prefix}%");
387
388 let mut stmt = self.conn.prepare(
389 "SELECT ar.name, a.title FROM albums a
390 JOIN artists ar ON a.artist_id = ar.id
391 WHERE ar.name LIKE ?1 AND a.title LIKE ?2
392 ORDER BY
393 CASE WHEN ar.name LIKE ?3 THEN 0 ELSE 1 END,
394 CASE WHEN a.title LIKE ?4 THEN 0 ELSE 1 END,
395 ar.name, a.title
396 LIMIT 25",
397 )?;
398 let rows = stmt.query_map(
399 rusqlite::params![
400 &artist_pattern,
401 &album_pattern,
402 &artist_prefix,
403 &album_prefix_pattern,
404 ],
405 |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
406 )?;
407 for (name, album) in rows.flatten() {
408 results.push(format!("{name}/{album}/"));
409 }
410 }
411 3 => {
412 let artist = segments[0];
413 let album = segments[1];
414 let track_prefix = segments[2];
415 if artist.is_empty() || album.is_empty() {
416 return Ok(results);
417 }
418
419 let artist_pattern = format!("%{artist}%");
420 let artist_prefix = format!("{artist}%");
421 let album_pattern = format!("%{album}%");
422 let album_prefix = format!("{album}%");
423 let track_pattern = format!("%{track_prefix}%");
424 let track_prefix_pattern = format!("{track_prefix}%");
425
426 let mut stmt = self.conn.prepare(
427 "SELECT ar.name, a.title, t.title FROM tracks t
428 JOIN albums a ON t.album_id = a.id
429 JOIN artists ar ON a.artist_id = ar.id
430 WHERE ar.name LIKE ?1 AND a.title LIKE ?2 AND t.title LIKE ?3
431 ORDER BY
432 CASE WHEN ar.name LIKE ?4 THEN 0 ELSE 1 END,
433 CASE WHEN a.title LIKE ?5 THEN 0 ELSE 1 END,
434 CASE WHEN t.title LIKE ?6 THEN 0 ELSE 1 END,
435 t.track_number, t.title
436 LIMIT 25",
437 )?;
438 let rows = stmt.query_map(
439 rusqlite::params![
440 &artist_pattern,
441 &album_pattern,
442 &track_pattern,
443 &artist_prefix,
444 &album_prefix,
445 &track_prefix_pattern,
446 ],
447 |row| {
448 Ok((
449 row.get::<_, String>(0)?,
450 row.get::<_, String>(1)?,
451 row.get::<_, String>(2)?,
452 ))
453 },
454 )?;
455 for (name, album_title, track) in rows.flatten() {
456 results.push(format!("{name}/{album_title}/{track}"));
457 }
458 }
459 _ => {}
460 }
307 } 461 }
308 let (artist, album, track_prefix) = (parts[0], parts[1], parts[2]); 462
309 let pattern = format!("{}%", track_prefix); 463 results.truncate(25);
310 let mut stmt = self.conn.prepare( 464
311 "SELECT t.title FROM tracks t 465 Ok(results)
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 } 466 }
322 467
323 #[allow(dead_code)] 468 #[allow(dead_code)]
@@ -328,14 +473,14 @@ impl Database {
328 Ok(db) 473 Ok(db)
329 } 474 }
330 475
331 /// Find all tracks in an album by exact artist name and album title. 476 /// Find all tracks in an album by case-insensitive artist and album match.
332 pub fn find_tracks_by_album(&self, artist: &str, album: &str) -> SqlResult<Vec<Track>> { 477 pub fn find_tracks_by_album(&self, artist: &str, album: &str) -> SqlResult<Vec<Track>> {
333 let mut stmt = self.conn.prepare( 478 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 479 "SELECT t.id, t.album_id, t.title, t.path, t.duration, t.track_number, ar.name, a.title
335 FROM tracks t 480 FROM tracks t
336 JOIN albums a ON t.album_id = a.id 481 JOIN albums a ON t.album_id = a.id
337 JOIN artists ar ON a.artist_id = ar.id 482 JOIN artists ar ON a.artist_id = ar.id
338 WHERE ar.name = ?1 AND a.title = ?2 483 WHERE ar.name COLLATE NOCASE = ?1 AND a.title COLLATE NOCASE = ?2
339 ORDER BY t.track_number", 484 ORDER BY t.track_number",
340 )?; 485 )?;
341 let rows = stmt.query_map(rusqlite::params![artist, album], |row| { 486 let rows = stmt.query_map(rusqlite::params![artist, album], |row| {
@@ -367,7 +512,7 @@ impl Database {
367 FROM tracks t 512 FROM tracks t
368 JOIN albums a ON t.album_id = a.id 513 JOIN albums a ON t.album_id = a.id
369 JOIN artists ar ON a.artist_id = ar.id 514 JOIN artists ar ON a.artist_id = ar.id
370 WHERE ar.name = ?1 AND a.title = ?2 AND t.title LIKE ?3 515 WHERE ar.name COLLATE NOCASE = ?1 AND a.title COLLATE NOCASE = ?2 AND t.title LIKE ?3
371 ORDER BY t.track_number LIMIT 1", 516 ORDER BY t.track_number LIMIT 1",
372 )?; 517 )?;
373 let mut rows = stmt.query_map(rusqlite::params![artist, album, pattern], |row| { 518 let mut rows = stmt.query_map(rusqlite::params![artist, album, pattern], |row| {
@@ -462,11 +607,19 @@ pub fn extract_metadata(path: &Path) -> (String, String, String, Option<i32>, Op
462 .and_then(|s| s.to_str()) 607 .and_then(|s| s.to_str())
463 .unwrap_or("Unknown"); 608 .unwrap_or("Unknown");
464 609
465 ( 610 let fs_artist = grandparent.to_string();
466 grandparent.to_string(), 611 let fs_album = parent.to_string();
467 parent.to_string(), 612 let fs_title = file_stem.to_string();
468 file_stem.to_string(), 613
469 None, 614 if let Some(audio_meta) = crate::queue::extract_metadata(path) {
470 None, 615 (
471 ) 616 audio_meta.artist.unwrap_or(fs_artist),
617 audio_meta.album.unwrap_or(fs_album),
618 audio_meta.title.unwrap_or(fs_title),
619 audio_meta.track_number,
620 audio_meta.duration_secs.map(|d| d as u64),
621 )
622 } else {
623 (fs_artist, fs_album, fs_title, None, None)
624 }
472} 625}