mirror of
				https://github.com/johrpan/musicus.git
				synced 2025-10-26 19:57:25 +01:00 
			
		
		
		
	Restructure backend and database
This commit is contained in:
		
							parent
							
								
									d0c25531d3
								
							
						
					
					
						commit
						a93c7276d2
					
				
					 49 changed files with 1705 additions and 1920 deletions
				
			
		|  | @ -8,8 +8,6 @@ DROP TABLE instrumentations; | ||||||
| 
 | 
 | ||||||
| DROP TABLE work_parts; | DROP TABLE work_parts; | ||||||
| 
 | 
 | ||||||
| DROP TABLE part_instrumentations; |  | ||||||
| 
 |  | ||||||
| DROP TABLE work_sections; | DROP TABLE work_sections; | ||||||
| 
 | 
 | ||||||
| DROP TABLE ensembles; | DROP TABLE ensembles; | ||||||
|  |  | ||||||
|  | @ -18,21 +18,15 @@ CREATE TABLE works ( | ||||||
| CREATE TABLE instrumentations ( | CREATE TABLE instrumentations ( | ||||||
|     id BIGINT NOT NULL PRIMARY KEY, |     id BIGINT NOT NULL PRIMARY KEY, | ||||||
|     work BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE, |     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 ( | CREATE TABLE work_parts ( | ||||||
|     id BIGINT NOT NULL PRIMARY KEY, |     id BIGINT NOT NULL PRIMARY KEY, | ||||||
|     work BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE, |     work BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE, | ||||||
|     part_index BIGINT NOT NULL, |     part_index BIGINT NOT NULL, | ||||||
|     composer BIGINT REFERENCES persons(id), |     title TEXT NOT NULL, | ||||||
|     title TEXT NOT NULL |     composer BIGINT REFERENCES persons(id) | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| 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) |  | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE work_sections ( | CREATE TABLE work_sections ( | ||||||
|  |  | ||||||
|  | @ -6,8 +6,7 @@ | ||||||
|   <object class="HdyWindow" id="window"> |   <object class="HdyWindow" id="window"> | ||||||
|     <property name="can-focus">False</property> |     <property name="can-focus">False</property> | ||||||
|     <property name="modal">True</property> |     <property name="modal">True</property> | ||||||
|     <property name="default-width">450</property> |     <property name="default-width">350</property> | ||||||
|     <property name="default-height">300</property> |  | ||||||
|     <property name="destroy-with-parent">True</property> |     <property name="destroy-with-parent">True</property> | ||||||
|     <property name="type-hint">dialog</property> |     <property name="type-hint">dialog</property> | ||||||
|     <child> |     <child> | ||||||
|  | @ -50,10 +49,6 @@ | ||||||
|             <property name="position">0</property> |             <property name="position">0</property> | ||||||
|           </packing> |           </packing> | ||||||
|         </child> |         </child> | ||||||
|         <child> |  | ||||||
|           <object class="GtkNotebook"> |  | ||||||
|             <property name="visible">True</property> |  | ||||||
|             <property name="can-focus">True</property> |  | ||||||
|         <child> |         <child> | ||||||
|           <!-- n-columns=2 n-rows=2 --> |           <!-- n-columns=2 n-rows=2 --> | ||||||
|           <object class="GtkGrid"> |           <object class="GtkGrid"> | ||||||
|  | @ -151,110 +146,9 @@ | ||||||
|               </packing> |               </packing> | ||||||
|             </child> |             </child> | ||||||
|           </object> |           </object> | ||||||
|             </child> |  | ||||||
|             <child type="tab"> |  | ||||||
|               <object class="GtkLabel"> |  | ||||||
|                 <property name="visible">True</property> |  | ||||||
|                 <property name="can-focus">False</property> |  | ||||||
|                 <property name="label" translatable="yes">Overview</property> |  | ||||||
|               </object> |  | ||||||
|               <packing> |  | ||||||
|                 <property name="tab-fill">False</property> |  | ||||||
|               </packing> |  | ||||||
|             </child> |  | ||||||
|             <child> |  | ||||||
|               <object class="GtkBox"> |  | ||||||
|                 <property name="visible">True</property> |  | ||||||
|                 <property name="can-focus">False</property> |  | ||||||
|                 <property name="border-width">18</property> |  | ||||||
|                 <property name="spacing">6</property> |  | ||||||
|                 <child> |  | ||||||
|                   <object class="GtkScrolledWindow" id="scroll"> |  | ||||||
|                     <property name="visible">True</property> |  | ||||||
|                     <property name="can-focus">True</property> |  | ||||||
|                     <property name="shadow-type">in</property> |  | ||||||
|                     <child> |  | ||||||
|                       <placeholder/> |  | ||||||
|                     </child> |  | ||||||
|                   </object> |  | ||||||
|                   <packing> |  | ||||||
|                     <property name="expand">True</property> |  | ||||||
|                     <property name="fill">True</property> |  | ||||||
|                     <property name="position">0</property> |  | ||||||
|                   </packing> |  | ||||||
|                 </child> |  | ||||||
|                 <child> |  | ||||||
|                   <object class="GtkBox"> |  | ||||||
|                     <property name="visible">True</property> |  | ||||||
|                     <property name="can-focus">False</property> |  | ||||||
|                     <property name="border-width">0</property> |  | ||||||
|                     <property name="orientation">vertical</property> |  | ||||||
|                     <property name="spacing">6</property> |  | ||||||
|                     <child> |  | ||||||
|                       <object class="GtkButton" id="add_instrument_button"> |  | ||||||
|                         <property name="visible">True</property> |  | ||||||
|                         <property name="can-focus">True</property> |  | ||||||
|                         <property name="receives-default">True</property> |  | ||||||
|                         <child> |  | ||||||
|                           <object class="GtkImage"> |  | ||||||
|                             <property name="visible">True</property> |  | ||||||
|                             <property name="can-focus">False</property> |  | ||||||
|                             <property name="icon-name">list-add-symbolic</property> |  | ||||||
|                           </object> |  | ||||||
|                         </child> |  | ||||||
|                       </object> |  | ||||||
|           <packing> |           <packing> | ||||||
|             <property name="expand">False</property> |             <property name="expand">False</property> | ||||||
|             <property name="fill">True</property> |             <property name="fill">True</property> | ||||||
|                         <property name="position">0</property> |  | ||||||
|                       </packing> |  | ||||||
|                     </child> |  | ||||||
|                     <child> |  | ||||||
|                       <object class="GtkButton" id="remove_instrument_button"> |  | ||||||
|                         <property name="visible">True</property> |  | ||||||
|                         <property name="can-focus">True</property> |  | ||||||
|                         <property name="receives-default">True</property> |  | ||||||
|                         <child> |  | ||||||
|                           <object class="GtkImage"> |  | ||||||
|                             <property name="visible">True</property> |  | ||||||
|                             <property name="can-focus">False</property> |  | ||||||
|                             <property name="icon-name">list-remove-symbolic</property> |  | ||||||
|                           </object> |  | ||||||
|                         </child> |  | ||||||
|                       </object> |  | ||||||
|                       <packing> |  | ||||||
|                         <property name="expand">False</property> |  | ||||||
|                         <property name="fill">True</property> |  | ||||||
|                         <property name="position">1</property> |  | ||||||
|                       </packing> |  | ||||||
|                     </child> |  | ||||||
|                   </object> |  | ||||||
|                   <packing> |  | ||||||
|                     <property name="expand">False</property> |  | ||||||
|                     <property name="fill">True</property> |  | ||||||
|                     <property name="position">1</property> |  | ||||||
|                   </packing> |  | ||||||
|                 </child> |  | ||||||
|               </object> |  | ||||||
|               <packing> |  | ||||||
|                 <property name="position">1</property> |  | ||||||
|               </packing> |  | ||||||
|             </child> |  | ||||||
|             <child type="tab"> |  | ||||||
|               <object class="GtkLabel"> |  | ||||||
|                 <property name="visible">True</property> |  | ||||||
|                 <property name="can-focus">False</property> |  | ||||||
|                 <property name="label" translatable="yes">Instruments</property> |  | ||||||
|               </object> |  | ||||||
|               <packing> |  | ||||||
|                 <property name="position">1</property> |  | ||||||
|                 <property name="tab-fill">False</property> |  | ||||||
|               </packing> |  | ||||||
|             </child> |  | ||||||
|           </object> |  | ||||||
|           <packing> |  | ||||||
|             <property name="expand">True</property> |  | ||||||
|             <property name="fill">True</property> |  | ||||||
|             <property name="position">1</property> |             <property name="position">1</property> | ||||||
|           </packing> |           </packing> | ||||||
|         </child> |         </child> | ||||||
|  |  | ||||||
|  | @ -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<Result<()>>), |  | ||||||
|     GetPerson(i64, Sender<Result<Person>>), |  | ||||||
|     DeletePerson(i64, Sender<Result<()>>), |  | ||||||
|     GetPersons(Sender<Result<Vec<Person>>>), |  | ||||||
|     UpdateInstrument(Instrument, Sender<Result<()>>), |  | ||||||
|     GetInstrument(i64, Sender<Result<Instrument>>), |  | ||||||
|     DeleteInstrument(i64, Sender<Result<()>>), |  | ||||||
|     GetInstruments(Sender<Result<Vec<Instrument>>>), |  | ||||||
|     UpdateWork(WorkInsertion, Sender<Result<()>>), |  | ||||||
|     GetWorkDescription(i64, Sender<Result<WorkDescription>>), |  | ||||||
|     DeleteWork(i64, Sender<Result<()>>), |  | ||||||
|     GetWorkDescriptions(i64, Sender<Result<Vec<WorkDescription>>>), |  | ||||||
|     UpdateEnsemble(Ensemble, Sender<Result<()>>), |  | ||||||
|     GetEnsemble(i64, Sender<Result<Ensemble>>), |  | ||||||
|     DeleteEnsemble(i64, Sender<Result<()>>), |  | ||||||
|     GetEnsembles(Sender<Result<Vec<Ensemble>>>), |  | ||||||
|     UpdateRecording(RecordingInsertion, Sender<Result<()>>), |  | ||||||
|     GetRecordingDescription(i64, Sender<Result<RecordingDescription>>), |  | ||||||
|     DeleteRecording(i64, Sender<Result<()>>), |  | ||||||
|     GetRecordingsForPerson(i64, Sender<Result<Vec<RecordingDescription>>>), |  | ||||||
|     GetRecordingsForEnsemble(i64, Sender<Result<Vec<RecordingDescription>>>), |  | ||||||
|     GetRecordingsForWork(i64, Sender<Result<Vec<RecordingDescription>>>), |  | ||||||
|     UpdateTracks(i64, Vec<TrackDescription>, Sender<Result<()>>), |  | ||||||
|     DeleteTracks(i64, Sender<Result<()>>), |  | ||||||
|     GetTracks(i64, Sender<Result<Vec<TrackDescription>>>), |  | ||||||
|     Stop, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| use BackendAction::*; |  | ||||||
| 
 |  | ||||||
| pub struct Backend { |  | ||||||
|     pub state_stream: RefCell<mpsc::Receiver<BackendState>>, |  | ||||||
|     state_sender: RefCell<mpsc::Sender<BackendState>>, |  | ||||||
|     action_sender: RefCell<Option<std::sync::mpsc::Sender<BackendAction>>>, |  | ||||||
|     settings: gio::Settings, |  | ||||||
|     secrets: secret_service::SecretService, |  | ||||||
|     server_url: RefCell<Option<String>>, |  | ||||||
|     login_data: RefCell<Option<LoginData>>, |  | ||||||
|     token: RefCell<Option<String>>, |  | ||||||
|     music_library_path: RefCell<Option<PathBuf>>, |  | ||||||
|     player: RefCell<Option<Rc<Player>>>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 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<Backend>) { |  | ||||||
|         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<Person> { |  | ||||||
|         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<Vec<Person>> { |  | ||||||
|         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<Instrument> { |  | ||||||
|         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<Vec<Instrument>> { |  | ||||||
|         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<WorkDescription> { |  | ||||||
|         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<Vec<WorkDescription>> { |  | ||||||
|         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<Ensemble> { |  | ||||||
|         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<Vec<Ensemble>> { |  | ||||||
|         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<RecordingDescription> { |  | ||||||
|         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<Vec<RecordingDescription>> { |  | ||||||
|         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<Vec<RecordingDescription>> { |  | ||||||
|         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<Vec<RecordingDescription>> { |  | ||||||
|         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<TrackDescription>, |  | ||||||
|     ) -> 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<Vec<TrackDescription>> { |  | ||||||
|         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<PathBuf> { |  | ||||||
|         self.music_library_path.borrow().clone() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// Get the currently stored login credentials.
 |  | ||||||
|     pub fn get_login_data(&self) -> Option<LoginData> { |  | ||||||
|         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<String> { |  | ||||||
|         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<String> { |  | ||||||
|         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<Rc<Player>> { |  | ||||||
|         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<std::sync::mpsc::Sender<BackendAction>> { |  | ||||||
|         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::<BackendAction>(); |  | ||||||
| 
 |  | ||||||
|         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(()) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,9 +1,69 @@ | ||||||
|  | use super::secure; | ||||||
| use super::Backend; | use super::Backend; | ||||||
| use anyhow::{anyhow, bail, Result}; | use anyhow::{anyhow, bail, Result}; | ||||||
|  | use gio::prelude::*; | ||||||
| use isahc::http::StatusCode; | use isahc::http::StatusCode; | ||||||
| use isahc::prelude::*; | 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 { | 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<String> { | ||||||
|  |         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<String> { | ||||||
|  |         self.server_url.borrow().clone() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Get the currently stored login credentials.
 | ||||||
|  |     pub fn get_login_data(&self) -> Option<LoginData> { | ||||||
|  |         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.
 |     /// Try to login a user with the provided credentials and return, wether the login suceeded.
 | ||||||
|     pub async fn login(&self) -> Result<bool> { |     pub async fn login(&self) -> Result<bool> { | ||||||
|         let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?; |         let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?; | ||||||
|  |  | ||||||
							
								
								
									
										73
									
								
								musicus/src/backend/library.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								musicus/src/backend/library.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<PathBuf> { | ||||||
|  |         self.music_library_path.borrow().clone() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Get an interface to the current music library database.
 | ||||||
|  |     pub fn get_database(&self) -> Option<Rc<DbThread>> { | ||||||
|  |         self.database.borrow().clone() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Get an interface to the database and panic if there is none.
 | ||||||
|  |     pub fn db(&self) -> Rc<DbThread> { | ||||||
|  |         self.get_database().unwrap() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Get an interface to the playback service.
 | ||||||
|  |     pub fn get_player(&self) -> Option<Rc<Player>> { | ||||||
|  |         self.player.borrow().clone() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Get an interface to the player and panic if there is none.
 | ||||||
|  |     pub fn pl(&self) -> Rc<Player> { | ||||||
|  |         self.get_player().unwrap() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,7 +1,76 @@ | ||||||
| pub mod backend; | use crate::database::DbThread; | ||||||
| pub use backend::*; | 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 mod client; | ||||||
| pub use client::*; | pub use client::*; | ||||||
| 
 | 
 | ||||||
|  | pub mod library; | ||||||
|  | pub use library::*; | ||||||
|  | 
 | ||||||
| mod secure; | 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<mpsc::Receiver<BackendState>>, | ||||||
|  |     state_sender: RefCell<mpsc::Sender<BackendState>>, | ||||||
|  |     settings: gio::Settings, | ||||||
|  |     music_library_path: RefCell<Option<PathBuf>>, | ||||||
|  |     database: RefCell<Option<Rc<DbThread>>>, | ||||||
|  |     player: RefCell<Option<Rc<Player>>>, | ||||||
|  |     server_url: RefCell<Option<String>>, | ||||||
|  |     login_data: RefCell<Option<LoginData>>, | ||||||
|  |     token: RefCell<Option<String>>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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<Backend>) -> 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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -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<Database> { |  | ||||||
|         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<Person> { |  | ||||||
|         persons::table |  | ||||||
|             .filter(persons::id.eq(id)) |  | ||||||
|             .load::<Person>(&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<Vec<Person>> { |  | ||||||
|         let persons = persons::table.load::<Person>(&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<Instrument> { |  | ||||||
|         instruments::table |  | ||||||
|             .filter(instruments::id.eq(id)) |  | ||||||
|             .load::<Instrument>(&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<Vec<Instrument>> { |  | ||||||
|         let instruments = instruments::table.load::<Instrument>(&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<Work> { |  | ||||||
|         works::table |  | ||||||
|             .filter(works::id.eq(id)) |  | ||||||
|             .load::<Work>(&self.c)? |  | ||||||
|             .first() |  | ||||||
|             .cloned() |  | ||||||
|             .ok_or(anyhow!("No work with ID: {}", id)) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn get_work_description_for_work(&self, work: &Work) -> Result<WorkDescription> { |  | ||||||
|         let mut instruments: Vec<Instrument> = Vec::new(); |  | ||||||
| 
 |  | ||||||
|         let instrumentations = instrumentations::table |  | ||||||
|             .filter(instrumentations::work.eq(work.id)) |  | ||||||
|             .load::<Instrumentation>(&self.c)?; |  | ||||||
| 
 |  | ||||||
|         for instrumentation in instrumentations { |  | ||||||
|             instruments.push(self.get_instrument(instrumentation.instrument)?); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let mut part_descriptions: Vec<WorkPartDescription> = Vec::new(); |  | ||||||
| 
 |  | ||||||
|         let work_parts = work_parts::table |  | ||||||
|             .filter(work_parts::work.eq(work.id)) |  | ||||||
|             .load::<WorkPart>(&self.c)?; |  | ||||||
| 
 |  | ||||||
|         for work_part in work_parts { |  | ||||||
|             let mut part_instruments: Vec<Instrument> = Vec::new(); |  | ||||||
| 
 |  | ||||||
|             let part_instrumentations = part_instrumentations::table |  | ||||||
|                 .filter(part_instrumentations::work_part.eq(work_part.id)) |  | ||||||
|                 .load::<PartInstrumentation>(&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<WorkSectionDescription> = Vec::new(); |  | ||||||
| 
 |  | ||||||
|         let sections = work_sections::table |  | ||||||
|             .filter(work_sections::work.eq(work.id)) |  | ||||||
|             .load::<WorkSection>(&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<WorkDescription> { |  | ||||||
|         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<Vec<Work>> { |  | ||||||
|         let works = works::table |  | ||||||
|             .filter(works::composer.eq(composer_id)) |  | ||||||
|             .load::<Work>(&self.c)?; |  | ||||||
| 
 |  | ||||||
|         Ok(works) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn get_work_descriptions(&self, composer_id: i64) -> Result<Vec<WorkDescription>> { |  | ||||||
|         let mut work_descriptions: Vec<WorkDescription> = 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<Ensemble> { |  | ||||||
|         ensembles::table |  | ||||||
|             .filter(ensembles::id.eq(id)) |  | ||||||
|             .load::<Ensemble>(&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<Vec<Ensemble>> { |  | ||||||
|         let ensembles = ensembles::table.load::<Ensemble>(&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<Recording> { |  | ||||||
|         recordings::table |  | ||||||
|             .filter(recordings::id.eq(id)) |  | ||||||
|             .load::<Recording>(&self.c)? |  | ||||||
|             .first() |  | ||||||
|             .cloned() |  | ||||||
|             .ok_or(anyhow!("No recording with ID: {}", id)) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn get_recording_description_for_recording( |  | ||||||
|         &self, |  | ||||||
|         recording: &Recording, |  | ||||||
|     ) -> Result<RecordingDescription> { |  | ||||||
|         let mut performance_descriptions: Vec<PerformanceDescription> = Vec::new(); |  | ||||||
| 
 |  | ||||||
|         let performances = performances::table |  | ||||||
|             .filter(performances::recording.eq(recording.id)) |  | ||||||
|             .load::<Performance>(&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<RecordingDescription> { |  | ||||||
|         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<Vec<RecordingDescription>> { |  | ||||||
|         let mut recording_descriptions: Vec<RecordingDescription> = 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::<Recording>(&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<Vec<RecordingDescription>> { |  | ||||||
|         let mut recording_descriptions: Vec<RecordingDescription> = 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::<Recording>(&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<Vec<RecordingDescription>> { |  | ||||||
|         let mut recording_descriptions: Vec<RecordingDescription> = 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::<Recording>(&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<Vec<Recording>> { |  | ||||||
|         let recordings = recordings::table |  | ||||||
|             .filter(recordings::work.eq(work_id)) |  | ||||||
|             .load::<Recording>(&self.c)?; |  | ||||||
| 
 |  | ||||||
|         Ok(recordings) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn update_tracks(&self, recording_id: i64, tracks: Vec<TrackDescription>) -> 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::<Vec<String>>() |  | ||||||
|                     .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<Vec<TrackDescription>> { |  | ||||||
|         let tracks = tracks::table |  | ||||||
|             .filter(tracks::recording.eq(recording_id)) |  | ||||||
|             .order_by(tracks::track_index) |  | ||||||
|             .load::<Track>(&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!"); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										96
									
								
								musicus/src/database/ensembles.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								musicus/src/database/ensembles.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<Ensemble> 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<EnsembleRow> for Ensemble { | ||||||
|  |     type Error = Error; | ||||||
|  |     fn try_from(row: EnsembleRow) -> Result<Self> { | ||||||
|  |         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<Option<Ensemble>> { | ||||||
|  |         let row = ensembles::table | ||||||
|  |             .filter(ensembles::id.eq(id as i64)) | ||||||
|  |             .load::<EnsembleRow>(&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<Vec<Ensemble>> { | ||||||
|  |         let mut ensembles = Vec::<Ensemble>::new(); | ||||||
|  | 
 | ||||||
|  |         let rows = ensembles::table.load::<EnsembleRow>(&self.connection)?; | ||||||
|  |         for row in rows { | ||||||
|  |             ensembles.push(row.try_into()?); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(ensembles) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										96
									
								
								musicus/src/database/instruments.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								musicus/src/database/instruments.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<Instrument> 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<InstrumentRow> for Instrument { | ||||||
|  |     type Error = Error; | ||||||
|  |     fn try_from(row: InstrumentRow) -> Result<Self> { | ||||||
|  |         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<Option<Instrument>> { | ||||||
|  |         let row = instruments::table | ||||||
|  |             .filter(instruments::id.eq(id as i64)) | ||||||
|  |             .load::<InstrumentRow>(&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<Vec<Instrument>> { | ||||||
|  |         let mut instruments = Vec::<Instrument>::new(); | ||||||
|  | 
 | ||||||
|  |         let rows = instruments::table.load::<InstrumentRow>(&self.connection)?; | ||||||
|  |         for row in rows { | ||||||
|  |             instruments.push(row.try_into()?); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(instruments) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,10 +1,51 @@ | ||||||
| pub mod database; | use anyhow::Result; | ||||||
| pub use database::*; | use diesel::prelude::*; | ||||||
| 
 | 
 | ||||||
| pub mod models; | pub mod ensembles; | ||||||
| pub use models::*; | pub use ensembles::*; | ||||||
| 
 | 
 | ||||||
| pub mod schema; | pub mod instruments; | ||||||
|  | pub use instruments::*; | ||||||
| 
 | 
 | ||||||
| pub mod tables; | pub mod persons; | ||||||
| pub use tables::*; | 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<Database> { | ||||||
|  |         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(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,205 +0,0 @@ | ||||||
| use super::tables::*; |  | ||||||
| use std::convert::TryInto; |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone)] |  | ||||||
| pub struct WorkPartDescription { |  | ||||||
|     pub title: String, |  | ||||||
|     pub composer: Option<Person>, |  | ||||||
|     pub instruments: Vec<Instrument>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[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<Instrument>, |  | ||||||
|     pub parts: Vec<WorkPartDescription>, |  | ||||||
|     pub sections: Vec<WorkSectionDescription>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 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<i64>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone)] |  | ||||||
| pub struct WorkInsertion { |  | ||||||
|     pub work: Work, |  | ||||||
|     pub instrument_ids: Vec<i64>, |  | ||||||
|     pub parts: Vec<WorkPartInsertion>, |  | ||||||
|     pub sections: Vec<WorkSection>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl From<WorkDescription> 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<Person>, |  | ||||||
|     pub ensemble: Option<Ensemble>, |  | ||||||
|     pub role: Option<Instrument>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 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<PerformanceDescription>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl RecordingDescription { |  | ||||||
|     pub fn get_performers(&self) -> String { |  | ||||||
|         let texts: Vec<String> = self |  | ||||||
|             .performances |  | ||||||
|             .iter() |  | ||||||
|             .map(|performance| performance.get_title()) |  | ||||||
|             .collect(); |  | ||||||
| 
 |  | ||||||
|         texts.join(", ") |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone)] |  | ||||||
| pub struct RecordingInsertion { |  | ||||||
|     pub recording: Recording, |  | ||||||
|     pub performances: Vec<Performance>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl From<RecordingDescription> 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<usize>, |  | ||||||
|     pub file_name: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl From<Track> for TrackDescription { |  | ||||||
|     fn from(track: Track) -> Self { |  | ||||||
|         let mut work_parts = Vec::<usize>::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, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										111
									
								
								musicus/src/database/persons.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								musicus/src/database/persons.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<Person> 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<PersonRow> for Person { | ||||||
|  |     type Error = Error; | ||||||
|  |     fn try_from(row: PersonRow) -> Result<Self> { | ||||||
|  |         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<Option<Person>> { | ||||||
|  |         let row = persons::table | ||||||
|  |             .filter(persons::id.eq(id as i64)) | ||||||
|  |             .load::<PersonRow>(&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<Vec<Person>> { | ||||||
|  |         let mut persons = Vec::<Person>::new(); | ||||||
|  | 
 | ||||||
|  |         let rows = persons::table.load::<PersonRow>(&self.connection)?; | ||||||
|  |         for row in rows { | ||||||
|  |             persons.push(row.try_into()?); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(persons) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										252
									
								
								musicus/src/database/recordings.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								musicus/src/database/recordings.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<Recording> 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<i64>, | ||||||
|  |     pub ensemble: Option<i64>, | ||||||
|  |     pub role: Option<i64>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// 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<Person>, | ||||||
|  |     pub ensemble: Option<Ensemble>, | ||||||
|  |     pub role: Option<Instrument>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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<Performance>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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<String> = 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<Recording> { | ||||||
|  |         let mut performance_descriptions: Vec<Performance> = Vec::new(); | ||||||
|  | 
 | ||||||
|  |         let performance_rows = performances::table | ||||||
|  |             .filter(performances::recording.eq(row.id)) | ||||||
|  |             .load::<PerformanceRow>(&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<Vec<Recording>> { | ||||||
|  |         let mut recordings: Vec<Recording> = 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::<RecordingRow>(&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<Vec<Recording>> { | ||||||
|  |         let mut recordings: Vec<Recording> = 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::<RecordingRow>(&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<Vec<Recording>> { | ||||||
|  |         let mut recordings: Vec<Recording> = Vec::new(); | ||||||
|  | 
 | ||||||
|  |         let rows = recordings::table | ||||||
|  |             .filter(recordings::work.eq(work_id as i64)) | ||||||
|  |             .load::<RecordingRow>(&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(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -20,14 +20,6 @@ table! { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| table! { |  | ||||||
|     part_instrumentations (id) { |  | ||||||
|         id -> BigInt, |  | ||||||
|         work_part -> BigInt, |  | ||||||
|         instrument -> BigInt, |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| table! { | table! { | ||||||
|     performances (id) { |     performances (id) { | ||||||
|         id -> BigInt, |         id -> BigInt, | ||||||
|  | @ -69,8 +61,8 @@ table! { | ||||||
|         id -> BigInt, |         id -> BigInt, | ||||||
|         work -> BigInt, |         work -> BigInt, | ||||||
|         part_index -> BigInt, |         part_index -> BigInt, | ||||||
|         composer -> Nullable<BigInt>, |  | ||||||
|         title -> Text, |         title -> Text, | ||||||
|  |         composer -> Nullable<BigInt>, | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -93,8 +85,6 @@ table! { | ||||||
| 
 | 
 | ||||||
| joinable!(instrumentations -> instruments (instrument)); | joinable!(instrumentations -> instruments (instrument)); | ||||||
| joinable!(instrumentations -> works (work)); | joinable!(instrumentations -> works (work)); | ||||||
| joinable!(part_instrumentations -> instruments (instrument)); |  | ||||||
| joinable!(part_instrumentations -> works (work_part)); |  | ||||||
| joinable!(performances -> ensembles (ensemble)); | joinable!(performances -> ensembles (ensemble)); | ||||||
| joinable!(performances -> instruments (role)); | joinable!(performances -> instruments (role)); | ||||||
| joinable!(performances -> persons (person)); | joinable!(performances -> persons (person)); | ||||||
|  | @ -110,7 +100,6 @@ allow_tables_to_appear_in_same_query!( | ||||||
|     ensembles, |     ensembles, | ||||||
|     instrumentations, |     instrumentations, | ||||||
|     instruments, |     instruments, | ||||||
|     part_instrumentations, |  | ||||||
|     performances, |     performances, | ||||||
|     persons, |     persons, | ||||||
|     recordings, |     recordings, | ||||||
|  |  | ||||||
|  | @ -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<i64>, |  | ||||||
|     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<i64>, |  | ||||||
|     pub ensemble: Option<i64>, |  | ||||||
|     pub role: Option<i64>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[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, |  | ||||||
| } |  | ||||||
							
								
								
									
										327
									
								
								musicus/src/database/thread.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										327
									
								
								musicus/src/database/thread.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<Result<()>>), | ||||||
|  |     GetPerson(u32, Sender<Result<Option<Person>>>), | ||||||
|  |     DeletePerson(u32, Sender<Result<()>>), | ||||||
|  |     GetPersons(Sender<Result<Vec<Person>>>), | ||||||
|  |     UpdateInstrument(Instrument, Sender<Result<()>>), | ||||||
|  |     GetInstrument(u32, Sender<Result<Option<Instrument>>>), | ||||||
|  |     DeleteInstrument(u32, Sender<Result<()>>), | ||||||
|  |     GetInstruments(Sender<Result<Vec<Instrument>>>), | ||||||
|  |     UpdateWork(Work, Sender<Result<()>>), | ||||||
|  |     DeleteWork(u32, Sender<Result<()>>), | ||||||
|  |     GetWorks(u32, Sender<Result<Vec<Work>>>), | ||||||
|  |     UpdateEnsemble(Ensemble, Sender<Result<()>>), | ||||||
|  |     GetEnsemble(u32, Sender<Result<Option<Ensemble>>>), | ||||||
|  |     DeleteEnsemble(u32, Sender<Result<()>>), | ||||||
|  |     GetEnsembles(Sender<Result<Vec<Ensemble>>>), | ||||||
|  |     UpdateRecording(Recording, Sender<Result<()>>), | ||||||
|  |     DeleteRecording(u32, Sender<Result<()>>), | ||||||
|  |     GetRecordingsForPerson(u32, Sender<Result<Vec<Recording>>>), | ||||||
|  |     GetRecordingsForEnsemble(u32, Sender<Result<Vec<Recording>>>), | ||||||
|  |     GetRecordingsForWork(u32, Sender<Result<Vec<Recording>>>), | ||||||
|  |     UpdateTracks(u32, Vec<Track>, Sender<Result<()>>), | ||||||
|  |     DeleteTracks(u32, Sender<Result<()>>), | ||||||
|  |     GetTracks(u32, Sender<Result<Vec<Track>>>), | ||||||
|  |     Stop(Sender<()>), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | use Action::*; | ||||||
|  | 
 | ||||||
|  | /// A database running within a thread.
 | ||||||
|  | pub struct DbThread { | ||||||
|  |     action_sender: mpsc::Sender<Action>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl DbThread { | ||||||
|  |     /// Create a new database connection in a background thread.
 | ||||||
|  |     pub async fn new(path: String) -> Result<Self> { | ||||||
|  |         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<Option<Person>> { | ||||||
|  |         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<Vec<Person>> { | ||||||
|  |         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<Option<Instrument>> { | ||||||
|  |         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<Vec<Instrument>> { | ||||||
|  |         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<Vec<Work>> { | ||||||
|  |         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<Option<Ensemble>> { | ||||||
|  |         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<Vec<Ensemble>> { | ||||||
|  |         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<Vec<Recording>> { | ||||||
|  |         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<Vec<Recording>> { | ||||||
|  |         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<Vec<Recording>> { | ||||||
|  |         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<Track>, | ||||||
|  |     ) -> 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<Vec<Track>> { | ||||||
|  |         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?) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										94
									
								
								musicus/src/database/tracks.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								musicus/src/database/tracks.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<usize>, | ||||||
|  |     pub file_name: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl TryFrom<TrackRow> for Track { | ||||||
|  |     type Error = Error; | ||||||
|  |     fn try_from(row: TrackRow) -> Result<Self> { | ||||||
|  |         let mut work_parts = Vec::<usize>::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<Track>) -> 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::<Vec<String>>() | ||||||
|  |                     .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<Vec<Track>> { | ||||||
|  |         let mut tracks = Vec::<Track>::new(); | ||||||
|  | 
 | ||||||
|  |         let rows = tracks::table | ||||||
|  |             .filter(tracks::recording.eq(recording_id as i64)) | ||||||
|  |             .order_by(tracks::track_index) | ||||||
|  |             .load::<TrackRow>(&self.connection)?; | ||||||
|  | 
 | ||||||
|  |         for row in rows { | ||||||
|  |             tracks.push(row.try_into()?); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(tracks) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										262
									
								
								musicus/src/database/works.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								musicus/src/database/works.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<Work> 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<i64>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// 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<Person>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// 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<Instrument>, | ||||||
|  |     pub parts: Vec<WorkPart>, | ||||||
|  |     pub sections: Vec<WorkSection>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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<Option<Work>> { | ||||||
|  |         let row = works::table | ||||||
|  |             .filter(works::id.eq(id as i64)) | ||||||
|  |             .load::<WorkRow>(&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<Work> { | ||||||
|  |         let mut instruments: Vec<Instrument> = Vec::new(); | ||||||
|  | 
 | ||||||
|  |         let instrumentations = instrumentations::table | ||||||
|  |             .filter(instrumentations::work.eq(row.id)) | ||||||
|  |             .load::<InstrumentationRow>(&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<WorkPart> = Vec::new(); | ||||||
|  | 
 | ||||||
|  |         let part_rows = work_parts::table | ||||||
|  |             .filter(work_parts::work.eq(row.id)) | ||||||
|  |             .load::<WorkPartRow>(&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<WorkSection> = Vec::new(); | ||||||
|  | 
 | ||||||
|  |         let section_rows = work_sections::table | ||||||
|  |             .filter(work_sections::work.eq(row.id)) | ||||||
|  |             .load::<WorkSectionRow>(&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<Vec<Work>> { | ||||||
|  |         let mut works: Vec<Work> = Vec::new(); | ||||||
|  | 
 | ||||||
|  |         let rows = works::table | ||||||
|  |             .filter(works::composer.eq(composer_id as i64)) | ||||||
|  |             .load::<WorkRow>(&self.connection)?; | ||||||
|  | 
 | ||||||
|  |         for row in rows { | ||||||
|  |             works.push(self.get_work_data(row)?); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(works) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -12,7 +12,7 @@ where | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     window: libhandy::Window, |     window: libhandy::Window, | ||||||
|     callback: F, |     callback: F, | ||||||
|     id: i64, |     id: u32, | ||||||
|     name_entry: gtk::Entry, |     name_entry: gtk::Entry, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -26,8 +26,7 @@ where | ||||||
|         ensemble: Option<Ensemble>, |         ensemble: Option<Ensemble>, | ||||||
|         callback: F, |         callback: F, | ||||||
|     ) -> Rc<Self> { |     ) -> Rc<Self> { | ||||||
|         let builder = |         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_editor.ui"); | ||||||
|             gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_editor.ui"); |  | ||||||
| 
 | 
 | ||||||
|         get_widget!(builder, libhandy::Window, window); |         get_widget!(builder, libhandy::Window, window); | ||||||
|         get_widget!(builder, gtk::Button, cancel_button); |         get_widget!(builder, gtk::Button, cancel_button); | ||||||
|  | @ -63,7 +62,7 @@ where | ||||||
|             let clone = result.clone(); |             let clone = result.clone(); | ||||||
|             let c = glib::MainContext::default(); |             let c = glib::MainContext::default(); | ||||||
|             c.spawn_local(async move { |             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.window.close(); | ||||||
|                 (clone.callback)(ensemble.clone()); |                 (clone.callback)(ensemble.clone()); | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|  | @ -25,8 +25,7 @@ where | ||||||
|     F: Fn(Ensemble) -> () + 'static, |     F: Fn(Ensemble) -> () + 'static, | ||||||
| { | { | ||||||
|     pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> { |     pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> { | ||||||
|         let builder = |         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_selector.ui"); | ||||||
|             gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_selector.ui"); |  | ||||||
| 
 | 
 | ||||||
|         get_widget!(builder, libhandy::Window, window); |         get_widget!(builder, libhandy::Window, window); | ||||||
|         get_widget!(builder, gtk::Button, add_button); |         get_widget!(builder, gtk::Button, add_button); | ||||||
|  | @ -44,7 +43,7 @@ where | ||||||
|         let c = glib::MainContext::default(); |         let c = glib::MainContext::default(); | ||||||
|         let clone = result.clone(); |         let clone = result.clone(); | ||||||
|         c.spawn_local(async move { |         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() { |             for (index, ensemble) in ensembles.iter().enumerate() { | ||||||
|                 let label = gtk::Label::new(Some(&ensemble.name)); |                 let label = gtk::Label::new(Some(&ensemble.name)); | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ where | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     window: libhandy::Window, |     window: libhandy::Window, | ||||||
|     callback: F, |     callback: F, | ||||||
|     id: i64, |     id: u32, | ||||||
|     name_entry: gtk::Entry, |     name_entry: gtk::Entry, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -63,7 +63,7 @@ where | ||||||
|             let c = glib::MainContext::default(); |             let c = glib::MainContext::default(); | ||||||
|             let clone = result.clone(); |             let clone = result.clone(); | ||||||
|             c.spawn_local(async move { |             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.window.close(); | ||||||
|                 (clone.callback)(instrument.clone()); |                 (clone.callback)(instrument.clone()); | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|  | @ -25,8 +25,7 @@ where | ||||||
|     F: Fn(Instrument) -> () + 'static, |     F: Fn(Instrument) -> () + 'static, | ||||||
| { | { | ||||||
|     pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> { |     pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> { | ||||||
|         let builder = |         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_selector.ui"); | ||||||
|             gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_selector.ui"); |  | ||||||
| 
 | 
 | ||||||
|         get_widget!(builder, libhandy::Window, window); |         get_widget!(builder, libhandy::Window, window); | ||||||
|         get_widget!(builder, gtk::Button, add_button); |         get_widget!(builder, gtk::Button, add_button); | ||||||
|  | @ -44,7 +43,7 @@ where | ||||||
|         let c = glib::MainContext::default(); |         let c = glib::MainContext::default(); | ||||||
|         let clone = result.clone(); |         let clone = result.clone(); | ||||||
|         c.spawn_local(async move { |         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() { |             for (index, instrument) in instruments.iter().enumerate() { | ||||||
|                 let label = gtk::Label::new(Some(&instrument.name)); |                 let label = gtk::Label::new(Some(&instrument.name)); | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ where | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     window: libhandy::Window, |     window: libhandy::Window, | ||||||
|     callback: F, |     callback: F, | ||||||
|     id: i64, |     id: u32, | ||||||
|     first_name_entry: gtk::Entry, |     first_name_entry: gtk::Entry, | ||||||
|     last_name_entry: gtk::Entry, |     last_name_entry: gtk::Entry, | ||||||
| } | } | ||||||
|  | @ -67,7 +67,7 @@ where | ||||||
|             let c = glib::MainContext::default(); |             let c = glib::MainContext::default(); | ||||||
|             let clone = result.clone(); |             let clone = result.clone(); | ||||||
|             c.spawn_local(async move { |             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.window.close(); | ||||||
|                 (clone.callback)(person.clone()); |                 (clone.callback)(person.clone()); | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ pub struct PerformanceEditor { | ||||||
|     person: RefCell<Option<Person>>, |     person: RefCell<Option<Person>>, | ||||||
|     ensemble: RefCell<Option<Ensemble>>, |     ensemble: RefCell<Option<Ensemble>>, | ||||||
|     role: RefCell<Option<Instrument>>, |     role: RefCell<Option<Instrument>>, | ||||||
|     selected_cb: RefCell<Option<Box<dyn Fn(PerformanceDescription) -> ()>>>, |     selected_cb: RefCell<Option<Box<dyn Fn(Performance) -> ()>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl PerformanceEditor { | impl PerformanceEditor { | ||||||
|  | @ -28,7 +28,7 @@ impl PerformanceEditor { | ||||||
|     pub fn new<P: IsA<gtk::Window>>( |     pub fn new<P: IsA<gtk::Window>>( | ||||||
|         backend: Rc<Backend>, |         backend: Rc<Backend>, | ||||||
|         parent: &P, |         parent: &P, | ||||||
|         performance: Option<PerformanceDescription>, |         performance: Option<Performance>, | ||||||
|     ) -> Rc<Self> { |     ) -> Rc<Self> { | ||||||
|         // Create UI
 |         // Create UI
 | ||||||
| 
 | 
 | ||||||
|  | @ -70,7 +70,7 @@ impl PerformanceEditor { | ||||||
|         this.save_button |         this.save_button | ||||||
|             .connect_clicked(clone!(@strong this => move |_| { |             .connect_clicked(clone!(@strong this => move |_| { | ||||||
|                 if let Some(cb) = &*this.selected_cb.borrow() { |                 if let Some(cb) = &*this.selected_cb.borrow() { | ||||||
|                     cb(PerformanceDescription { |                     cb(Performance { | ||||||
|                         person: this.person.borrow().clone(), |                         person: this.person.borrow().clone(), | ||||||
|                         ensemble: this.ensemble.borrow().clone(), |                         ensemble: this.ensemble.borrow().clone(), | ||||||
|                         role: this.role.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.
 |     /// Set a closure to be called when the user has chosen to save the performance.
 | ||||||
|     pub fn set_selected_cb<F: Fn(PerformanceDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_selected_cb<F: Fn(Performance) -> () + 'static>(&self, cb: F) { | ||||||
|         self.selected_cb.replace(Some(Box::new(cb))); |         self.selected_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ pub struct RecordingDialog { | ||||||
|     stack: gtk::Stack, |     stack: gtk::Stack, | ||||||
|     selector: Rc<RecordingSelector>, |     selector: Rc<RecordingSelector>, | ||||||
|     editor: Rc<RecordingEditor>, |     editor: Rc<RecordingEditor>, | ||||||
|     selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>, |     selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl RecordingDialog { | impl RecordingDialog { | ||||||
|  | @ -75,7 +75,7 @@ impl RecordingDialog { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Set the closure to be called when the user has selected or created a recording.
 |     /// Set the closure to be called when the user has selected or created a recording.
 | ||||||
|     pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) { | ||||||
|         self.selected_cb.replace(Some(Box::new(cb))); |         self.selected_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,11 +19,11 @@ pub struct RecordingEditor { | ||||||
|     save_button: gtk::Button, |     save_button: gtk::Button, | ||||||
|     work_label: gtk::Label, |     work_label: gtk::Label, | ||||||
|     comment_entry: gtk::Entry, |     comment_entry: gtk::Entry, | ||||||
|     performance_list: Rc<List<PerformanceDescription>>, |     performance_list: Rc<List<Performance>>, | ||||||
|     id: i64, |     id: u32, | ||||||
|     work: RefCell<Option<WorkDescription>>, |     work: RefCell<Option<Work>>, | ||||||
|     performances: RefCell<Vec<PerformanceDescription>>, |     performances: RefCell<Vec<Performance>>, | ||||||
|     selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>, |     selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>, | ||||||
|     back_cb: RefCell<Option<Box<dyn Fn() -> ()>>>, |     back_cb: RefCell<Option<Box<dyn Fn() -> ()>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -33,7 +33,7 @@ impl RecordingEditor { | ||||||
|     pub fn new<W: IsA<gtk::Window>>( |     pub fn new<W: IsA<gtk::Window>>( | ||||||
|         backend: Rc<Backend>, |         backend: Rc<Backend>, | ||||||
|         parent: &W, |         parent: &W, | ||||||
|         recording: Option<RecordingDescription>, |         recording: Option<Recording>, | ||||||
|     ) -> Rc<Self> { |     ) -> Rc<Self> { | ||||||
|         // Create UI
 |         // Create UI
 | ||||||
| 
 | 
 | ||||||
|  | @ -87,7 +87,7 @@ impl RecordingEditor { | ||||||
| 
 | 
 | ||||||
|         this.save_button |         this.save_button | ||||||
|             .connect_clicked(clone!(@strong this => move |_| { |             .connect_clicked(clone!(@strong this => move |_| { | ||||||
|                 let recording = RecordingDescription { |                 let recording = Recording { | ||||||
|                     id: this.id, |                     id: this.id, | ||||||
|                     work: this.work.borrow().clone().expect("Tried to create recording without work!"), |                     work: this.work.borrow().clone().expect("Tried to create recording without work!"), | ||||||
|                     comment: this.comment_entry.get_text().to_string(), |                     comment: this.comment_entry.get_text().to_string(), | ||||||
|  | @ -97,7 +97,7 @@ impl RecordingEditor { | ||||||
|                 let c = glib::MainContext::default(); |                 let c = glib::MainContext::default(); | ||||||
|                 let clone = this.clone(); |                 let clone = this.clone(); | ||||||
|                 c.spawn_local(async move { |                 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() { |                     if let Some(cb) = &*clone.selected_cb.borrow() { | ||||||
|                         cb(recording.clone()); |                         cb(recording.clone()); | ||||||
|                     } |                     } | ||||||
|  | @ -192,12 +192,12 @@ impl RecordingEditor { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Set the closure to be called if the recording was created.
 |     /// Set the closure to be called if the recording was created.
 | ||||||
|     pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) { | ||||||
|         self.selected_cb.replace(Some(Box::new(cb))); |         self.selected_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Update the UI according to work.    
 |     /// 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.work_label.set_text(&format!("{}: {}", work.composer.name_fl(), work.title)); | ||||||
|         self.save_button.set_sensitive(true); |         self.save_button.set_sensitive(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ use std::rc::Rc; | ||||||
| /// A dialog for creating or editing a recording.
 | /// A dialog for creating or editing a recording.
 | ||||||
| pub struct RecordingEditorDialog { | pub struct RecordingEditorDialog { | ||||||
|     pub window: libhandy::Window, |     pub window: libhandy::Window, | ||||||
|     selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>, |     selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl RecordingEditorDialog { | impl RecordingEditorDialog { | ||||||
|  | @ -17,7 +17,7 @@ impl RecordingEditorDialog { | ||||||
|     pub fn new<W: IsA<gtk::Window>>( |     pub fn new<W: IsA<gtk::Window>>( | ||||||
|         backend: Rc<Backend>, |         backend: Rc<Backend>, | ||||||
|         parent: &W, |         parent: &W, | ||||||
|         recording: Option<RecordingDescription>, |         recording: Option<Recording>, | ||||||
|     ) -> Rc<Self> { |     ) -> Rc<Self> { | ||||||
|         // Create UI
 |         // Create UI
 | ||||||
| 
 | 
 | ||||||
|  | @ -52,7 +52,7 @@ impl RecordingEditorDialog { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Set the closure to be called when the user edited or created a recording.
 |     /// Set the closure to be called when the user edited or created a recording.
 | ||||||
|     pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) { | ||||||
|         self.selected_cb.replace(Some(Box::new(cb))); |         self.selected_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ pub struct RecordingSelector { | ||||||
|     pub widget: libhandy::Leaflet, |     pub widget: libhandy::Leaflet, | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     sidebar_box: gtk::Box, |     sidebar_box: gtk::Box, | ||||||
|     selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>, |     selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>, | ||||||
|     add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>, |     add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>, | ||||||
|     navigator: Rc<Navigator>, |     navigator: Rc<Navigator>, | ||||||
| } | } | ||||||
|  | @ -83,7 +83,7 @@ impl RecordingSelector { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Set the closure to be called when the user has selected a recording.
 |     /// Set the closure to be called when the user has selected a recording.
 | ||||||
|     pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) { | ||||||
|         self.selected_cb.replace(Some(Box::new(cb))); |         self.selected_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -16,8 +16,8 @@ pub struct RecordingSelectorPersonScreen { | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     widget: gtk::Box, |     widget: gtk::Box, | ||||||
|     stack: gtk::Stack, |     stack: gtk::Stack, | ||||||
|     work_list: Rc<List<WorkDescription>>, |     work_list: Rc<List<Work>>, | ||||||
|     selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>, |     selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>, | ||||||
|     navigator: RefCell<Option<Rc<Navigator>>>, |     navigator: RefCell<Option<Rc<Navigator>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -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)); |             let label = gtk::Label::new(Some(&work.title)); | ||||||
|             label.set_ellipsize(pango::EllipsizeMode::End); |             label.set_ellipsize(pango::EllipsizeMode::End); | ||||||
|             label.set_halign(gtk::Align::Start); |             label.set_halign(gtk::Align::Start); | ||||||
|  | @ -92,11 +92,7 @@ impl RecordingSelectorPersonScreen { | ||||||
|         let context = glib::MainContext::default(); |         let context = glib::MainContext::default(); | ||||||
|         let clone = this.clone(); |         let clone = this.clone(); | ||||||
|         context.spawn_local(async move { |         context.spawn_local(async move { | ||||||
|             let works = clone |             let works = clone.backend.db().get_works(person.id).await.unwrap(); | ||||||
|                 .backend |  | ||||||
|                 .get_work_descriptions(person.id) |  | ||||||
|                 .await |  | ||||||
|                 .unwrap(); |  | ||||||
| 
 | 
 | ||||||
|             clone.work_list.show_items(works); |             clone.work_list.show_items(works); | ||||||
|             clone.stack.set_visible_child_name("content"); |             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.
 |     /// Sets a closure to be called when the user has selected a recording.
 | ||||||
|     pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) { | ||||||
|         self.selected_cb.replace(Some(Box::new(cb))); |         self.selected_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -14,14 +14,14 @@ pub struct RecordingSelectorWorkScreen { | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     widget: gtk::Box, |     widget: gtk::Box, | ||||||
|     stack: gtk::Stack, |     stack: gtk::Stack, | ||||||
|     recording_list: Rc<List<RecordingDescription>>, |     recording_list: Rc<List<Recording>>, | ||||||
|     selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>, |     selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>, | ||||||
|     navigator: RefCell<Option<Rc<Navigator>>>, |     navigator: RefCell<Option<Rc<Navigator>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl RecordingSelectorWorkScreen { | impl RecordingSelectorWorkScreen { | ||||||
|     /// Create a new recording selector work screen.
 |     /// Create a new recording selector work screen.
 | ||||||
|     pub fn new(backend: Rc<Backend>, work: WorkDescription) -> Rc<Self> { |     pub fn new(backend: Rc<Backend>, work: Work) -> Rc<Self> { | ||||||
|         // Create UI
 |         // Create UI
 | ||||||
| 
 | 
 | ||||||
|         let builder = |         let builder = | ||||||
|  | @ -56,7 +56,8 @@ impl RecordingSelectorWorkScreen { | ||||||
|             } |             } | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         this.recording_list.set_make_widget(|recording: &RecordingDescription| { |         this.recording_list | ||||||
|  |             .set_make_widget(|recording: &Recording| { | ||||||
|                 let work_label = gtk::Label::new(Some(&recording.work.get_title())); |                 let work_label = gtk::Label::new(Some(&recording.work.get_title())); | ||||||
|                 work_label.set_ellipsize(pango::EllipsizeMode::End); |                 work_label.set_ellipsize(pango::EllipsizeMode::End); | ||||||
|                 work_label.set_halign(gtk::Align::Start); |                 work_label.set_halign(gtk::Align::Start); | ||||||
|  | @ -88,6 +89,7 @@ impl RecordingSelectorWorkScreen { | ||||||
|         context.spawn_local(async move { |         context.spawn_local(async move { | ||||||
|             let recordings = clone |             let recordings = clone | ||||||
|                 .backend |                 .backend | ||||||
|  |                 .db() | ||||||
|                 .get_recordings_for_work(work.id) |                 .get_recordings_for_work(work.id) | ||||||
|                 .await |                 .await | ||||||
|                 .unwrap(); |                 .unwrap(); | ||||||
|  | @ -100,7 +102,7 @@ impl RecordingSelectorWorkScreen { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Sets a closure to be called when the user has selected a recording.
 |     /// Sets a closure to be called when the user has selected a recording.
 | ||||||
|     pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) { | ||||||
|         self.selected_cb.replace(Some(Box::new(cb))); |         self.selected_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -11,10 +11,10 @@ pub struct TrackEditor { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl TrackEditor { | impl TrackEditor { | ||||||
|     pub fn new<W, F>(parent: &W, track: TrackDescription, work: WorkDescription, callback: F) -> Self |     pub fn new<W, F>(parent: &W, track: Track, work: Work, callback: F) -> Self | ||||||
|     where |     where | ||||||
|         W: IsA<gtk::Window>, |         W: IsA<gtk::Window>, | ||||||
|         F: Fn(TrackDescription) -> () + 'static, |         F: Fn(Track) -> () + 'static, | ||||||
|     { |     { | ||||||
|         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui"); |         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(); |             let mut work_parts = work_parts.borrow_mut(); | ||||||
|             work_parts.sort(); |             work_parts.sort(); | ||||||
| 
 | 
 | ||||||
|             callback(TrackDescription { |             callback(Track { | ||||||
|                 work_parts: work_parts.clone(), |                 work_parts: work_parts.clone(), | ||||||
|                 file_name: file_name.clone(), |                 file_name: file_name.clone(), | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|  | @ -18,9 +18,9 @@ pub struct TracksEditor { | ||||||
|     recording_stack: gtk::Stack, |     recording_stack: gtk::Stack, | ||||||
|     work_label: gtk::Label, |     work_label: gtk::Label, | ||||||
|     performers_label: gtk::Label, |     performers_label: gtk::Label, | ||||||
|     track_list: Rc<List<TrackDescription>>, |     track_list: Rc<List<Track>>, | ||||||
|     recording: RefCell<Option<RecordingDescription>>, |     recording: RefCell<Option<Recording>>, | ||||||
|     tracks: RefCell<Vec<TrackDescription>>, |     tracks: RefCell<Vec<Track>>, | ||||||
|     callback: RefCell<Option<Box<dyn Fn() -> ()>>>, |     callback: RefCell<Option<Box<dyn Fn() -> ()>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -30,8 +30,8 @@ impl TracksEditor { | ||||||
|     pub fn new<P: IsA<gtk::Window>>( |     pub fn new<P: IsA<gtk::Window>>( | ||||||
|         backend: Rc<Backend>, |         backend: Rc<Backend>, | ||||||
|         parent: &P, |         parent: &P, | ||||||
|         recording: Option<RecordingDescription>, |         recording: Option<Recording>, | ||||||
|         tracks: Vec<TrackDescription>, |         tracks: Vec<Track>, | ||||||
|     ) -> Rc<Self> { |     ) -> Rc<Self> { | ||||||
|         // UI setup
 |         // UI setup
 | ||||||
| 
 | 
 | ||||||
|  | @ -80,8 +80,8 @@ impl TracksEditor { | ||||||
|                 let context = glib::MainContext::default(); |                 let context = glib::MainContext::default(); | ||||||
|                 let this = this.clone(); |                 let this = this.clone(); | ||||||
|                 context.spawn_local(async move { |                 context.spawn_local(async move { | ||||||
|                     this.backend.update_tracks( |                     this.backend.db().update_tracks( | ||||||
|                         this.recording.borrow().as_ref().unwrap().id, |                         this.recording.borrow().as_ref().unwrap().id as u32, | ||||||
|                         this.tracks.borrow().clone(), |                         this.tracks.borrow().clone(), | ||||||
|                     ).await.unwrap(); |                     ).await.unwrap(); | ||||||
| 
 | 
 | ||||||
|  | @ -135,7 +135,7 @@ impl TracksEditor { | ||||||
|                     let mut tracks = this.tracks.borrow_mut(); |                     let mut tracks = this.tracks.borrow_mut(); | ||||||
|                     for file_name in dialog.get_filenames() { |                     for file_name in dialog.get_filenames() { | ||||||
|                         let file_name = file_name.strip_prefix(&music_library_path).unwrap(); |                         let file_name = file_name.strip_prefix(&music_library_path).unwrap(); | ||||||
|                         tracks.insert(index, TrackDescription { |                         tracks.insert(index, Track { | ||||||
|                             work_parts: Vec::new(), |                             work_parts: Vec::new(), | ||||||
|                             file_name: String::from(file_name.to_str().unwrap()), |                             file_name: String::from(file_name.to_str().unwrap()), | ||||||
|                         }); |                         }); | ||||||
|  | @ -224,7 +224,7 @@ impl TracksEditor { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Create a widget representing a track.
 |     /// 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::<String>::new(); |         let mut title_parts = Vec::<String>::new(); | ||||||
|         for part in &track.work_parts { |         for part in &track.work_parts { | ||||||
|             if let Some(recording) = &*self.recording.borrow() { |             if let Some(recording) = &*self.recording.borrow() { | ||||||
|  | @ -256,7 +256,7 @@ impl TracksEditor { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Set everything up after selecting a recording.
 |     /// 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.work_label.set_text(&recording.work.get_title()); | ||||||
|         self.performers_label.set_text(&recording.get_performers()); |         self.performers_label.set_text(&recording.get_performers()); | ||||||
|         self.recording_stack.set_visible_child_name("selected"); |         self.recording_stack.set_visible_child_name("selected"); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| use crate::backend::*; | use crate::backend::*; | ||||||
| use crate::database::*; | use crate::database::*; | ||||||
| use crate::dialogs::*; | use crate::dialogs::*; | ||||||
| use crate::widgets::*; |  | ||||||
| use gettextrs::gettext; | use gettextrs::gettext; | ||||||
| use glib::clone; | use glib::clone; | ||||||
| use gtk::prelude::*; | use gtk::prelude::*; | ||||||
|  | @ -16,10 +15,8 @@ pub struct PartEditor { | ||||||
|     title_entry: gtk::Entry, |     title_entry: gtk::Entry, | ||||||
|     composer_label: gtk::Label, |     composer_label: gtk::Label, | ||||||
|     reset_composer_button: gtk::Button, |     reset_composer_button: gtk::Button, | ||||||
|     instrument_list: Rc<List<Instrument>>, |  | ||||||
|     composer: RefCell<Option<Person>>, |     composer: RefCell<Option<Person>>, | ||||||
|     instruments: RefCell<Vec<Instrument>>, |     ready_cb: RefCell<Option<Box<dyn Fn(WorkPart) -> ()>>>, | ||||||
|     ready_cb: RefCell<Option<Box<dyn Fn(WorkPartDescription) -> ()>>>, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl PartEditor { | impl PartEditor { | ||||||
|  | @ -27,7 +24,7 @@ impl PartEditor { | ||||||
|     pub fn new<P: IsA<gtk::Window>>( |     pub fn new<P: IsA<gtk::Window>>( | ||||||
|         backend: Rc<Backend>, |         backend: Rc<Backend>, | ||||||
|         parent: &P, |         parent: &P, | ||||||
|         part: Option<WorkPartDescription>, |         part: Option<WorkPart>, | ||||||
|     ) -> Rc<Self> { |     ) -> Rc<Self> { | ||||||
|         // Create UI
 |         // Create UI
 | ||||||
| 
 | 
 | ||||||
|  | @ -40,21 +37,15 @@ impl PartEditor { | ||||||
|         get_widget!(builder, gtk::Button, composer_button); |         get_widget!(builder, gtk::Button, composer_button); | ||||||
|         get_widget!(builder, gtk::Label, composer_label); |         get_widget!(builder, gtk::Label, composer_label); | ||||||
|         get_widget!(builder, gtk::Button, reset_composer_button); |         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)); |         window.set_transient_for(Some(parent)); | ||||||
| 
 | 
 | ||||||
|         let instrument_list = List::new(&gettext("No instruments added.")); |         let composer = match part { | ||||||
|         scroll.add(&instrument_list.widget); |  | ||||||
| 
 |  | ||||||
|         let (composer, instruments) = match part { |  | ||||||
|             Some(part) => { |             Some(part) => { | ||||||
|                 title_entry.set_text(&part.title); |                 title_entry.set_text(&part.title); | ||||||
|                 (part.composer, part.instruments) |                 part.composer | ||||||
|             } |             } | ||||||
|             None => (None, Vec::new()), |             None => None, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let this = Rc::new(Self { |         let this = Rc::new(Self { | ||||||
|  | @ -63,9 +54,7 @@ impl PartEditor { | ||||||
|             title_entry, |             title_entry, | ||||||
|             composer_label, |             composer_label, | ||||||
|             reset_composer_button, |             reset_composer_button, | ||||||
|             instrument_list, |  | ||||||
|             composer: RefCell::new(composer), |             composer: RefCell::new(composer), | ||||||
|             instruments: RefCell::new(instruments), |  | ||||||
|             ready_cb: RefCell::new(None), |             ready_cb: RefCell::new(None), | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -77,10 +66,9 @@ impl PartEditor { | ||||||
| 
 | 
 | ||||||
|         save_button.connect_clicked(clone!(@strong this => move |_| { |         save_button.connect_clicked(clone!(@strong this => move |_| { | ||||||
|             if let Some(cb) = &*this.ready_cb.borrow() { |             if let Some(cb) = &*this.ready_cb.borrow() { | ||||||
|                 cb(WorkPartDescription { |                 cb(WorkPart { | ||||||
|                     title: this.title_entry.get_text().to_string(), |                     title: this.title_entry.get_text().to_string(), | ||||||
|                     composer: this.composer.borrow().clone(), |                     composer: this.composer.borrow().clone(), | ||||||
|                     instruments: this.instruments.borrow().clone(), |  | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -100,55 +88,17 @@ impl PartEditor { | ||||||
|                 this.show_composer(None); |                 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
 |         // Initialize
 | ||||||
| 
 | 
 | ||||||
|         if let Some(composer) = &*this.composer.borrow() { |         if let Some(composer) = &*this.composer.borrow() { | ||||||
|             this.show_composer(Some(composer)); |             this.show_composer(Some(composer)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.instrument_list |  | ||||||
|             .show_items(this.instruments.borrow().clone()); |  | ||||||
| 
 |  | ||||||
|         this |         this | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Set the closure to be called when the user wants to save the part.
 |     /// Set the closure to be called when the user wants to save the part.
 | ||||||
|     pub fn set_ready_cb<F: Fn(WorkPartDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_ready_cb<F: Fn(WorkPart) -> () + 'static>(&self, cb: F) { | ||||||
|         self.ready_cb.replace(Some(Box::new(cb))); |         self.ready_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -9,15 +9,12 @@ use std::rc::Rc; | ||||||
| pub struct SectionEditor { | pub struct SectionEditor { | ||||||
|     window: libhandy::Window, |     window: libhandy::Window, | ||||||
|     title_entry: gtk::Entry, |     title_entry: gtk::Entry, | ||||||
|     ready_cb: RefCell<Option<Box<dyn Fn(WorkSectionDescription) -> ()>>>, |     ready_cb: RefCell<Option<Box<dyn Fn(WorkSection) -> ()>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl SectionEditor { | impl SectionEditor { | ||||||
|     /// Create a new section editor and optionally initialize it.
 |     /// Create a new section editor and optionally initialize it.
 | ||||||
|     pub fn new<P: IsA<gtk::Window>>( |     pub fn new<P: IsA<gtk::Window>>(parent: &P, section: Option<WorkSection>) -> Rc<Self> { | ||||||
|         parent: &P, |  | ||||||
|         section: Option<WorkSectionDescription>, |  | ||||||
|     ) -> Rc<Self> { |  | ||||||
|         // Create UI
 |         // Create UI
 | ||||||
| 
 | 
 | ||||||
|         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/section_editor.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 |_| { |         save_button.connect_clicked(clone!(@strong this => move |_| { | ||||||
|             if let Some(cb) = &*this.ready_cb.borrow() { |             if let Some(cb) = &*this.ready_cb.borrow() { | ||||||
|                 cb(WorkSectionDescription { |                 cb(WorkSection { | ||||||
|                     before_index: 0, |                     before_index: 0, | ||||||
|                     title: this.title_entry.get_text().to_string(), |                     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
 |     /// 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
 |     /// 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.
 |     /// change that later before adding the section to the database.
 | ||||||
|     pub fn set_ready_cb<F: Fn(WorkSectionDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_ready_cb<F: Fn(WorkSection) -> () + 'static>(&self, cb: F) { | ||||||
|         self.ready_cb.replace(Some(Box::new(cb))); |         self.ready_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ pub struct WorkDialog { | ||||||
|     stack: gtk::Stack, |     stack: gtk::Stack, | ||||||
|     selector: Rc<WorkSelector>, |     selector: Rc<WorkSelector>, | ||||||
|     editor: Rc<WorkEditor>, |     editor: Rc<WorkEditor>, | ||||||
|     selected_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>, |     selected_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl WorkDialog { | impl WorkDialog { | ||||||
|  | @ -75,7 +75,7 @@ impl WorkDialog { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Set the closure to be called when the user has selected or created a work.
 |     /// Set the closure to be called when the user has selected or created a work.
 | ||||||
|     pub fn set_selected_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_selected_cb<F: Fn(Work) -> () + 'static>(&self, cb: F) { | ||||||
|         self.selected_cb.replace(Some(Box::new(cb))); |         self.selected_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -15,8 +15,8 @@ use std::rc::Rc; | ||||||
| /// Either a work part or a work section.
 | /// Either a work part or a work section.
 | ||||||
| #[derive(Clone)] | #[derive(Clone)] | ||||||
| enum PartOrSection { | enum PartOrSection { | ||||||
|     Part(WorkPartDescription), |     Part(WorkPart), | ||||||
|     Section(WorkSectionDescription), |     Section(WorkSection), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// A widget for editing and creating works.
 | /// A widget for editing and creating works.
 | ||||||
|  | @ -29,12 +29,12 @@ pub struct WorkEditor { | ||||||
|     composer_label: gtk::Label, |     composer_label: gtk::Label, | ||||||
|     instrument_list: Rc<List<Instrument>>, |     instrument_list: Rc<List<Instrument>>, | ||||||
|     part_list: Rc<List<PartOrSection>>, |     part_list: Rc<List<PartOrSection>>, | ||||||
|     id: i64, |     id: u32, | ||||||
|     composer: RefCell<Option<Person>>, |     composer: RefCell<Option<Person>>, | ||||||
|     instruments: RefCell<Vec<Instrument>>, |     instruments: RefCell<Vec<Instrument>>, | ||||||
|     structure: RefCell<Vec<PartOrSection>>, |     structure: RefCell<Vec<PartOrSection>>, | ||||||
|     cancel_cb: RefCell<Option<Box<dyn Fn() -> ()>>>, |     cancel_cb: RefCell<Option<Box<dyn Fn() -> ()>>>, | ||||||
|     saved_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>, |     saved_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl WorkEditor { | impl WorkEditor { | ||||||
|  | @ -43,7 +43,7 @@ impl WorkEditor { | ||||||
|     pub fn new<P: IsA<gtk::Window>>( |     pub fn new<P: IsA<gtk::Window>>( | ||||||
|         backend: Rc<Backend>, |         backend: Rc<Backend>, | ||||||
|         parent: &P, |         parent: &P, | ||||||
|         work: Option<WorkDescription>, |         work: Option<Work>, | ||||||
|     ) -> Rc<Self> { |     ) -> Rc<Self> { | ||||||
|         // Create UI
 |         // Create UI
 | ||||||
| 
 | 
 | ||||||
|  | @ -120,7 +120,7 @@ impl WorkEditor { | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         this.save_button.connect_clicked(clone!(@strong this => move |_| { |         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 parts = Vec::new(); | ||||||
|             let mut sections = Vec::new(); |             let mut sections = Vec::new(); | ||||||
| 
 | 
 | ||||||
|  | @ -129,7 +129,6 @@ impl WorkEditor { | ||||||
|                     PartOrSection::Part(part) => parts.push(part.clone()), |                     PartOrSection::Part(part) => parts.push(part.clone()), | ||||||
|                     PartOrSection::Section(section) => { |                     PartOrSection::Section(section) => { | ||||||
|                         let mut section = section.clone(); |                         let mut section = section.clone(); | ||||||
|                         let index: i64 = index.try_into().unwrap(); |  | ||||||
|                         section.before_index = index - section_count; |                         section.before_index = index - section_count; | ||||||
|                         sections.push(section); |                         sections.push(section); | ||||||
|                         section_count += 1; |                         section_count += 1; | ||||||
|  | @ -137,7 +136,7 @@ impl WorkEditor { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             let work = WorkDescription { |             let work = Work { | ||||||
|                 id: this.id, |                 id: this.id, | ||||||
|                 title: this.title_entry.get_text().to_string(), |                 title: this.title_entry.get_text().to_string(), | ||||||
|                 composer: this.composer.borrow().clone().expect("Tried to create work without composer!"), |                 composer: this.composer.borrow().clone().expect("Tried to create work without composer!"), | ||||||
|  | @ -149,7 +148,7 @@ impl WorkEditor { | ||||||
|             let c = glib::MainContext::default(); |             let c = glib::MainContext::default(); | ||||||
|             let clone = this.clone(); |             let clone = this.clone(); | ||||||
|             c.spawn_local(async move { |             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() { |                 if let Some(cb) = &*clone.saved_cb.borrow() { | ||||||
|                     cb(work); |                     cb(work); | ||||||
|                 } |                 } | ||||||
|  | @ -333,7 +332,8 @@ impl WorkEditor { | ||||||
|             this.show_composer(composer); |             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.part_list.show_items(this.structure.borrow().clone()); | ||||||
| 
 | 
 | ||||||
|         this |         this | ||||||
|  | @ -345,7 +345,7 @@ impl WorkEditor { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// The closure to call when a work was created.
 |     /// The closure to call when a work was created.
 | ||||||
|     pub fn set_saved_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_saved_cb<F: Fn(Work) -> () + 'static>(&self, cb: F) { | ||||||
|         self.saved_cb.replace(Some(Box::new(cb))); |         self.saved_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ use std::rc::Rc; | ||||||
| /// A dialog for creating or editing a work.
 | /// A dialog for creating or editing a work.
 | ||||||
| pub struct WorkEditorDialog { | pub struct WorkEditorDialog { | ||||||
|     pub window: libhandy::Window, |     pub window: libhandy::Window, | ||||||
|     saved_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>, |     saved_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl WorkEditorDialog { | impl WorkEditorDialog { | ||||||
|  | @ -17,7 +17,7 @@ impl WorkEditorDialog { | ||||||
|     pub fn new<W: IsA<gtk::Window>>( |     pub fn new<W: IsA<gtk::Window>>( | ||||||
|         backend: Rc<Backend>, |         backend: Rc<Backend>, | ||||||
|         parent: &W, |         parent: &W, | ||||||
|         work: Option<WorkDescription>, |         work: Option<Work>, | ||||||
|     ) -> Rc<Self> { |     ) -> Rc<Self> { | ||||||
|         // Create UI
 |         // Create UI
 | ||||||
| 
 | 
 | ||||||
|  | @ -52,7 +52,7 @@ impl WorkEditorDialog { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Set the closure to be called when the user edited or created a work.
 |     /// Set the closure to be called when the user edited or created a work.
 | ||||||
|     pub fn set_saved_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_saved_cb<F: Fn(Work) -> () + 'static>(&self, cb: F) { | ||||||
|         self.saved_cb.replace(Some(Box::new(cb))); |         self.saved_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ pub struct WorkSelector { | ||||||
|     pub widget: libhandy::Leaflet, |     pub widget: libhandy::Leaflet, | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     sidebar_box: gtk::Box, |     sidebar_box: gtk::Box, | ||||||
|     selected_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>, |     selected_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>, | ||||||
|     add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>, |     add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>, | ||||||
|     navigator: Rc<Navigator>, |     navigator: Rc<Navigator>, | ||||||
| } | } | ||||||
|  | @ -83,7 +83,7 @@ impl WorkSelector { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Set the closure to be called when the user has selected a work.
 |     /// Set the closure to be called when the user has selected a work.
 | ||||||
|     pub fn set_selected_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_selected_cb<F: Fn(Work) -> () + 'static>(&self, cb: F) { | ||||||
|         self.selected_cb.replace(Some(Box::new(cb))); |         self.selected_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -14,8 +14,8 @@ pub struct WorkSelectorPersonScreen { | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     widget: gtk::Box, |     widget: gtk::Box, | ||||||
|     stack: gtk::Stack, |     stack: gtk::Stack, | ||||||
|     work_list: Rc<List<WorkDescription>>, |     work_list: Rc<List<Work>>, | ||||||
|     selected_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>, |     selected_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>, | ||||||
|     navigator: RefCell<Option<Rc<Navigator>>>, |     navigator: RefCell<Option<Rc<Navigator>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -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)); |             let label = gtk::Label::new(Some(&work.title)); | ||||||
|             label.set_ellipsize(pango::EllipsizeMode::End); |             label.set_ellipsize(pango::EllipsizeMode::End); | ||||||
|             label.set_halign(gtk::Align::Start); |             label.set_halign(gtk::Align::Start); | ||||||
|  | @ -80,11 +80,7 @@ impl WorkSelectorPersonScreen { | ||||||
|         let context = glib::MainContext::default(); |         let context = glib::MainContext::default(); | ||||||
|         let clone = this.clone(); |         let clone = this.clone(); | ||||||
|         context.spawn_local(async move { |         context.spawn_local(async move { | ||||||
|             let works = clone |             let works = clone.backend.db().get_works(person.id).await.unwrap(); | ||||||
|                 .backend |  | ||||||
|                 .get_work_descriptions(person.id) |  | ||||||
|                 .await |  | ||||||
|                 .unwrap(); |  | ||||||
| 
 | 
 | ||||||
|             clone.work_list.show_items(works); |             clone.work_list.show_items(works); | ||||||
|             clone.stack.set_visible_child_name("content"); |             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.
 |     /// Sets a closure to be called when the user has selected a work.
 | ||||||
|     pub fn set_selected_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) { |     pub fn set_selected_cb<F: Fn(Work) -> () + 'static>(&self, cb: F) { | ||||||
|         self.selected_cb.replace(Some(Box::new(cb))); |         self.selected_cb.replace(Some(Box::new(cb))); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -33,15 +33,19 @@ run_command( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| sources = files( | sources = files( | ||||||
|   'backend/backend.rs', |  | ||||||
|   'backend/client.rs', |   'backend/client.rs', | ||||||
|  |   'backend/library.rs', | ||||||
|   'backend/mod.rs', |   'backend/mod.rs', | ||||||
|   'backend/secure.rs', |   'backend/secure.rs', | ||||||
|   'database/database.rs', |   'database/ensembles.rs', | ||||||
|  |   'database/instruments.rs', | ||||||
|   'database/mod.rs', |   'database/mod.rs', | ||||||
|   'database/models.rs', |   'database/persons.rs', | ||||||
|  |   'database/recordings.rs', | ||||||
|   'database/schema.rs', |   'database/schema.rs', | ||||||
|   'database/tables.rs', |   'database/thread.rs', | ||||||
|  |   'database/tracks.rs', | ||||||
|  |   'database/works.rs', | ||||||
|   'dialogs/about.rs', |   'dialogs/about.rs', | ||||||
|   'dialogs/ensemble_editor.rs', |   'dialogs/ensemble_editor.rs', | ||||||
|   'dialogs/ensemble_selector.rs', |   'dialogs/ensemble_selector.rs', | ||||||
|  |  | ||||||
|  | @ -8,8 +8,8 @@ use std::rc::Rc; | ||||||
| 
 | 
 | ||||||
| #[derive(Clone)] | #[derive(Clone)] | ||||||
| pub struct PlaylistItem { | pub struct PlaylistItem { | ||||||
|     pub recording: RecordingDescription, |     pub recording: Recording, | ||||||
|     pub tracks: Vec<TrackDescription>, |     pub tracks: Vec<Track>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub struct Player { | pub struct Player { | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ pub struct EnsembleScreen { | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     widget: gtk::Box, |     widget: gtk::Box, | ||||||
|     stack: gtk::Stack, |     stack: gtk::Stack, | ||||||
|     recording_list: Rc<List<RecordingDescription>>, |     recording_list: Rc<List<Recording>>, | ||||||
|     navigator: RefCell<Option<Rc<Navigator>>>, |     navigator: RefCell<Option<Rc<Navigator>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -52,7 +52,7 @@ impl EnsembleScreen { | ||||||
| 
 | 
 | ||||||
|         let recording_list = List::new(&gettext("No recordings found.")); |         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())); |             let work_label = gtk::Label::new(Some(&recording.work.get_title())); | ||||||
| 
 | 
 | ||||||
|             work_label.set_ellipsize(pango::EllipsizeMode::End); |             work_label.set_ellipsize(pango::EllipsizeMode::End); | ||||||
|  | @ -72,7 +72,7 @@ impl EnsembleScreen { | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         recording_list.set_filter( |         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 search = search_entry.get_text().to_string().to_lowercase(); | ||||||
|                 let text = recording.work.get_title() + &recording.get_performers(); |                 let text = recording.work.get_title() + &recording.get_performers(); | ||||||
|                 search.is_empty() || text.contains(&search) |                 search.is_empty() || text.contains(&search) | ||||||
|  | @ -114,7 +114,8 @@ impl EnsembleScreen { | ||||||
|         context.spawn_local(async move { |         context.spawn_local(async move { | ||||||
|             let recordings = clone |             let recordings = clone | ||||||
|                 .backend |                 .backend | ||||||
|                 .get_recordings_for_ensemble(ensemble.id) |                 .db() | ||||||
|  |                 .get_recordings_for_ensemble(ensemble.id as u32) | ||||||
|                 .await |                 .await | ||||||
|                 .unwrap(); |                 .unwrap(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,8 +14,8 @@ pub struct PersonScreen { | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     widget: gtk::Box, |     widget: gtk::Box, | ||||||
|     stack: gtk::Stack, |     stack: gtk::Stack, | ||||||
|     work_list: Rc<List<WorkDescription>>, |     work_list: Rc<List<Work>>, | ||||||
|     recording_list: Rc<List<RecordingDescription>>, |     recording_list: Rc<List<Recording>>, | ||||||
|     navigator: RefCell<Option<Rc<Navigator>>>, |     navigator: RefCell<Option<Rc<Navigator>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -56,7 +56,7 @@ impl PersonScreen { | ||||||
| 
 | 
 | ||||||
|         let work_list = List::new(&gettext("No works found.")); |         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)); |             let label = gtk::Label::new(Some(&work.title)); | ||||||
|             label.set_halign(gtk::Align::Start); |             label.set_halign(gtk::Align::Start); | ||||||
|             label.set_margin_start(6); |             label.set_margin_start(6); | ||||||
|  | @ -66,17 +66,15 @@ impl PersonScreen { | ||||||
|             label.upcast() |             label.upcast() | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         work_list.set_filter( |         work_list.set_filter(clone!(@strong search_entry => move |work: &Work| { | ||||||
|             clone!(@strong search_entry => move |work: &WorkDescription| { |  | ||||||
|             let search = search_entry.get_text().to_string().to_lowercase(); |             let search = search_entry.get_text().to_string().to_lowercase(); | ||||||
|             let title = work.title.to_lowercase(); |             let title = work.title.to_lowercase(); | ||||||
|             search.is_empty() || title.contains(&search) |             search.is_empty() || title.contains(&search) | ||||||
|             }), |         })); | ||||||
|         ); |  | ||||||
| 
 | 
 | ||||||
|         let recording_list = List::new(&gettext("No recordings found.")); |         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())); |             let work_label = gtk::Label::new(Some(&recording.work.get_title())); | ||||||
| 
 | 
 | ||||||
|             work_label.set_ellipsize(pango::EllipsizeMode::End); |             work_label.set_ellipsize(pango::EllipsizeMode::End); | ||||||
|  | @ -96,7 +94,7 @@ impl PersonScreen { | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         recording_list.set_filter( |         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 search = search_entry.get_text().to_string().to_lowercase(); | ||||||
|                 let text = recording.work.get_title() + &recording.get_performers(); |                 let text = recording.work.get_title() + &recording.get_performers(); | ||||||
|                 search.is_empty() || text.contains(&search) |                 search.is_empty() || text.contains(&search) | ||||||
|  | @ -152,12 +150,14 @@ impl PersonScreen { | ||||||
|         context.spawn_local(async move { |         context.spawn_local(async move { | ||||||
|             let works = clone |             let works = clone | ||||||
|                 .backend |                 .backend | ||||||
|                 .get_work_descriptions(person.id) |                 .db() | ||||||
|  |                 .get_works(person.id as u32) | ||||||
|                 .await |                 .await | ||||||
|                 .unwrap(); |                 .unwrap(); | ||||||
|             let recordings = clone |             let recordings = clone | ||||||
|                 .backend |                 .backend | ||||||
|                 .get_recordings_for_person(person.id) |                 .db() | ||||||
|  |                 .get_recordings_for_person(person.id as u32) | ||||||
|                 .await |                 .await | ||||||
|                 .unwrap(); |                 .unwrap(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,12 +14,12 @@ pub struct RecordingScreen { | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     widget: gtk::Box, |     widget: gtk::Box, | ||||||
|     stack: gtk::Stack, |     stack: gtk::Stack, | ||||||
|     tracks: RefCell<Vec<TrackDescription>>, |     tracks: RefCell<Vec<Track>>, | ||||||
|     navigator: RefCell<Option<Rc<Navigator>>>, |     navigator: RefCell<Option<Rc<Navigator>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl RecordingScreen { | impl RecordingScreen { | ||||||
|     pub fn new(backend: Rc<Backend>, recording: RecordingDescription) -> Rc<Self> { |     pub fn new(backend: Rc<Backend>, recording: Recording) -> Rc<Self> { | ||||||
|         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui"); |         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui"); | ||||||
| 
 | 
 | ||||||
|         get_widget!(builder, gtk::Box, widget); |         get_widget!(builder, gtk::Box, widget); | ||||||
|  | @ -69,7 +69,7 @@ impl RecordingScreen { | ||||||
|         let list = List::new(&gettext("No tracks found.")); |         let list = List::new(&gettext("No tracks found.")); | ||||||
| 
 | 
 | ||||||
|         list.set_make_widget( |         list.set_make_widget( | ||||||
|             clone!(@strong recording => move |track: &TrackDescription| { |             clone!(@strong recording => move |track: &Track| { | ||||||
|                 let mut title_parts = Vec::<String>::new(); |                 let mut title_parts = Vec::<String>::new(); | ||||||
|                 for part in &track.work_parts { |                 for part in &track.work_parts { | ||||||
|                     title_parts.push(recording.work.parts[*part].title.clone()); |                     title_parts.push(recording.work.parts[*part].title.clone()); | ||||||
|  | @ -131,7 +131,7 @@ impl RecordingScreen { | ||||||
|         let clone = result.clone(); |         let clone = result.clone(); | ||||||
|         let id = recording.id; |         let id = recording.id; | ||||||
|         context.spawn_local(async move { |         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()); |             list.show_items(tracks.clone()); | ||||||
|             clone.stack.set_visible_child_name("content"); |             clone.stack.set_visible_child_name("content"); | ||||||
|             clone.tracks.replace(tracks); |             clone.tracks.replace(tracks); | ||||||
|  |  | ||||||
|  | @ -14,12 +14,12 @@ pub struct WorkScreen { | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     widget: gtk::Box, |     widget: gtk::Box, | ||||||
|     stack: gtk::Stack, |     stack: gtk::Stack, | ||||||
|     recording_list: Rc<List<RecordingDescription>>, |     recording_list: Rc<List<Recording>>, | ||||||
|     navigator: RefCell<Option<Rc<Navigator>>>, |     navigator: RefCell<Option<Rc<Navigator>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl WorkScreen { | impl WorkScreen { | ||||||
|     pub fn new(backend: Rc<Backend>, work: WorkDescription) -> Rc<Self> { |     pub fn new(backend: Rc<Backend>, work: Work) -> Rc<Self> { | ||||||
|         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_screen.ui"); |         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_screen.ui"); | ||||||
| 
 | 
 | ||||||
|         get_widget!(builder, gtk::Box, widget); |         get_widget!(builder, gtk::Box, widget); | ||||||
|  | @ -53,7 +53,7 @@ impl WorkScreen { | ||||||
| 
 | 
 | ||||||
|         let recording_list = List::new(&gettext("No recordings found.")); |         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())); |             let work_label = gtk::Label::new(Some(&recording.work.get_title())); | ||||||
| 
 | 
 | ||||||
|             work_label.set_ellipsize(pango::EllipsizeMode::End); |             work_label.set_ellipsize(pango::EllipsizeMode::End); | ||||||
|  | @ -72,7 +72,7 @@ impl WorkScreen { | ||||||
|             vbox.upcast() |             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 search = search_entry.get_text().to_string().to_lowercase(); | ||||||
|             let text = recording.work.get_title().to_lowercase() + &recording.get_performers().to_lowercase(); |             let text = recording.work.get_title().to_lowercase() + &recording.get_performers().to_lowercase(); | ||||||
|             search.is_empty() || text.contains(&search) |             search.is_empty() || text.contains(&search) | ||||||
|  | @ -113,7 +113,8 @@ impl WorkScreen { | ||||||
|         context.spawn_local(async move { |         context.spawn_local(async move { | ||||||
|             let recordings = clone |             let recordings = clone | ||||||
|                 .backend |                 .backend | ||||||
|                 .get_recordings_for_work(work.id) |                 .db() | ||||||
|  |                 .get_recordings_for_work(work.id as u32) | ||||||
|                 .await |                 .await | ||||||
|                 .unwrap(); |                 .unwrap(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -74,7 +74,7 @@ impl PersonList { | ||||||
|         let list = self.list.clone(); |         let list = self.list.clone(); | ||||||
| 
 | 
 | ||||||
|         context.spawn_local(async move { |         context.spawn_local(async move { | ||||||
|             let persons = backend.get_persons().await.unwrap(); |             let persons = backend.db().get_persons().await.unwrap(); | ||||||
|             list.show_items(persons); |             list.show_items(persons); | ||||||
|             self.stack.set_visible_child_name("content"); |             self.stack.set_visible_child_name("content"); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  | @ -89,8 +89,8 @@ impl PoeList { | ||||||
|         let list = self.list.clone(); |         let list = self.list.clone(); | ||||||
| 
 | 
 | ||||||
|         context.spawn_local(async move { |         context.spawn_local(async move { | ||||||
|             let persons = backend.get_persons().await.unwrap(); |             let persons = backend.db().get_persons().await.unwrap(); | ||||||
|             let ensembles = backend.get_ensembles().await.unwrap(); |             let ensembles = backend.db().get_ensembles().await.unwrap(); | ||||||
|             let mut poes: Vec<PersonOrEnsemble> = Vec::new(); |             let mut poes: Vec<PersonOrEnsemble> = Vec::new(); | ||||||
| 
 | 
 | ||||||
|             for person in persons { |             for person in persons { | ||||||
|  |  | ||||||
|  | @ -37,7 +37,6 @@ impl Window { | ||||||
|         get_widget!(builder, gtk::Box, empty_screen); |         get_widget!(builder, gtk::Box, empty_screen); | ||||||
| 
 | 
 | ||||||
|         let backend = Rc::new(Backend::new()); |         let backend = Rc::new(Backend::new()); | ||||||
|         backend.clone().init(); |  | ||||||
| 
 | 
 | ||||||
|         let player_screen = PlayerScreen::new(); |         let player_screen = PlayerScreen::new(); | ||||||
|         stack.add_named(&player_screen.widget, "player_screen"); |         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 context = glib::MainContext::default(); | ||||||
|         let clone = result.clone(); |         let clone = result.clone(); | ||||||
|         context.spawn_local(async move { |         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.leaflet.add(&result.navigator.widget); | ||||||
| 
 | 
 | ||||||
|         result |         result | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Elias Projahn
						Elias Projahn