diff --git a/Cargo.lock b/Cargo.lock index 8148afb..3e9886b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.0.2" @@ -11,6 +22,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -124,53 +141,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] -name = "deranged" -version = "0.3.8" +name = "fallible-iterator" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] -name = "diesel" -version = "2.1.2" +name = "fallible-streaming-iterator" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c8a2cb22327206568569e5a45bb5a2c946455efdd76e24d15b7e82171af95e" -dependencies = [ - "diesel_derives", - "libsqlite3-sys", - "time", -] - -[[package]] -name = "diesel_derives" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8337737574f55a468005a83499da720f20c65586241ffea339db9ecdfd2b44" -dependencies = [ - "diesel_table_macro_syntax", - "proc-macro2", - "quote", - "syn 2.0.37", -] - -[[package]] -name = "diesel_migrations" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" -dependencies = [ - "diesel", - "migrations_internals", - "migrations_macros", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" -dependencies = [ - "syn 2.0.37", -] +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "field-offset" @@ -538,6 +518,25 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.1", +] + [[package]] name = "heck" version = "0.4.1" @@ -574,15 +573,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", ] -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - [[package]] name = "js-sys" version = "0.3.64" @@ -642,6 +635,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -689,40 +683,18 @@ dependencies = [ "autocfg", ] -[[package]] -name = "migrations_internals" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" -dependencies = [ - "serde", - "toml", -] - -[[package]] -name = "migrations_macros" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" -dependencies = [ - "migrations_internals", - "proc-macro2", - "quote", -] - [[package]] name = "musicus" version = "0.1.0" dependencies = [ "chrono", - "diesel", - "diesel_migrations", "gettext-rs", "gtk4", "libadwaita", "log", "once_cell", "rand", + "rusqlite", "thiserror", "tracing-subscriber", "uuid", @@ -936,6 +908,20 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +[[package]] +name = "rusqlite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -1081,34 +1067,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "time" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" -dependencies = [ - "deranged", - "itoa", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" -dependencies = [ - "time-core", -] - [[package]] name = "toml" version = "0.7.4" diff --git a/Cargo.toml b/Cargo.toml index 49943f1..f942531 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,15 +4,14 @@ version = "0.1.0" edition = "2021" [dependencies] -adw = { package = "libadwaita", version = "0.5", features = ["v1_4"]} +adw = { package = "libadwaita", version = "0.5", features = ["v1_4"] } chrono = "0.4" -diesel = { version = "2", features = ["sqlite"] } -diesel_migrations = "2" gettext-rs = { version = "0.7", features = ["gettext-system"] } -gtk = { package = "gtk4", version = "0.7", features = ["v4_10", "blueprint"]} +gtk = { package = "gtk4", version = "0.7", features = ["v4_10", "blueprint"] } log = "0.4" once_cell = "1" rand = "0.8" +rusqlite = { version = "0.29", features = ["bundled"] } thiserror = "1" tracing-subscriber = "0.3" uuid = { version = "1", features = ["v4"] } \ No newline at end of file diff --git a/src/db/ensembles.rs b/src/db/ensembles.rs deleted file mode 100644 index 47493d9..0000000 --- a/src/db/ensembles.rs +++ /dev/null @@ -1,74 +0,0 @@ -use chrono::Utc; -use diesel::prelude::*; -use log::info; - -use crate::db::{defer_foreign_keys, schema::ensembles, Result}; - -/// An ensemble that takes part in recordings. -#[derive(Insertable, Queryable, PartialEq, Eq, Hash, Debug, Clone)] -pub struct Ensemble { - pub id: String, - pub name: String, - pub last_used: Option, - pub last_played: Option, -} - -impl Ensemble { - pub fn new(id: String, name: String) -> Self { - Self { - id, - name, - last_used: Some(Utc::now().timestamp()), - last_played: None, - } - } -} - -/// Update an existing ensemble or insert a new one. -pub fn update_ensemble(connection: &mut SqliteConnection, mut ensemble: Ensemble) -> Result<()> { - info!("Updating ensemble {:?}", ensemble); - defer_foreign_keys(connection)?; - - ensemble.last_used = Some(Utc::now().timestamp()); - - connection.transaction(|connection| { - diesel::replace_into(ensembles::table) - .values(ensemble) - .execute(connection) - })?; - - Ok(()) -} - -/// Get an existing ensemble. -pub fn get_ensemble(connection: &mut SqliteConnection, id: &str) -> Result> { - let ensemble = ensembles::table - .filter(ensembles::id.eq(id)) - .load::(connection)? - .into_iter() - .next(); - - Ok(ensemble) -} - -/// Delete an existing ensemble. -pub fn delete_ensemble(connection: &mut SqliteConnection, id: &str) -> Result<()> { - info!("Deleting ensemble {}", id); - diesel::delete(ensembles::table.filter(ensembles::id.eq(id))).execute(connection)?; - Ok(()) -} - -/// Get all existing ensembles. -pub fn get_ensembles(connection: &mut SqliteConnection) -> Result> { - let ensembles = ensembles::table.load::(connection)?; - Ok(ensembles) -} - -/// Get recently used ensembles. -pub fn get_recent_ensembles(connection: &mut SqliteConnection) -> Result> { - let ensembles = ensembles::table - .order(ensembles::last_used.desc()) - .load::(connection)?; - - Ok(ensembles) -} diff --git a/src/db/error.rs b/src/db/error.rs deleted file mode 100644 index 2ad4e59..0000000 --- a/src/db/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -/// Error that happens within the database module. -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - Connection(#[from] diesel::result::ConnectionError), - - #[error(transparent)] - Migrations(#[from] Box), - - #[error(transparent)] - Query(#[from] diesel::result::Error), - - #[error("Missing item dependency ({0} {1})")] - MissingItem(&'static str, String), - - #[error("Failed to parse {0} from '{1}'")] - Parsing(&'static str, String), - - #[error("{0}")] - Other(&'static str), -} - -/// Return type for database methods. -pub type Result = std::result::Result; diff --git a/src/db/instruments.rs b/src/db/instruments.rs deleted file mode 100644 index 0f63569..0000000 --- a/src/db/instruments.rs +++ /dev/null @@ -1,79 +0,0 @@ -use chrono::Utc; -use diesel::prelude::*; -use log::info; - -use crate::db::{defer_foreign_keys, schema::instruments, Result}; - -/// An instrument or any other possible role within a recording. -#[derive(Insertable, Queryable, PartialEq, Eq, Hash, Debug, Clone)] -pub struct Instrument { - pub id: String, - pub name: String, - pub last_used: Option, - pub last_played: Option, -} - -impl Instrument { - pub fn new(id: String, name: String) -> Self { - Self { - id, - name, - last_used: Some(Utc::now().timestamp()), - last_played: None, - } - } -} - -/// Update an existing instrument or insert a new one. -pub fn update_instrument( - connection: &mut SqliteConnection, - mut instrument: Instrument, -) -> Result<()> { - info!("Updating instrument {:?}", instrument); - defer_foreign_keys(connection)?; - - instrument.last_used = Some(Utc::now().timestamp()); - - connection.transaction(|connection| { - diesel::replace_into(instruments::table) - .values(instrument) - .execute(connection) - })?; - - Ok(()) -} - -/// Get an existing instrument. -pub fn get_instrument(connection: &mut SqliteConnection, id: &str) -> Result> { - let instrument = instruments::table - .filter(instruments::id.eq(id)) - .load::(connection)? - .into_iter() - .next(); - - Ok(instrument) -} - -/// Delete an existing instrument. -pub fn delete_instrument(connection: &mut SqliteConnection, id: &str) -> Result<()> { - info!("Deleting instrument {}", id); - diesel::delete(instruments::table.filter(instruments::id.eq(id))).execute(connection)?; - - Ok(()) -} - -/// Get all existing instruments. -pub fn get_instruments(connection: &mut SqliteConnection) -> Result> { - let instruments = instruments::table.load::(connection)?; - - Ok(instruments) -} - -/// Get recently used instruments. -pub fn get_recent_instruments(connection: &mut SqliteConnection) -> Result> { - let instruments = instruments::table - .order(instruments::last_used.desc()) - .load::(connection)?; - - Ok(instruments) -} diff --git a/src/db/medium.rs b/src/db/medium.rs deleted file mode 100644 index 87f68f8..0000000 --- a/src/db/medium.rs +++ /dev/null @@ -1,351 +0,0 @@ -use chrono::{DateTime, TimeZone, Utc}; -use diesel::prelude::*; -use log::info; - -use crate::db::{ - defer_foreign_keys, generate_id, get_recording, - schema::{ensembles, mediums, performances, persons, recordings, tracks}, - update_recording, Error, Recording, Result, -}; - -/// Representation of someting like a physical audio disc or a folder with -/// audio files (i.e. a collection of tracks for one or more recordings). -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct Medium { - /// An unique ID for the medium. - pub id: String, - - /// The human identifier for the medium. - pub name: String, - - /// If applicable, the MusicBrainz DiscID. - pub discid: Option, - - /// The tracks of the medium. - pub tracks: Vec, - - pub last_used: Option>, - pub last_played: Option>, -} - -impl Medium { - pub fn new(id: String, name: String, discid: Option, tracks: Vec) -> Self { - Self { - id, - name, - discid, - tracks, - last_used: Some(Utc::now()), - last_played: None, - } - } -} - -/// A track on a medium. -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct Track { - /// The recording on this track. - pub recording: Recording, - - /// The work parts that are played on this track. They are indices to the - /// work parts of the work that is associated with the recording. - pub work_parts: Vec, - - /// The index of the track within its source. This is used to associate - /// the metadata with the audio data from the source when importing. - pub source_index: usize, - - /// The path to the audio file containing this track. - pub path: String, - - pub last_used: Option>, - pub last_played: Option>, -} - -impl Track { - pub fn new( - recording: Recording, - work_parts: Vec, - source_index: usize, - path: String, - ) -> Self { - Self { - recording, - work_parts, - source_index, - path, - last_used: Some(Utc::now()), - last_played: None, - } - } -} - -/// Table data for a [`Medium`]. -#[derive(Insertable, Queryable, Debug, Clone)] -#[diesel(table_name = mediums)] -struct MediumRow { - pub id: String, - pub name: String, - pub discid: Option, - pub last_used: Option, - pub last_played: Option, -} - -/// Table data for a [`Track`]. -#[derive(Insertable, Queryable, QueryableByName, Debug, Clone)] -#[diesel(table_name = tracks)] -struct TrackRow { - pub id: String, - pub medium: Option, - pub index: i32, - pub recording: String, - pub work_parts: String, - pub source_index: i32, - pub path: String, - pub last_used: Option, - pub last_played: Option, -} - -/// Update an existing medium or insert a new one. -pub fn update_medium(connection: &mut SqliteConnection, medium: Medium) -> Result<()> { - info!("Updating medium {:?}", medium); - defer_foreign_keys(connection)?; - - connection.transaction::<(), Error, _>(|connection| { - let medium_id = &medium.id; - - // This will also delete the tracks. - delete_medium(connection, medium_id)?; - - // Add the new medium. - - let medium_row = MediumRow { - id: medium_id.to_owned(), - name: medium.name.clone(), - discid: medium.discid.clone(), - last_used: Some(Utc::now().timestamp()), - last_played: medium.last_played.map(|t| t.timestamp()), - }; - - diesel::insert_into(mediums::table) - .values(medium_row) - .execute(connection)?; - - for (index, track) in medium.tracks.iter().enumerate() { - // Add associated items from the server, if they don't already exist. - - if get_recording(connection, &track.recording.id)?.is_none() { - update_recording(connection, track.recording.clone())?; - } - - // Add the actual track data. - - let work_parts = track - .work_parts - .iter() - .map(|part_index| part_index.to_string()) - .collect::>() - .join(","); - - let track_row = TrackRow { - id: generate_id(), - medium: Some(medium_id.to_owned()), - index: index as i32, - recording: track.recording.id.clone(), - work_parts, - source_index: track.source_index as i32, - path: track.path.clone(), - last_used: Some(Utc::now().timestamp()), - last_played: track.last_played.map(|t| t.timestamp()), - }; - - diesel::insert_into(tracks::table) - .values(track_row) - .execute(connection)?; - } - - Ok(()) - })?; - - Ok(()) -} - -/// Get an existing medium. -pub fn get_medium(connection: &mut SqliteConnection, id: &str) -> Result> { - let row = mediums::table - .filter(mediums::id.eq(id)) - .load::(connection)? - .into_iter() - .next(); - - let medium = match row { - Some(row) => Some(get_medium_data(connection, row)?), - None => None, - }; - - Ok(medium) -} - -/// Get mediums that have a specific source ID. -pub fn get_mediums_by_source_id( - connection: &mut SqliteConnection, - source_id: &str, -) -> Result> { - let mut mediums: Vec = Vec::new(); - - let rows = mediums::table - .filter(mediums::discid.nullable().eq(source_id)) - .load::(connection)?; - - for row in rows { - let medium = get_medium_data(connection, row)?; - mediums.push(medium); - } - - Ok(mediums) -} - -/// Get mediums on which this person is performing. -pub fn get_mediums_for_person( - connection: &mut SqliteConnection, - person_id: &str, -) -> Result> { - let mut mediums: Vec = Vec::new(); - - let rows = mediums::table - .inner_join(tracks::table.on(tracks::medium.eq(mediums::id.nullable()))) - .inner_join(recordings::table.on(recordings::id.eq(tracks::recording))) - .inner_join(performances::table.on(performances::recording.eq(recordings::id))) - .inner_join(persons::table.on(persons::id.nullable().eq(performances::person))) - .filter(persons::id.eq(person_id)) - .select(mediums::table::all_columns()) - .distinct() - .load::(connection)?; - - for row in rows { - let medium = get_medium_data(connection, row)?; - mediums.push(medium); - } - - Ok(mediums) -} - -/// Get mediums on which this ensemble is performing. -pub fn get_mediums_for_ensemble( - connection: &mut SqliteConnection, - ensemble_id: &str, -) -> Result> { - let mut mediums: Vec = Vec::new(); - - let rows = mediums::table - .inner_join(tracks::table.on(tracks::medium.eq(tracks::id.nullable()))) - .inner_join(recordings::table.on(recordings::id.eq(tracks::recording))) - .inner_join(performances::table.on(performances::recording.eq(recordings::id))) - .inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble))) - .filter(ensembles::id.eq(ensemble_id)) - .select(mediums::table::all_columns()) - .distinct() - .load::(connection)?; - - for row in rows { - let medium = get_medium_data(connection, row)?; - mediums.push(medium); - } - - Ok(mediums) -} - -/// Delete a medium and all of its tracks. This will fail, if the music -/// library contains audio files referencing any of those tracks. -pub fn delete_medium(connection: &mut SqliteConnection, id: &str) -> Result<()> { - info!("Deleting medium {}", id); - diesel::delete(mediums::table.filter(mediums::id.eq(id))).execute(connection)?; - Ok(()) -} - -/// Get all available tracks for a recording. -pub fn get_tracks(connection: &mut SqliteConnection, recording_id: &str) -> Result> { - let mut tracks: Vec = Vec::new(); - - let rows = tracks::table - .inner_join(recordings::table.on(recordings::id.eq(tracks::recording))) - .filter(recordings::id.eq(recording_id)) - .select(tracks::table::all_columns()) - .load::(connection)?; - - for row in rows { - let track = get_track_from_row(connection, row)?; - tracks.push(track); - } - - Ok(tracks) -} - -/// Get a random track from the database. -pub fn random_track(connection: &mut SqliteConnection) -> Result { - let row = diesel::sql_query("SELECT * FROM tracks ORDER BY RANDOM() LIMIT 1") - .load::(connection)? - .into_iter() - .next() - .ok_or(Error::Other("Failed to generate random track"))?; - - get_track_from_row(connection, row) -} - -/// Retrieve all available information on a medium from related tables. -fn get_medium_data(connection: &mut SqliteConnection, row: MediumRow) -> Result { - let track_rows = tracks::table - .filter(tracks::medium.eq(&row.id)) - .order_by(tracks::index) - .load::(connection)?; - - let mut tracks = Vec::new(); - - for track_row in track_rows { - let track = get_track_from_row(connection, track_row)?; - tracks.push(track); - } - - let medium = Medium { - id: row.id, - name: row.name, - discid: row.discid, - tracks, - last_used: row.last_used.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - last_played: row.last_played.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - }; - - Ok(medium) -} - -/// Convert a track row from the database to an actual track. -fn get_track_from_row(connection: &mut SqliteConnection, row: TrackRow) -> Result { - let recording_id = row.recording; - - let recording = get_recording(connection, &recording_id)? - .ok_or(Error::MissingItem("recording", recording_id))?; - - let mut part_indices = Vec::new(); - - let work_parts = row.work_parts.split(','); - - for part_index in work_parts { - if !part_index.is_empty() { - let index = str::parse(part_index) - .map_err(|_| Error::Parsing("part index", String::from(part_index)))?; - - part_indices.push(index); - } - } - - let track = Track { - recording, - work_parts: part_indices, - source_index: row.source_index as usize, - path: row.path, - last_used: row.last_used.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - last_played: row.last_played.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - }; - - Ok(track) -} diff --git a/src/db/mod.rs b/src/db/mod.rs deleted file mode 100644 index 50aa92d..0000000 --- a/src/db/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -use diesel::prelude::*; -use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; -use log::info; - -pub use diesel::SqliteConnection; - -pub mod ensembles; -pub use ensembles::*; - -pub mod error; -pub use error::*; - -pub mod instruments; -pub use instruments::*; - -pub mod medium; -pub use medium::*; - -pub mod persons; -pub use persons::*; - -pub mod recordings; -pub use recordings::*; - -pub mod works; -pub use works::*; - -mod schema; - -// This makes the SQL migration scripts accessible from the code. -const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); - -/// Connect to a Musicus database running migrations if necessary. -pub fn connect(file_name: &str) -> Result { - info!("Opening database file '{}'", file_name); - let mut connection = SqliteConnection::establish(file_name)?; - diesel::sql_query("PRAGMA foreign_keys = ON").execute(&mut connection)?; - - info!("Running migrations if necessary"); - connection.run_pending_migrations(MIGRATIONS)?; - - Ok(connection) -} - -/// Generate a random string suitable as an item ID. -pub fn generate_id() -> String { - uuid::Uuid::new_v4().simple().to_string() -} - -/// Defer all foreign keys for the next transaction. -fn defer_foreign_keys(connection: &mut SqliteConnection) -> Result<()> { - diesel::sql_query("PRAGMA defer_foreign_keys = ON").execute(connection)?; - Ok(()) -} diff --git a/src/db/persons.rs b/src/db/persons.rs deleted file mode 100644 index 2186438..0000000 --- a/src/db/persons.rs +++ /dev/null @@ -1,86 +0,0 @@ -use chrono::Utc; -use diesel::prelude::*; -use log::info; - -use crate::db::{defer_foreign_keys, schema::persons, Result}; - -/// A person that is a composer, an interpret or both. -#[derive(Insertable, Queryable, PartialEq, Eq, Hash, Debug, Clone)] -pub struct Person { - pub id: String, - pub first_name: String, - pub last_name: String, - pub last_used: Option, - pub last_played: Option, -} - -impl Person { - pub fn new(id: String, first_name: String, last_name: String) -> Self { - Self { - id, - first_name, - last_name, - last_used: Some(Utc::now().timestamp()), - last_played: None, - } - } - - /// Get the full name in the form "First Last". - pub fn name_fl(&self) -> String { - format!("{} {}", self.first_name, self.last_name) - } - - /// Get the full name in the form "Last, First". - pub fn name_lf(&self) -> String { - format!("{}, {}", self.last_name, self.first_name) - } -} -/// Update an existing person or insert a new one. -pub fn update_person(connection: &mut SqliteConnection, mut person: Person) -> Result<()> { - info!("Updating person {:?}", person); - defer_foreign_keys(connection)?; - - person.last_used = Some(Utc::now().timestamp()); - - connection.transaction(|connection| { - diesel::replace_into(persons::table) - .values(person) - .execute(connection) - })?; - - Ok(()) -} - -/// Get an existing person. -pub fn get_person(connection: &mut SqliteConnection, id: &str) -> Result> { - let person = persons::table - .filter(persons::id.eq(id)) - .load::(connection)? - .into_iter() - .next(); - - Ok(person) -} - -/// Delete an existing person. -pub fn delete_person(connection: &mut SqliteConnection, id: &str) -> Result<()> { - info!("Deleting person {}", id); - diesel::delete(persons::table.filter(persons::id.eq(id))).execute(connection)?; - Ok(()) -} - -/// Get all existing persons. -pub fn get_persons(connection: &mut SqliteConnection) -> Result> { - let persons = persons::table.load::(connection)?; - - Ok(persons) -} - -/// Get recently used persons. -pub fn get_recent_persons(connection: &mut SqliteConnection) -> Result> { - let persons = persons::table - .order(persons::last_used.desc()) - .load::(connection)?; - - Ok(persons) -} diff --git a/src/db/recordings.rs b/src/db/recordings.rs deleted file mode 100644 index b2f33c2..0000000 --- a/src/db/recordings.rs +++ /dev/null @@ -1,350 +0,0 @@ -use chrono::{DateTime, TimeZone, Utc}; -use diesel::prelude::*; -use log::info; - -use crate::db::{ - defer_foreign_keys, generate_id, get_ensemble, get_instrument, get_person, get_work, - schema::{ensembles, performances, persons, recordings}, - update_ensemble, update_instrument, update_person, update_work, Ensemble, Error, Instrument, - Person, Result, Work, -}; - -/// A specific recording of a work. -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct Recording { - pub id: String, - pub work: Work, - pub comment: String, - pub performances: Vec, - pub last_used: Option>, - pub last_played: Option>, -} - -impl Recording { - pub fn new(id: String, work: Work, comment: String, performances: Vec) -> Self { - Self { - id, - work, - comment, - performances, - last_used: Some(Utc::now()), - last_played: None, - } - } - - /// Initialize a new recording with a work. - pub fn from_work(work: Work) -> Self { - Self { - id: generate_id(), - work, - comment: String::new(), - performances: Vec::new(), - last_used: Some(Utc::now()), - last_played: None, - } - } - - /// Get a string representation of the performances in this recording. - // TODO: Maybe replace with impl Display? - pub fn get_performers(&self) -> String { - let texts: Vec = self - .performances - .iter() - .map(|performance| performance.get_title()) - .collect(); - - texts.join(", ") - } -} - -/// How a person or ensemble was involved in a recording. -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct Performance { - pub performer: PersonOrEnsemble, - pub role: Option, -} - -impl Performance { - /// Get a string representation of the performance. - // TODO: Replace with impl Display. - pub fn get_title(&self) -> String { - let performer_title = self.performer.get_title(); - - if let Some(role) = &self.role { - format!("{} ({})", performer_title, role.name) - } else { - performer_title - } - } -} - -/// Either a person or an ensemble. -#[derive(PartialEq, Eq, Hash, Clone, Debug)] -pub enum PersonOrEnsemble { - Person(Person), - Ensemble(Ensemble), -} - -impl PersonOrEnsemble { - /// Get a short textual representation of the item. - pub fn get_title(&self) -> String { - match self { - PersonOrEnsemble::Person(person) => person.name_lf(), - PersonOrEnsemble::Ensemble(ensemble) => ensemble.name.clone(), - } - } -} - -/// Database table data for a recording. -#[derive(Insertable, Queryable, QueryableByName, Debug, Clone)] -#[diesel(table_name = recordings)] -struct RecordingRow { - pub id: String, - pub work: String, - pub comment: String, - pub last_used: Option, - pub last_played: Option, -} - -impl From for RecordingRow { - fn from(recording: Recording) -> Self { - RecordingRow { - id: recording.id, - work: recording.work.id, - comment: recording.comment, - last_used: Some(Utc::now().timestamp()), - last_played: recording.last_played.map(|t| t.timestamp()), - } - } -} - -/// Database table data for a performance. -#[derive(Insertable, Queryable, Debug, Clone)] -#[diesel(table_name = performances)] -struct PerformanceRow { - pub id: i64, - pub recording: String, - pub person: Option, - pub ensemble: Option, - pub role: Option, -} - -/// Update an existing recording or insert a new one. -// TODO: Think about whether to also insert the other items. -pub fn update_recording(connection: &mut SqliteConnection, recording: Recording) -> Result<()> { - info!("Updating recording {:?}", recording); - defer_foreign_keys(connection)?; - - connection.transaction::<(), Error, _>(|connection| { - let recording_id = &recording.id; - delete_recording(connection, recording_id)?; - - // Add associated items from the server, if they don't already exist. - - if get_work(connection, &recording.work.id)?.is_none() { - update_work(connection, recording.work.clone())?; - } - - for performance in &recording.performances { - match &performance.performer { - PersonOrEnsemble::Person(person) => { - if get_person(connection, &person.id)?.is_none() { - update_person(connection, person.clone())?; - } - } - PersonOrEnsemble::Ensemble(ensemble) => { - if get_ensemble(connection, &ensemble.id)?.is_none() { - update_ensemble(connection, ensemble.clone())?; - } - } - } - - if let Some(role) = &performance.role { - if get_instrument(connection, &role.id)?.is_none() { - update_instrument(connection, role.clone())?; - } - } - } - - // Add the actual recording. - - let row: RecordingRow = recording.clone().into(); - diesel::insert_into(recordings::table) - .values(row) - .execute(connection)?; - - for performance in recording.performances { - let (person, ensemble) = match performance.performer { - PersonOrEnsemble::Person(person) => (Some(person.id), None), - PersonOrEnsemble::Ensemble(ensemble) => (None, Some(ensemble.id)), - }; - - let row = PerformanceRow { - id: rand::random(), - recording: recording_id.to_string(), - person, - ensemble, - role: performance.role.map(|role| role.id), - }; - - diesel::insert_into(performances::table) - .values(row) - .execute(connection)?; - } - - Ok(()) - })?; - - Ok(()) -} - -/// Check whether the database contains a recording. -pub fn recording_exists(connection: &mut SqliteConnection, id: &str) -> Result { - let exists = recordings::table - .filter(recordings::id.eq(id)) - .load::(connection)? - .first() - .is_some(); - - Ok(exists) -} - -/// Get an existing recording. -pub fn get_recording(connection: &mut SqliteConnection, id: &str) -> Result> { - let row = recordings::table - .filter(recordings::id.eq(id)) - .load::(connection)? - .into_iter() - .next(); - - let recording = match row { - Some(row) => Some(get_recording_data(connection, row)?), - None => None, - }; - - Ok(recording) -} - -/// Get a random recording from the database. -pub fn random_recording(connection: &mut SqliteConnection) -> Result { - let row = diesel::sql_query("SELECT * FROM recordings ORDER BY RANDOM() LIMIT 1") - .load::(connection)? - .into_iter() - .next() - .ok_or(Error::Other("Failed to find random recording."))?; - - get_recording_data(connection, row) -} - -/// Retrieve all available information on a recording from related tables. -fn get_recording_data(connection: &mut SqliteConnection, row: RecordingRow) -> Result { - let mut performance_descriptions: Vec = Vec::new(); - - let performance_rows = performances::table - .filter(performances::recording.eq(&row.id)) - .load::(connection)?; - - for row in performance_rows { - performance_descriptions.push(Performance { - performer: if let Some(id) = row.person { - PersonOrEnsemble::Person( - get_person(connection, &id)?.ok_or(Error::MissingItem("person", id))?, - ) - } else if let Some(id) = row.ensemble { - PersonOrEnsemble::Ensemble( - get_ensemble(connection, &id)?.ok_or(Error::MissingItem("ensemble", id))?, - ) - } else { - return Err(Error::Other("Performance without performer")); - }, - role: match row.role { - Some(id) => Some( - get_instrument(connection, &id)?.ok_or(Error::MissingItem("instrument", id))?, - ), - None => None, - }, - }); - } - - let work_id = row.work; - let work = get_work(connection, &work_id)?.ok_or(Error::MissingItem("work", work_id))?; - - let recording_description = Recording { - id: row.id, - work, - comment: row.comment, - performances: performance_descriptions, - last_used: row.last_used.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - last_played: row.last_played.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - }; - - Ok(recording_description) -} - -/// Get all available information on all recordings where a person is performing. -pub fn get_recordings_for_person( - connection: &mut SqliteConnection, - person_id: &str, -) -> Result> { - let mut recordings: Vec = Vec::new(); - - let rows = recordings::table - .inner_join(performances::table.on(performances::recording.eq(recordings::id))) - .inner_join(persons::table.on(persons::id.nullable().eq(performances::person))) - .filter(persons::id.eq(person_id)) - .select(recordings::table::all_columns()) - .load::(connection)?; - - for row in rows { - recordings.push(get_recording_data(connection, row)?); - } - - Ok(recordings) -} - -/// Get all available information on all recordings where an ensemble is performing. -pub fn get_recordings_for_ensemble( - connection: &mut SqliteConnection, - ensemble_id: &str, -) -> Result> { - let mut recordings: Vec = Vec::new(); - - let rows = recordings::table - .inner_join(performances::table.on(performances::recording.eq(recordings::id))) - .inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble))) - .filter(ensembles::id.eq(ensemble_id)) - .select(recordings::table::all_columns()) - .load::(connection)?; - - for row in rows { - recordings.push(get_recording_data(connection, row)?); - } - - Ok(recordings) -} - -/// Get allavailable information on all recordings of a work. -pub fn get_recordings_for_work( - connection: &mut SqliteConnection, - work_id: &str, -) -> Result> { - let mut recordings: Vec = Vec::new(); - - let rows = recordings::table - .filter(recordings::work.eq(work_id)) - .load::(connection)?; - - for row in rows { - recordings.push(get_recording_data(connection, row)?); - } - - Ok(recordings) -} - -/// Delete an existing recording. This will fail if there are still references to this -/// recording from other tables that are not directly part of the recording data. -pub fn delete_recording(connection: &mut SqliteConnection, id: &str) -> Result<()> { - info!("Deleting recording {}", id); - diesel::delete(recordings::table.filter(recordings::id.eq(id))).execute(connection)?; - Ok(()) -} diff --git a/src/db/schema.rs b/src/db/schema.rs deleted file mode 100644 index fe72d06..0000000 --- a/src/db/schema.rs +++ /dev/null @@ -1,125 +0,0 @@ -// @generated automatically by Diesel CLI. - -diesel::table! { - ensembles (id) { - id -> Text, - name -> Text, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::table! { - instrumentations (id) { - id -> BigInt, - work -> Text, - instrument -> Text, - } -} - -diesel::table! { - instruments (id) { - id -> Text, - name -> Text, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::table! { - mediums (id) { - id -> Text, - name -> Text, - discid -> Nullable, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::table! { - performances (id) { - id -> BigInt, - recording -> Text, - person -> Nullable, - ensemble -> Nullable, - role -> Nullable, - } -} - -diesel::table! { - persons (id) { - id -> Text, - first_name -> Text, - last_name -> Text, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::table! { - recordings (id) { - id -> Text, - work -> Text, - comment -> Text, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::table! { - tracks (id) { - id -> Text, - medium -> Nullable, - index -> Integer, - recording -> Text, - work_parts -> Text, - source_index -> Integer, - path -> Text, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::table! { - work_parts (id) { - id -> BigInt, - work -> Text, - part_index -> BigInt, - title -> Text, - } -} - -diesel::table! { - works (id) { - id -> Text, - composer -> Text, - title -> Text, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::joinable!(instrumentations -> instruments (instrument)); -diesel::joinable!(instrumentations -> works (work)); -diesel::joinable!(performances -> ensembles (ensemble)); -diesel::joinable!(performances -> instruments (role)); -diesel::joinable!(performances -> persons (person)); -diesel::joinable!(performances -> recordings (recording)); -diesel::joinable!(recordings -> works (work)); -diesel::joinable!(tracks -> mediums (medium)); -diesel::joinable!(tracks -> recordings (recording)); -diesel::joinable!(work_parts -> works (work)); -diesel::joinable!(works -> persons (composer)); - -diesel::allow_tables_to_appear_in_same_query!( - ensembles, - instrumentations, - instruments, - mediums, - performances, - persons, - recordings, - tracks, - work_parts, - works, -); diff --git a/src/db/works.rs b/src/db/works.rs deleted file mode 100644 index ef92795..0000000 --- a/src/db/works.rs +++ /dev/null @@ -1,252 +0,0 @@ -use chrono::{DateTime, TimeZone, Utc}; -use diesel::{prelude::*, Insertable, Queryable}; -use log::info; - -use crate::db::{ - defer_foreign_keys, generate_id, get_instrument, get_person, - schema::{instrumentations, work_parts, works}, - update_instrument, update_person, Error, Instrument, Person, Result, -}; - -/// Table row data for a work. -#[derive(Insertable, Queryable, Debug, Clone)] -#[diesel(table_name = works)] -struct WorkRow { - pub id: String, - pub composer: String, - pub title: String, - pub last_used: Option, - pub last_played: Option, -} - -impl From for WorkRow { - fn from(work: Work) -> Self { - WorkRow { - id: work.id, - composer: work.composer.id, - title: work.title, - last_used: Some(Utc::now().timestamp()), - last_played: work.last_played.map(|t| t.timestamp()), - } - } -} - -/// Definition that a work uses an instrument. -#[derive(Insertable, Queryable, Debug, Clone)] -#[diesel(table_name = instrumentations)] -struct InstrumentationRow { - pub id: i64, - pub work: String, - pub instrument: String, -} - -/// Table row data for a work part. -#[derive(Insertable, Queryable, Debug, Clone)] -#[diesel(table_name = work_parts)] -struct WorkPartRow { - pub id: i64, - pub work: String, - pub part_index: i64, - pub title: String, -} - -/// A concrete work part that can be recorded. -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct WorkPart { - pub title: String, -} - -/// A specific work by a composer. -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct Work { - pub id: String, - pub title: String, - pub composer: Person, - pub instruments: Vec, - pub parts: Vec, - pub last_used: Option>, - pub last_played: Option>, -} - -impl Work { - pub fn new( - id: String, - title: String, - composer: Person, - instruments: Vec, - parts: Vec, - ) -> Self { - Self { - id, - title, - composer, - instruments, - parts, - last_used: Some(Utc::now()), - last_played: None, - } - } - - /// Initialize a new work with a composer. - pub fn from_composer(composer: Person) -> Self { - Self { - id: generate_id(), - title: String::new(), - composer, - instruments: Vec::new(), - parts: Vec::new(), - last_used: Some(Utc::now()), - last_played: None, - } - } - - /// Get a string including the composer and title of the work. - // TODO: Replace with impl Display. - pub fn get_title(&self) -> String { - format!("{}: {}", self.composer.name_fl(), self.title) - } -} - -/// Update an existing work or insert a new one. -// TODO: Think about also inserting related items. -pub fn update_work(connection: &mut SqliteConnection, work: Work) -> Result<()> { - info!("Updating work {:?}", work); - defer_foreign_keys(connection)?; - - connection.transaction::<(), Error, _>(|connection| { - let work_id = &work.id; - delete_work(connection, work_id)?; - - // Add associated items from the server, if they don't already exist. - - if get_person(connection, &work.composer.id)?.is_none() { - update_person(connection, work.composer.clone())?; - } - - for instrument in &work.instruments { - if get_instrument(connection, &instrument.id)?.is_none() { - update_instrument(connection, instrument.clone())?; - } - } - - // Add the actual work. - - let row: WorkRow = work.clone().into(); - diesel::insert_into(works::table) - .values(row) - .execute(connection)?; - - let Work { - instruments, parts, .. - } = work; - - for instrument in instruments { - let row = InstrumentationRow { - id: rand::random(), - work: work_id.to_string(), - instrument: instrument.id, - }; - - diesel::insert_into(instrumentations::table) - .values(row) - .execute(connection)?; - } - - for (index, part) in parts.into_iter().enumerate() { - let row = WorkPartRow { - id: rand::random(), - work: work_id.to_string(), - part_index: index as i64, - title: part.title, - }; - - diesel::insert_into(work_parts::table) - .values(row) - .execute(connection)?; - } - - Ok(()) - })?; - - Ok(()) -} - -/// Get an existing work. -pub fn get_work(connection: &mut SqliteConnection, id: &str) -> Result> { - let row = works::table - .filter(works::id.eq(id)) - .load::(connection)? - .first() - .cloned(); - - let work = match row { - Some(row) => Some(get_work_data(connection, row)?), - None => None, - }; - - Ok(work) -} - -/// Retrieve all available information on a work from related tables. -fn get_work_data(connection: &mut SqliteConnection, row: WorkRow) -> Result { - let mut instruments: Vec = Vec::new(); - - let instrumentations = instrumentations::table - .filter(instrumentations::work.eq(&row.id)) - .load::(connection)?; - - for instrumentation in instrumentations { - let id = instrumentation.instrument; - instruments - .push(get_instrument(connection, &id)?.ok_or(Error::MissingItem("instrument", id))?); - } - - let mut parts: Vec = Vec::new(); - - let part_rows = work_parts::table - .filter(work_parts::work.eq(&row.id)) - .load::(connection)?; - - for part_row in part_rows { - parts.push(WorkPart { - title: part_row.title, - }); - } - - let person_id = row.composer; - let person = - get_person(connection, &person_id)?.ok_or(Error::MissingItem("person", person_id))?; - - Ok(Work { - id: row.id, - composer: person, - title: row.title, - instruments, - parts, - last_used: row.last_used.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - last_played: row.last_played.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - }) -} - -/// Delete an existing work. This will fail if there are still other tables that relate to -/// this work except for the things that are part of the information on the work it -pub fn delete_work(connection: &mut SqliteConnection, id: &str) -> Result<()> { - info!("Deleting work {}", id); - diesel::delete(works::table.filter(works::id.eq(id))).execute(connection)?; - Ok(()) -} - -/// Get all existing works by a composer and related information from other tables. -pub fn get_works(connection: &mut SqliteConnection, composer_id: &str) -> Result> { - let mut works: Vec = Vec::new(); - - let rows = works::table - .filter(works::composer.eq(composer_id)) - .load::(connection)?; - - for row in rows { - works.push(get_work_data(connection, row)?); - } - - Ok(works) -} diff --git a/src/home_page.rs b/src/home_page.rs index 4d30ef6..fde4273 100644 --- a/src/home_page.rs +++ b/src/home_page.rs @@ -1,10 +1,15 @@ use crate::{ - library::MusicusLibrary, player::MusicusPlayer, search_entry::MusicusSearchEntry, + library::{LibraryQuery, MusicusLibrary}, + player::MusicusPlayer, + search_entry::MusicusSearchEntry, tile::MusicusTile, }; use adw::subclass::{navigation_page::NavigationPageImpl, prelude::*}; -use gtk::{glib, glib::Properties, prelude::*}; -use std::cell::{OnceCell, RefCell}; +use gtk::{ + glib::{self, clone, Properties}, + prelude::*, +}; +use std::cell::OnceCell; mod imp { use super::*; @@ -13,11 +18,12 @@ mod imp { #[properties(wrapper_type = super::MusicusHomePage)] #[template(file = "data/ui/home_page.blp")] pub struct MusicusHomePage { - #[property(get, set)] - pub player: RefCell, - + #[property(get, construct_only)] pub library: OnceCell, + #[property(get, construct_only)] + pub player: OnceCell, + #[template_child] pub search_entry: TemplateChild, #[template_child] @@ -53,21 +59,25 @@ mod imp { self.search_entry.set_key_capture_widget(&*self.obj()); - self.search_entry.connect_query_changed(|entry| { - log::info!("{}", entry.query()); - }); + self.search_entry + .connect_query_changed(clone!(@weak self as _self => move |entry| { + _self.obj().query(&entry.query()); + })); self.player - .borrow() + .get() + .unwrap() .bind_property("active", &self.play_button.get(), "visible") .invert_boolean() .sync_create() .build(); + self.obj().query(&LibraryQuery::default()); + for _ in 0..9 { - self.persons_flow_box.append(&MusicusTile::new()); - self.works_flow_box.append(&MusicusTile::new()); - self.recordings_flow_box.append(&MusicusTile::new()); + self.works_flow_box.append(&MusicusTile::with_title("Test")); + self.recordings_flow_box + .append(&MusicusTile::with_title("Test")); } } } @@ -84,19 +94,37 @@ glib::wrapper! { #[gtk::template_callbacks] impl MusicusHomePage { pub fn new(library: &MusicusLibrary, player: &MusicusPlayer) -> Self { - let obj: MusicusHomePage = glib::Object::builder().property("player", player).build(); - obj.imp().library.set(library.to_owned()).unwrap(); - obj + glib::Object::builder() + .property("library", library) + .property("player", player) + .build() } #[template_callback] fn play(&self, _: >k::Button) { log::info!("Play button clicked"); - self.imp().player.borrow().play(); + self.player().play(); } #[template_callback] fn select(&self, search_entry: &MusicusSearchEntry) { search_entry.add_tag("Tag"); } + + fn query(&self, query: &LibraryQuery) { + let results = self.library().query(query); + + clear_flowbox(&self.imp().persons_flow_box); + for person in results.persons { + self.imp() + .persons_flow_box + .append(&MusicusTile::with_title(&person.name_fl())); + } + } +} + +fn clear_flowbox(flowbox: >k::FlowBox) { + while let Some(widget) = flowbox.first_child() { + flowbox.remove(&widget); + } } diff --git a/src/library.rs b/src/library.rs index d9c4b10..7d245b9 100644 --- a/src/library.rs +++ b/src/library.rs @@ -1,8 +1,8 @@ -use crate::db::{self, SqliteConnection}; use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*}; +use rusqlite::{Connection, Row}; use std::{ - cell::{OnceCell, RefCell}, - path::Path, + cell::OnceCell, + path::{Path, PathBuf}, }; mod imp { @@ -11,9 +11,9 @@ mod imp { #[derive(Properties, Default)] #[properties(wrapper_type = super::MusicusLibrary)] pub struct MusicusLibrary { - #[property(get, set)] - pub folder: RefCell, - pub connection: OnceCell, + #[property(get, construct_only)] + pub folder: OnceCell, + pub connection: OnceCell, } #[glib::object_subclass] @@ -23,7 +23,15 @@ mod imp { } #[glib::derived_properties] - impl ObjectImpl for MusicusLibrary {} + impl ObjectImpl for MusicusLibrary { + fn constructed(&self) { + self.parent_constructed(); + let db_path = PathBuf::from(self.folder.get().unwrap()).join("musicus.db"); + self.connection + .set(Connection::open(db_path).unwrap()) + .unwrap(); + } + } } glib::wrapper! { @@ -32,22 +40,89 @@ glib::wrapper! { impl MusicusLibrary { pub fn new(path: impl AsRef) -> Self { - let path = path.as_ref(); - let obj: MusicusLibrary = glib::Object::builder() - .property("folder", path.to_str().unwrap()) - .build(); - - let connection = db::connect(path.join("musicus.db").to_str().unwrap()).unwrap(); - - obj.imp() - .connection - .set(connection) - .unwrap_or_else(|_| panic!("Database connection already set")); - - obj + glib::Object::builder() + .property("folder", path.as_ref().to_str().unwrap()) + .build() } - pub fn db(&self) -> &SqliteConnection { + pub fn query(&self, query: &LibraryQuery) -> LibraryResults { + let search = format!("%{}%", query.search); + + match query { + LibraryQuery { + person: None, + ensemble: None, + work: None, + .. + } => { + let persons = self.con() + .prepare("SELECT first_name, last_name FROM persons WHERE first_name LIKE ?1 OR last_name LIKE ?1 LIMIT 9") + .unwrap() + .query_map([&search], Person::from_row) + .unwrap() + .collect::>>() + .unwrap(); + + LibraryResults { + persons, + ..Default::default() + } + } + _ => LibraryResults::default(), + } + } + + fn con(&self) -> &Connection { self.imp().connection.get().unwrap() } } + +#[derive(Default)] +pub struct LibraryQuery { + pub person: Option, + pub ensemble: Option, + pub work: Option, + pub search: String, +} + +#[derive(Default)] +pub struct LibraryResults { + pub persons: Vec, + pub ensembles: Vec, + pub works: Vec, + pub recordings: Vec, +} + +impl LibraryResults { + pub fn is_empty(&self) -> bool { + self.persons.is_empty() && self.ensembles.is_empty() && self.works.is_empty() + } +} + +pub struct Person { + pub first_name: String, + pub last_name: String, +} + +impl Person { + pub fn from_row(row: &Row) -> rusqlite::Result { + Ok(Self { + first_name: row.get(0)?, + last_name: row.get(1)?, + }) + } + + pub fn name_fl(&self) -> String { + format!("{} {}", self.first_name, self.last_name) + } +} + +pub struct Ensemble { + pub name: String, +} + +pub struct Work { + pub title: String, +} + +pub struct Recording {} diff --git a/src/main.rs b/src/main.rs index 1f1deb5..d0dd28c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -mod db; mod application; mod config; mod home_page; diff --git a/src/search_entry.rs b/src/search_entry.rs index 60f1b9e..ab3ee8a 100644 --- a/src/search_entry.rs +++ b/src/search_entry.rs @@ -1,4 +1,4 @@ -use crate::search_tag::MusicusSearchTag; +use crate::{library::LibraryQuery, search_tag::MusicusSearchTag}; use adw::{gdk, gio, glib, glib::clone, glib::subclass::Signal, prelude::*, subclass::prelude::*}; use once_cell::sync::Lazy; use std::{cell::RefCell, time::Duration}; @@ -147,8 +147,11 @@ impl MusicusSearchEntry { self.imp().tags.borrow_mut().push(tag); } - pub fn query(&self) -> String { - self.imp().text.text().to_string() + pub fn query(&self) -> LibraryQuery { + LibraryQuery { + search: self.imp().text.text().to_string(), + ..Default::default() + } } #[template_callback]