From b239ce365786019a3ab0da055e1d7616e9f42fd0 Mon Sep 17 00:00:00 2001 From: RetricSu Date: Tue, 14 Apr 2026 07:13:23 +0800 Subject: [PATCH 1/4] Refactor database architecture: dirty tracking, UUIDs, ghost files --- Cargo.lock | 312 ++++++++++++++---- Cargo.toml | 1 + docs/DATABASE_ISSUES.md | 5 - src/app/components/player_component.rs | 2 +- src/app/components/playlist_table/actions.rs | 4 +- src/app/components/playlist_table/columns.rs | 2 +- src/app/components/playlist_table/services.rs | 2 +- src/app/core.rs | 4 +- src/app/db.rs | 9 +- src/app/services/db_persistence.rs | 2 +- src/app/services/lyrics_service.rs | 6 +- src/app/services/persistence_service.rs | 2 +- src/app/services/player_service.rs | 2 +- src/lib/library.rs | 152 ++++++--- src/lib/playlist.rs | 30 +- 15 files changed, 399 insertions(+), 136 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa1e075..f3023be 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,17 @@ 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", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.0" @@ -4409,48 +4471,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 +4523,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 +4688,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 +4926,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4835,7 +4937,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -5180,6 +5282,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 +5516,7 @@ checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "zbus-lockstep", "zbus_xml", "zvariant", @@ -5341,7 +5531,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "zvariant_utils", ] @@ -5386,7 +5576,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -5426,7 +5616,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "zvariant_utils", ] @@ -5438,5 +5628,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..d944a8e 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", features = ["v4"] } [features] default = [] diff --git a/docs/DATABASE_ISSUES.md b/docs/DATABASE_ISSUES.md index ef949d9..47ae049 100644 --- a/docs/DATABASE_ISSUES.md +++ b/docs/DATABASE_ISSUES.md @@ -39,8 +39,3 @@ Currently, a music file's uniqueness is determined solely by its absolute file p ``` - **Impact**: Relying on SQLite's implicit type conversion (from `TEXT` to `i64`) can cause unexpected query failures, index misses, and potential panics if a generated `usize` exceeds `i64::MAX`. - **Solution**: Use standardized unique identifiers (such as ULIDs or UUIDs), store them strictly as `TEXT`, and query them as strings. - -## 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. 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..b049531 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, 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/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..19e874c 100644 --- a/src/lib/library.rs +++ b/src/lib/library.rs @@ -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,25 @@ 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); + item.set_file_hash(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()); + + item.set_key(key_str); Ok(item) })?; @@ -305,32 +311,35 @@ impl Library { items.push(item_result?); } - // Load pictures for each item - for item in &mut items { - let item_key = item.key() as i64; + // 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 mut pic_stmt = conn_guard.prepare( - "SELECT mime_type, picture_type, description, file_path - FROM pictures WHERE library_item_id = ?", - )?; + 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((item_id, Picture::new(mime_type, picture_type, description, PathBuf::from(file_path)))) + })?; + + for pic_res in picture_rows { + if let Ok((item_id, pic)) = pic_res { + pictures_map.entry(item_id).or_default().push(pic); + } + } - 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)?; - - Ok(Picture::new( - mime_type, - picture_type, - description, - PathBuf::from(file_path), - )) - })?; - - for picture_result in picture_rows { - item.add_picture(picture_result?); + 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 +453,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_{}", uuid::Uuid::new_v4())); + Self { library_id, path, @@ -461,9 +486,11 @@ 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, } } @@ -479,11 +506,11 @@ impl LibraryItem { &self.path } - pub fn key(&self) -> usize { - self.key + pub fn key(&self) -> String { + self.key.clone() } - pub fn set_key(&mut self, key: usize) { + pub fn set_key(&mut self, key: String) { self.key = key; } @@ -491,7 +518,7 @@ impl LibraryItem { if let Some(title) = title { self.title = Some(title.to_string()); } - + self.is_dirty = true; self.to_owned() } @@ -507,6 +534,7 @@ impl LibraryItem { if let Some(artist) = artist { self.artist = Some(artist.to_string()); } + self.is_dirty = true; self.to_owned() } @@ -522,6 +550,7 @@ impl LibraryItem { if let Some(album) = album { self.album = Some(album.to_string()); } + self.is_dirty = true; self.to_owned() } @@ -535,6 +564,7 @@ impl LibraryItem { pub fn set_year(&mut self, year: Option) -> Self { self.year = year; + self.is_dirty = true; self.to_owned() } @@ -546,6 +576,7 @@ impl LibraryItem { if let Some(genre) = genre { self.genre = Some(genre.to_string()); } + self.is_dirty = true; self.to_owned() } @@ -559,6 +590,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 +604,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 +621,7 @@ impl LibraryItem { Some(trimmed.to_owned()) } }); + self.is_dirty = true; self.to_owned() } @@ -601,6 +636,7 @@ impl LibraryItem { Some(trimmed.to_owned()) } }); + self.is_dirty = true; } pub fn lyrics(&self) -> Option { @@ -610,6 +646,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..a953e73 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,10 +115,10 @@ impl Playlist { } pub fn get_pos(&self, track: &LibraryItem) -> Option { - self.get_pos_by_key(track.key()) + self.get_pos_by_key(&track.key()) } - pub fn get_pos_by_key(&self, key: usize) -> Option { + pub fn get_pos_by_key(&self, key: &str) -> Option { self.tracks.iter().position(|t| t.key() == key) } @@ -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); From bfdfa23bb4521d8fa5409340f5059852ef1175a6 Mon Sep 17 00:00:00 2001 From: RetricSu Date: Tue, 14 Apr 2026 07:22:33 +0800 Subject: [PATCH 2/4] Fix logic errors blocking CI pipeline --- src/app/services/library_service.rs | 2 +- src/lib/library.rs | 23 +++++++++++++++-------- src/lib/services/library_import.rs | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) 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/lib/library.rs b/src/lib/library.rs index 19e874c..e76c935 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), } @@ -312,9 +312,10 @@ impl Library { } // Load all pictures at once to prevent N+1 queries - let mut pictures_map: std::collections::HashMap> = std::collections::HashMap::new(); + 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" + "SELECT library_item_id, mime_type, picture_type, description, file_path FROM pictures", )?; let picture_rows = pic_stmt.query_map([], |row| { @@ -324,13 +325,19 @@ impl Library { let description: String = row.get(3)?; let file_path: String = row.get(4)?; - Ok((item_id, Picture::new(mime_type, picture_type, description, PathBuf::from(file_path)))) + Ok(( + item_id, + Picture::new( + mime_type, + picture_type, + description, + PathBuf::from(file_path), + ), + )) })?; - for pic_res in picture_rows { - if let Ok((item_id, pic)) = pic_res { - pictures_map.entry(item_id).or_default().push(pic); - } + for (item_id, pic) in picture_rows.flatten() { + pictures_map.entry(item_id).or_default().push(pic); } for item in &mut items { 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))?; } From aec9c7537a3987fba237ea43fcaa443d42a81b25 Mon Sep 17 00:00:00 2001 From: RetricSu Date: Tue, 14 Apr 2026 07:55:07 +0800 Subject: [PATCH 3/4] Restore destructive migrations docs note during pre-MVP --- docs/DATABASE_ISSUES.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/DATABASE_ISSUES.md b/docs/DATABASE_ISSUES.md index 47ae049..636a25a 100644 --- a/docs/DATABASE_ISSUES.md +++ b/docs/DATABASE_ISSUES.md @@ -39,3 +39,8 @@ Currently, a music file's uniqueness is determined solely by its absolute file p ``` - **Impact**: Relying on SQLite's implicit type conversion (from `TEXT` to `i64`) can cause unexpected query failures, index misses, and potential panics if a generated `usize` exceeds `i64::MAX`. - **Solution**: Use standardized unique identifiers (such as ULIDs or UUIDs), store them strictly as `TEXT`, and query them as strings. + +## 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. +- **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. From a6888f6c320015b76bcee904b8f92fa4cf533850 Mon Sep 17 00:00:00 2001 From: RetricSu Date: Tue, 14 Apr 2026 08:00:12 +0800 Subject: [PATCH 4/4] Address Copilot and Gemini review comments --- Cargo.lock | 2 -- Cargo.toml | 2 +- src/app/db.rs | 2 +- src/lib/library.rs | 39 ++++++++++++++++++++++++++++++++------- src/lib/playlist.rs | 4 ++-- 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3023be..ba6c830 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4426,8 +4426,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d944a8e..6d49327 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +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", features = ["v4"] } +uuid = { version = "1.23.0", default-features = false, features = ["v4"] } [features] default = [] diff --git a/src/app/db.rs b/src/app/db.rs index b049531..063fc69 100644 --- a/src/app/db.rs +++ b/src/app/db.rs @@ -89,7 +89,7 @@ impl Database { key TEXT PRIMARY KEY, library_path_id INTEGER NOT NULL, path TEXT NOT NULL, - file_hash TEXT, + file_hash TEXT NOT NULL DEFAULT '', title TEXT, artist TEXT, album TEXT, diff --git a/src/lib/library.rs b/src/lib/library.rs index e76c935..cb0c70c 100644 --- a/src/lib/library.rs +++ b/src/lib/library.rs @@ -288,9 +288,8 @@ impl Library { 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); - item.set_file_hash(file_hash); + // 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>(4)?.as_deref()); @@ -301,8 +300,6 @@ impl Library { item.set_track_number(row.get::<_, Option>(9)?); item.set_lyrics(row.get::<_, Option>(10)?.as_deref()); - item.set_key(key_str); - Ok(item) })?; @@ -336,7 +333,8 @@ impl Library { )) })?; - for (item_id, pic) in picture_rows.flatten() { + for picture_result in picture_rows { + let (item_id, pic) = picture_result?; pictures_map.entry(item_id).or_default().push(pic); } @@ -482,7 +480,7 @@ impl LibraryItem { .as_secs() ) }) - .unwrap_or_else(|_| format!("unknown_{}", uuid::Uuid::new_v4())); + .unwrap_or_else(|_| format!("unknown_{}", path.to_string_lossy())); Self { library_id, @@ -501,6 +499,29 @@ impl LibraryItem { } } + 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, + } + } + pub fn library_id(&self) -> LibraryPathId { self.library_id } @@ -517,6 +538,10 @@ impl LibraryItem { self.key.clone() } + pub fn key_str(&self) -> &str { + &self.key + } + pub fn set_key(&mut self, key: String) { self.key = key; } diff --git a/src/lib/playlist.rs b/src/lib/playlist.rs index a953e73..d1f7869 100644 --- a/src/lib/playlist.rs +++ b/src/lib/playlist.rs @@ -115,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: &str) -> Option { - self.tracks.iter().position(|t| t.key() == key) + self.tracks.iter().position(|t| t.key_str() == key) } pub fn select_all(&mut self) {