diff --git a/Cargo.lock b/Cargo.lock index fa1e075..ba6c830 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,7 +129,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -202,6 +202,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arboard" version = "3.4.0" @@ -352,7 +358,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -387,7 +393,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -494,7 +500,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -527,6 +533,7 @@ dependencies = [ "tracing-subscriber", "ureq", "urlencoding", + "uuid", "walkdir", ] @@ -626,7 +633,7 @@ checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1273,7 +1280,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1294,7 +1301,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1483,7 +1490,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1545,7 +1552,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1638,6 +1645,19 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.13.1" @@ -1904,6 +1924,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "id3" version = "1.16.2" @@ -1992,6 +2018,7 @@ checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown 0.15.2", + "serde", ] [[package]] @@ -2079,10 +2106,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2125,6 +2154,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.2" @@ -2515,7 +2550,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -2575,7 +2610,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -2983,7 +3018,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -3049,6 +3084,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "primal-check" version = "0.3.3" @@ -3135,6 +3180,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -3162,7 +3213,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -3251,7 +3302,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox 0.1.3", "thiserror 1.0.69", ] @@ -3345,7 +3396,7 @@ checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "untrusted", "windows-sys 0.52.0", @@ -3527,7 +3578,7 @@ checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -3549,7 +3600,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -3748,7 +3799,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -3938,9 +3989,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.93" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4014,7 +4065,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4025,7 +4076,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4154,7 +4205,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4368,6 +4419,15 @@ dependencies = [ "tiny-skia-path", ] +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "getrandom 0.4.2", +] + [[package]] name = "valuable" version = "0.1.0" @@ -4409,48 +4469,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.99" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.99" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.93", + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4458,22 +4521,59 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.93", - "wasm-bindgen-backend", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.9.0", + "hashbrown 0.15.2", + "indexmap", + "semver", +] [[package]] name = "wayland-backend" @@ -4586,9 +4686,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4824,7 +4924,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4835,7 +4935,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -5180,6 +5280,94 @@ dependencies = [ "winapi", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -5326,7 +5514,7 @@ checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "zbus-lockstep", "zbus_xml", "zvariant", @@ -5341,7 +5529,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "zvariant_utils", ] @@ -5386,7 +5574,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -5426,7 +5614,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "zvariant_utils", ] @@ -5438,5 +5626,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] diff --git a/Cargo.toml b/Cargo.toml index f65cae2..6d49327 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ libpulse-binding = { version = "2.27.1", optional = true } libpulse-simple-binding = { version = "2.27.1", optional = true } urlencoding = "2.1" ureq = "2.9" +uuid = { version = "1.23.0", default-features = false, features = ["v4"] } [features] default = [] diff --git a/docs/DATABASE_ISSUES.md b/docs/DATABASE_ISSUES.md index ef949d9..636a25a 100644 --- a/docs/DATABASE_ISSUES.md +++ b/docs/DATABASE_ISSUES.md @@ -43,4 +43,4 @@ Currently, a music file's uniqueness is determined solely by its absolute file p ## 5. Destructive Schema Migrations - **The Issue**: The initialization logic compares `current_version` with `SCHEMA_VERSION`. If they don't match, it executes `drop_tables_if_exist()`. - **Impact**: Any future update that increments the schema version will permanently and silently delete all user playlists, play records, and library data during application startup. -- **Solution**: Implement a proper migration system (using `ALTER TABLE` statements or via a migration crate like `refinery` or `rusqlite_migration`) to evolve the database schema safely without data loss. +- **Status (Pre-MVP)**: While a non-destructive migration system (e.g. `ALTER TABLE` or migration crates) is normally required, we are intentionally bypassing forward compatibility during our rapid iteration phase prior to reaching an MVP. Destructive migrations are currently acceptable. diff --git a/src/app/components/player_component.rs b/src/app/components/player_component.rs index f4c2dad..87b5d90 100644 --- a/src/app/components/player_component.rs +++ b/src/app/components/player_component.rs @@ -337,7 +337,7 @@ impl AppComponent for PlayerComponent { ctx.playlists.get_mut(playlist_idx) { if let Some(track_position) = - playlist.get_pos_by_key(removed_key) + playlist.get_pos_by_key(&removed_key) { playlist.remove(track_position); } diff --git a/src/app/components/playlist_table/actions.rs b/src/app/components/playlist_table/actions.rs index 9b0e982..c8194e9 100644 --- a/src/app/components/playlist_table/actions.rs +++ b/src/app/components/playlist_table/actions.rs @@ -1,6 +1,6 @@ #[derive(Default)] pub(crate) struct PendingActions { - pub clear_lyrics: Vec, + pub clear_lyrics: Vec, pub toggle_selection: Option, pub metadata_updates: Vec<(usize, String, String)>, pub play_track: Option, @@ -8,7 +8,7 @@ pub(crate) struct PendingActions { } impl PendingActions { - pub(crate) fn clear_lyrics(&mut self, key: usize) { + pub(crate) fn clear_lyrics(&mut self, key: String) { self.clear_lyrics.push(key); } diff --git a/src/app/components/playlist_table/columns.rs b/src/app/components/playlist_table/columns.rs index ffa780d..82a514b 100644 --- a/src/app/components/playlist_table/columns.rs +++ b/src/app/components/playlist_table/columns.rs @@ -268,7 +268,7 @@ pub(crate) fn render_lyrics_column( row_id: egui::Id, column_width: f32, has_lyrics: bool, - track_key: usize, + track_key: String, track_path: &Path, localization: &PlaylistLocalization, actions: &mut PendingActions, diff --git a/src/app/components/playlist_table/services.rs b/src/app/components/playlist_table/services.rs index adea6c6..5caef21 100644 --- a/src/app/components/playlist_table/services.rs +++ b/src/app/components/playlist_table/services.rs @@ -15,7 +15,7 @@ impl<'a> PlaylistTableService<'a> { } } - pub(crate) fn clear_lyrics(&mut self, track_key: usize) { + pub(crate) fn clear_lyrics(&mut self, track_key: String) { self.ctx.update_track_lyrics(track_key, None); } diff --git a/src/app/core.rs b/src/app/core.rs index 3a40449..0823460 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -214,7 +214,7 @@ impl App { let db_conn = self.db().connection(); PersistenceService::save_state( &self.app_settings, - &self.library, + &mut self.library, &mut self.playlists, &db_conn, ); @@ -270,7 +270,7 @@ impl App { LibraryImportService::import_library_path(lib_path, lib_cmd_tx, album_art_dir); } - pub fn update_track_lyrics(&mut self, track_key: usize, lyrics: Option<&str>) { + pub fn update_track_lyrics(&mut self, track_key: String, lyrics: Option<&str>) { let db_conn = self.db().connection(); let player = self.runtime.as_mut().map(|rt| &mut rt.player); diff --git a/src/app/db.rs b/src/app/db.rs index b68d31c..063fc69 100644 --- a/src/app/db.rs +++ b/src/app/db.rs @@ -8,7 +8,7 @@ pub struct Database { impl Database { // The current schema version - increment this when making schema changes - const SCHEMA_VERSION: i32 = 2; + const SCHEMA_VERSION: i32 = 3; pub fn new() -> Result { // Get the app's configuration directory @@ -89,6 +89,7 @@ impl Database { key TEXT PRIMARY KEY, library_path_id INTEGER NOT NULL, path TEXT NOT NULL, + file_hash TEXT NOT NULL DEFAULT '', title TEXT, artist TEXT, album TEXT, @@ -115,6 +116,12 @@ impl Database { [], )?; + // Create index on pictures table to prevent N+1 full table scans + connection.execute( + "CREATE INDEX IF NOT EXISTS idx_pictures_lib_id ON pictures(library_item_id)", + [], + )?; + // Create the playlists table connection.execute( "CREATE TABLE IF NOT EXISTS playlists ( diff --git a/src/app/services/db_persistence.rs b/src/app/services/db_persistence.rs index ad2c01c..c6ab52b 100644 --- a/src/app/services/db_persistence.rs +++ b/src/app/services/db_persistence.rs @@ -7,7 +7,7 @@ pub struct DBPersistence; impl DBPersistence { /// Save library to database pub fn save_library( - library: &crate::app::library::Library, + library: &mut crate::app::library::Library, db_conn: &std::sync::Arc>, ) -> Result<(), Box> { library.save_to_db(db_conn)?; diff --git a/src/app/services/library_service.rs b/src/app/services/library_service.rs index bba788d..a9d27c1 100644 --- a/src/app/services/library_service.rs +++ b/src/app/services/library_service.rs @@ -54,7 +54,7 @@ impl LibraryService { /// Process a library command and update the library accordingly pub fn process_library_command(library: &mut Library, lib_cmd: LibraryCommand) { match lib_cmd { - LibraryCommand::AddItem(lib_item) => library.add_item(lib_item), + LibraryCommand::AddItem(lib_item) => library.add_item(*lib_item), LibraryCommand::AddView(lib_view) => library.add_view(lib_view), LibraryCommand::AddPathId(path_id) => library.set_path_to_imported(path_id), } diff --git a/src/app/services/lyrics_service.rs b/src/app/services/lyrics_service.rs index d725204..37d8be2 100644 --- a/src/app/services/lyrics_service.rs +++ b/src/app/services/lyrics_service.rs @@ -40,14 +40,14 @@ impl LyricsService { /// Update lyrics for a track and persist to database pub fn update_track_lyrics( &mut self, - track_key: usize, + track_key: String, lyrics: Option<&str>, library: &mut Library, playlists: &mut [crate::app::Playlist], player: Option<&mut Player>, db_conn: &Arc>, ) { - let lyrics_owned = library.update_item_lyrics(track_key, lyrics); + let lyrics_owned = library.update_item_lyrics(track_key.clone(), lyrics); // Update lyrics in all playlists for playlist in playlists.iter_mut() { @@ -83,7 +83,7 @@ impl LyricsService { match db_conn.lock() { Ok(conn_guard) => conn_guard.execute( "UPDATE library_items SET lyrics = ?1 WHERE key = ?2", - rusqlite::params![lyrics_param, track_key.to_string()], + rusqlite::params![lyrics_param, track_key], ), Err(e) => { tracing::error!( diff --git a/src/app/services/persistence_service.rs b/src/app/services/persistence_service.rs index 63412bb..fcb32a0 100644 --- a/src/app/services/persistence_service.rs +++ b/src/app/services/persistence_service.rs @@ -120,7 +120,7 @@ impl PersistenceService { /// Save all application state pub fn save_state( config: &AppSettings, - library: &Library, + library: &mut Library, playlists: &mut [Playlist], db_conn: &Arc>, ) { diff --git a/src/app/services/player_service.rs b/src/app/services/player_service.rs index d10564f..393c9e7 100644 --- a/src/app/services/player_service.rs +++ b/src/app/services/player_service.rs @@ -59,7 +59,7 @@ impl PlayerService { /// Remove the currently selected track from the playlist and handle playback continuation /// Returns the track key that was removed, or None if no track was removed - pub fn remove_current_track(player: &mut Player) -> Option { + pub fn remove_current_track(player: &mut Player) -> Option { if let Some(track) = &player.selected_track { let track_key = track.key(); // Clear the selected track - the playlist removal will be handled by the caller diff --git a/src/lib/library.rs b/src/lib/library.rs index 8749b22..cb0c70c 100644 --- a/src/lib/library.rs +++ b/src/lib/library.rs @@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex}; pub enum LibraryCommand { AddView(LibraryView), - AddItem(LibraryItem), + AddItem(Box), AddPathId(LibraryPathId), } @@ -110,11 +110,11 @@ impl Library { } pub fn add_item(&mut self, library_item: LibraryItem) { - // Check if an item with this path already exists + // Check if an item with this file_hash already exists if let Some(idx) = self .items .iter() - .position(|item| item.path() == library_item.path()) + .position(|item| item.file_hash() == library_item.file_hash()) { // Update the existing item but preserve its key let existing_key = self.items[idx].key(); @@ -133,7 +133,7 @@ impl Library { self.library_view.containers.append(&mut new); } - pub fn update_item_lyrics(&mut self, key: usize, lyrics: Option<&str>) -> Option { + pub fn update_item_lyrics(&mut self, key: String, lyrics: Option<&str>) -> Option { let lyrics_owned = lyrics.map(|text| text.to_string()); for item in self.items.iter_mut() { @@ -155,7 +155,7 @@ impl Library { // Database methods - pub fn save_to_db(&self, conn: &Arc>) -> SqlResult<()> { + pub fn save_to_db(&mut self, conn: &Arc>) -> SqlResult<()> { let mut conn_guard = conn.lock().unwrap(); // Start a transaction @@ -181,15 +181,20 @@ impl Library { } // Save all library items - for item in &self.items { + for item in &mut self.items { + if !item.is_dirty() { + continue; + } + tx.execute( "INSERT OR REPLACE INTO library_items - (key, library_path_id, path, title, artist, album, year, genre, track_number, lyrics) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + (key, library_path_id, path, file_hash, title, artist, album, year, genre, track_number, lyrics) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", rusqlite::params![ item.key().to_string(), item.library_id().0 as i64, item.path().to_string_lossy().to_string(), + item.file_hash(), item.title(), item.artist(), item.album(), @@ -222,6 +227,8 @@ impl Library { ], )?; } + + item.reset_dirty(); } // Commit the transaction @@ -268,7 +275,7 @@ impl Library { // Load library items let mut item_stmt = conn_guard.prepare( - "SELECT key, library_path_id, path, title, artist, album, year, genre, track_number, lyrics + "SELECT key, library_path_id, path, file_hash, title, artist, album, year, genre, track_number, lyrics FROM library_items" )?; @@ -276,26 +283,22 @@ impl Library { let key_str: String = row.get(0)?; let library_id_raw: i64 = row.get(1)?; let path_str: String = row.get(2)?; + let file_hash: String = row.get(3)?; let library_id = LibraryPathId::new(library_id_raw as usize); let path = PathBuf::from(path_str); - // Create a new library item - let mut item = LibraryItem::new(path, library_id); + // Create a new library item from database records directly without filesystem I/O + let mut item = LibraryItem::from_db(key_str, library_id, path, file_hash); // Set all metadata - item.set_title(row.get::<_, Option>(3)?.as_deref()); - item.set_artist(row.get::<_, Option>(4)?.as_deref()); - item.set_album(row.get::<_, Option>(5)?.as_deref()); - item.set_year(row.get::<_, Option>(6)?); - item.set_genre(row.get::<_, Option>(7)?.as_deref()); - item.set_track_number(row.get::<_, Option>(8)?); - item.set_lyrics(row.get::<_, Option>(9)?.as_deref()); - - // Force the key to match the database - if let Ok(key_val) = key_str.parse::() { - item.set_key(key_val); - } + item.set_title(row.get::<_, Option>(4)?.as_deref()); + item.set_artist(row.get::<_, Option>(5)?.as_deref()); + item.set_album(row.get::<_, Option>(6)?.as_deref()); + item.set_year(row.get::<_, Option>(7)?); + item.set_genre(row.get::<_, Option>(8)?.as_deref()); + item.set_track_number(row.get::<_, Option>(9)?); + item.set_lyrics(row.get::<_, Option>(10)?.as_deref()); Ok(item) })?; @@ -305,32 +308,43 @@ impl Library { items.push(item_result?); } - // Load pictures for each item - for item in &mut items { - let item_key = item.key() as i64; - - let mut pic_stmt = conn_guard.prepare( - "SELECT mime_type, picture_type, description, file_path - FROM pictures WHERE library_item_id = ?", - )?; + // Load all pictures at once to prevent N+1 queries + let mut pictures_map: std::collections::HashMap> = + std::collections::HashMap::new(); + let mut pic_stmt = conn_guard.prepare( + "SELECT library_item_id, mime_type, picture_type, description, file_path FROM pictures", + )?; - let picture_rows = pic_stmt.query_map(rusqlite::params![item_key], |row| { - let mime_type: String = row.get(0)?; - let picture_type: u8 = row.get(1)?; - let description: String = row.get(2)?; - let file_path: String = row.get(3)?; + let picture_rows = pic_stmt.query_map([], |row| { + let item_id: String = row.get(0)?; + let mime_type: String = row.get(1)?; + let picture_type: u8 = row.get(2)?; + let description: String = row.get(3)?; + let file_path: String = row.get(4)?; - Ok(Picture::new( + Ok(( + item_id, + Picture::new( mime_type, picture_type, description, PathBuf::from(file_path), - )) - })?; + ), + )) + })?; - for picture_result in picture_rows { - item.add_picture(picture_result?); + for picture_result in picture_rows { + let (item_id, pic) = picture_result?; + pictures_map.entry(item_id).or_default().push(pic); + } + + for item in &mut items { + if let Some(pics) = pictures_map.remove(&item.key()) { + for pic in pics { + item.add_picture(pic); + } } + item.reset_dirty(); } // Add items to the library @@ -444,14 +458,30 @@ pub struct LibraryItem { year: Option, genre: Option, track_number: Option, - key: usize, + key: String, pictures: Vec, lyrics: Option, + #[serde(skip)] + is_dirty: bool, + file_hash: String, } impl LibraryItem { pub fn new(path: PathBuf, library_id: LibraryPathId) -> Self { - use rand::Rng; // TODO - use ULID? + let file_hash = std::fs::metadata(&path) + .map(|m| { + format!( + "{}_{}", + m.len(), + m.modified() + .unwrap_or(std::time::SystemTime::UNIX_EPOCH) + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + ) + }) + .unwrap_or_else(|_| format!("unknown_{}", path.to_string_lossy())); + Self { library_id, path, @@ -461,9 +491,34 @@ impl LibraryItem { year: None, genre: None, track_number: None, - key: rand::thread_rng().gen(), + key: uuid::Uuid::new_v4().to_string(), pictures: Vec::new(), lyrics: None, + is_dirty: true, + file_hash, + } + } + + pub fn from_db( + key: String, + library_id: LibraryPathId, + path: PathBuf, + file_hash: String, + ) -> Self { + Self { + library_id, + path, + title: None, + artist: None, + album: None, + year: None, + genre: None, + track_number: None, + key, + pictures: vec![], + lyrics: None, + is_dirty: false, + file_hash, } } @@ -479,11 +534,15 @@ impl LibraryItem { &self.path } - pub fn key(&self) -> usize { - self.key + pub fn key(&self) -> String { + self.key.clone() + } + + pub fn key_str(&self) -> &str { + &self.key } - pub fn set_key(&mut self, key: usize) { + pub fn set_key(&mut self, key: String) { self.key = key; } @@ -491,7 +550,7 @@ impl LibraryItem { if let Some(title) = title { self.title = Some(title.to_string()); } - + self.is_dirty = true; self.to_owned() } @@ -507,6 +566,7 @@ impl LibraryItem { if let Some(artist) = artist { self.artist = Some(artist.to_string()); } + self.is_dirty = true; self.to_owned() } @@ -522,6 +582,7 @@ impl LibraryItem { if let Some(album) = album { self.album = Some(album.to_string()); } + self.is_dirty = true; self.to_owned() } @@ -535,6 +596,7 @@ impl LibraryItem { pub fn set_year(&mut self, year: Option) -> Self { self.year = year; + self.is_dirty = true; self.to_owned() } @@ -546,6 +608,7 @@ impl LibraryItem { if let Some(genre) = genre { self.genre = Some(genre.to_string()); } + self.is_dirty = true; self.to_owned() } @@ -559,6 +622,7 @@ impl LibraryItem { pub fn set_track_number(&mut self, track_number: Option) -> Self { self.track_number = track_number; + self.is_dirty = true; self.to_owned() } @@ -572,10 +636,12 @@ impl LibraryItem { pub fn add_picture(&mut self, picture: Picture) { self.pictures.push(picture); + self.is_dirty = true; } pub fn clear_pictures(&mut self) { self.pictures.clear(); + self.is_dirty = true; } pub fn set_lyrics(&mut self, lyrics: Option<&str>) -> Self { @@ -587,6 +653,7 @@ impl LibraryItem { Some(trimmed.to_owned()) } }); + self.is_dirty = true; self.to_owned() } @@ -601,6 +668,7 @@ impl LibraryItem { Some(trimmed.to_owned()) } }); + self.is_dirty = true; } pub fn lyrics(&self) -> Option { @@ -610,6 +678,22 @@ impl LibraryItem { pub fn has_lyrics(&self) -> bool { self.lyrics.is_some() } + + pub fn is_dirty(&self) -> bool { + self.is_dirty + } + + pub fn reset_dirty(&mut self) { + self.is_dirty = false; + } + + pub fn file_hash(&self) -> &str { + &self.file_hash + } + + pub fn set_file_hash(&mut self, file_hash: String) { + self.file_hash = file_hash; + } } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] diff --git a/src/lib/playlist.rs b/src/lib/playlist.rs index 65787ea..d1f7869 100644 --- a/src/lib/playlist.rs +++ b/src/lib/playlist.rs @@ -14,6 +14,8 @@ pub struct Playlist { pub selected: Option, #[serde(skip_serializing, skip_deserializing)] pub selected_indices: HashSet, + #[serde(skip)] + pub is_dirty: bool, } impl Default for Playlist { @@ -30,11 +32,13 @@ impl Playlist { tracks: vec![], selected: None, selected_indices: HashSet::new(), + is_dirty: true, } } pub fn set_name(&mut self, name: String) { self.name = Some(name); + self.is_dirty = true; } pub fn get_name(&self) -> Option { @@ -43,12 +47,14 @@ impl Playlist { pub fn add(&mut self, track: LibraryItem) { self.tracks.push(track); + self.is_dirty = true; } // TODO - should probably return a Result pub fn remove(&mut self, idx: usize) { self.tracks.remove(idx); self.selected_indices.remove(&idx); + self.is_dirty = true; // Update indices greater than the removed index let mut to_remove = Vec::new(); @@ -74,6 +80,7 @@ impl Playlist { pub fn reorder(&mut self, current_pos: usize, destination_pos: usize) { let track = self.tracks.remove(current_pos); self.tracks.insert(destination_pos, track); + self.is_dirty = true; // Update selected indices after reordering let mut new_selected = HashSet::new(); @@ -108,11 +115,11 @@ impl Playlist { } pub fn get_pos(&self, track: &LibraryItem) -> Option { - self.get_pos_by_key(track.key()) + self.get_pos_by_key(track.key_str()) } - pub fn get_pos_by_key(&self, key: usize) -> Option { - self.tracks.iter().position(|t| t.key() == key) + pub fn get_pos_by_key(&self, key: &str) -> Option { + self.tracks.iter().position(|t| t.key_str() == key) } pub fn select_all(&mut self) { @@ -140,7 +147,11 @@ impl Playlist { // Database methods - pub fn save_to_db(&self, conn: &Arc>) -> SqlResult<()> { + pub fn save_to_db(&mut self, conn: &Arc>) -> SqlResult<()> { + if !self.is_dirty { + return Ok(()); + } + let mut conn = conn.lock().unwrap(); // Start a transaction @@ -183,11 +194,16 @@ impl Playlist { // Commit the transaction tx.commit()?; + self.is_dirty = false; Ok(()) } pub fn save_to_db_and_update_id(&mut self, conn: &Arc>) -> SqlResult<()> { + if !self.is_dirty { + return Ok(()); + } + let mut conn = conn.lock().unwrap(); // Start a transaction @@ -235,6 +251,7 @@ impl Playlist { // Commit the transaction tx.commit()?; + self.is_dirty = false; Ok(()) } @@ -258,6 +275,7 @@ impl Playlist { tracks: vec![], selected: None, selected_indices: HashSet::new(), + is_dirty: false, }; // Get the tracks @@ -289,9 +307,7 @@ impl Playlist { item.set_lyrics(row.get::<_, Option>(9)?.as_deref()); // Set the key from the database - if let Ok(key_val) = key_str.parse::() { - item.set_key(key_val); - } + item.set_key(key_str.clone()); // Load album art (pictures) from the database let mut pic_stmt = conn_guard.prepare( @@ -431,6 +447,7 @@ mod tests { ], selected: None, selected_indices: HashSet::new(), + is_dirty: false, }; assert_eq!(playlist.tracks.len(), 3); @@ -458,6 +475,7 @@ mod tests { ], selected: None, selected_indices: HashSet::new(), + is_dirty: false, }; assert_eq!(playlist.tracks.len(), 3); diff --git a/src/lib/services/library_import.rs b/src/lib/services/library_import.rs index 3416191..d7f8672 100644 --- a/src/lib/services/library_import.rs +++ b/src/lib/services/library_import.rs @@ -92,7 +92,7 @@ impl LibraryImportService { // Send items as they're processed for item in &items { lib_cmd_tx - .send(LibraryCommand::AddItem(item.clone())) + .send(LibraryCommand::AddItem(Box::new(item.clone()))) .map_err(|e| format!("Failed to send library item: {}", e))?; }