mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 23:35:44 +00:00
perf(library): index local lossless albums
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
-- Drive Lossless Albums from the small suffix-qualified candidate set instead
|
||||
-- of scanning every album track on large mixed-format libraries.
|
||||
CREATE INDEX IF NOT EXISTS idx_track_lossless_album_browse
|
||||
ON track(server_id, suffix COLLATE NOCASE, library_id, album_id)
|
||||
WHERE deleted = 0
|
||||
AND album_id IS NOT NULL
|
||||
AND album_id != ''
|
||||
AND suffix IS NOT NULL;
|
||||
@@ -62,21 +62,7 @@ pub fn list_lossless_albums(
|
||||
let where_sql = where_clauses.join(" AND ");
|
||||
let la_artist = crate::album_compilation_filter::sql_track_group_display_artist("la");
|
||||
let sql = format!(
|
||||
"SELECT \
|
||||
la.server_id, \
|
||||
la.album_id, \
|
||||
COALESCE(a.name, la.album_name), \
|
||||
COALESCE(a.artist, {la_artist}), \
|
||||
COALESCE(a.artist_id, la.artist_id), \
|
||||
COALESCE(a.song_count, la.track_count), \
|
||||
COALESCE(a.duration_sec, la.duration_sec), \
|
||||
COALESCE(a.year, la.year), \
|
||||
COALESCE(a.genre, la.genre), \
|
||||
COALESCE(a.cover_art_id, la.cover_art_id), \
|
||||
COALESCE(a.starred_at, la.starred_at), \
|
||||
COALESCE(a.synced_at, la.synced_at), \
|
||||
a.raw_json \
|
||||
FROM ( \
|
||||
"WITH lossless_album AS ( \
|
||||
SELECT \
|
||||
t.server_id, \
|
||||
t.album_id, \
|
||||
@@ -89,16 +75,38 @@ pub fn list_lossless_albums(
|
||||
MAX(t.cover_art_id) AS cover_art_id, \
|
||||
MAX(t.starred_at) AS starred_at, \
|
||||
MAX(t.synced_at) AS synced_at, \
|
||||
(SELECT COUNT(*) FROM track c \
|
||||
WHERE c.server_id = t.server_id AND c.album_id = t.album_id AND c.deleted = 0) AS track_count, \
|
||||
(SELECT COALESCE(SUM(c.duration_sec), 0) FROM track c \
|
||||
WHERE c.server_id = t.server_id AND c.album_id = t.album_id AND c.deleted = 0) AS duration_sec, \
|
||||
MAX(COALESCE(CAST(json_extract(t.raw_json, '$.bitDepth') AS INTEGER), 0)) AS max_bit_depth \
|
||||
FROM track t \
|
||||
WHERE {where_sql} \
|
||||
GROUP BY t.server_id, t.album_id \
|
||||
) la \
|
||||
LEFT JOIN album a ON a.server_id = la.server_id AND a.id = la.album_id \
|
||||
), \
|
||||
album_totals AS ( \
|
||||
SELECT c.server_id, c.album_id, COUNT(*) AS track_count, \
|
||||
COALESCE(SUM(c.duration_sec), 0) AS duration_sec \
|
||||
FROM track c \
|
||||
INNER JOIN lossless_album la \
|
||||
ON la.server_id = c.server_id AND la.album_id = c.album_id \
|
||||
WHERE c.deleted = 0 \
|
||||
GROUP BY c.server_id, c.album_id \
|
||||
) \
|
||||
SELECT \
|
||||
la.server_id, \
|
||||
la.album_id, \
|
||||
COALESCE(a.name, la.album_name), \
|
||||
COALESCE(a.artist, {la_artist}), \
|
||||
COALESCE(a.artist_id, la.artist_id), \
|
||||
COALESCE(a.song_count, totals.track_count), \
|
||||
COALESCE(a.duration_sec, totals.duration_sec), \
|
||||
COALESCE(a.year, la.year), \
|
||||
COALESCE(a.genre, la.genre), \
|
||||
COALESCE(a.cover_art_id, la.cover_art_id), \
|
||||
COALESCE(a.starred_at, la.starred_at), \
|
||||
COALESCE(a.synced_at, la.synced_at), \
|
||||
a.raw_json \
|
||||
FROM lossless_album la \
|
||||
INNER JOIN album_totals totals \
|
||||
ON totals.server_id = la.server_id AND totals.album_id = la.album_id \
|
||||
LEFT JOIN album a ON a.server_id = la.server_id AND a.id = la.album_id \
|
||||
ORDER BY la.max_bit_depth DESC, \
|
||||
COALESCE(a.name, la.album_name) COLLATE NOCASE ASC, \
|
||||
la.album_id ASC \
|
||||
|
||||
@@ -7,14 +7,14 @@ pub const LOSSLESS_SUFFIXES: &[&str] = &[
|
||||
"flac", "wav", "wave", "aiff", "aif", "dsf", "dff", "ape", "wv", "shn", "tta",
|
||||
];
|
||||
|
||||
/// `LOWER(alias.suffix) IN ('flac', …)` for SQL WHERE clauses.
|
||||
/// Case-insensitive suffix predicate that can use the lossless browse index.
|
||||
pub fn track_is_lossless_sql(table_alias: &str) -> String {
|
||||
let list = LOSSLESS_SUFFIXES
|
||||
.iter()
|
||||
.map(|s| format!("'{s}'"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("LOWER({table_alias}.suffix) IN ({list})")
|
||||
format!("{table_alias}.suffix COLLATE NOCASE IN ({list})")
|
||||
}
|
||||
|
||||
/// Album has at least one indexed lossless track (same allowlist as browse).
|
||||
@@ -50,6 +50,6 @@ mod tests {
|
||||
let sql = track_is_lossless_sql("t");
|
||||
assert!(sql.contains("'flac'"));
|
||||
assert!(sql.contains("'tta'"));
|
||||
assert!(sql.starts_with("LOWER(t.suffix) IN ("));
|
||||
assert!(sql.starts_with("t.suffix COLLATE NOCASE IN ("));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use tauri::Manager;
|
||||
///
|
||||
/// Migration checklist (wiring, data backfill, open/swap path):
|
||||
/// psysonic-workdocs `ai/agent-rules/08-library-db-migrations.md`.
|
||||
pub const LIBRARY_DB_SCHEMA_VERSION: i64 = 20;
|
||||
pub const LIBRARY_DB_SCHEMA_VERSION: i64 = 21;
|
||||
|
||||
/// One-time data repair after migration 014 (`artist.name_sort`).
|
||||
pub(crate) const ARTIST_NAME_SORT_RECONCILE_ID: &str = "artist_name_sort_reconcile_v1";
|
||||
@@ -66,6 +66,9 @@ pub(crate) const MIGRATION_019_MAINSTAGE_FEED_INDEXES: &str =
|
||||
/// Version 20: owner-scoped cache for track, album, and artist user ratings.
|
||||
pub(crate) const MIGRATION_020_ENTITY_USER_RATING: &str =
|
||||
include_str!("../migrations/020_entity_user_rating.sql");
|
||||
/// Version 21: suffix-selective candidate index for local lossless album browse.
|
||||
pub(crate) const MIGRATION_021_LOSSLESS_ALBUM_BROWSE_INDEX: &str =
|
||||
include_str!("../migrations/021_lossless_album_browse_index.sql");
|
||||
|
||||
/// Embedded migrations. Ordered ascending by `version`; the runner sorts
|
||||
/// defensively before applying so the source order can stay readable.
|
||||
@@ -80,6 +83,7 @@ const MIGRATIONS: &[(i64, &str)] = &[
|
||||
(18, MIGRATION_018_ARTIST_SYNCED_INDEX),
|
||||
(19, MIGRATION_019_MAINSTAGE_FEED_INDEXES),
|
||||
(20, MIGRATION_020_ENTITY_USER_RATING),
|
||||
(21, MIGRATION_021_LOSSLESS_ALBUM_BROWSE_INDEX),
|
||||
];
|
||||
|
||||
/// Idempotent repair — also runs after the migration runner on every open so
|
||||
@@ -101,6 +105,12 @@ pub(crate) fn ensure_entity_user_rating_schema(conn: &Connection) -> rusqlite::R
|
||||
conn.execute_batch(MIGRATION_020_ENTITY_USER_RATING)
|
||||
}
|
||||
|
||||
/// Repairs a partial-v21 open where the migration marker exists but the
|
||||
/// additive lossless candidate index was not retained.
|
||||
pub(crate) fn ensure_lossless_album_browse_index(conn: &Connection) -> rusqlite::Result<()> {
|
||||
conn.execute_batch(MIGRATION_021_LOSSLESS_ALBUM_BROWSE_INDEX)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum MigrationOutcome {
|
||||
/// Every missing migration was applied (or the DB was already at head).
|
||||
@@ -682,6 +692,7 @@ fn prepare_write_connection_for_open(conn: &Connection) -> rusqlite::Result<()>
|
||||
ensure_genre_tags_schema(conn)?;
|
||||
ensure_mainstage_feed_indexes(conn)?;
|
||||
ensure_entity_user_rating_schema(conn)?;
|
||||
ensure_lossless_album_browse_index(conn)?;
|
||||
checkpoint_wal_conn(conn, "open")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user