Restructure backend and database

This commit is contained in:
Elias Projahn 2020-11-17 15:52:47 +01:00
parent d0c25531d3
commit a93c7276d2
49 changed files with 1705 additions and 1920 deletions

View file

@ -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;

View file

@ -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 (

View file

@ -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>

View file

@ -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(())
}
}

View file

@ -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!"))?;

View 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()
}
}

View file

@ -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();
}
}

View file

@ -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!");
}
}

View 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)
}
}

View 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)
}
}

View file

@ -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(())
}
}

View file

@ -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,
}
}
}

View 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)
}
}

View 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(())
}
}

View file

@ -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,

View file

@ -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,
}

View 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?)
}
}

View 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)
}
}

View 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)
}
}

View file

@ -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());
}); });

View file

@ -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));

View file

@ -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());
}); });

View file

@ -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));

View file

@ -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());
}); });

View file

@ -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)));
} }

View file

@ -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)));
} }

View file

@ -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);
} }

View file

@ -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)));
} }

View file

@ -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)));
} }
} }

View file

@ -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)));
} }
} }

View file

@ -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)));
} }
} }

View file

@ -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(),
}); });

View file

@ -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");

View file

@ -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)));
} }

View file

@ -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)));
} }

View file

@ -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)));
} }

View file

@ -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)));
} }

View file

@ -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)));
} }

View file

@ -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)));
} }
} }

View file

@ -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)));
} }
} }

View file

@ -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',

View file

@ -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 {

View file

@ -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();

View file

@ -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();

View file

@ -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);

View file

@ -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();

View file

@ -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");
}); });

View file

@ -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 {

View file

@ -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