From a93c7276d213318b63eab96fd34a292fd1d3e0f8 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Tue, 17 Nov 2020 15:52:47 +0100 Subject: [PATCH] Restructure backend and database --- .../2020-09-27-201047_initial_schema/down.sql | 2 - .../2020-09-27-201047_initial_schema/up.sql | 12 +- musicus/res/ui/part_editor.ui | 220 ++------ musicus/src/backend/backend.rs | 528 ------------------ musicus/src/backend/client.rs | 60 ++ musicus/src/backend/library.rs | 73 +++ musicus/src/backend/mod.rs | 75 ++- musicus/src/database/database.rs | 448 --------------- musicus/src/database/ensembles.rs | 96 ++++ musicus/src/database/instruments.rs | 96 ++++ musicus/src/database/mod.rs | 55 +- musicus/src/database/models.rs | 205 ------- musicus/src/database/persons.rs | 111 ++++ musicus/src/database/recordings.rs | 252 +++++++++ musicus/src/database/schema.rs | 13 +- musicus/src/database/tables.rs | 94 ---- musicus/src/database/thread.rs | 327 +++++++++++ musicus/src/database/tracks.rs | 94 ++++ musicus/src/database/works.rs | 262 +++++++++ musicus/src/dialogs/ensemble_editor.rs | 7 +- musicus/src/dialogs/ensemble_selector.rs | 5 +- musicus/src/dialogs/instrument_editor.rs | 4 +- musicus/src/dialogs/instrument_selector.rs | 5 +- musicus/src/dialogs/person_editor.rs | 4 +- .../dialogs/recording/performance_editor.rs | 8 +- .../src/dialogs/recording/recording_dialog.rs | 4 +- .../src/dialogs/recording/recording_editor.rs | 20 +- .../recording/recording_editor_dialog.rs | 6 +- .../dialogs/recording/recording_selector.rs | 4 +- .../recording_selector_person_screen.rs | 14 +- .../recording_selector_work_screen.rs | 38 +- musicus/src/dialogs/track_editor.rs | 6 +- musicus/src/dialogs/tracks_editor.rs | 20 +- musicus/src/dialogs/work/part_editor.rs | 64 +-- musicus/src/dialogs/work/section_editor.rs | 11 +- musicus/src/dialogs/work/work_dialog.rs | 4 +- musicus/src/dialogs/work/work_editor.rs | 22 +- .../src/dialogs/work/work_editor_dialog.rs | 6 +- musicus/src/dialogs/work/work_selector.rs | 4 +- .../work/work_selector_person_screen.rs | 14 +- musicus/src/meson.build | 12 +- musicus/src/player.rs | 4 +- musicus/src/screens/ensemble_screen.rs | 9 +- musicus/src/screens/person_screen.rs | 28 +- musicus/src/screens/recording_screen.rs | 8 +- musicus/src/screens/work_screen.rs | 11 +- musicus/src/widgets/person_list.rs | 2 +- musicus/src/widgets/poe_list.rs | 4 +- musicus/src/window.rs | 254 +-------- 49 files changed, 1705 insertions(+), 1920 deletions(-) delete mode 100644 musicus/src/backend/backend.rs create mode 100644 musicus/src/backend/library.rs delete mode 100644 musicus/src/database/database.rs create mode 100644 musicus/src/database/ensembles.rs create mode 100644 musicus/src/database/instruments.rs delete mode 100644 musicus/src/database/models.rs create mode 100644 musicus/src/database/persons.rs create mode 100644 musicus/src/database/recordings.rs delete mode 100644 musicus/src/database/tables.rs create mode 100644 musicus/src/database/thread.rs create mode 100644 musicus/src/database/tracks.rs create mode 100644 musicus/src/database/works.rs diff --git a/musicus/migrations/2020-09-27-201047_initial_schema/down.sql b/musicus/migrations/2020-09-27-201047_initial_schema/down.sql index 4a556b3..4d21111 100644 --- a/musicus/migrations/2020-09-27-201047_initial_schema/down.sql +++ b/musicus/migrations/2020-09-27-201047_initial_schema/down.sql @@ -8,8 +8,6 @@ DROP TABLE instrumentations; DROP TABLE work_parts; -DROP TABLE part_instrumentations; - DROP TABLE work_sections; DROP TABLE ensembles; diff --git a/musicus/migrations/2020-09-27-201047_initial_schema/up.sql b/musicus/migrations/2020-09-27-201047_initial_schema/up.sql index d53668e..7d30680 100644 --- a/musicus/migrations/2020-09-27-201047_initial_schema/up.sql +++ b/musicus/migrations/2020-09-27-201047_initial_schema/up.sql @@ -18,21 +18,15 @@ CREATE TABLE works ( CREATE TABLE instrumentations ( id BIGINT NOT NULL PRIMARY KEY, work BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE, - instrument BIGINT NOT NULL REFERENCES instruments(id) + instrument BIGINT NOT NULL REFERENCES instruments(id) ON DELETE CASCADE ); CREATE TABLE work_parts ( id BIGINT NOT NULL PRIMARY KEY, work BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE, part_index BIGINT NOT NULL, - composer BIGINT REFERENCES persons(id), - title TEXT NOT NULL -); - -CREATE TABLE part_instrumentations ( - id BIGINT NOT NULL PRIMARY KEY, - work_part BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE, - instrument BIGINT NOT NULL REFERENCES instruments(id) + title TEXT NOT NULL, + composer BIGINT REFERENCES persons(id) ); CREATE TABLE work_sections ( diff --git a/musicus/res/ui/part_editor.ui b/musicus/res/ui/part_editor.ui index 80a01b4..d74cb2f 100644 --- a/musicus/res/ui/part_editor.ui +++ b/musicus/res/ui/part_editor.ui @@ -6,8 +6,7 @@ False True - 450 - 300 + 350 True dialog @@ -51,182 +50,84 @@ - + + True - True + False + 18 + 12 + 6 - - - True - False - 18 - 12 - 6 - - - True - False - end - Composer - - - 0 - 1 - - - - - True - True - True - - - 1 - 0 - - - - - True - False - end - Title - - - 0 - 0 - - - - - True - False - True - - - True - True - True - True - - - True - False - start - Select … - - - - - False - True - 0 - - - - - True - True - - - True - False - user-trash-symbolic - - - - - False - True - 1 - - - - - - 1 - 1 - - - - - True False - Overview + end + Composer - False + 0 + 1 + + + + + True + True + True + + + 1 + 0 + + + + + True + False + end + Title + + + 0 + 0 True False - 18 - 6 + True - + True True - in + True + True - + + True + False + start + Select … + - True + False True 0 - - True - False - 0 - vertical - 6 + + True + True - + True - True - True - - - True - False - list-add-symbolic - - + False + user-trash-symbolic - - False - True - 0 - - - - - True - True - True - - - True - False - list-remove-symbolic - - - - - False - True - 1 - @@ -235,25 +136,18 @@ 1 + - 1 - - - - - True - False - Instruments - - - 1 - False + 1 + 1 - True + False True 1 diff --git a/musicus/src/backend/backend.rs b/musicus/src/backend/backend.rs deleted file mode 100644 index eaf15f0..0000000 --- a/musicus/src/backend/backend.rs +++ /dev/null @@ -1,528 +0,0 @@ -use super::secure; -use crate::database::*; -use crate::player::*; -use anyhow::{anyhow, Result}; -use futures_channel::oneshot::Sender; -use futures_channel::{mpsc, oneshot}; -use gio::prelude::*; -use serde::Serialize; -use std::cell::RefCell; -use std::path::PathBuf; -use std::rc::Rc; - -/// Credentials used for login. -#[derive(Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct LoginData { - pub username: String, - pub password: String, -} - -pub enum BackendState { - NoMusicLibrary, - Loading, - Ready, -} - -enum BackendAction { - UpdatePerson(Person, Sender>), - GetPerson(i64, Sender>), - DeletePerson(i64, Sender>), - GetPersons(Sender>>), - UpdateInstrument(Instrument, Sender>), - GetInstrument(i64, Sender>), - DeleteInstrument(i64, Sender>), - GetInstruments(Sender>>), - UpdateWork(WorkInsertion, Sender>), - GetWorkDescription(i64, Sender>), - DeleteWork(i64, Sender>), - GetWorkDescriptions(i64, Sender>>), - UpdateEnsemble(Ensemble, Sender>), - GetEnsemble(i64, Sender>), - DeleteEnsemble(i64, Sender>), - GetEnsembles(Sender>>), - UpdateRecording(RecordingInsertion, Sender>), - GetRecordingDescription(i64, Sender>), - DeleteRecording(i64, Sender>), - GetRecordingsForPerson(i64, Sender>>), - GetRecordingsForEnsemble(i64, Sender>>), - GetRecordingsForWork(i64, Sender>>), - UpdateTracks(i64, Vec, Sender>), - DeleteTracks(i64, Sender>), - GetTracks(i64, Sender>>), - Stop, -} - -use BackendAction::*; - -pub struct Backend { - pub state_stream: RefCell>, - state_sender: RefCell>, - action_sender: RefCell>>, - settings: gio::Settings, - secrets: secret_service::SecretService, - server_url: RefCell>, - login_data: RefCell>, - token: RefCell>, - music_library_path: RefCell>, - player: RefCell>>, -} - -impl Backend { - pub fn new() -> Self { - let (state_sender, state_stream) = mpsc::channel(1024); - let secrets = secret_service::SecretService::new(secret_service::EncryptionType::Dh) - .expect("Failed to connect to SecretsService!"); - - Backend { - state_stream: RefCell::new(state_stream), - state_sender: RefCell::new(state_sender), - action_sender: RefCell::new(None), - settings: gio::Settings::new("de.johrpan.musicus"), - secrets, - music_library_path: RefCell::new(None), - server_url: RefCell::new(None), - login_data: RefCell::new(None), - token: RefCell::new(None), - player: RefCell::new(None), - } - } - - pub fn init(self: Rc) { - if let Some(path) = self.settings.get_string("music-library-path") { - if !path.is_empty() { - let context = glib::MainContext::default(); - let clone = self.clone(); - context.spawn_local(async move { - clone - .set_music_library_path_priv(PathBuf::from(path.to_string())) - .await - .unwrap(); - }); - } - } - - if let Some(data) = secure::load_login_data().unwrap() { - self.login_data.replace(Some(data)); - } - - if let Some(url) = self.settings.get_string("server-url") { - if !url.is_empty() { - self.server_url.replace(Some(url.to_string())); - } - } - } - - pub async fn update_person(&self, person: Person) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(UpdatePerson(person, sender))?; - receiver.await? - } - - pub async fn get_person(&self, id: i64) -> Result { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()?.send(GetPerson(id, sender))?; - receiver.await? - } - - pub async fn delete_person(&self, id: i64) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(DeletePerson(id, sender))?; - receiver.await? - } - - pub async fn get_persons(&self) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()?.send(GetPersons(sender))?; - receiver.await? - } - - pub async fn update_instrument(&self, instrument: Instrument) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(UpdateInstrument(instrument, sender))?; - receiver.await? - } - - pub async fn get_instrument(&self, id: i64) -> Result { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(GetInstrument(id, sender))?; - receiver.await? - } - - pub async fn delete_instrument(&self, id: i64) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(DeleteInstrument(id, sender))?; - receiver.await? - } - - pub async fn get_instruments(&self) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()?.send(GetInstruments(sender))?; - receiver.await? - } - - pub async fn update_work(&self, work_insertion: WorkInsertion) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(UpdateWork(work_insertion, sender))?; - receiver.await? - } - - pub async fn get_work_description(&self, id: i64) -> Result { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(GetWorkDescription(id, sender))?; - receiver.await? - } - - pub async fn delete_work(&self, id: i64) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()?.send(DeleteWork(id, sender))?; - receiver.await? - } - - pub async fn get_work_descriptions(&self, person_id: i64) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(GetWorkDescriptions(person_id, sender))?; - receiver.await? - } - - pub async fn update_ensemble(&self, ensemble: Ensemble) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(UpdateEnsemble(ensemble, sender))?; - receiver.await? - } - - pub async fn get_ensemble(&self, id: i64) -> Result { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()?.send(GetEnsemble(id, sender))?; - receiver.await? - } - - pub async fn delete_ensemble(&self, id: i64) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(DeleteEnsemble(id, sender))?; - receiver.await? - } - - pub async fn get_ensembles(&self) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()?.send(GetEnsembles(sender))?; - receiver.await? - } - - pub async fn update_recording(&self, recording_insertion: RecordingInsertion) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(UpdateRecording(recording_insertion, sender))?; - receiver.await? - } - - pub async fn get_recording_description(&self, id: i64) -> Result { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(GetRecordingDescription(id, sender))?; - receiver.await? - } - - pub async fn delete_recording(&self, id: i64) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(DeleteRecording(id, sender))?; - receiver.await? - } - - pub async fn get_recordings_for_person( - &self, - person_id: i64, - ) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(GetRecordingsForPerson(person_id, sender))?; - receiver.await? - } - - pub async fn get_recordings_for_ensemble( - &self, - ensemble_id: i64, - ) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(GetRecordingsForEnsemble(ensemble_id, sender))?; - receiver.await? - } - - pub async fn get_recordings_for_work(&self, work_id: i64) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(GetRecordingsForWork(work_id, sender))?; - receiver.await? - } - - pub async fn update_tracks( - &self, - recording_id: i64, - tracks: Vec, - ) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(UpdateTracks(recording_id, tracks, sender))?; - receiver.await? - } - - pub async fn delete_tracks(&self, recording_id: i64) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(DeleteTracks(recording_id, sender))?; - receiver.await? - } - - pub async fn get_tracks(&self, recording_id: i64) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.unwrap_action_sender()? - .send(GetTracks(recording_id, sender))?; - receiver.await? - } - - pub async fn set_music_library_path(&self, path: PathBuf) -> Result<()> { - self.settings - .set_string("music-library-path", path.to_str().unwrap())?; - self.set_music_library_path_priv(path).await - } - - pub fn get_music_library_path(&self) -> Option { - self.music_library_path.borrow().clone() - } - - /// Get the currently stored login credentials. - pub fn get_login_data(&self) -> Option { - self.login_data.borrow().clone() - } - - /// Set the URL of the Musicus server to connect to. - pub fn set_server_url(&self, url: &str) -> Result<()> { - self.settings.set_string("server-url", url)?; - self.server_url.replace(Some(url.to_string())); - Ok(()) - } - - /// Get the currently used login token. - pub fn get_token(&self) -> Option { - self.token.borrow().clone() - } - - /// Set the login token to use. This will be done automatically by the login method. - pub fn set_token(&self, token: &str) { - self.token.replace(Some(token.to_string())); - } - - /// Get the currently set server URL. - pub fn get_server_url(&self) -> Option { - self.server_url.borrow().clone() - } - - /// Set the user credentials to use. - pub async fn set_login_data(&self, data: LoginData) -> Result<()> { - secure::store_login_data(data.clone()).await?; - self.login_data.replace(Some(data)); - Ok(()) - } - - pub fn get_player(&self) -> Option> { - self.player.borrow().clone() - } - - async fn set_music_library_path_priv(&self, path: PathBuf) -> Result<()> { - self.set_state(BackendState::Loading); - - if let Some(player) = &*self.player.borrow() { - player.clear(); - } - - self.music_library_path.replace(Some(path.clone())); - self.player.replace(Some(Player::new(path.clone()))); - - if let Some(action_sender) = self.action_sender.borrow_mut().take() { - action_sender.send(Stop)?; - } - - let mut db_path = path.clone(); - db_path.push("musicus.db"); - - self.start_db_thread(String::from(db_path.to_str().unwrap())) - .await?; - - self.set_state(BackendState::Ready); - - Ok(()) - } - - fn set_state(&self, state: BackendState) { - self.state_sender.borrow_mut().try_send(state).unwrap(); - } - - fn unwrap_action_sender(&self) -> Result> { - match &*self.action_sender.borrow() { - Some(action_sender) => Ok(action_sender.clone()), - None => Err(anyhow!("Database thread is not running!")), - } - } - - async fn start_db_thread(&self, url: String) -> Result<()> { - let (ready_sender, ready_receiver) = oneshot::channel(); - let (action_sender, action_receiver) = std::sync::mpsc::channel::(); - - std::thread::spawn(move || { - let db = Database::new(&url).expect("Failed to open database!"); - - ready_sender - .send(()) - .expect("Failed to communicate to main thread!"); - - for action in action_receiver { - match action { - UpdatePerson(person, sender) => { - sender - .send(db.update_person(person)) - .expect("Failed to send result from database thread!"); - } - GetPerson(id, sender) => { - sender - .send(db.get_person(id)) - .expect("Failed to send result from database thread!"); - } - DeletePerson(id, sender) => { - sender - .send(db.delete_person(id)) - .expect("Failed to send result from database thread!"); - } - GetPersons(sender) => { - sender - .send(db.get_persons()) - .expect("Failed to send result from database thread!"); - } - UpdateInstrument(instrument, sender) => { - sender - .send(db.update_instrument(instrument)) - .expect("Failed to send result from database thread!"); - } - GetInstrument(id, sender) => { - sender - .send(db.get_instrument(id)) - .expect("Failed to send result from database thread!"); - } - DeleteInstrument(id, sender) => { - sender - .send(db.delete_instrument(id)) - .expect("Failed to send result from database thread!"); - } - GetInstruments(sender) => { - sender - .send(db.get_instruments()) - .expect("Failed to send result from database thread!"); - } - UpdateWork(work, sender) => { - sender - .send(db.update_work(work)) - .expect("Failed to send result from database thread!"); - } - GetWorkDescription(id, sender) => { - sender - .send(db.get_work_description(id)) - .expect("Failed to send result from database thread!"); - } - DeleteWork(id, sender) => { - sender - .send(db.delete_work(id)) - .expect("Failed to send result from database thread!"); - } - GetWorkDescriptions(id, sender) => { - sender - .send(db.get_work_descriptions(id)) - .expect("Failed to send result from database thread!"); - } - UpdateEnsemble(ensemble, sender) => { - sender - .send(db.update_ensemble(ensemble)) - .expect("Failed to send result from database thread!"); - } - GetEnsemble(id, sender) => { - sender - .send(db.get_ensemble(id)) - .expect("Failed to send result from database thread!"); - } - DeleteEnsemble(id, sender) => { - sender - .send(db.delete_ensemble(id)) - .expect("Failed to send result from database thread!"); - } - GetEnsembles(sender) => { - sender - .send(db.get_ensembles()) - .expect("Failed to send result from database thread!"); - } - UpdateRecording(recording, sender) => { - sender - .send(db.update_recording(recording)) - .expect("Failed to send result from database thread!"); - } - GetRecordingDescription(id, sender) => { - sender - .send(db.get_recording_description(id)) - .expect("Failed to send result from database thread!"); - } - DeleteRecording(id, sender) => { - sender - .send(db.delete_recording(id)) - .expect("Failed to send result from database thread!"); - } - GetRecordingsForPerson(id, sender) => { - sender - .send(db.get_recordings_for_person(id)) - .expect("Failed to send result from database thread!"); - } - GetRecordingsForEnsemble(id, sender) => { - sender - .send(db.get_recordings_for_ensemble(id)) - .expect("Failed to send result from database thread!"); - } - GetRecordingsForWork(id, sender) => { - sender - .send(db.get_recordings_for_work(id)) - .expect("Failed to send result from database thread!"); - } - UpdateTracks(recording_id, tracks, sender) => { - sender - .send(db.update_tracks(recording_id, tracks)) - .expect("Failed to send result from database thread!"); - } - DeleteTracks(recording_id, sender) => { - sender - .send(db.delete_tracks(recording_id)) - .expect("Failed to send result from database thread!"); - } - GetTracks(recording_id, sender) => { - sender - .send(db.get_tracks(recording_id)) - .expect("Failed to send result from database thread!"); - } - Stop => { - break; - } - } - } - }); - - ready_receiver.await?; - self.action_sender.replace(Some(action_sender)); - Ok(()) - } -} diff --git a/musicus/src/backend/client.rs b/musicus/src/backend/client.rs index c8955a4..8899fe7 100644 --- a/musicus/src/backend/client.rs +++ b/musicus/src/backend/client.rs @@ -1,9 +1,69 @@ +use super::secure; use super::Backend; use anyhow::{anyhow, bail, Result}; +use gio::prelude::*; use isahc::http::StatusCode; use isahc::prelude::*; +use serde::Serialize; + +/// Credentials used for login. +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LoginData { + pub username: String, + pub password: String, +} impl Backend { + /// Initialize the client. + pub(super) fn init_client(&self) -> Result<()> { + if let Some(data) = secure::load_login_data()? { + self.login_data.replace(Some(data)); + } + + if let Some(url) = self.settings.get_string("server-url") { + if !url.is_empty() { + self.server_url.replace(Some(url.to_string())); + } + } + + Ok(()) + } + + /// Set the URL of the Musicus server to connect to. + pub fn set_server_url(&self, url: &str) -> Result<()> { + self.settings.set_string("server-url", url)?; + self.server_url.replace(Some(url.to_string())); + Ok(()) + } + + /// Get the currently used login token. + pub fn get_token(&self) -> Option { + self.token.borrow().clone() + } + + /// Set the login token to use. This will be done automatically by the login method. + pub fn set_token(&self, token: &str) { + self.token.replace(Some(token.to_string())); + } + + /// Get the currently set server URL. + pub fn get_server_url(&self) -> Option { + self.server_url.borrow().clone() + } + + /// Get the currently stored login credentials. + pub fn get_login_data(&self) -> Option { + self.login_data.borrow().clone() + } + + /// Set the user credentials to use. + pub async fn set_login_data(&self, data: LoginData) -> Result<()> { + secure::store_login_data(data.clone()).await?; + self.login_data.replace(Some(data)); + Ok(()) + } + /// Try to login a user with the provided credentials and return, wether the login suceeded. pub async fn login(&self) -> Result { let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?; diff --git a/musicus/src/backend/library.rs b/musicus/src/backend/library.rs new file mode 100644 index 0000000..456eb03 --- /dev/null +++ b/musicus/src/backend/library.rs @@ -0,0 +1,73 @@ +use super::{Backend, BackendState}; +use crate::database::DbThread; +use crate::player::Player; +use anyhow::Result; +use gio::prelude::*; +use std::path::PathBuf; +use std::rc::Rc; + +impl Backend { + /// Initialize the music library if it is set in the settings. + pub(super) async fn init_library(&self) -> Result<()> { + if let Some(path) = self.settings.get_string("music-library-path") { + if !path.is_empty() { + self.set_music_library_path_priv(PathBuf::from(path.to_string())) + .await?; + } + } + + Ok(()) + } + + /// Set the path to the music library folder and start a database thread in the background. + pub async fn set_music_library_path(&self, path: PathBuf) -> Result<()> { + self.settings + .set_string("music-library-path", path.to_str().unwrap())?; + self.set_music_library_path_priv(path).await + } + + /// Set the path to the music library folder and start a database thread in the background. + pub async fn set_music_library_path_priv(&self, path: PathBuf) -> Result<()> { + self.set_state(BackendState::Loading); + + self.music_library_path.replace(Some(path.clone())); + + let mut db_path = path.clone(); + db_path.push("musicus.db"); + + let database = DbThread::new(db_path.to_str().unwrap().to_string()).await?; + self.database.replace(Some(Rc::new(database))); + + let player = Player::new(path); + self.player.replace(Some(player)); + + self.set_state(BackendState::Ready); + + Ok(()) + } + + /// Get the currently set music library path. + pub fn get_music_library_path(&self) -> Option { + self.music_library_path.borrow().clone() + } + + /// Get an interface to the current music library database. + pub fn get_database(&self) -> Option> { + self.database.borrow().clone() + } + + /// Get an interface to the database and panic if there is none. + pub fn db(&self) -> Rc { + self.get_database().unwrap() + } + + /// Get an interface to the playback service. + pub fn get_player(&self) -> Option> { + self.player.borrow().clone() + } + + /// Get an interface to the player and panic if there is none. + pub fn pl(&self) -> Rc { + self.get_player().unwrap() + } +} diff --git a/musicus/src/backend/mod.rs b/musicus/src/backend/mod.rs index 02ec571..ceb9aeb 100644 --- a/musicus/src/backend/mod.rs +++ b/musicus/src/backend/mod.rs @@ -1,7 +1,76 @@ -pub mod backend; -pub use backend::*; +use crate::database::DbThread; +use crate::player::Player; +use anyhow::Result; +use futures_channel::mpsc; +use std::cell::RefCell; +use std::path::PathBuf; +use std::rc::Rc; pub mod client; pub use client::*; -mod secure; \ No newline at end of file +pub mod library; +pub use library::*; + +mod secure; + +/// General states the application can be in. +pub enum BackendState { + /// The backend is not set up yet. This means that no backend methods except for setting the + /// music library path should be called. The user interface should adapt and only present this + /// option. + NoMusicLibrary, + + /// The backend is loading the music library. No methods should be called. The user interface + /// should represent that state by prohibiting all interaction. + Loading, + + /// The backend is ready and all methods may be called. + Ready, +} + +/// A collection of all backend state and functionality. +pub struct Backend { + pub state_stream: RefCell>, + state_sender: RefCell>, + settings: gio::Settings, + music_library_path: RefCell>, + database: RefCell>>, + player: RefCell>>, + server_url: RefCell>, + login_data: RefCell>, + token: RefCell>, +} + +impl Backend { + /// Create a new backend initerface. The user interface should subscribe to the state stream + /// and call init() afterwards. + pub fn new() -> Self { + let (state_sender, state_stream) = mpsc::channel(1024); + + Backend { + state_stream: RefCell::new(state_stream), + state_sender: RefCell::new(state_sender), + settings: gio::Settings::new("de.johrpan.musicus"), + music_library_path: RefCell::new(None), + database: RefCell::new(None), + player: RefCell::new(None), + server_url: RefCell::new(None), + login_data: RefCell::new(None), + token: RefCell::new(None), + } + } + + /// Initialize the backend updating the state accordingly. + pub async fn init(self: Rc) -> Result<()> { + self.init_library().await?; + self.init_client()?; + + Ok(()) + } + + /// Set the current state and notify the user interface. + fn set_state(&self, state: BackendState) { + self.state_sender.borrow_mut().try_send(state).unwrap(); + } +} diff --git a/musicus/src/database/database.rs b/musicus/src/database/database.rs deleted file mode 100644 index 39617a9..0000000 --- a/musicus/src/database/database.rs +++ /dev/null @@ -1,448 +0,0 @@ -use super::models::*; -use super::schema::*; -use super::tables::*; -use anyhow::{anyhow, Error, Result}; -use diesel::prelude::*; -use std::convert::TryInto; - -embed_migrations!(); - -pub struct Database { - c: SqliteConnection, -} - -impl Database { - pub fn new(path: &str) -> Result { - let c = SqliteConnection::establish(path)?; - - diesel::sql_query("PRAGMA foreign_keys = ON;").execute(&c)?; - embedded_migrations::run(&c)?; - - Ok(Database { c: c }) - } - - pub fn update_person(&self, person: Person) -> Result<()> { - self.defer_foreign_keys(); - self.c.transaction(|| { - diesel::replace_into(persons::table) - .values(person) - .execute(&self.c) - })?; - - Ok(()) - } - - pub fn get_person(&self, id: i64) -> Result { - persons::table - .filter(persons::id.eq(id)) - .load::(&self.c)? - .first() - .cloned() - .ok_or(anyhow!("No person with ID: {}", id)) - } - - pub fn delete_person(&self, id: i64) -> Result<()> { - diesel::delete(persons::table.filter(persons::id.eq(id))).execute(&self.c)?; - Ok(()) - } - - pub fn get_persons(&self) -> Result> { - let persons = persons::table.load::(&self.c)?; - Ok(persons) - } - - pub fn update_instrument(&self, instrument: Instrument) -> Result<()> { - self.defer_foreign_keys(); - self.c.transaction(|| { - diesel::replace_into(instruments::table) - .values(instrument) - .execute(&self.c) - })?; - - Ok(()) - } - - pub fn get_instrument(&self, id: i64) -> Result { - instruments::table - .filter(instruments::id.eq(id)) - .load::(&self.c)? - .first() - .cloned() - .ok_or(anyhow!("No instrument with ID: {}", id)) - } - - pub fn delete_instrument(&self, id: i64) -> Result<()> { - diesel::delete(instruments::table.filter(instruments::id.eq(id))).execute(&self.c)?; - Ok(()) - } - - pub fn get_instruments(&self) -> Result> { - let instruments = instruments::table.load::(&self.c)?; - Ok(instruments) - } - - pub fn update_work(&self, work_insertion: WorkInsertion) -> Result<()> { - let id = work_insertion.work.id; - - self.defer_foreign_keys(); - self.c.transaction::<(), Error, _>(|| { - self.delete_work(id)?; - - diesel::insert_into(works::table) - .values(work_insertion.work) - .execute(&self.c)?; - - for instrument_id in work_insertion.instrument_ids { - diesel::insert_into(instrumentations::table) - .values(Instrumentation { - id: rand::random(), - work: id, - instrument: instrument_id, - }) - .execute(&self.c)?; - } - - for part_insertion in work_insertion.parts { - let part_id = part_insertion.part.id; - - diesel::insert_into(work_parts::table) - .values(part_insertion.part) - .execute(&self.c)?; - - for instrument_id in part_insertion.instrument_ids { - diesel::insert_into(part_instrumentations::table) - .values(PartInstrumentation { - id: rand::random(), - work_part: part_id, - instrument: instrument_id, - }) - .execute(&self.c)?; - } - } - - for section in work_insertion.sections { - diesel::insert_into(work_sections::table) - .values(section) - .execute(&self.c)?; - } - - Ok(()) - })?; - - Ok(()) - } - - pub fn get_work(&self, id: i64) -> Result { - works::table - .filter(works::id.eq(id)) - .load::(&self.c)? - .first() - .cloned() - .ok_or(anyhow!("No work with ID: {}", id)) - } - - pub fn get_work_description_for_work(&self, work: &Work) -> Result { - let mut instruments: Vec = Vec::new(); - - let instrumentations = instrumentations::table - .filter(instrumentations::work.eq(work.id)) - .load::(&self.c)?; - - for instrumentation in instrumentations { - instruments.push(self.get_instrument(instrumentation.instrument)?); - } - - let mut part_descriptions: Vec = Vec::new(); - - let work_parts = work_parts::table - .filter(work_parts::work.eq(work.id)) - .load::(&self.c)?; - - for work_part in work_parts { - let mut part_instruments: Vec = Vec::new(); - - let part_instrumentations = part_instrumentations::table - .filter(part_instrumentations::work_part.eq(work_part.id)) - .load::(&self.c)?; - - for part_instrumentation in part_instrumentations { - part_instruments.push(self.get_instrument(part_instrumentation.instrument)?); - } - - part_descriptions.push(WorkPartDescription { - composer: match work_part.composer { - Some(composer) => Some(self.get_person(composer)?), - None => None, - }, - title: work_part.title.clone(), - instruments: part_instruments, - }); - } - - let mut section_descriptions: Vec = Vec::new(); - - let sections = work_sections::table - .filter(work_sections::work.eq(work.id)) - .load::(&self.c)?; - - for section in sections { - section_descriptions.push(WorkSectionDescription { - title: section.title.clone(), - before_index: section.before_index, - }); - } - - let work_description = WorkDescription { - id: work.id, - composer: self.get_person(work.composer)?, - title: work.title.clone(), - instruments: instruments, - parts: part_descriptions, - sections: section_descriptions, - }; - - Ok(work_description) - } - - pub fn get_work_description(&self, id: i64) -> Result { - let work = self.get_work(id)?; - let work_description = self.get_work_description_for_work(&work)?; - Ok(work_description) - } - - pub fn delete_work(&self, id: i64) -> Result<()> { - diesel::delete(works::table.filter(works::id.eq(id))).execute(&self.c)?; - Ok(()) - } - - pub fn get_works(&self, composer_id: i64) -> Result> { - let works = works::table - .filter(works::composer.eq(composer_id)) - .load::(&self.c)?; - - Ok(works) - } - - pub fn get_work_descriptions(&self, composer_id: i64) -> Result> { - let mut work_descriptions: Vec = Vec::new(); - - let works = self.get_works(composer_id)?; - for work in works { - work_descriptions.push(self.get_work_description_for_work(&work)?); - } - - Ok(work_descriptions) - } - - pub fn update_ensemble(&self, ensemble: Ensemble) -> Result<()> { - self.defer_foreign_keys(); - self.c.transaction(|| { - diesel::replace_into(ensembles::table) - .values(ensemble) - .execute(&self.c) - })?; - - Ok(()) - } - - pub fn get_ensemble(&self, id: i64) -> Result { - ensembles::table - .filter(ensembles::id.eq(id)) - .load::(&self.c)? - .first() - .cloned() - .ok_or(anyhow!("No ensemble with ID: {}", id)) - } - - pub fn delete_ensemble(&self, id: i64) -> Result<()> { - diesel::delete(ensembles::table.filter(ensembles::id.eq(id))).execute(&self.c)?; - Ok(()) - } - - pub fn get_ensembles(&self) -> Result> { - let ensembles = ensembles::table.load::(&self.c)?; - Ok(ensembles) - } - - pub fn update_recording(&self, recording_insertion: RecordingInsertion) -> Result<()> { - let id = recording_insertion.recording.id; - - self.defer_foreign_keys(); - self.c.transaction::<(), Error, _>(|| { - self.delete_recording(id)?; - - diesel::insert_into(recordings::table) - .values(recording_insertion.recording) - .execute(&self.c)?; - - for performance in recording_insertion.performances { - diesel::insert_into(performances::table) - .values(performance) - .execute(&self.c)?; - } - - Ok(()) - })?; - - Ok(()) - } - - pub fn get_recording(&self, id: i64) -> Result { - recordings::table - .filter(recordings::id.eq(id)) - .load::(&self.c)? - .first() - .cloned() - .ok_or(anyhow!("No recording with ID: {}", id)) - } - - pub fn get_recording_description_for_recording( - &self, - recording: &Recording, - ) -> Result { - let mut performance_descriptions: Vec = Vec::new(); - - let performances = performances::table - .filter(performances::recording.eq(recording.id)) - .load::(&self.c)?; - - for performance in performances { - performance_descriptions.push(PerformanceDescription { - person: match performance.person { - Some(id) => Some(self.get_person(id)?), - None => None, - }, - ensemble: match performance.ensemble { - Some(id) => Some(self.get_ensemble(id)?), - None => None, - }, - role: match performance.role { - Some(id) => Some(self.get_instrument(id)?), - None => None, - }, - }); - } - - Ok(RecordingDescription { - id: recording.id, - work: self.get_work_description(recording.work)?, - comment: recording.comment.clone(), - performances: performance_descriptions, - }) - } - - pub fn get_recording_description(&self, id: i64) -> Result { - let recording = self.get_recording(id)?; - let recording_description = self.get_recording_description_for_recording(&recording)?; - Ok(recording_description) - } - - pub fn get_recordings_for_person(&self, id: i64) -> Result> { - let mut recording_descriptions: Vec = Vec::new(); - - let recordings = 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(id)) - .select(recordings::table::all_columns()) - .load::(&self.c)?; - - for recording in recordings { - recording_descriptions.push(self.get_recording_description_for_recording(&recording)?); - } - - Ok(recording_descriptions) - } - - pub fn get_recordings_for_ensemble(&self, id: i64) -> Result> { - let mut recording_descriptions: Vec = Vec::new(); - - let recordings = 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(id)) - .select(recordings::table::all_columns()) - .load::(&self.c)?; - - for recording in recordings { - recording_descriptions.push(self.get_recording_description_for_recording(&recording)?); - } - - Ok(recording_descriptions) - } - - pub fn get_recordings_for_work(&self, id: i64) -> Result> { - let mut recording_descriptions: Vec = Vec::new(); - - let recordings = recordings::table - .inner_join(works::table.on(works::id.eq(recordings::work))) - .filter(works::id.eq(id)) - .select(recordings::table::all_columns()) - .load::(&self.c)?; - - for recording in recordings { - recording_descriptions.push(self.get_recording_description_for_recording(&recording)?); - } - - Ok(recording_descriptions) - } - - pub fn delete_recording(&self, id: i64) -> Result<()> { - self.delete_tracks(id)?; - diesel::delete(recordings::table.filter(recordings::id.eq(id))).execute(&self.c)?; - Ok(()) - } - - pub fn get_recordings(&self, work_id: i64) -> Result> { - let recordings = recordings::table - .filter(recordings::work.eq(work_id)) - .load::(&self.c)?; - - Ok(recordings) - } - - pub fn update_tracks(&self, recording_id: i64, tracks: Vec) -> Result<()> { - self.delete_tracks(recording_id)?; - - for (index, track_description) in tracks.iter().enumerate() { - let track = Track { - id: rand::random(), - file_name: track_description.file_name.clone(), - recording: recording_id, - track_index: index.try_into().unwrap(), - work_parts: track_description - .work_parts - .iter() - .map(|i| i.to_string()) - .collect::>() - .join(","), - }; - - diesel::insert_into(tracks::table) - .values(track) - .execute(&self.c)?; - } - - Ok(()) - } - - pub fn delete_tracks(&self, recording_id: i64) -> Result<()> { - diesel::delete(tracks::table.filter(tracks::recording.eq(recording_id))).execute(&self.c)?; - Ok(()) - } - - pub fn get_tracks(&self, recording_id: i64) -> Result> { - let tracks = tracks::table - .filter(tracks::recording.eq(recording_id)) - .order_by(tracks::track_index) - .load::(&self.c)?; - - Ok(tracks.iter().map(|track| track.clone().into()).collect()) - } - - fn defer_foreign_keys(&self) { - diesel::sql_query("PRAGMA defer_foreign_keys = ON;") - .execute(&self.c) - .expect("Failed to enable defer_foreign_keys_pragma!"); - } -} diff --git a/musicus/src/database/ensembles.rs b/musicus/src/database/ensembles.rs new file mode 100644 index 0000000..1b51deb --- /dev/null +++ b/musicus/src/database/ensembles.rs @@ -0,0 +1,96 @@ +use super::schema::ensembles; +use super::Database; +use anyhow::{Error, Result}; +use diesel::prelude::*; +use diesel::{Insertable, Queryable}; +use serde::{Deserialize, Serialize}; +use std::convert::{TryFrom, TryInto}; + +/// Database table data for an ensemble. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "ensembles"] +struct EnsembleRow { + pub id: i64, + pub name: String, +} + +impl From for EnsembleRow { + fn from(ensemble: Ensemble) -> Self { + EnsembleRow { + id: ensemble.id as i64, + name: ensemble.name, + } + } +} + +/// An ensemble that takes part in recordings. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Ensemble { + pub id: u32, + pub name: String, +} + +impl TryFrom for Ensemble { + type Error = Error; + fn try_from(row: EnsembleRow) -> Result { + let ensemble = Ensemble { + id: row.id.try_into()?, + name: row.name, + }; + + Ok(ensemble) + } +} + +impl Database { + /// Update an existing ensemble or insert a new one. + pub fn update_ensemble(&self, ensemble: Ensemble) -> Result<()> { + self.defer_foreign_keys()?; + + self.connection.transaction(|| { + let row: EnsembleRow = ensemble.into(); + diesel::replace_into(ensembles::table) + .values(row) + .execute(&self.connection) + })?; + + Ok(()) + } + + /// Get an existing ensemble. + pub fn get_ensemble(&self, id: u32) -> Result> { + let row = ensembles::table + .filter(ensembles::id.eq(id as i64)) + .load::(&self.connection)? + .first() + .cloned(); + + let ensemble = match row { + Some(row) => Some(row.try_into()?), + None => None, + }; + + Ok(ensemble) + } + + /// Delete an existing ensemble. + pub fn delete_ensemble(&self, id: u32) -> Result<()> { + diesel::delete(ensembles::table.filter(ensembles::id.eq(id as i64))) + .execute(&self.connection)?; + + Ok(()) + } + + /// Get all existing ensembles. + pub fn get_ensembles(&self) -> Result> { + let mut ensembles = Vec::::new(); + + let rows = ensembles::table.load::(&self.connection)?; + for row in rows { + ensembles.push(row.try_into()?); + } + + Ok(ensembles) + } +} diff --git a/musicus/src/database/instruments.rs b/musicus/src/database/instruments.rs new file mode 100644 index 0000000..83e0905 --- /dev/null +++ b/musicus/src/database/instruments.rs @@ -0,0 +1,96 @@ +use super::schema::instruments; +use super::Database; +use anyhow::{Error, Result}; +use diesel::prelude::*; +use diesel::{Insertable, Queryable}; +use serde::{Deserialize, Serialize}; +use std::convert::{TryFrom, TryInto}; + +/// Table row data for an instrument. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "instruments"] +struct InstrumentRow { + pub id: i64, + pub name: String, +} + +impl From for InstrumentRow { + fn from(instrument: Instrument) -> Self { + InstrumentRow { + id: instrument.id as i64, + name: instrument.name, + } + } +} + +/// An instrument or any other possible role within a recording. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Instrument { + pub id: u32, + pub name: String, +} + +impl TryFrom for Instrument { + type Error = Error; + fn try_from(row: InstrumentRow) -> Result { + let instrument = Instrument { + id: row.id.try_into()?, + name: row.name, + }; + + Ok(instrument) + } +} + +impl Database { + /// Update an existing instrument or insert a new one. + pub fn update_instrument(&self, instrument: Instrument) -> Result<()> { + self.defer_foreign_keys()?; + + self.connection.transaction(|| { + let row: InstrumentRow = instrument.into(); + diesel::replace_into(instruments::table) + .values(row) + .execute(&self.connection) + })?; + + Ok(()) + } + + /// Get an existing instrument. + pub fn get_instrument(&self, id: u32) -> Result> { + let row = instruments::table + .filter(instruments::id.eq(id as i64)) + .load::(&self.connection)? + .first() + .cloned(); + + let instrument = match row { + Some(row) => Some(row.try_into()?), + None => None, + }; + + Ok(instrument) + } + + /// Delete an existing instrument. + pub fn delete_instrument(&self, id: u32) -> Result<()> { + diesel::delete(instruments::table.filter(instruments::id.eq(id as i64))) + .execute(&self.connection)?; + + Ok(()) + } + + /// Get all existing instruments. + pub fn get_instruments(&self) -> Result> { + let mut instruments = Vec::::new(); + + let rows = instruments::table.load::(&self.connection)?; + for row in rows { + instruments.push(row.try_into()?); + } + + Ok(instruments) + } +} diff --git a/musicus/src/database/mod.rs b/musicus/src/database/mod.rs index 9ec5e08..409435f 100644 --- a/musicus/src/database/mod.rs +++ b/musicus/src/database/mod.rs @@ -1,10 +1,51 @@ -pub mod database; -pub use database::*; +use anyhow::Result; +use diesel::prelude::*; -pub mod models; -pub use models::*; +pub mod ensembles; +pub use ensembles::*; -pub mod schema; +pub mod instruments; +pub use instruments::*; -pub mod tables; -pub use tables::*; +pub mod persons; +pub use persons::*; + +pub mod recordings; +pub use recordings::*; + +pub mod thread; +pub use thread::*; + +pub mod tracks; +pub use tracks::*; + +pub mod works; +pub use works::*; + +mod schema; + +// This makes the SQL migration scripts accessible from the code. +embed_migrations!(); + +/// Interface to a Musicus database. +pub struct Database { + connection: SqliteConnection, +} + +impl Database { + /// Create a new database interface and run migrations if necessary. + pub fn new(file_name: &str) -> Result { + let connection = SqliteConnection::establish(file_name)?; + + diesel::sql_query("PRAGMA foreign_keys = ON").execute(&connection)?; + embedded_migrations::run(&connection)?; + + Ok(Database { connection }) + } + + /// Defer all foreign keys for the next transaction. + fn defer_foreign_keys(&self) -> Result<()> { + diesel::sql_query("PRAGMA defer_foreign_keys = ON").execute(&self.connection)?; + Ok(()) + } +} diff --git a/musicus/src/database/models.rs b/musicus/src/database/models.rs deleted file mode 100644 index e9cdd6b..0000000 --- a/musicus/src/database/models.rs +++ /dev/null @@ -1,205 +0,0 @@ -use super::tables::*; -use std::convert::TryInto; - -#[derive(Debug, Clone)] -pub struct WorkPartDescription { - pub title: String, - pub composer: Option, - pub instruments: Vec, -} - -#[derive(Debug, Clone)] -pub struct WorkSectionDescription { - pub title: String, - pub before_index: i64, -} - -#[derive(Debug, Clone)] -pub struct WorkDescription { - pub id: i64, - pub title: String, - pub composer: Person, - pub instruments: Vec, - pub parts: Vec, - pub sections: Vec, -} - -impl WorkDescription { - pub fn get_title(&self) -> String { - format!("{}: {}", self.composer.name_fl(), self.title) - } -} - -#[derive(Debug, Clone)] -pub struct WorkPartInsertion { - pub part: WorkPart, - pub instrument_ids: Vec, -} - -#[derive(Debug, Clone)] -pub struct WorkInsertion { - pub work: Work, - pub instrument_ids: Vec, - pub parts: Vec, - pub sections: Vec, -} - -impl From for WorkInsertion { - fn from(description: WorkDescription) -> Self { - WorkInsertion { - work: Work { - id: description.id, - composer: description.composer.id, - title: description.title.clone(), - }, - instrument_ids: description - .instruments - .iter() - .map(|instrument| instrument.id) - .collect(), - parts: description - .parts - .iter() - .enumerate() - .map(|(index, part)| WorkPartInsertion { - part: WorkPart { - id: rand::random(), - work: description.id, - part_index: index.try_into().expect("Part index didn't fit into u32!"), - composer: part.composer.as_ref().map(|person| person.id), - title: part.title.clone(), - }, - instrument_ids: part - .instruments - .iter() - .map(|instrument| instrument.id) - .collect(), - }) - .collect(), - sections: description - .sections - .iter() - .map(|section| WorkSection { - id: rand::random(), - work: description.id, - title: section.title.clone(), - before_index: section.before_index, - }) - .collect(), - } - } -} - -#[derive(Debug, Clone)] -pub struct PerformanceDescription { - pub person: Option, - pub ensemble: Option, - pub role: Option, -} - -impl PerformanceDescription { - pub fn get_title(&self) -> String { - let mut text = String::from(if self.is_person() { - self.unwrap_person().name_fl() - } else { - self.unwrap_ensemble().name - }); - - if self.has_role() { - text = text + " (" + &self.unwrap_role().name + ")"; - } - - text - } - - pub fn is_person(&self) -> bool { - self.person.is_some() - } - - pub fn unwrap_person(&self) -> Person { - self.person.clone().unwrap() - } - - pub fn unwrap_ensemble(&self) -> Ensemble { - self.ensemble.clone().unwrap() - } - - pub fn has_role(&self) -> bool { - self.role.clone().is_some() - } - - pub fn unwrap_role(&self) -> Instrument { - self.role.clone().unwrap() - } -} - -#[derive(Debug, Clone)] -pub struct RecordingDescription { - pub id: i64, - pub work: WorkDescription, - pub comment: String, - pub performances: Vec, -} - -impl RecordingDescription { - pub fn get_performers(&self) -> String { - let texts: Vec = self - .performances - .iter() - .map(|performance| performance.get_title()) - .collect(); - - texts.join(", ") - } -} - -#[derive(Debug, Clone)] -pub struct RecordingInsertion { - pub recording: Recording, - pub performances: Vec, -} - -impl From for RecordingInsertion { - fn from(description: RecordingDescription) -> Self { - RecordingInsertion { - recording: Recording { - id: description.id, - work: description.work.id, - comment: description.comment.clone(), - }, - performances: description - .performances - .iter() - .map(|performance| Performance { - id: rand::random(), - recording: description.id, - person: performance.person.as_ref().map(|person| person.id), - ensemble: performance.ensemble.as_ref().map(|ensemble| ensemble.id), - role: performance.role.as_ref().map(|role| role.id), - }) - .collect(), - } - } -} - -#[derive(Debug, Clone)] -pub struct TrackDescription { - pub work_parts: Vec, - pub file_name: String, -} - -impl From for TrackDescription { - fn from(track: Track) -> Self { - let mut work_parts = Vec::::new(); - for part in track.work_parts.split(",") { - if !part.is_empty() { - work_parts.push(part.parse().unwrap()); - } - } - - TrackDescription { - work_parts, - file_name: track.file_name, - } - } -} diff --git a/musicus/src/database/persons.rs b/musicus/src/database/persons.rs new file mode 100644 index 0000000..7c67c8f --- /dev/null +++ b/musicus/src/database/persons.rs @@ -0,0 +1,111 @@ +use super::schema::persons; +use super::Database; +use anyhow::{Error, Result}; +use diesel::prelude::*; +use diesel::{Insertable, Queryable}; +use serde::{Deserialize, Serialize}; +use std::convert::{TryFrom, TryInto}; + +/// Database table data for a person. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "persons"] +struct PersonRow { + pub id: i64, + pub first_name: String, + pub last_name: String, +} + +impl From for PersonRow { + fn from(person: Person) -> Self { + PersonRow { + id: person.id as i64, + first_name: person.first_name, + last_name: person.last_name, + } + } +} + +/// A person that is a composer, an interpret or both. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Person { + pub id: u32, + pub first_name: String, + pub last_name: String, +} + +impl TryFrom for Person { + type Error = Error; + fn try_from(row: PersonRow) -> Result { + let person = Person { + id: row.id.try_into()?, + first_name: row.first_name, + last_name: row.last_name, + }; + + Ok(person) + } +} + +impl Person { + /// 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) + } +} + +impl Database { + /// Update an existing person or insert a new one. + pub fn update_person(&self, person: Person) -> Result<()> { + self.defer_foreign_keys()?; + + self.connection.transaction(|| { + let row: PersonRow = person.into(); + diesel::replace_into(persons::table) + .values(row) + .execute(&self.connection) + })?; + + Ok(()) + } + + /// Get an existing person. + pub fn get_person(&self, id: u32) -> Result> { + let row = persons::table + .filter(persons::id.eq(id as i64)) + .load::(&self.connection)? + .first() + .cloned(); + + let person = match row { + Some(row) => Some(row.try_into()?), + None => None, + }; + + Ok(person) + } + + /// Delete an existing person. + pub fn delete_person(&self, id: u32) -> Result<()> { + diesel::delete(persons::table.filter(persons::id.eq(id as i64))) + .execute(&self.connection)?; + Ok(()) + } + + /// Get all existing persons. + pub fn get_persons(&self) -> Result> { + let mut persons = Vec::::new(); + + let rows = persons::table.load::(&self.connection)?; + for row in rows { + persons.push(row.try_into()?); + } + + Ok(persons) + } +} diff --git a/musicus/src/database/recordings.rs b/musicus/src/database/recordings.rs new file mode 100644 index 0000000..9284679 --- /dev/null +++ b/musicus/src/database/recordings.rs @@ -0,0 +1,252 @@ +use super::schema::{ensembles, performances, persons, recordings}; +use super::{Database, Ensemble, Instrument, Person, Work}; +use anyhow::{anyhow, Error, Result}; +use diesel::prelude::*; +use diesel::{Insertable, Queryable}; +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; + +/// Database table data for a recording. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "recordings"] +struct RecordingRow { + pub id: i64, + pub work: i64, + pub comment: String, +} + +impl From for RecordingRow { + fn from(recording: Recording) -> Self { + RecordingRow { + id: recording.id as i64, + work: recording.work.id as i64, + comment: recording.comment, + } + } +} + +/// Database table data for a performance. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "performances"] +struct PerformanceRow { + pub id: i64, + pub recording: i64, + pub person: Option, + pub ensemble: Option, + pub role: Option, +} + +/// How a person or ensemble was involved in a recording. +// TODO: Replace person/ensemble with an enum. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Performance { + pub person: Option, + pub ensemble: Option, + pub role: Option, +} + +impl Performance { + /// Get a string representation of the performance. + // TODO: Replace with impl Display. + pub fn get_title(&self) -> String { + let mut text = String::from(if self.is_person() { + self.unwrap_person().name_fl() + } else { + self.unwrap_ensemble().name + }); + + if self.has_role() { + text = text + " (" + &self.unwrap_role().name + ")"; + } + + text + } + + pub fn is_person(&self) -> bool { + self.person.is_some() + } + + pub fn unwrap_person(&self) -> Person { + self.person.clone().unwrap() + } + + pub fn unwrap_ensemble(&self) -> Ensemble { + self.ensemble.clone().unwrap() + } + + pub fn has_role(&self) -> bool { + self.role.clone().is_some() + } + + pub fn unwrap_role(&self) -> Instrument { + self.role.clone().unwrap() + } +} + +/// A specific recording of a work. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Recording { + pub id: u32, + pub work: Work, + pub comment: String, + pub performances: Vec, +} + +impl Recording { + /// 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(", ") + } +} + +impl Database { + /// Update an existing recording or insert a new one. + // TODO: Think about whether to also insert the other items. + pub fn update_recording(&self, recording: Recording) -> Result<()> { + self.defer_foreign_keys()?; + self.connection.transaction::<(), Error, _>(|| { + self.delete_recording(recording.id)?; + + let recording_id = recording.id as i64; + let row: RecordingRow = recording.clone().into(); + diesel::insert_into(recordings::table) + .values(row) + .execute(&self.connection)?; + + for performance in recording.performances { + let row = PerformanceRow { + id: rand::random(), + recording: recording_id, + person: performance.person.map(|person| person.id as i64), + ensemble: performance.ensemble.map(|ensemble| ensemble.id as i64), + role: performance.role.map(|role| role.id as i64), + }; + + diesel::insert_into(performances::table) + .values(row) + .execute(&self.connection)?; + } + + Ok(()) + })?; + + Ok(()) + } + + /// Retrieve all available information on a recording from related tables. + fn get_recording_data(&self, row: RecordingRow) -> Result { + let mut performance_descriptions: Vec = Vec::new(); + + let performance_rows = performances::table + .filter(performances::recording.eq(row.id)) + .load::(&self.connection)?; + + for row in performance_rows { + performance_descriptions.push(Performance { + person: match row.person { + Some(id) => Some( + self.get_person(id.try_into()?)? + .ok_or(anyhow!("No person with ID: {}", id))?, + ), + None => None, + }, + ensemble: match row.ensemble { + Some(id) => Some( + self.get_ensemble(id.try_into()?)? + .ok_or(anyhow!("No ensemble with ID: {}", id))?, + ), + None => None, + }, + role: match row.role { + Some(id) => Some( + self.get_instrument(id.try_into()?)? + .ok_or(anyhow!("No instrument with ID: {}", id))?, + ), + None => None, + }, + }); + } + + let work_id: u32 = row.work.try_into()?; + let work = self + .get_work(work_id)? + .ok_or(anyhow!("Work doesn't exist: {}", work_id))?; + + let recording_description = Recording { + id: row.id.try_into()?, + work, + comment: row.comment.clone(), + performances: performance_descriptions, + }; + + Ok(recording_description) + } + + /// Get all available information on all recordings where a person is performing. + pub fn get_recordings_for_person(&self, person_id: u32) -> 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 as i64)) + .select(recordings::table::all_columns()) + .load::(&self.connection)?; + + for row in rows { + recordings.push(self.get_recording_data(row)?); + } + + Ok(recordings) + } + + /// Get all available information on all recordings where an ensemble is performing. + pub fn get_recordings_for_ensemble(&self, ensemble_id: u32) -> 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 as i64)) + .select(recordings::table::all_columns()) + .load::(&self.connection)?; + + for row in rows { + recordings.push(self.get_recording_data(row)?); + } + + Ok(recordings) + } + + /// Get allavailable information on all recordings of a work. + pub fn get_recordings_for_work(&self, work_id: u32) -> Result> { + let mut recordings: Vec = Vec::new(); + + let rows = recordings::table + .filter(recordings::work.eq(work_id as i64)) + .load::(&self.connection)?; + + for row in rows { + recordings.push(self.get_recording_data(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(&self, id: u32) -> Result<()> { + diesel::delete(recordings::table.filter(recordings::id.eq(id as i64))) + .execute(&self.connection)?; + Ok(()) + } +} diff --git a/musicus/src/database/schema.rs b/musicus/src/database/schema.rs index f44ac19..8c7c441 100644 --- a/musicus/src/database/schema.rs +++ b/musicus/src/database/schema.rs @@ -20,14 +20,6 @@ table! { } } -table! { - part_instrumentations (id) { - id -> BigInt, - work_part -> BigInt, - instrument -> BigInt, - } -} - table! { performances (id) { id -> BigInt, @@ -69,8 +61,8 @@ table! { id -> BigInt, work -> BigInt, part_index -> BigInt, - composer -> Nullable, title -> Text, + composer -> Nullable, } } @@ -93,8 +85,6 @@ table! { joinable!(instrumentations -> instruments (instrument)); joinable!(instrumentations -> works (work)); -joinable!(part_instrumentations -> instruments (instrument)); -joinable!(part_instrumentations -> works (work_part)); joinable!(performances -> ensembles (ensemble)); joinable!(performances -> instruments (role)); joinable!(performances -> persons (person)); @@ -110,7 +100,6 @@ allow_tables_to_appear_in_same_query!( ensembles, instrumentations, instruments, - part_instrumentations, performances, persons, recordings, diff --git a/musicus/src/database/tables.rs b/musicus/src/database/tables.rs deleted file mode 100644 index 894a0c3..0000000 --- a/musicus/src/database/tables.rs +++ /dev/null @@ -1,94 +0,0 @@ -use super::schema::*; -use diesel::Queryable; - -#[derive(Insertable, Queryable, Debug, Clone)] -pub struct Person { - pub id: i64, - pub first_name: String, - pub last_name: String, -} - -impl Person { - pub fn name_fl(&self) -> String { - format!("{} {}", self.first_name, self.last_name) - } - - pub fn name_lf(&self) -> String { - format!("{}, {}", self.last_name, self.first_name) - } -} - -#[derive(Insertable, Queryable, Debug, Clone)] -pub struct Instrument { - pub id: i64, - pub name: String, -} - -#[derive(Insertable, Queryable, Debug, Clone)] -pub struct Work { - pub id: i64, - pub composer: i64, - pub title: String, -} - -#[derive(Insertable, Queryable, Debug, Clone)] -pub struct Instrumentation { - pub id: i64, - pub work: i64, - pub instrument: i64, -} - -#[derive(Insertable, Queryable, Debug, Clone)] -pub struct WorkPart { - pub id: i64, - pub work: i64, - pub part_index: i64, - pub composer: Option, - pub title: String, -} - -#[derive(Insertable, Queryable, Debug, Clone)] -pub struct PartInstrumentation { - pub id: i64, - pub work_part: i64, - pub instrument: i64, -} - -#[derive(Insertable, Queryable, Debug, Clone)] -pub struct WorkSection { - pub id: i64, - pub work: i64, - pub title: String, - pub before_index: i64, -} - -#[derive(Insertable, Queryable, Debug, Clone)] -pub struct Ensemble { - pub id: i64, - pub name: String, -} - -#[derive(Insertable, Queryable, Debug, Clone)] -pub struct Recording { - pub id: i64, - pub work: i64, - pub comment: String, -} - -#[derive(Insertable, Queryable, Debug, Clone)] -pub struct Performance { - pub id: i64, - pub recording: i64, - pub person: Option, - pub ensemble: Option, - pub role: Option, -} - -#[derive(Insertable, Queryable, Debug, Clone)] -pub struct Track { - pub id: i64, - pub file_name: String, - pub recording: i64, - pub track_index: i32, - pub work_parts: String, -} diff --git a/musicus/src/database/thread.rs b/musicus/src/database/thread.rs new file mode 100644 index 0000000..7681d04 --- /dev/null +++ b/musicus/src/database/thread.rs @@ -0,0 +1,327 @@ +use super::*; +use anyhow::Result; +use futures_channel::oneshot; +use futures_channel::oneshot::Sender; +use std::sync::mpsc; +use std::thread; + +/// An action the database thread can perform. +enum Action { + UpdatePerson(Person, Sender>), + GetPerson(u32, Sender>>), + DeletePerson(u32, Sender>), + GetPersons(Sender>>), + UpdateInstrument(Instrument, Sender>), + GetInstrument(u32, Sender>>), + DeleteInstrument(u32, Sender>), + GetInstruments(Sender>>), + UpdateWork(Work, Sender>), + DeleteWork(u32, Sender>), + GetWorks(u32, Sender>>), + UpdateEnsemble(Ensemble, Sender>), + GetEnsemble(u32, Sender>>), + DeleteEnsemble(u32, Sender>), + GetEnsembles(Sender>>), + UpdateRecording(Recording, Sender>), + DeleteRecording(u32, Sender>), + GetRecordingsForPerson(u32, Sender>>), + GetRecordingsForEnsemble(u32, Sender>>), + GetRecordingsForWork(u32, Sender>>), + UpdateTracks(u32, Vec, Sender>), + DeleteTracks(u32, Sender>), + GetTracks(u32, Sender>>), + Stop(Sender<()>), +} + +use Action::*; + +/// A database running within a thread. +pub struct DbThread { + action_sender: mpsc::Sender, +} + +impl DbThread { + /// Create a new database connection in a background thread. + pub async fn new(path: String) -> Result { + let (action_sender, action_receiver) = mpsc::channel(); + let (ready_sender, ready_receiver) = oneshot::channel(); + + thread::spawn(move || { + let db = match Database::new(&path) { + Ok(db) => { + ready_sender.send(Ok(())).unwrap(); + db + } + Err(error) => { + ready_sender.send(Err(error)).unwrap(); + return; + } + }; + + for action in action_receiver { + match action { + UpdatePerson(person, sender) => { + sender.send(db.update_person(person)).unwrap(); + } + GetPerson(id, sender) => { + sender.send(db.get_person(id)).unwrap(); + } + DeletePerson(id, sender) => { + sender.send(db.delete_person(id)).unwrap(); + } + GetPersons(sender) => { + sender.send(db.get_persons()).unwrap(); + } + UpdateInstrument(instrument, sender) => { + sender.send(db.update_instrument(instrument)).unwrap(); + } + GetInstrument(id, sender) => { + sender.send(db.get_instrument(id)).unwrap(); + } + DeleteInstrument(id, sender) => { + sender.send(db.delete_instrument(id)).unwrap(); + } + GetInstruments(sender) => { + sender.send(db.get_instruments()).unwrap(); + } + UpdateWork(work, sender) => { + sender.send(db.update_work(work)).unwrap(); + } + DeleteWork(id, sender) => { + sender.send(db.delete_work(id)).unwrap(); + } + GetWorks(id, sender) => { + sender.send(db.get_works(id)).unwrap(); + } + UpdateEnsemble(ensemble, sender) => { + sender.send(db.update_ensemble(ensemble)).unwrap(); + } + GetEnsemble(id, sender) => { + sender.send(db.get_ensemble(id)).unwrap(); + } + DeleteEnsemble(id, sender) => { + sender.send(db.delete_ensemble(id)).unwrap(); + } + GetEnsembles(sender) => { + sender.send(db.get_ensembles()).unwrap(); + } + UpdateRecording(recording, sender) => { + sender.send(db.update_recording(recording)).unwrap(); + } + DeleteRecording(id, sender) => { + sender.send(db.delete_recording(id)).unwrap(); + } + GetRecordingsForPerson(id, sender) => { + sender.send(db.get_recordings_for_person(id)).unwrap(); + } + GetRecordingsForEnsemble(id, sender) => { + sender.send(db.get_recordings_for_ensemble(id)).unwrap(); + } + GetRecordingsForWork(id, sender) => { + sender.send(db.get_recordings_for_work(id)).unwrap(); + } + UpdateTracks(recording_id, tracks, sender) => { + sender.send(db.update_tracks(recording_id, tracks)).unwrap(); + } + DeleteTracks(recording_id, sender) => { + sender.send(db.delete_tracks(recording_id)).unwrap(); + } + GetTracks(recording_id, sender) => { + sender.send(db.get_tracks(recording_id)).unwrap(); + } + Stop(sender) => { + sender.send(()).unwrap(); + break; + } + } + } + }); + + ready_receiver.await??; + Ok(Self { action_sender }) + } + + /// Update an existing person or insert a new one. + pub async fn update_person(&self, person: Person) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(UpdatePerson(person, sender))?; + receiver.await? + } + + /// Get an existing person. + pub async fn get_person(&self, id: u32) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(GetPerson(id, sender))?; + receiver.await? + } + + /// Delete an existing person. This will fail, if there are still other items referencing + /// this person. + pub async fn delete_person(&self, id: u32) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(DeletePerson(id, sender))?; + receiver.await? + } + + /// Get all existing persons. + pub async fn get_persons(&self) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(GetPersons(sender))?; + receiver.await? + } + + /// Update an existing instrument or insert a new one. + pub async fn update_instrument(&self, instrument: Instrument) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender + .send(UpdateInstrument(instrument, sender))?; + receiver.await? + } + + /// Get an existing instrument. + pub async fn get_instrument(&self, id: u32) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(GetInstrument(id, sender))?; + receiver.await? + } + + /// Delete an existing instrument. This will fail, if there are still other items referencing + /// this instrument. + pub async fn delete_instrument(&self, id: u32) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(DeleteInstrument(id, sender))?; + receiver.await? + } + + /// Get all existing instruments. + pub async fn get_instruments(&self) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(GetInstruments(sender))?; + receiver.await? + } + + /// Update an existing work or insert a new one. + pub async fn update_work(&self, work: Work) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(UpdateWork(work, sender))?; + receiver.await? + } + + /// Delete an existing work. This will fail, if there are still other items referencing + /// this work. + pub async fn delete_work(&self, id: u32) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(DeleteWork(id, sender))?; + receiver.await? + } + + /// Get information on all existing works by a composer. + pub async fn get_works(&self, person_id: u32) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(GetWorks(person_id, sender))?; + receiver.await? + } + + /// Update an existing ensemble or insert a new one. + pub async fn update_ensemble(&self, ensemble: Ensemble) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(UpdateEnsemble(ensemble, sender))?; + receiver.await? + } + + /// Get an existing ensemble. + pub async fn get_ensemble(&self, id: u32) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(GetEnsemble(id, sender))?; + receiver.await? + } + + /// Delete an existing ensemble. This will fail, if there are still other items referencing + /// this ensemble. + pub async fn delete_ensemble(&self, id: u32) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(DeleteEnsemble(id, sender))?; + receiver.await? + } + + /// Get all existing ensembles. + pub async fn get_ensembles(&self) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(GetEnsembles(sender))?; + receiver.await? + } + + /// Update an existing recording or insert a new one. + pub async fn update_recording(&self, recording: Recording) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender + .send(UpdateRecording(recording, sender))?; + receiver.await? + } + + /// Delete an existing recording. + pub async fn delete_recording(&self, id: u32) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(DeleteRecording(id, sender))?; + receiver.await? + } + + /// Get information on all recordings in which a person performs. + pub async fn get_recordings_for_person(&self, person_id: u32) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.action_sender + .send(GetRecordingsForPerson(person_id, sender))?; + receiver.await? + } + + /// Get information on all recordings in which an ensemble performs. + pub async fn get_recordings_for_ensemble(&self, ensemble_id: u32) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.action_sender + .send(GetRecordingsForEnsemble(ensemble_id, sender))?; + receiver.await? + } + + /// Get information on all recordings of a work. + pub async fn get_recordings_for_work(&self, work_id: u32) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.action_sender + .send(GetRecordingsForWork(work_id, sender))?; + receiver.await? + } + + /// Add or change the tracks associated with a recording. This will fail, if there are still + /// other items referencing this recording. + pub async fn update_tracks( + &self, + recording_id: u32, + tracks: Vec, + ) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender + .send(UpdateTracks(recording_id, tracks, sender))?; + receiver.await? + } + + /// Delete all tracks associated with a recording. + pub async fn delete_tracks(&self, recording_id: u32) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender + .send(DeleteTracks(recording_id, sender))?; + receiver.await? + } + + /// Get all tracks associated with a recording. + pub async fn get_tracks(&self, recording_id: u32) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(GetTracks(recording_id, sender))?; + receiver.await? + } + + /// Stop the database thread. Any future access to the database will fail. + pub async fn stop(&self) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender.send(Stop(sender))?; + Ok(receiver.await?) + } +} diff --git a/musicus/src/database/tracks.rs b/musicus/src/database/tracks.rs new file mode 100644 index 0000000..1c758ab --- /dev/null +++ b/musicus/src/database/tracks.rs @@ -0,0 +1,94 @@ +use super::schema::tracks; +use super::Database; +use anyhow::{Error, Result}; +use diesel::prelude::*; +use std::convert::{TryFrom, TryInto}; + +/// Table row data for a track. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "tracks"] +struct TrackRow { + pub id: i64, + pub file_name: String, + pub recording: i64, + pub track_index: i32, + pub work_parts: String, +} + +/// A structure representing one playable audio file. +#[derive(Debug, Clone)] +pub struct Track { + pub work_parts: Vec, + pub file_name: String, +} + +impl TryFrom for Track { + type Error = Error; + fn try_from(row: TrackRow) -> Result { + let mut work_parts = Vec::::new(); + for part in row.work_parts.split(",") { + if !part.is_empty() { + work_parts.push(part.parse()?); + } + } + + let track = Track { + work_parts, + file_name: row.file_name, + }; + + Ok(track) + } +} + +impl Database { + /// Insert or update tracks for the specified recording. + pub fn update_tracks(&self, recording_id: u32, tracks: Vec) -> Result<()> { + self.delete_tracks(recording_id)?; + + for (index, track) in tracks.iter().enumerate() { + let row = TrackRow { + id: rand::random(), + file_name: track.file_name.clone(), + recording: recording_id as i64, + track_index: index.try_into()?, + work_parts: track + .work_parts + .iter() + .map(|i| i.to_string()) + .collect::>() + .join(","), + }; + + diesel::insert_into(tracks::table) + .values(row) + .execute(&self.connection)?; + } + + Ok(()) + } + + /// Delete all tracks for the specified recording. + pub fn delete_tracks(&self, recording_id: u32) -> Result<()> { + diesel::delete(tracks::table.filter(tracks::recording.eq(recording_id as i64))) + .execute(&self.connection)?; + + Ok(()) + } + + /// Get all tracks of the specified recording. + pub fn get_tracks(&self, recording_id: u32) -> Result> { + let mut tracks = Vec::::new(); + + let rows = tracks::table + .filter(tracks::recording.eq(recording_id as i64)) + .order_by(tracks::track_index) + .load::(&self.connection)?; + + for row in rows { + tracks.push(row.try_into()?); + } + + Ok(tracks) + } +} diff --git a/musicus/src/database/works.rs b/musicus/src/database/works.rs new file mode 100644 index 0000000..cbc92d4 --- /dev/null +++ b/musicus/src/database/works.rs @@ -0,0 +1,262 @@ +use super::schema::{instrumentations, work_parts, work_sections, works}; +use super::{Database, Instrument, Person}; +use anyhow::{anyhow, Error, Result}; +use diesel::prelude::*; +use diesel::{Insertable, Queryable}; +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; + +/// Table row data for a work. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "works"] +struct WorkRow { + pub id: i64, + pub composer: i64, + pub title: String, +} + +impl From for WorkRow { + fn from(work: Work) -> Self { + WorkRow { + id: work.id as i64, + composer: work.composer.id as i64, + title: work.title, + } + } +} + +/// Definition that a work uses an instrument. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "instrumentations"] +struct InstrumentationRow { + pub id: i64, + pub work: i64, + pub instrument: i64, +} + +/// Table row data for a work part. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "work_parts"] +struct WorkPartRow { + pub id: i64, + pub work: i64, + pub part_index: i64, + pub title: String, + pub composer: Option, +} + +/// Table row data for a work section. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "work_sections"] +struct WorkSectionRow { + pub id: i64, + pub work: i64, + pub title: String, + pub before_index: i64, +} +/// A concrete work part that can be recorded. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkPart { + pub title: String, + pub composer: Option, +} + +/// A heading between work parts. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkSection { + pub title: String, + pub before_index: usize, +} + +/// A specific work by a composer. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Work { + pub id: u32, + pub title: String, + pub composer: Person, + pub instruments: Vec, + pub parts: Vec, + pub sections: Vec, +} + +impl Work { + /// 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) + } +} + +impl Database { + /// Update an existing work or insert a new one. + // TODO: Think about also inserting related items. + pub fn update_work(&self, work: Work) -> Result<()> { + self.defer_foreign_keys()?; + + self.connection.transaction::<(), Error, _>(|| { + self.delete_work(work.id)?; + + let work_id = work.id as i64; + let row: WorkRow = work.clone().into(); + diesel::insert_into(works::table) + .values(row) + .execute(&self.connection)?; + + match work { + Work { + instruments, + parts, + sections, + .. + } => { + for instrument in instruments { + let row = InstrumentationRow { + id: rand::random(), + work: work_id, + instrument: instrument.id as i64, + }; + + diesel::insert_into(instrumentations::table) + .values(row) + .execute(&self.connection)?; + } + + for (index, part) in parts.into_iter().enumerate() { + let row = WorkPartRow { + id: rand::random(), + work: work_id, + part_index: index.try_into()?, + title: part.title, + composer: part.composer.map(|person| person.id as i64), + }; + + diesel::insert_into(work_parts::table) + .values(row) + .execute(&self.connection)?; + } + + for section in sections { + let row = WorkSectionRow { + id: rand::random(), + work: work_id, + title: section.title, + before_index: section.before_index.try_into()?, + }; + + diesel::insert_into(work_sections::table) + .values(row) + .execute(&self.connection)?; + } + } + } + + Ok(()) + })?; + + Ok(()) + } + + /// Get an existing work. + pub fn get_work(&self, id: u32) -> Result> { + let row = works::table + .filter(works::id.eq(id as i64)) + .load::(&self.connection)? + .first() + .cloned(); + + let work = match row { + Some(row) => Some(self.get_work_data(row)?), + None => None, + }; + + Ok(work) + } + + /// Retrieve all available information on a work from related tables. + fn get_work_data(&self, row: WorkRow) -> Result { + let mut instruments: Vec = Vec::new(); + + let instrumentations = instrumentations::table + .filter(instrumentations::work.eq(row.id)) + .load::(&self.connection)?; + + for instrumentation in instrumentations { + let id: u32 = instrumentation.instrument.try_into()?; + instruments.push( + self.get_instrument(id)? + .ok_or(anyhow!("No instrument with ID: {}", id))?, + ); + } + + let mut parts: Vec = Vec::new(); + + let part_rows = work_parts::table + .filter(work_parts::work.eq(row.id)) + .load::(&self.connection)?; + + for part_row in part_rows { + parts.push(WorkPart { + title: part_row.title, + composer: match part_row.composer { + Some(composer) => Some( + self.get_person(composer.try_into()?)? + .ok_or(anyhow!("No person with ID: {}", composer))?, + ), + None => None, + }, + }); + } + + let mut sections: Vec = Vec::new(); + + let section_rows = work_sections::table + .filter(work_sections::work.eq(row.id)) + .load::(&self.connection)?; + + for section_row in section_rows { + sections.push(WorkSection { + title: section_row.title, + before_index: section_row.before_index.try_into()?, + }); + } + + let person_id = row.composer.try_into()?; + let person = self + .get_person(person_id)? + .ok_or(anyhow!("Person doesn't exist: {}", person_id))?; + + Ok(Work { + id: row.id.try_into()?, + composer: person, + title: row.title, + instruments, + parts, + sections, + }) + } + + /// 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(&self, id: u32) -> Result<()> { + diesel::delete(works::table.filter(works::id.eq(id as i64))).execute(&self.connection)?; + Ok(()) + } + + /// Get all existing works by a composer and related information from other tables. + pub fn get_works(&self, composer_id: u32) -> Result> { + let mut works: Vec = Vec::new(); + + let rows = works::table + .filter(works::composer.eq(composer_id as i64)) + .load::(&self.connection)?; + + for row in rows { + works.push(self.get_work_data(row)?); + } + + Ok(works) + } +} diff --git a/musicus/src/dialogs/ensemble_editor.rs b/musicus/src/dialogs/ensemble_editor.rs index 84faec6..ba5ccf9 100644 --- a/musicus/src/dialogs/ensemble_editor.rs +++ b/musicus/src/dialogs/ensemble_editor.rs @@ -12,7 +12,7 @@ where backend: Rc, window: libhandy::Window, callback: F, - id: i64, + id: u32, name_entry: gtk::Entry, } @@ -26,8 +26,7 @@ where ensemble: Option, callback: F, ) -> Rc { - let builder = - gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_editor.ui"); + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_editor.ui"); get_widget!(builder, libhandy::Window, window); get_widget!(builder, gtk::Button, cancel_button); @@ -63,7 +62,7 @@ where let clone = result.clone(); let c = glib::MainContext::default(); c.spawn_local(async move { - clone.backend.update_ensemble(ensemble.clone()).await.unwrap(); + clone.backend.db().update_ensemble(ensemble.clone()).await.unwrap(); clone.window.close(); (clone.callback)(ensemble.clone()); }); diff --git a/musicus/src/dialogs/ensemble_selector.rs b/musicus/src/dialogs/ensemble_selector.rs index 0d64225..516fae5 100644 --- a/musicus/src/dialogs/ensemble_selector.rs +++ b/musicus/src/dialogs/ensemble_selector.rs @@ -25,8 +25,7 @@ where F: Fn(Ensemble) -> () + 'static, { pub fn new>(backend: Rc, parent: &P, callback: F) -> Rc { - let builder = - gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_selector.ui"); + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_selector.ui"); get_widget!(builder, libhandy::Window, window); get_widget!(builder, gtk::Button, add_button); @@ -44,7 +43,7 @@ where let c = glib::MainContext::default(); let clone = result.clone(); c.spawn_local(async move { - let ensembles = clone.backend.get_ensembles().await.unwrap(); + let ensembles = clone.backend.db().get_ensembles().await.unwrap(); for (index, ensemble) in ensembles.iter().enumerate() { let label = gtk::Label::new(Some(&ensemble.name)); diff --git a/musicus/src/dialogs/instrument_editor.rs b/musicus/src/dialogs/instrument_editor.rs index 5a93810..1b397a6 100644 --- a/musicus/src/dialogs/instrument_editor.rs +++ b/musicus/src/dialogs/instrument_editor.rs @@ -12,7 +12,7 @@ where backend: Rc, window: libhandy::Window, callback: F, - id: i64, + id: u32, name_entry: gtk::Entry, } @@ -63,7 +63,7 @@ where let c = glib::MainContext::default(); let clone = result.clone(); c.spawn_local(async move { - clone.backend.update_instrument(instrument.clone()).await.unwrap(); + clone.backend.db().update_instrument(instrument.clone()).await.unwrap(); clone.window.close(); (clone.callback)(instrument.clone()); }); diff --git a/musicus/src/dialogs/instrument_selector.rs b/musicus/src/dialogs/instrument_selector.rs index 381ce23..172e0da 100644 --- a/musicus/src/dialogs/instrument_selector.rs +++ b/musicus/src/dialogs/instrument_selector.rs @@ -25,8 +25,7 @@ where F: Fn(Instrument) -> () + 'static, { pub fn new>(backend: Rc, parent: &P, callback: F) -> Rc { - let builder = - gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_selector.ui"); + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_selector.ui"); get_widget!(builder, libhandy::Window, window); get_widget!(builder, gtk::Button, add_button); @@ -44,7 +43,7 @@ where let c = glib::MainContext::default(); let clone = result.clone(); c.spawn_local(async move { - let instruments = clone.backend.get_instruments().await.unwrap(); + let instruments = clone.backend.db().get_instruments().await.unwrap(); for (index, instrument) in instruments.iter().enumerate() { let label = gtk::Label::new(Some(&instrument.name)); diff --git a/musicus/src/dialogs/person_editor.rs b/musicus/src/dialogs/person_editor.rs index acb890b..0457a78 100644 --- a/musicus/src/dialogs/person_editor.rs +++ b/musicus/src/dialogs/person_editor.rs @@ -12,7 +12,7 @@ where backend: Rc, window: libhandy::Window, callback: F, - id: i64, + id: u32, first_name_entry: gtk::Entry, last_name_entry: gtk::Entry, } @@ -67,7 +67,7 @@ where let c = glib::MainContext::default(); let clone = result.clone(); c.spawn_local(async move { - clone.backend.update_person(person.clone()).await.unwrap(); + clone.backend.db().update_person(person.clone()).await.unwrap(); clone.window.close(); (clone.callback)(person.clone()); }); diff --git a/musicus/src/dialogs/recording/performance_editor.rs b/musicus/src/dialogs/recording/performance_editor.rs index 000c208..84c58ea 100644 --- a/musicus/src/dialogs/recording/performance_editor.rs +++ b/musicus/src/dialogs/recording/performance_editor.rs @@ -20,7 +20,7 @@ pub struct PerformanceEditor { person: RefCell>, ensemble: RefCell>, role: RefCell>, - selected_cb: RefCell ()>>>, + selected_cb: RefCell ()>>>, } impl PerformanceEditor { @@ -28,7 +28,7 @@ impl PerformanceEditor { pub fn new>( backend: Rc, parent: &P, - performance: Option, + performance: Option, ) -> Rc { // Create UI @@ -70,7 +70,7 @@ impl PerformanceEditor { this.save_button .connect_clicked(clone!(@strong this => move |_| { if let Some(cb) = &*this.selected_cb.borrow() { - cb(PerformanceDescription { + cb(Performance { person: this.person.borrow().clone(), ensemble: this.ensemble.borrow().clone(), role: this.role.borrow().clone(), @@ -132,7 +132,7 @@ impl PerformanceEditor { } /// Set a closure to be called when the user has chosen to save the performance. - pub fn set_selected_cb () + 'static>(&self, cb: F) { + pub fn set_selected_cb () + 'static>(&self, cb: F) { self.selected_cb.replace(Some(Box::new(cb))); } diff --git a/musicus/src/dialogs/recording/recording_dialog.rs b/musicus/src/dialogs/recording/recording_dialog.rs index 3a0e1ba..0b4d184 100644 --- a/musicus/src/dialogs/recording/recording_dialog.rs +++ b/musicus/src/dialogs/recording/recording_dialog.rs @@ -13,7 +13,7 @@ pub struct RecordingDialog { stack: gtk::Stack, selector: Rc, editor: Rc, - selected_cb: RefCell ()>>>, + selected_cb: RefCell ()>>>, } impl RecordingDialog { @@ -75,7 +75,7 @@ impl RecordingDialog { } /// Set the closure to be called when the user has selected or created a recording. - pub fn set_selected_cb () + 'static>(&self, cb: F) { + pub fn set_selected_cb () + 'static>(&self, cb: F) { self.selected_cb.replace(Some(Box::new(cb))); } diff --git a/musicus/src/dialogs/recording/recording_editor.rs b/musicus/src/dialogs/recording/recording_editor.rs index 2e6253a..2847c30 100644 --- a/musicus/src/dialogs/recording/recording_editor.rs +++ b/musicus/src/dialogs/recording/recording_editor.rs @@ -19,11 +19,11 @@ pub struct RecordingEditor { save_button: gtk::Button, work_label: gtk::Label, comment_entry: gtk::Entry, - performance_list: Rc>, - id: i64, - work: RefCell>, - performances: RefCell>, - selected_cb: RefCell ()>>>, + performance_list: Rc>, + id: u32, + work: RefCell>, + performances: RefCell>, + selected_cb: RefCell ()>>>, back_cb: RefCell ()>>>, } @@ -33,7 +33,7 @@ impl RecordingEditor { pub fn new>( backend: Rc, parent: &W, - recording: Option, + recording: Option, ) -> Rc { // Create UI @@ -87,7 +87,7 @@ impl RecordingEditor { this.save_button .connect_clicked(clone!(@strong this => move |_| { - let recording = RecordingDescription { + let recording = Recording { id: this.id, work: this.work.borrow().clone().expect("Tried to create recording without work!"), comment: this.comment_entry.get_text().to_string(), @@ -97,7 +97,7 @@ impl RecordingEditor { let c = glib::MainContext::default(); let clone = this.clone(); c.spawn_local(async move { - clone.backend.update_recording(recording.clone().into()).await.unwrap(); + clone.backend.db().update_recording(recording.clone().into()).await.unwrap(); if let Some(cb) = &*clone.selected_cb.borrow() { cb(recording.clone()); } @@ -192,12 +192,12 @@ impl RecordingEditor { } /// Set the closure to be called if the recording was created. - pub fn set_selected_cb () + 'static>(&self, cb: F) { + pub fn set_selected_cb () + 'static>(&self, cb: F) { self.selected_cb.replace(Some(Box::new(cb))); } /// Update the UI according to work. - fn work_selected(&self, work: &WorkDescription) { + fn work_selected(&self, work: &Work) { self.work_label.set_text(&format!("{}: {}", work.composer.name_fl(), work.title)); self.save_button.set_sensitive(true); } diff --git a/musicus/src/dialogs/recording/recording_editor_dialog.rs b/musicus/src/dialogs/recording/recording_editor_dialog.rs index d036667..f831d98 100644 --- a/musicus/src/dialogs/recording/recording_editor_dialog.rs +++ b/musicus/src/dialogs/recording/recording_editor_dialog.rs @@ -9,7 +9,7 @@ use std::rc::Rc; /// A dialog for creating or editing a recording. pub struct RecordingEditorDialog { pub window: libhandy::Window, - selected_cb: RefCell ()>>>, + selected_cb: RefCell ()>>>, } impl RecordingEditorDialog { @@ -17,7 +17,7 @@ impl RecordingEditorDialog { pub fn new>( backend: Rc, parent: &W, - recording: Option, + recording: Option, ) -> Rc { // Create UI @@ -52,7 +52,7 @@ impl RecordingEditorDialog { } /// Set the closure to be called when the user edited or created a recording. - pub fn set_selected_cb () + 'static>(&self, cb: F) { + pub fn set_selected_cb () + 'static>(&self, cb: F) { self.selected_cb.replace(Some(Box::new(cb))); } diff --git a/musicus/src/dialogs/recording/recording_selector.rs b/musicus/src/dialogs/recording/recording_selector.rs index 4122d6b..a1f0e2f 100644 --- a/musicus/src/dialogs/recording/recording_selector.rs +++ b/musicus/src/dialogs/recording/recording_selector.rs @@ -14,7 +14,7 @@ pub struct RecordingSelector { pub widget: libhandy::Leaflet, backend: Rc, sidebar_box: gtk::Box, - selected_cb: RefCell ()>>>, + selected_cb: RefCell ()>>>, add_cb: RefCell ()>>>, navigator: Rc, } @@ -83,7 +83,7 @@ impl RecordingSelector { } /// Set the closure to be called when the user has selected a recording. - pub fn set_selected_cb () + 'static>(&self, cb: F) { + pub fn set_selected_cb () + 'static>(&self, cb: F) { self.selected_cb.replace(Some(Box::new(cb))); } } diff --git a/musicus/src/dialogs/recording/recording_selector_person_screen.rs b/musicus/src/dialogs/recording/recording_selector_person_screen.rs index c144368..c0279a1 100644 --- a/musicus/src/dialogs/recording/recording_selector_person_screen.rs +++ b/musicus/src/dialogs/recording/recording_selector_person_screen.rs @@ -16,8 +16,8 @@ pub struct RecordingSelectorPersonScreen { backend: Rc, widget: gtk::Box, stack: gtk::Stack, - work_list: Rc>, - selected_cb: RefCell ()>>>, + work_list: Rc>, + selected_cb: RefCell ()>>>, navigator: RefCell>>, } @@ -57,7 +57,7 @@ impl RecordingSelectorPersonScreen { } })); - this.work_list.set_make_widget(|work: &WorkDescription| { + this.work_list.set_make_widget(|work: &Work| { let label = gtk::Label::new(Some(&work.title)); label.set_ellipsize(pango::EllipsizeMode::End); label.set_halign(gtk::Align::Start); @@ -92,11 +92,7 @@ impl RecordingSelectorPersonScreen { let context = glib::MainContext::default(); let clone = this.clone(); context.spawn_local(async move { - let works = clone - .backend - .get_work_descriptions(person.id) - .await - .unwrap(); + let works = clone.backend.db().get_works(person.id).await.unwrap(); clone.work_list.show_items(works); clone.stack.set_visible_child_name("content"); @@ -106,7 +102,7 @@ impl RecordingSelectorPersonScreen { } /// Sets a closure to be called when the user has selected a recording. - pub fn set_selected_cb () + 'static>(&self, cb: F) { + pub fn set_selected_cb () + 'static>(&self, cb: F) { self.selected_cb.replace(Some(Box::new(cb))); } } diff --git a/musicus/src/dialogs/recording/recording_selector_work_screen.rs b/musicus/src/dialogs/recording/recording_selector_work_screen.rs index 975ee98..b91c30b 100644 --- a/musicus/src/dialogs/recording/recording_selector_work_screen.rs +++ b/musicus/src/dialogs/recording/recording_selector_work_screen.rs @@ -14,14 +14,14 @@ pub struct RecordingSelectorWorkScreen { backend: Rc, widget: gtk::Box, stack: gtk::Stack, - recording_list: Rc>, - selected_cb: RefCell ()>>>, + recording_list: Rc>, + selected_cb: RefCell ()>>>, navigator: RefCell>>, } impl RecordingSelectorWorkScreen { /// Create a new recording selector work screen. - pub fn new(backend: Rc, work: WorkDescription) -> Rc { + pub fn new(backend: Rc, work: Work) -> Rc { // Create UI let builder = @@ -56,23 +56,24 @@ impl RecordingSelectorWorkScreen { } })); - this.recording_list.set_make_widget(|recording: &RecordingDescription| { - let work_label = gtk::Label::new(Some(&recording.work.get_title())); - work_label.set_ellipsize(pango::EllipsizeMode::End); - work_label.set_halign(gtk::Align::Start); + this.recording_list + .set_make_widget(|recording: &Recording| { + let work_label = gtk::Label::new(Some(&recording.work.get_title())); + work_label.set_ellipsize(pango::EllipsizeMode::End); + work_label.set_halign(gtk::Align::Start); - let performers_label = gtk::Label::new(Some(&recording.get_performers())); - performers_label.set_ellipsize(pango::EllipsizeMode::End); - performers_label.set_opacity(0.5); - performers_label.set_halign(gtk::Align::Start); + let performers_label = gtk::Label::new(Some(&recording.get_performers())); + performers_label.set_ellipsize(pango::EllipsizeMode::End); + performers_label.set_opacity(0.5); + performers_label.set_halign(gtk::Align::Start); - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - vbox.set_border_width(6); - vbox.add(&work_label); - vbox.add(&performers_label); + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + vbox.set_border_width(6); + vbox.add(&work_label); + vbox.add(&performers_label); - vbox.upcast() - }); + vbox.upcast() + }); this.recording_list .set_selected(clone!(@strong this => move |recording| { @@ -88,6 +89,7 @@ impl RecordingSelectorWorkScreen { context.spawn_local(async move { let recordings = clone .backend + .db() .get_recordings_for_work(work.id) .await .unwrap(); @@ -100,7 +102,7 @@ impl RecordingSelectorWorkScreen { } /// Sets a closure to be called when the user has selected a recording. - pub fn set_selected_cb () + 'static>(&self, cb: F) { + pub fn set_selected_cb () + 'static>(&self, cb: F) { self.selected_cb.replace(Some(Box::new(cb))); } } diff --git a/musicus/src/dialogs/track_editor.rs b/musicus/src/dialogs/track_editor.rs index afc87ce..a0da7b3 100644 --- a/musicus/src/dialogs/track_editor.rs +++ b/musicus/src/dialogs/track_editor.rs @@ -11,10 +11,10 @@ pub struct TrackEditor { } impl TrackEditor { - pub fn new(parent: &W, track: TrackDescription, work: WorkDescription, callback: F) -> Self + pub fn new(parent: &W, track: Track, work: Work, callback: F) -> Self where W: IsA, - F: Fn(TrackDescription) -> () + 'static, + F: Fn(Track) -> () + 'static, { let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui"); @@ -37,7 +37,7 @@ impl TrackEditor { let mut work_parts = work_parts.borrow_mut(); work_parts.sort(); - callback(TrackDescription { + callback(Track { work_parts: work_parts.clone(), file_name: file_name.clone(), }); diff --git a/musicus/src/dialogs/tracks_editor.rs b/musicus/src/dialogs/tracks_editor.rs index e173242..ecab187 100644 --- a/musicus/src/dialogs/tracks_editor.rs +++ b/musicus/src/dialogs/tracks_editor.rs @@ -18,9 +18,9 @@ pub struct TracksEditor { recording_stack: gtk::Stack, work_label: gtk::Label, performers_label: gtk::Label, - track_list: Rc>, - recording: RefCell>, - tracks: RefCell>, + track_list: Rc>, + recording: RefCell>, + tracks: RefCell>, callback: RefCell ()>>>, } @@ -30,8 +30,8 @@ impl TracksEditor { pub fn new>( backend: Rc, parent: &P, - recording: Option, - tracks: Vec, + recording: Option, + tracks: Vec, ) -> Rc { // UI setup @@ -80,8 +80,8 @@ impl TracksEditor { let context = glib::MainContext::default(); let this = this.clone(); context.spawn_local(async move { - this.backend.update_tracks( - this.recording.borrow().as_ref().unwrap().id, + this.backend.db().update_tracks( + this.recording.borrow().as_ref().unwrap().id as u32, this.tracks.borrow().clone(), ).await.unwrap(); @@ -135,7 +135,7 @@ impl TracksEditor { let mut tracks = this.tracks.borrow_mut(); for file_name in dialog.get_filenames() { let file_name = file_name.strip_prefix(&music_library_path).unwrap(); - tracks.insert(index, TrackDescription { + tracks.insert(index, Track { work_parts: Vec::new(), file_name: String::from(file_name.to_str().unwrap()), }); @@ -224,7 +224,7 @@ impl TracksEditor { } /// Create a widget representing a track. - fn build_track_row(&self, track: &TrackDescription) -> gtk::Widget { + fn build_track_row(&self, track: &Track) -> gtk::Widget { let mut title_parts = Vec::::new(); for part in &track.work_parts { if let Some(recording) = &*self.recording.borrow() { @@ -256,7 +256,7 @@ impl TracksEditor { } /// Set everything up after selecting a recording. - fn recording_selected(&self, recording: &RecordingDescription) { + fn recording_selected(&self, recording: &Recording) { self.work_label.set_text(&recording.work.get_title()); self.performers_label.set_text(&recording.get_performers()); self.recording_stack.set_visible_child_name("selected"); diff --git a/musicus/src/dialogs/work/part_editor.rs b/musicus/src/dialogs/work/part_editor.rs index e0e472f..4e9784a 100644 --- a/musicus/src/dialogs/work/part_editor.rs +++ b/musicus/src/dialogs/work/part_editor.rs @@ -1,7 +1,6 @@ use crate::backend::*; use crate::database::*; use crate::dialogs::*; -use crate::widgets::*; use gettextrs::gettext; use glib::clone; use gtk::prelude::*; @@ -16,10 +15,8 @@ pub struct PartEditor { title_entry: gtk::Entry, composer_label: gtk::Label, reset_composer_button: gtk::Button, - instrument_list: Rc>, composer: RefCell>, - instruments: RefCell>, - ready_cb: RefCell ()>>>, + ready_cb: RefCell ()>>>, } impl PartEditor { @@ -27,7 +24,7 @@ impl PartEditor { pub fn new>( backend: Rc, parent: &P, - part: Option, + part: Option, ) -> Rc { // Create UI @@ -40,21 +37,15 @@ impl PartEditor { get_widget!(builder, gtk::Button, composer_button); get_widget!(builder, gtk::Label, composer_label); get_widget!(builder, gtk::Button, reset_composer_button); - get_widget!(builder, gtk::ScrolledWindow, scroll); - get_widget!(builder, gtk::Button, add_instrument_button); - get_widget!(builder, gtk::Button, remove_instrument_button); window.set_transient_for(Some(parent)); - let instrument_list = List::new(&gettext("No instruments added.")); - scroll.add(&instrument_list.widget); - - let (composer, instruments) = match part { + let composer = match part { Some(part) => { title_entry.set_text(&part.title); - (part.composer, part.instruments) + part.composer } - None => (None, Vec::new()), + None => None, }; let this = Rc::new(Self { @@ -63,9 +54,7 @@ impl PartEditor { title_entry, composer_label, reset_composer_button, - instrument_list, composer: RefCell::new(composer), - instruments: RefCell::new(instruments), ready_cb: RefCell::new(None), }); @@ -77,10 +66,9 @@ impl PartEditor { save_button.connect_clicked(clone!(@strong this => move |_| { if let Some(cb) = &*this.ready_cb.borrow() { - cb(WorkPartDescription { + cb(WorkPart { title: this.title_entry.get_text().to_string(), composer: this.composer.borrow().clone(), - instruments: this.instruments.borrow().clone(), }); } @@ -100,55 +88,17 @@ impl PartEditor { this.show_composer(None); })); - this.instrument_list.set_make_widget(|instrument| { - let label = gtk::Label::new(Some(&instrument.name)); - label.set_ellipsize(pango::EllipsizeMode::End); - label.set_halign(gtk::Align::Start); - label.set_margin_start(6); - label.set_margin_end(6); - label.set_margin_top(6); - label.set_margin_bottom(6); - label.upcast() - }); - - add_instrument_button.connect_clicked(clone!(@strong this => move |_| { - InstrumentSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |instrument| { - let mut instruments = this.instruments.borrow_mut(); - - let index = match this.instrument_list.get_selected_index() { - Some(index) => index + 1, - None => instruments.len(), - }; - - instruments.insert(index, instrument); - this.instrument_list.show_items(instruments.clone()); - this.instrument_list.select_index(index); - })).show(); - })); - - remove_instrument_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(index) = this.instrument_list.get_selected_index() { - let mut instruments = this.instruments.borrow_mut(); - instruments.remove(index); - this.instrument_list.show_items(instruments.clone()); - this.instrument_list.select_index(index); - } - })); - // Initialize if let Some(composer) = &*this.composer.borrow() { this.show_composer(Some(composer)); } - this.instrument_list - .show_items(this.instruments.borrow().clone()); - this } /// Set the closure to be called when the user wants to save the part. - pub fn set_ready_cb () + 'static>(&self, cb: F) { + pub fn set_ready_cb () + 'static>(&self, cb: F) { self.ready_cb.replace(Some(Box::new(cb))); } diff --git a/musicus/src/dialogs/work/section_editor.rs b/musicus/src/dialogs/work/section_editor.rs index 42a30db..e3b7002 100644 --- a/musicus/src/dialogs/work/section_editor.rs +++ b/musicus/src/dialogs/work/section_editor.rs @@ -9,15 +9,12 @@ use std::rc::Rc; pub struct SectionEditor { window: libhandy::Window, title_entry: gtk::Entry, - ready_cb: RefCell ()>>>, + ready_cb: RefCell ()>>>, } impl SectionEditor { /// Create a new section editor and optionally initialize it. - pub fn new>( - parent: &P, - section: Option, - ) -> Rc { + pub fn new>(parent: &P, section: Option) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/section_editor.ui"); @@ -47,7 +44,7 @@ impl SectionEditor { save_button.connect_clicked(clone!(@strong this => move |_| { if let Some(cb) = &*this.ready_cb.borrow() { - cb(WorkSectionDescription { + cb(WorkSection { before_index: 0, title: this.title_entry.get_text().to_string(), }); @@ -62,7 +59,7 @@ impl SectionEditor { /// Set the closure to be called when the user wants to save the section. Note that the /// resulting object will always have `before_index` set to 0. The caller is expected to /// change that later before adding the section to the database. - pub fn set_ready_cb () + 'static>(&self, cb: F) { + pub fn set_ready_cb () + 'static>(&self, cb: F) { self.ready_cb.replace(Some(Box::new(cb))); } diff --git a/musicus/src/dialogs/work/work_dialog.rs b/musicus/src/dialogs/work/work_dialog.rs index db5c260..ca57e22 100644 --- a/musicus/src/dialogs/work/work_dialog.rs +++ b/musicus/src/dialogs/work/work_dialog.rs @@ -13,7 +13,7 @@ pub struct WorkDialog { stack: gtk::Stack, selector: Rc, editor: Rc, - selected_cb: RefCell ()>>>, + selected_cb: RefCell ()>>>, } impl WorkDialog { @@ -75,7 +75,7 @@ impl WorkDialog { } /// Set the closure to be called when the user has selected or created a work. - pub fn set_selected_cb () + 'static>(&self, cb: F) { + pub fn set_selected_cb () + 'static>(&self, cb: F) { self.selected_cb.replace(Some(Box::new(cb))); } diff --git a/musicus/src/dialogs/work/work_editor.rs b/musicus/src/dialogs/work/work_editor.rs index 82d2745..33da150 100644 --- a/musicus/src/dialogs/work/work_editor.rs +++ b/musicus/src/dialogs/work/work_editor.rs @@ -15,8 +15,8 @@ use std::rc::Rc; /// Either a work part or a work section. #[derive(Clone)] enum PartOrSection { - Part(WorkPartDescription), - Section(WorkSectionDescription), + Part(WorkPart), + Section(WorkSection), } /// A widget for editing and creating works. @@ -29,12 +29,12 @@ pub struct WorkEditor { composer_label: gtk::Label, instrument_list: Rc>, part_list: Rc>, - id: i64, + id: u32, composer: RefCell>, instruments: RefCell>, structure: RefCell>, cancel_cb: RefCell ()>>>, - saved_cb: RefCell ()>>>, + saved_cb: RefCell ()>>>, } impl WorkEditor { @@ -43,7 +43,7 @@ impl WorkEditor { pub fn new>( backend: Rc, parent: &P, - work: Option, + work: Option, ) -> Rc { // Create UI @@ -120,7 +120,7 @@ impl WorkEditor { })); this.save_button.connect_clicked(clone!(@strong this => move |_| { - let mut section_count = 0; + let mut section_count: usize = 0; let mut parts = Vec::new(); let mut sections = Vec::new(); @@ -129,7 +129,6 @@ impl WorkEditor { PartOrSection::Part(part) => parts.push(part.clone()), PartOrSection::Section(section) => { let mut section = section.clone(); - let index: i64 = index.try_into().unwrap(); section.before_index = index - section_count; sections.push(section); section_count += 1; @@ -137,7 +136,7 @@ impl WorkEditor { } } - let work = WorkDescription { + let work = Work { id: this.id, title: this.title_entry.get_text().to_string(), composer: this.composer.borrow().clone().expect("Tried to create work without composer!"), @@ -149,7 +148,7 @@ impl WorkEditor { let c = glib::MainContext::default(); let clone = this.clone(); c.spawn_local(async move { - clone.backend.update_work(work.clone().into()).await.unwrap(); + clone.backend.db().update_work(work.clone().into()).await.unwrap(); if let Some(cb) = &*clone.saved_cb.borrow() { cb(work); } @@ -333,7 +332,8 @@ impl WorkEditor { this.show_composer(composer); } - this.instrument_list.show_items(this.instruments.borrow().clone()); + this.instrument_list + .show_items(this.instruments.borrow().clone()); this.part_list.show_items(this.structure.borrow().clone()); this @@ -345,7 +345,7 @@ impl WorkEditor { } /// The closure to call when a work was created. - pub fn set_saved_cb () + 'static>(&self, cb: F) { + pub fn set_saved_cb () + 'static>(&self, cb: F) { self.saved_cb.replace(Some(Box::new(cb))); } diff --git a/musicus/src/dialogs/work/work_editor_dialog.rs b/musicus/src/dialogs/work/work_editor_dialog.rs index 6092c82..83ec329 100644 --- a/musicus/src/dialogs/work/work_editor_dialog.rs +++ b/musicus/src/dialogs/work/work_editor_dialog.rs @@ -9,7 +9,7 @@ use std::rc::Rc; /// A dialog for creating or editing a work. pub struct WorkEditorDialog { pub window: libhandy::Window, - saved_cb: RefCell ()>>>, + saved_cb: RefCell ()>>>, } impl WorkEditorDialog { @@ -17,7 +17,7 @@ impl WorkEditorDialog { pub fn new>( backend: Rc, parent: &W, - work: Option, + work: Option, ) -> Rc { // Create UI @@ -52,7 +52,7 @@ impl WorkEditorDialog { } /// Set the closure to be called when the user edited or created a work. - pub fn set_saved_cb () + 'static>(&self, cb: F) { + pub fn set_saved_cb () + 'static>(&self, cb: F) { self.saved_cb.replace(Some(Box::new(cb))); } diff --git a/musicus/src/dialogs/work/work_selector.rs b/musicus/src/dialogs/work/work_selector.rs index a727860..dd0ad73 100644 --- a/musicus/src/dialogs/work/work_selector.rs +++ b/musicus/src/dialogs/work/work_selector.rs @@ -14,7 +14,7 @@ pub struct WorkSelector { pub widget: libhandy::Leaflet, backend: Rc, sidebar_box: gtk::Box, - selected_cb: RefCell ()>>>, + selected_cb: RefCell ()>>>, add_cb: RefCell ()>>>, navigator: Rc, } @@ -83,7 +83,7 @@ impl WorkSelector { } /// Set the closure to be called when the user has selected a work. - pub fn set_selected_cb () + 'static>(&self, cb: F) { + pub fn set_selected_cb () + 'static>(&self, cb: F) { self.selected_cb.replace(Some(Box::new(cb))); } } diff --git a/musicus/src/dialogs/work/work_selector_person_screen.rs b/musicus/src/dialogs/work/work_selector_person_screen.rs index 049b061..95fd233 100644 --- a/musicus/src/dialogs/work/work_selector_person_screen.rs +++ b/musicus/src/dialogs/work/work_selector_person_screen.rs @@ -14,8 +14,8 @@ pub struct WorkSelectorPersonScreen { backend: Rc, widget: gtk::Box, stack: gtk::Stack, - work_list: Rc>, - selected_cb: RefCell ()>>>, + work_list: Rc>, + selected_cb: RefCell ()>>>, navigator: RefCell>>, } @@ -54,7 +54,7 @@ impl WorkSelectorPersonScreen { } })); - this.work_list.set_make_widget(|work: &WorkDescription| { + this.work_list.set_make_widget(|work: &Work| { let label = gtk::Label::new(Some(&work.title)); label.set_ellipsize(pango::EllipsizeMode::End); label.set_halign(gtk::Align::Start); @@ -80,11 +80,7 @@ impl WorkSelectorPersonScreen { let context = glib::MainContext::default(); let clone = this.clone(); context.spawn_local(async move { - let works = clone - .backend - .get_work_descriptions(person.id) - .await - .unwrap(); + let works = clone.backend.db().get_works(person.id).await.unwrap(); clone.work_list.show_items(works); clone.stack.set_visible_child_name("content"); @@ -94,7 +90,7 @@ impl WorkSelectorPersonScreen { } /// Sets a closure to be called when the user has selected a work. - pub fn set_selected_cb () + 'static>(&self, cb: F) { + pub fn set_selected_cb () + 'static>(&self, cb: F) { self.selected_cb.replace(Some(Box::new(cb))); } } diff --git a/musicus/src/meson.build b/musicus/src/meson.build index 5d851d2..36cb69d 100644 --- a/musicus/src/meson.build +++ b/musicus/src/meson.build @@ -33,15 +33,19 @@ run_command( ) sources = files( - 'backend/backend.rs', 'backend/client.rs', + 'backend/library.rs', 'backend/mod.rs', 'backend/secure.rs', - 'database/database.rs', + 'database/ensembles.rs', + 'database/instruments.rs', 'database/mod.rs', - 'database/models.rs', + 'database/persons.rs', + 'database/recordings.rs', 'database/schema.rs', - 'database/tables.rs', + 'database/thread.rs', + 'database/tracks.rs', + 'database/works.rs', 'dialogs/about.rs', 'dialogs/ensemble_editor.rs', 'dialogs/ensemble_selector.rs', diff --git a/musicus/src/player.rs b/musicus/src/player.rs index 84ca301..56d1e7d 100644 --- a/musicus/src/player.rs +++ b/musicus/src/player.rs @@ -8,8 +8,8 @@ use std::rc::Rc; #[derive(Clone)] pub struct PlaylistItem { - pub recording: RecordingDescription, - pub tracks: Vec, + pub recording: Recording, + pub tracks: Vec, } pub struct Player { diff --git a/musicus/src/screens/ensemble_screen.rs b/musicus/src/screens/ensemble_screen.rs index 40f8884..0d90296 100644 --- a/musicus/src/screens/ensemble_screen.rs +++ b/musicus/src/screens/ensemble_screen.rs @@ -14,7 +14,7 @@ pub struct EnsembleScreen { backend: Rc, widget: gtk::Box, stack: gtk::Stack, - recording_list: Rc>, + recording_list: Rc>, navigator: RefCell>>, } @@ -52,7 +52,7 @@ impl EnsembleScreen { let recording_list = List::new(&gettext("No recordings found.")); - recording_list.set_make_widget(|recording: &RecordingDescription| { + recording_list.set_make_widget(|recording: &Recording| { let work_label = gtk::Label::new(Some(&recording.work.get_title())); work_label.set_ellipsize(pango::EllipsizeMode::End); @@ -72,7 +72,7 @@ impl EnsembleScreen { }); recording_list.set_filter( - clone!(@strong search_entry => move |recording: &RecordingDescription| { + clone!(@strong search_entry => move |recording: &Recording| { let search = search_entry.get_text().to_string().to_lowercase(); let text = recording.work.get_title() + &recording.get_performers(); search.is_empty() || text.contains(&search) @@ -114,7 +114,8 @@ impl EnsembleScreen { context.spawn_local(async move { let recordings = clone .backend - .get_recordings_for_ensemble(ensemble.id) + .db() + .get_recordings_for_ensemble(ensemble.id as u32) .await .unwrap(); diff --git a/musicus/src/screens/person_screen.rs b/musicus/src/screens/person_screen.rs index e645f25..77fcbe4 100644 --- a/musicus/src/screens/person_screen.rs +++ b/musicus/src/screens/person_screen.rs @@ -14,8 +14,8 @@ pub struct PersonScreen { backend: Rc, widget: gtk::Box, stack: gtk::Stack, - work_list: Rc>, - recording_list: Rc>, + work_list: Rc>, + recording_list: Rc>, navigator: RefCell>>, } @@ -56,7 +56,7 @@ impl PersonScreen { let work_list = List::new(&gettext("No works found.")); - work_list.set_make_widget(|work: &WorkDescription| { + work_list.set_make_widget(|work: &Work| { let label = gtk::Label::new(Some(&work.title)); label.set_halign(gtk::Align::Start); label.set_margin_start(6); @@ -66,17 +66,15 @@ impl PersonScreen { label.upcast() }); - work_list.set_filter( - clone!(@strong search_entry => move |work: &WorkDescription| { - let search = search_entry.get_text().to_string().to_lowercase(); - let title = work.title.to_lowercase(); - search.is_empty() || title.contains(&search) - }), - ); + work_list.set_filter(clone!(@strong search_entry => move |work: &Work| { + let search = search_entry.get_text().to_string().to_lowercase(); + let title = work.title.to_lowercase(); + search.is_empty() || title.contains(&search) + })); let recording_list = List::new(&gettext("No recordings found.")); - recording_list.set_make_widget(|recording: &RecordingDescription| { + recording_list.set_make_widget(|recording: &Recording| { let work_label = gtk::Label::new(Some(&recording.work.get_title())); work_label.set_ellipsize(pango::EllipsizeMode::End); @@ -96,7 +94,7 @@ impl PersonScreen { }); recording_list.set_filter( - clone!(@strong search_entry => move |recording: &RecordingDescription| { + clone!(@strong search_entry => move |recording: &Recording| { let search = search_entry.get_text().to_string().to_lowercase(); let text = recording.work.get_title() + &recording.get_performers(); search.is_empty() || text.contains(&search) @@ -152,12 +150,14 @@ impl PersonScreen { context.spawn_local(async move { let works = clone .backend - .get_work_descriptions(person.id) + .db() + .get_works(person.id as u32) .await .unwrap(); let recordings = clone .backend - .get_recordings_for_person(person.id) + .db() + .get_recordings_for_person(person.id as u32) .await .unwrap(); diff --git a/musicus/src/screens/recording_screen.rs b/musicus/src/screens/recording_screen.rs index 92fa5bc..ee2bca0 100644 --- a/musicus/src/screens/recording_screen.rs +++ b/musicus/src/screens/recording_screen.rs @@ -14,12 +14,12 @@ pub struct RecordingScreen { backend: Rc, widget: gtk::Box, stack: gtk::Stack, - tracks: RefCell>, + tracks: RefCell>, navigator: RefCell>>, } impl RecordingScreen { - pub fn new(backend: Rc, recording: RecordingDescription) -> Rc { + pub fn new(backend: Rc, recording: Recording) -> Rc { let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui"); get_widget!(builder, gtk::Box, widget); @@ -69,7 +69,7 @@ impl RecordingScreen { let list = List::new(&gettext("No tracks found.")); list.set_make_widget( - clone!(@strong recording => move |track: &TrackDescription| { + clone!(@strong recording => move |track: &Track| { let mut title_parts = Vec::::new(); for part in &track.work_parts { title_parts.push(recording.work.parts[*part].title.clone()); @@ -131,7 +131,7 @@ impl RecordingScreen { let clone = result.clone(); let id = recording.id; context.spawn_local(async move { - let tracks = clone.backend.get_tracks(id).await.unwrap(); + let tracks = clone.backend.db().get_tracks(id as u32).await.unwrap(); list.show_items(tracks.clone()); clone.stack.set_visible_child_name("content"); clone.tracks.replace(tracks); diff --git a/musicus/src/screens/work_screen.rs b/musicus/src/screens/work_screen.rs index f3d5662..f417c6b 100644 --- a/musicus/src/screens/work_screen.rs +++ b/musicus/src/screens/work_screen.rs @@ -14,12 +14,12 @@ pub struct WorkScreen { backend: Rc, widget: gtk::Box, stack: gtk::Stack, - recording_list: Rc>, + recording_list: Rc>, navigator: RefCell>>, } impl WorkScreen { - pub fn new(backend: Rc, work: WorkDescription) -> Rc { + pub fn new(backend: Rc, work: Work) -> Rc { let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_screen.ui"); get_widget!(builder, gtk::Box, widget); @@ -53,7 +53,7 @@ impl WorkScreen { let recording_list = List::new(&gettext("No recordings found.")); - recording_list.set_make_widget(|recording: &RecordingDescription| { + recording_list.set_make_widget(|recording: &Recording| { let work_label = gtk::Label::new(Some(&recording.work.get_title())); work_label.set_ellipsize(pango::EllipsizeMode::End); @@ -72,7 +72,7 @@ impl WorkScreen { vbox.upcast() }); - recording_list.set_filter(clone!(@strong search_entry => move |recording: &RecordingDescription| { + recording_list.set_filter(clone!(@strong search_entry => move |recording: &Recording| { let search = search_entry.get_text().to_string().to_lowercase(); let text = recording.work.get_title().to_lowercase() + &recording.get_performers().to_lowercase(); search.is_empty() || text.contains(&search) @@ -113,7 +113,8 @@ impl WorkScreen { context.spawn_local(async move { let recordings = clone .backend - .get_recordings_for_work(work.id) + .db() + .get_recordings_for_work(work.id as u32) .await .unwrap(); diff --git a/musicus/src/widgets/person_list.rs b/musicus/src/widgets/person_list.rs index 17b2aca..df352e2 100644 --- a/musicus/src/widgets/person_list.rs +++ b/musicus/src/widgets/person_list.rs @@ -74,7 +74,7 @@ impl PersonList { let list = self.list.clone(); context.spawn_local(async move { - let persons = backend.get_persons().await.unwrap(); + let persons = backend.db().get_persons().await.unwrap(); list.show_items(persons); self.stack.set_visible_child_name("content"); }); diff --git a/musicus/src/widgets/poe_list.rs b/musicus/src/widgets/poe_list.rs index e27b0a5..7cf3982 100644 --- a/musicus/src/widgets/poe_list.rs +++ b/musicus/src/widgets/poe_list.rs @@ -89,8 +89,8 @@ impl PoeList { let list = self.list.clone(); context.spawn_local(async move { - let persons = backend.get_persons().await.unwrap(); - let ensembles = backend.get_ensembles().await.unwrap(); + let persons = backend.db().get_persons().await.unwrap(); + let ensembles = backend.db().get_ensembles().await.unwrap(); let mut poes: Vec = Vec::new(); for person in persons { diff --git a/musicus/src/window.rs b/musicus/src/window.rs index 0300994..ec85095 100644 --- a/musicus/src/window.rs +++ b/musicus/src/window.rs @@ -37,7 +37,6 @@ impl Window { get_widget!(builder, gtk::Box, empty_screen); let backend = Rc::new(Backend::new()); - backend.clone().init(); let player_screen = PlayerScreen::new(); stack.add_named(&player_screen.widget, "player_screen"); @@ -122,252 +121,6 @@ impl Window { }) ); - action!( - result.window, - "add-person", - clone!(@strong result => move |_, _| { - PersonEditor::new(result.backend.clone(), &result.window, None, clone!(@strong result => move |_| { - result.reload(); - })).show(); - }) - ); - - action!( - result.window, - "add-instrument", - clone!(@strong result => move |_, _| { - InstrumentEditor::new(result.backend.clone(), &result.window, None, |instrument| { - println!("{:?}", instrument); - }).show(); - }) - ); - - action!( - result.window, - "add-work", - clone!(@strong result => move |_, _| { - let dialog = WorkDialog::new(result.backend.clone(), &result.window); - - dialog.set_selected_cb(clone!(@strong result => move |_| { - result.reload(); - })); - - dialog.show(); - }) - ); - - action!( - result.window, - "add-ensemble", - clone!(@strong result => move |_, _| { - EnsembleEditor::new(result.backend.clone(), &result.window, None, clone!(@strong result => move |_| { - result.reload(); - })).show(); - }) - ); - - action!( - result.window, - "add-recording", - clone!(@strong result => move |_, _| { - let dialog = RecordingDialog::new(result.backend.clone(), &result.window); - - dialog.set_selected_cb(clone!(@strong result => move |_| { - result.reload(); - })); - - dialog.show(); - }) - ); - - action!( - result.window, - "add-tracks", - clone!(@strong result => move |_, _| { - let editor = TracksEditor::new(result.backend.clone(), &result.window, None, Vec::new()); - - editor.set_callback(clone!(@strong result => move || { - result.reload(); - })); - - editor.show(); - }) - ); - - action!( - result.window, - "edit-person", - Some(glib::VariantTy::new("x").unwrap()), - clone!(@strong result => move |_, id| { - let id = id.unwrap().get().unwrap(); - let result = result.clone(); - let c = glib::MainContext::default(); - c.spawn_local(async move { - let person = result.backend.get_person(id).await.unwrap(); - PersonEditor::new(result.backend.clone(), &result.window, Some(person), clone!(@strong result => move |_| { - result.reload(); - })).show(); - }); - }) - ); - - action!( - result.window, - "delete-person", - Some(glib::VariantTy::new("x").unwrap()), - clone!(@strong result => move |_, id| { - let id = id.unwrap().get().unwrap(); - let result = result.clone(); - let c = glib::MainContext::default(); - c.spawn_local(async move { - result.backend.delete_person(id).await.unwrap(); - result.reload(); - }); - }) - ); - - action!( - result.window, - "edit-ensemble", - Some(glib::VariantTy::new("x").unwrap()), - clone!(@strong result => move |_, id| { - let id = id.unwrap().get().unwrap(); - let result = result.clone(); - let c = glib::MainContext::default(); - c.spawn_local(async move { - let ensemble = result.backend.get_ensemble(id).await.unwrap(); - EnsembleEditor::new(result.backend.clone(), &result.window, Some(ensemble), clone!(@strong result => move |_| { - result.reload(); - })).show(); - }); - }) - ); - - action!( - result.window, - "delete-ensemble", - Some(glib::VariantTy::new("x").unwrap()), - clone!(@strong result => move |_, id| { - let id = id.unwrap().get().unwrap(); - let result = result.clone(); - let c = glib::MainContext::default(); - c.spawn_local(async move { - result.backend.delete_ensemble(id).await.unwrap(); - result.reload(); - }); - }) - ); - - action!( - result.window, - "edit-work", - Some(glib::VariantTy::new("x").unwrap()), - clone!(@strong result => move |_, id| { - let id = id.unwrap().get().unwrap(); - let result = result.clone(); - let c = glib::MainContext::default(); - c.spawn_local(async move { - let work = result.backend.get_work_description(id).await.unwrap(); - let dialog = WorkEditorDialog::new(result.backend.clone(), &result.window, Some(work)); - - dialog.set_saved_cb(clone!(@strong result => move |_| { - result.reload(); - })); - - dialog.show(); - }); - }) - ); - - action!( - result.window, - "delete-work", - Some(glib::VariantTy::new("x").unwrap()), - clone!(@strong result => move |_, id| { - let id = id.unwrap().get().unwrap(); - let result = result.clone(); - let c = glib::MainContext::default(); - c.spawn_local(async move { - result.backend.delete_work(id).await.unwrap(); - result.reload(); - }); - }) - ); - - action!( - result.window, - "edit-recording", - Some(glib::VariantTy::new("x").unwrap()), - clone!(@strong result => move |_, id| { - let id = id.unwrap().get().unwrap(); - let result = result.clone(); - let c = glib::MainContext::default(); - c.spawn_local(async move { - let recording = result.backend.get_recording_description(id).await.unwrap(); - let dialog = RecordingEditorDialog::new(result.backend.clone(), &result.window, Some(recording)); - - dialog.set_selected_cb(clone!(@strong result => move |_| { - result.reload(); - })); - - dialog.show(); - }); - }) - ); - - action!( - result.window, - "delete-recording", - Some(glib::VariantTy::new("x").unwrap()), - clone!(@strong result => move |_, id| { - let id = id.unwrap().get().unwrap(); - let result = result.clone(); - let c = glib::MainContext::default(); - c.spawn_local(async move { - result.backend.delete_recording(id).await.unwrap(); - result.reload(); - }); - }) - ); - - action!( - result.window, - "edit-tracks", - Some(glib::VariantTy::new("x").unwrap()), - clone!(@strong result => move |_, id| { - let id = id.unwrap().get().unwrap(); - let result = result.clone(); - let c = glib::MainContext::default(); - c.spawn_local(async move { - let recording = result.backend.get_recording_description(id).await.unwrap(); - let tracks = result.backend.get_tracks(id).await.unwrap(); - - let editor = TracksEditor::new(result.backend.clone(), &result.window, Some(recording), tracks); - - editor.set_callback(clone!(@strong result => move || { - result.reload(); - })); - - editor.show(); - }); - }) - ); - - action!( - result.window, - "delete-tracks", - Some(glib::VariantTy::new("x").unwrap()), - clone!(@strong result => move |_, id| { - let id = id.unwrap().get().unwrap(); - let result = result.clone(); - let c = glib::MainContext::default(); - c.spawn_local(async move { - result.backend.delete_tracks(id).await.unwrap(); - result.reload(); - }); - }) - ); - let context = glib::MainContext::default(); let clone = result.clone(); context.spawn_local(async move { @@ -393,6 +146,13 @@ impl Window { } }); + let clone = result.clone(); + context.spawn_local(async move { + // This is not done in the async block below, because backend state changes may happen + // while this method is running. + clone.backend.clone().init().await.unwrap(); + }); + result.leaflet.add(&result.navigator.widget); result