From 2016f856d0418e4586cddceef7ea9c08b6a508f7 Mon Sep 17 00:00:00 2001 From: Kasper Date: Mon, 7 Sep 2026 10:13:00 +0200 Subject: [PATCH] Use Rust tracks cache for fltering But the performance is really not good, selecting 11k i64 track ids takes 10ms --- Cargo.lock | 14 ++ Cargo.toml | 2 +- src-native/data.rs | 7 +- src-native/db.rs | 27 ++- src-native/filter.rs | 280 ++++++++++++++++-------------- src-native/library.rs | 18 +- src-native/migrations/1_start.sql | 47 ++--- src-native/page.rs | 243 ++++++++------------------ 8 files changed, 294 insertions(+), 344 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31b9d93..508a959 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4436,6 +4436,8 @@ dependencies = [ "sha2 0.10.9", "smallvec", "thiserror 2.0.18", + "tokio", + "tokio-stream", "tracing", "url", ] @@ -4475,6 +4477,7 @@ dependencies = [ "sqlx-sqlite", "syn 2.0.106", "thiserror 2.0.18", + "tokio", "url", ] @@ -5301,6 +5304,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" diff --git a/Cargo.toml b/Cargo.toml index c84a31f..4d3d896 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ simd-json = "0.16.0" mimalloc = "0.1.48" specta = { version = "=2.0.0-rc.22", features = ["derive"] } tempfile = "3.27.0" -sqlx = { version = "0.9.0", features = ["sqlite"] } +sqlx = { version = "0.9.0", features = ["sqlite", "runtime-tokio"] } [target.'cfg(not(target_os = "android"))'.dependencies] trash = "5.1" diff --git a/src-native/data.rs b/src-native/data.rs index 9baffec..834464a 100644 --- a/src-native/data.rs +++ b/src-native/data.rs @@ -1,3 +1,4 @@ +use crate::filter::TracksCache; use crate::library::{Paths, load_library_json, open_library}; use crate::library_types::Library; use crate::tracks::Tag; @@ -5,7 +6,7 @@ use anyhow::{Context, Result}; use atomicwrites::{AllowOverwrite, AtomicFile}; use dirs_next; use serde::Serialize; -use sqlx::SqliteConnection; +use sqlx::SqlitePool; use std::env; use std::io::Write; use std::path::{Path, PathBuf}; @@ -40,7 +41,8 @@ pub fn app_log_dir() -> Result { pub struct Data { pub paths: Paths, pub library: Library, - pub db: SqliteConnection, + pub db: SqlitePool, + pub tracks_cache: Option, /// Current tag being edited pub current_tag: Option, } @@ -129,6 +131,7 @@ impl Data { paths, library: loaded_library_json, db: library_sqlite, + tracks_cache: None, current_tag: None, }; // if it fails, it was already set. the user might just have reloaded diff --git a/src-native/db.rs b/src-native/db.rs index 955d95c..ff621bf 100644 --- a/src-native/db.rs +++ b/src-native/db.rs @@ -1,4 +1,4 @@ -pub type TrackID = String; +pub type TrackIDNew = i64; pub type TrackListID = String; #[derive(Debug, Clone, Copy, PartialEq, sqlx::Type)] @@ -19,6 +19,31 @@ impl ToString for TrackListKind { } } +pub enum SpecialTrackListId { + Root, +} +impl SpecialTrackListId { + pub fn from_id(id: &str) -> Self { + match id { + "root" => Self::Root, + _ => panic!("Unknown special track list id: {}", id), + } + } +} + +pub enum TrackListVariant { + Playlist, + Folder, + Root, +} +impl TrackListVariant { + pub fn from_special_track_list_id(id: SpecialTrackListId) -> Self { + match id { + SpecialTrackListId::Root => Self::Root, + } + } +} + // #[derive(sqlx::FromRow, Debug)] // pub struct TrackList { // id: String, diff --git a/src-native/filter.rs b/src-native/filter.rs index 67464fd..67268fe 100644 --- a/src-native/filter.rs +++ b/src-native/filter.rs @@ -1,14 +1,11 @@ -use crate::{ - data::Data, - library_types::{ItemId, Library, TRACK_ID_MAP}, -}; +use crate::db::TrackIDNew; use anyhow::{Context, Result}; use rayon::prelude::*; use serde::Deserialize; use specta::Type; -use sqlx::Connection; -use std::str::Chars; +use sqlx::SqlitePool; use std::time::Instant; +use std::{collections::HashMap, str::Chars}; use unicode_normalization::{Recompositions, UnicodeNormalization}; fn match_at_start(mut text: Recompositions, target: Chars) -> bool { @@ -161,58 +158,60 @@ fn feat_artists_match(track_name: &str, target: &str) -> bool { false } -fn filter_term(ids: Vec, term: FilterTerm, library: &Library) -> Vec { - let id_map = TRACK_ID_MAP.read().unwrap(); +fn filter_term(ids: Vec, term: FilterTerm, cache: &TracksCache) -> Vec { let filtered_tracks: Vec<_> = ids .into_par_iter() .with_min_len(2000) - .filter(|item_id| { - let track_id = &id_map[*item_id as usize]; - let track = match library.get_track(track_id) { + .filter(|track_id| { + let track = match cache.get_track(track_id) { Ok(track) => track, Err(_) => panic!("Track ID {} not found", track_id), }; if term.field.is_none() { - let is_match = find_match(&track.name, &term.literal) + let is_match = find_match(&track.title, &term.literal) || find_match(&track.artist, &term.literal) - || find_match_opt(&track.albumName, &term.literal) + || find_match_opt(&track.album_title, &term.literal) || find_match_opt(&track.comments, &term.literal) || find_match_opt(&track.genre, &&term.literal); return is_match; }; let is_match = match term.field.as_ref().unwrap() { - Field::Title => find_match(&track.name, &term.literal), + Field::Title => find_match(&track.title, &term.literal), Field::Artist => { find_match(&track.artist, &term.literal) - || feat_artists_match(&track.name, &term.literal) + || feat_artists_match(&track.title, &term.literal) } - Field::Album => find_match_opt(&track.albumName, &term.literal), - Field::AlbumArtist => find_match_opt(&track.albumArtist, &term.literal), + Field::Album => find_match_opt(&track.album_title, &term.literal), + Field::AlbumArtist => find_match_opt(&track.album_artist, &term.literal), Field::Comments => find_match_opt(&track.comments, &term.literal), Field::Genre => find_match_opt(&track.genre, &term.literal), Field::Composer => find_match_opt(&track.composer, &term.literal), Field::Group => find_match_opt(&track.grouping, &term.literal), Field::Year => { - track.year.map(|n| n.to_string()).unwrap_or("".to_string()) == term.literal + todo!(); + // track.year.map(|n| n.to_string()).unwrap_or("".to_string()) == term.literal } Field::Plays => { - track - .plays - .as_ref() - .map(|n| n.len().to_string()) - .unwrap_or("".to_string()) - == term.literal + todo!(); + // track + // .plays + // .as_ref() + // .map(|n| n.len().to_string()) + // .unwrap_or("".to_string()) + // == term.literal } Field::Skips => { - track - .skips - .as_ref() - .map(|n| n.len().to_string()) - .unwrap_or("".to_string()) - == term.literal + todo!(); + // track + // .skips + // .as_ref() + // .map(|n| n.len().to_string()) + // .unwrap_or("".to_string()) + // == term.literal } Field::Bpm => { - track.bpm.map(|n| n.to_string()).unwrap_or("".to_string()) == term.literal + todo!(); + // track.bpm.map(|n| n.to_string()).unwrap_or("".to_string()) == term.literal } }; is_match @@ -270,9 +269,8 @@ impl FilterTerm { } #[derive(sqlx::FromRow)] -struct QueuedTrack { - queue_id: i64, - track_id: i64, +pub struct CachedTrack { + id: i64, title: String, artist: String, album_title: Option, @@ -283,105 +281,131 @@ struct QueuedTrack { grouping: Option, } -pub async fn insert_queued_track_ngrams() -> Result<()> { - let start_time = Instant::now(); - - let mut data = Data::get_async().await; - // todo: maybe use tx for x individual tracks at a time - let mut tx = data.db.begin().await?; - - // todo: bulk insert - - // todo: add all fields - let tracks: Vec = sqlx::query_as( - " - SELECT - q.id AS queue_id, - t.id AS track_id, - t.title, - t.artist, - t.album_title, - t.album_artist, - t.comments, - t.genre, - t.composer, - t.grouping - FROM search_queue q - JOIN tracks t ON t.id = q.track_id - ORDER BY q.id ASC - ", - ) - .fetch_all(&mut *tx) - .await - .context("Failed to fetch search_queue tracks")?; - - let last_queue_id = match tracks.last() { - Some(track) => track.queue_id, - None => { - tx.commit().await?; +pub struct TracksCache { + cached_revision_n: i64, + tracks: HashMap, +} +impl TracksCache { + pub fn get_track(&self, id: &i64) -> Result<&CachedTrack> { + self.tracks + .get(id) + .with_context(|| format!("Track with ID {id} not found in cache")) + } + pub async fn load_all(db: &SqlitePool) -> Result { + let mut tx = db.begin().await?; + + let max_revision_n: i64 = + sqlx::query_scalar("SELECT COALESCE(MAX(revision_n), 0) FROM track_updates") + .fetch_one(&mut *tx) + .await + .context("Failed to get latest revision_n")?; + + let tracks: Vec = sqlx::query_as( + " + SELECT + id, + title, + artist, + album_title, + album_artist, + comments, + genre, + composer, + grouping + FROM tracks + ", + ) + .fetch_all(&mut *tx) + .await + .context("Failed to fetch all tracks")?; + + tx.commit().await?; + + let tracks: HashMap = + tracks.into_iter().map(|track| (track.id, track)).collect(); + + let cache = TracksCache { + cached_revision_n: max_revision_n, + tracks, + }; + Ok(cache) + } + + pub async fn refresh(&mut self, db: &SqlitePool) -> Result<()> { + let mut tx = db.begin().await?; + + let (min_revision_n, max_revision_n): (i64, i64) = sqlx::query_as( + " + SELECT COALESCE(MIN(revision_n), 0), COALESCE(MAX(revision_n), 0) + FROM track_updates + ", + ) + .fetch_one(&mut *tx) + .await + .context("Failed to check track revision_n")?; + if self.cached_revision_n + 1 < min_revision_n { + // If track_update rows were deleted (the cache is too old), refresh everything + drop(tx); + let new_cache = Self::load_all(db).await?; + *self = new_cache; return Ok(()); } - }; - - for track in &tracks { - sqlx::query("DELETE FROM search_ngrams WHERE track_id = ?") - .bind(&track.track_id) - .execute(&mut *tx) - .await?; - - let fields = [ - (NgramField::Title, Some(track.title.as_str())), - (NgramField::Artist, Some(track.artist.as_str())), - (NgramField::Album, track.album_title.as_deref()), - (NgramField::AlbumArtist, track.album_artist.as_deref()), - (NgramField::Comments, track.comments.as_deref()), - (NgramField::Genre, track.genre.as_deref()), - (NgramField::Composer, track.composer.as_deref()), - (NgramField::Group, track.grouping.as_deref()), - ]; - - for (field, text) in fields { - let Some(text) = text else { - continue; - }; - // todo: add normalise ngrams too using .to_lowercase().nfc() - - let chars: Vec = text.to_lowercase().chars().collect(); - - for n in 1..=3 { - for window in chars.windows(n) { - // todo: deduplicate rows before inserting - let ngram: String = window.iter().collect(); - - sqlx::query( - "INSERT OR IGNORE INTO search_ngrams - (track_id, field, ngram, is_normalised) - VALUES (?, ?, ?, FALSE)", - ) - .bind(&track.track_id) - .bind(field.as_id()) - .bind(ngram) - .execute(&mut *tx) - .await?; - } - } + let deletions: Vec = sqlx::query_scalar( + " + SELECT id + FROM track_updates + WHERE revision_n > ? + AND is_delete = true + ", + ) + .bind(self.cached_revision_n) + .fetch_all(&mut *tx) + .await + .context("Failed to check track deletions")?; + + for deletion in deletions { + self.tracks.remove(&deletion); } - } - - sqlx::query("DELETE FROM search_queue WHERE id <= ?") - .bind(last_queue_id) - .execute(&mut *tx) - .await?; - tx.commit().await?; - - println!("Indexing took {}ms", start_time.elapsed().as_millis()); + let tracks: Vec = sqlx::query_as( + " + SELECT + u.id, + t.title, + t.artist, + t.album_title, + t.album_artist, + t.comments, + t.genre, + t.composer, + t.grouping + FROM track_updates u + LEFT JOIN tracks t ON t.id = u.id + WHERE u.revision_n > ? + AND u.is_delete = false + ", + ) + .bind(self.cached_revision_n) + .fetch_all(&mut *tx) + .await + .context("Failed to get track updates")?; + + for track in tracks { + self.tracks.insert(track.id, track); + } + self.cached_revision_n = max_revision_n; - Ok(()) + tx.commit().await?; + Ok(()) + } } -pub fn filter(mut item_ids: Vec, terms: Vec, library: &Library) -> Vec { +pub fn filter( + mut ids: Vec, + terms: Vec, + library: &TracksCache, +) -> Vec { let now = Instant::now(); let terms: Vec<_> = terms .into_iter() @@ -392,14 +416,14 @@ pub fn filter(mut item_ids: Vec, terms: Vec, library: &Libra }) .collect(); if terms.len() == 0 { - return item_ids; + return ids; } for term in terms { - item_ids = filter_term(item_ids, term, &library); + ids = filter_term(ids, term, &library); } println!("Filter: {}ms", now.elapsed().as_millis()); - item_ids + ids } enum Eq { diff --git a/src-native/library.rs b/src-native/library.rs index 3646d91..190b8d3 100644 --- a/src-native/library.rs +++ b/src-native/library.rs @@ -5,7 +5,9 @@ use crate::migrate::migrate_to_sqlite; use anyhow::{Context, Result, bail}; use linked_hash_map::LinkedHashMap; use serde_json::{Value, json}; -use sqlx::{ConnectOptions, SqliteConnection, sqlite::SqliteConnectOptions}; +use sqlx::SqlitePool; +use sqlx::sqlite::SqliteConnectOptions; +use sqlx::sqlite::SqlitePoolOptions; use std::fs::File; use std::io::{ErrorKind, Read, Seek, SeekFrom}; use std::path::PathBuf; @@ -98,7 +100,7 @@ fn parse_old_versionless_library_json(library_file: &mut File) -> Result Result { +pub async fn open_library(paths: &Paths) -> Result { let now = Instant::now(); paths @@ -112,21 +114,23 @@ pub async fn open_library(paths: &Paths) -> Result { if !exists { migrate_to_sqlite(paths).await?; } - let mut connection = SqliteConnectOptions::new() + let options = SqliteConnectOptions::new() .filename(&paths.library_sqlite) - .foreign_keys(true) - .connect() + .foreign_keys(true); + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect_with(options) .await .context("Error connecting to library database")?; sqlx::migrate!("./src-native/migrations") - .run(&mut connection) + .run(&pool) .await .map_err(|e| anyhow::anyhow!("{:?}", e)) .context("Could not run database migrations")?; println!("Open library: {}ms", now.elapsed().as_millis()); - Ok(connection) + Ok(pool) } pub enum TrackField { diff --git a/src-native/migrations/1_start.sql b/src-native/migrations/1_start.sql index bc62daf..36de936 100644 --- a/src-native/migrations/1_start.sql +++ b/src-native/migrations/1_start.sql @@ -40,46 +40,23 @@ create table tracks ( volume INTEGER NULL -- from -100 to 100 ); -CREATE TRIGGER search_queue_after_track_insert AFTER INSERT ON tracks BEGIN - INSERT OR IGNORE INTO search_queue (track_id) VALUES (NEW.id); -END; +CREATE TABLE track_updates ( + revision_n INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id INTEGER NOT NULL, + is_delete BOOLEAN NOT NULL +); -CREATE TRIGGER search_queue_after_track_update AFTER UPDATE OF - title, - artist, - album_title, - album_artist, - comments, - genre, - composer, - grouping -ON tracks BEGIN - INSERT OR IGNORE INTO search_queue (track_id) VALUES (NEW.id); +CREATE TRIGGER tracks_after_insert AFTER INSERT ON tracks BEGIN + INSERT INTO track_updates (id, is_delete) VALUES (NEW.id, false); END; -CREATE TRIGGER search_queue_after_track_delete AFTER DELETE ON tracks BEGIN - DELETE FROM search_ngrams WHERE track_id = OLD.id; - DELETE FROM search_queue WHERE track_id = OLD.id; +CREATE TRIGGER tracks_after_update AFTER UPDATE ON tracks BEGIN + INSERT INTO track_updates (id, is_delete) VALUES (NEW.id, false); END; -CREATE TABLE search_ngrams ( - ngram TEXT NOT NULL, - -- field IDs are defined by the Fields enum in filter.rs - field INTEGER NOT NULL, - is_normalised BOOLEAN NOT NULL, - track_id INTEGER NOT NULL REFERENCES tracks(id), - -- `ngram` first, because we always filter for it. - -- if we do filter for `is_normalised`, we're doing an exact search. exact searches often return few results anyway (for example when searching é), so we can put `field` first. - PRIMARY KEY (ngram, field, is_normalised, track_id) -) WITHOUT ROWID; - --- needed to purge a track's ngrams cheaply on reindex/delete -CREATE INDEX search_ngrams_by_track ON search_ngrams (track_id, field); - -CREATE TABLE search_queue ( - id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - track_id INTEGER NOT NULL UNIQUE REFERENCES tracks(id) -); +CREATE TRIGGER tracks_after_delete AFTER DELETE ON tracks BEGIN + INSERT INTO track_updates (id, is_delete) VALUES (OLD.id, true); +END; CREATE TABLE plays ( date INTEGER NOT NULL, diff --git a/src-native/page.rs b/src-native/page.rs index 6c38191..d607bfb 100644 --- a/src-native/page.rs +++ b/src-native/page.rs @@ -1,11 +1,10 @@ use crate::data::Data; -use crate::db::TrackListKind; -use crate::filter::{FilterTerm, insert_queued_track_ngrams}; +use crate::db::{SpecialTrackListId, TrackListKind, TrackListVariant}; +use crate::filter::{FilterTerm, TracksCache, filter}; use crate::library_types::{ItemId, new_item_ids_from_track_ids}; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use specta::Type; -use sqlx::{AssertSqlSafe, Connection}; #[cfg_attr(feature = "napi", napi(object))] #[derive(Deserialize, Clone, Type)] @@ -30,9 +29,21 @@ pub struct TracksPage { #[derive(Debug, sqlx::FromRow)] struct TrackListPage { kind: TrackListKind, + id: String, name: String, description: String, } +impl TrackListPage { + pub fn variant(&self) -> TrackListVariant { + match self.kind { + TrackListKind::Playlist => TrackListVariant::Playlist, + TrackListKind::Folder => TrackListVariant::Folder, + TrackListKind::Special => { + TrackListVariant::from_special_track_list_id(SpecialTrackListId::from_id(&self.id)) + } + } + } +} // returns (column_name, is_text) fn to_sql_sort_key(sort_key: &str) -> (&'static str, bool) { @@ -55,46 +66,6 @@ fn to_sql_sort_key(sort_key: &str) -> (&'static str, bool) { } } -#[derive(sqlx::FromRow)] -struct AllTrackRow { - id: String, - title: String, - artist: String, - composer: Option, - genre: Option, - comments: Option, - grouping: Option, - album_title: Option, - album_artist: Option, - added_at: i64, - duration_s: f64, - bpm: Option, - play_count: u64, - skip_count: u64, - year: Option, -} - -#[derive(sqlx::FromRow)] -struct PlaylistTrackRow { - track_list_id: String, - item_pos: u64, - id: String, - title: String, - artist: String, - composer: Option, - genre: Option, - comments: Option, - grouping: Option, - album_title: Option, - album_artist: Option, - added_at: i64, - duration_s: f64, - bpm: Option, - play_count: u64, - skip_count: u64, - year: Option, -} - #[cfg(feature = "napi")] #[cfg_attr(feature = "napi", napi(js_name = "get_tracks_page"))] #[allow(dead_code)] @@ -102,140 +73,79 @@ pub async fn get_tracks_page_js(options: TracksPageOptions) -> Result, field: i32, literal: &str) { -// sql.push_str( -// " AND EXISTS ( -// SELECT 1 -// FROM search_ngrams sn -// WHERE sn.track_id = t.id -// AND sn.field = ? -// AND sn.ngram = ? -// )", -// ); - -// args.push(FilterArg::Integer(field as i64)); -// args.push(FilterArg::Text(literal.to_owned())); -// } - pub async fn get_tracks_page(options: TracksPageOptions) -> Result { - { - insert_queued_track_ngrams().await?; - } - let mut data = Data::get_async().await; + let db = data.db.clone(); + + match &mut data.tracks_cache { + Some(cache) => { + cache + .refresh(&db) + .await + .context("Failed to refresh tracks cache")?; + } + None => { + let tracks_cache = TracksCache::load_all(&db) + .await + .context("Failed to load tracks cache")?; + data.tracks_cache = Some(tracks_cache); + } + }; + let tracks_cache = data.tracks_cache.as_ref().unwrap(); + let mut tx = data.db.begin().await?; let start_time = std::time::Instant::now(); let track_list: TrackListPage = sqlx::query_as( - "SELECT kind, name, description + "SELECT kind, id, name, description FROM track_lists WHERE id = ?", ) .bind(&options.playlist_id) .fetch_one(&mut *tx) - .await?; - - let sql = String::from( - " - WITH term1 AS ( - SELECT track_id - FROM search_ngrams - WHERE ngram IN ('dev', 'evo', 'vot', 'oti', 'tio', 'ion') - AND field IN (0, 1, 2) - AND is_normalised = 0 - GROUP BY track_id, field - HAVING COUNT(DISTINCT ngram) = 6 - ), - term2 AS ( - SELECT track_id - FROM search_ngrams - WHERE ngram IN ('tri', 'ris', 'ist', 'sta', 'tam') - AND field IN (0, 1, 2) - AND is_normalised = 0 - GROUP BY track_id, field - HAVING COUNT(DISTINCT ngram) = 5 - ) - SELECT track_id FROM term1 - INTERSECT - SELECT track_id FROM term2; - ", - ); - // let mut sql = String::from( - // "SELECT pt.track_id - // FROM playlist_tracks pt - // JOIN tracks t ON t.id = pt.track_id - // WHERE pt.track_list_id = ?", - // ); - // let mut where_clauses = Vec::new(); - let mut args = sqlx::sqlite::SqliteArguments::default(); - - // for term in options.filter_terms.iter().filter(|t| !t.is_whitespace()) { - // match term.field { - // // Some(Field::Title) => add_text_filter(&mut sql, &mut args, 0, &term.literal), - // // Some(Field::Artist) => add_text_filter(&mut sql, &mut args, 1, &term.literal), - // // Some(Field::Album) => add_text_filter(&mut sql, &mut args, 2, &term.literal), - // // Some(Field::AlbumArtist) => add_text_filter(&mut sql, &mut args, 3, &term.literal), - // // Some(Field::Comments) => add_text_filter(&mut sql, &mut args, 4, &term.literal), - // // Some(Field::Genre) => add_text_filter(&mut sql, &mut args, 5, &term.literal), - // // Some(Field::Composer) => add_text_filter(&mut sql, &mut args, 6, &term.literal), - // // Some(Field::Group) => add_text_filter(&mut sql, &mut args, 7, &term.literal), - // None => { - // where_clauses.push( - // "EXISTS ( - // SELECT 1 - // FROM search_ngrams sn - // WHERE sn.track_id = t.id - // AND sn.field BETWEEN 0 AND 7 - // AND sn.ngram = ? - // )", - // ); - // args.add(&term.literal); - // } - // _ => todo!(), - // } - // } - - // sql.push_str(" ORDER BY "); - - // if options.group_album_tracks { - // sql.push_str( - // "t.album_artist COLLATE NOCASE, - // t.album_title COLLATE NOCASE, - // t.disc_num, - // t.track_num, - // ", - // ); - // } - - // let sort_column = match options.sort_key.as_str() { - // "title" => "t.title", - // "artist" => "t.artist", - // "album" => "t.album_title", - // "album_artist" => "t.album_artist", - // "comments" => "t.comments", - // "genre" => "t.genre", - // "composer" => "t.composer", - // "group" => "t.grouping", - // _ => "pt.item_pos", - // }; + .await + .context("Failed to get playlist")?; + + let track_ids = match track_list.variant() { + TrackListVariant::Playlist => { + // let track_ids: Vec = sqlx::query_scalar( + // "SELECT t.id + // FROM playlist_tracks pt + // JOIN tracks t ON t.id = pt.track_id + // WHERE pt.trck_list_id = ?", + // ) + // .bind(&options.playlist_id) + // .fetch_all(&mut *tx) + // .await + // .context("Failed to get playlist tracks")?; + todo!(); + } + TrackListVariant::Folder => todo!(), + TrackListVariant::Root => { + let track_ids: Vec = sqlx::query_scalar( + "SELECT id + FROM tracks + ORDER BY added_at DESC", + ) + .fetch_all(&mut *tx) + .await + .context("Failed to get playlist tracks")?; + track_ids + } + }; - // sql.push_str(sort_column); - // sql.push_str(if options.sort_desc { " DESC" } else { " ASC" }); + let track_ids = filter(track_ids, options.filter_terms, tracks_cache); - // // Stable ordering. - // sql.push_str(", pt.item_pos ASC"); + // todo: remove this + let text_ids: Vec = sqlx::query_scalar( + "SELECT text_id FROM tracks WHERE id IN (SELECT value FROM json_each(?))", + ) + .bind(serde_json::to_string(&track_ids)?) + .fetch_all(&mut *tx) + .await?; - let track_ids: Vec = sqlx::query_scalar_with(AssertSqlSafe(sql), args) - .fetch_all(&mut *tx) - .await - .context("Failed to select page track_ids")?; + tx.commit().await?; println!( "get_tracks_page took {:?}, {} results", @@ -243,16 +153,9 @@ pub async fn get_tracks_page(options: TracksPageOptions) -> Result { track_ids.len() ); - let text_ids: Vec = sqlx::query_scalar( - "SELECT text_id FROM tracks WHERE id IN (SELECT value FROM json_each(?))", - ) - .bind(serde_json::to_string(&track_ids)?) - .fetch_all(&mut *tx) - .await?; + // todo: remove this let item_ids = new_item_ids_from_track_ids(&text_ids); - tx.commit().await?; - Ok(TracksPage { playlist_kind: track_list.kind.to_string(), playlist_name: track_list.name, -- 2.51.2