Refactor db schema and use Diesel

This commit is contained in:
Elias Projahn 2024-03-23 18:06:46 +01:00
parent 2f6676ba3b
commit 220821a0e0
14 changed files with 1310 additions and 599 deletions

244
Cargo.lock generated
View file

@ -2,18 +2,6 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.2"
@ -23,12 +11,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
[[package]]
name = "android-tzdata"
version = "0.1.1"
@ -166,6 +148,59 @@ dependencies = [
"libdbus-sys",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "diesel"
version = "2.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03fc05c17098f21b89bc7d98fe1dd3cce2c11c2ad8e145f2a44fe08ed28eb559"
dependencies = [
"chrono",
"diesel_derives",
"libsqlite3-sys",
"time",
]
[[package]]
name = "diesel_derives"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d02eecb814ae714ffe61ddc2db2dd03e6c49a42e269b5001355500d431cce0c"
dependencies = [
"diesel_table_macro_syntax",
"proc-macro2",
"quote",
"syn 2.0.39",
]
[[package]]
name = "diesel_migrations"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac"
dependencies = [
"diesel",
"migrations_internals",
"migrations_macros",
]
[[package]]
name = "diesel_table_macro_syntax"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
dependencies = [
"syn 2.0.39",
]
[[package]]
name = "either"
version = "1.9.0"
@ -178,18 +213,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "field-offset"
version = "0.3.6"
@ -808,19 +831,6 @@ name = "hashbrown"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "hashlink"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown",
]
[[package]]
name = "heck"
@ -870,6 +880,12 @@ dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "js-sys"
version = "0.3.65"
@ -934,11 +950,10 @@ dependencies = [
[[package]]
name = "libsqlite3-sys"
version = "0.26.0"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
@ -986,6 +1001,27 @@ dependencies = [
"autocfg",
]
[[package]]
name = "migrations_internals"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada"
dependencies = [
"serde",
"toml 0.7.8",
]
[[package]]
name = "migrations_macros"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08"
dependencies = [
"migrations_internals",
"proc-macro2",
"quote",
]
[[package]]
name = "mpris-player"
version = "0.6.3"
@ -1006,7 +1042,10 @@ checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0"
name = "musicus"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"diesel",
"diesel_migrations",
"fragile",
"gettext-rs",
"gstreamer-play",
@ -1016,7 +1055,8 @@ dependencies = [
"mpris-player",
"once_cell",
"rand",
"rusqlite",
"serde",
"serde_json",
"thiserror",
"tracing-subscriber",
"uuid",
@ -1032,6 +1072,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.45"
@ -1161,6 +1207,12 @@ version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -1296,20 +1348,6 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "rusqlite"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [
"bitflags 2.4.1",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -1319,6 +1357,12 @@ dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "semver"
version = "1.0.20"
@ -1345,6 +1389,17 @@ dependencies = [
"syn 2.0.39",
]
[[package]]
name = "serde_json"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.4"
@ -1409,7 +1464,7 @@ dependencies = [
"cfg-expr",
"heck",
"pkg-config",
"toml",
"toml 0.8.8",
"version-compare",
]
@ -1455,6 +1510,49 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "toml"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.19.15",
]
[[package]]
name = "toml"
version = "0.8.8"
@ -1483,6 +1581,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
@ -1807,23 +1907,3 @@ checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b"
dependencies = [
"memchr",
]
[[package]]
name = "zerocopy"
version = "0.7.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
]

View file

@ -5,7 +5,10 @@ edition = "2021"
[dependencies]
adw = { package = "libadwaita", version = "0.5", features = ["v1_4"] }
anyhow = "1"
chrono = "0.4"
diesel = { version = "2", features = ["chrono", "sqlite"] }
diesel_migrations = "2"
fragile = "2"
gettext-rs = { version = "0.7", features = ["gettext-system"] }
gstreamer-play = "0.22"
@ -14,7 +17,8 @@ log = "0.4"
mpris-player = "0.6"
once_cell = "1"
rand = "0.8"
rusqlite = { version = "0.29", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1"
tracing-subscriber = "0.3"
uuid = { version = "1", features = ["v4"] }

View file

@ -0,0 +1 @@
-- This migration is intended to become the initial schema.

View file

@ -0,0 +1,194 @@
CREATE TABLE persons_new (
person_id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
edited_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_played_at TIMESTAMP
);
CREATE TABLE roles (
role_id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
edited_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE instruments_new (
instrument_id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
edited_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_played_at TIMESTAMP
);
CREATE TABLE works_new (
work_id TEXT NOT NULL PRIMARY KEY,
parent_work_id TEXT REFERENCES works(work_id),
sequence_number INTEGER,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
edited_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_played_at TIMESTAMP
);
CREATE TABLE work_persons (
work_id TEXT NOT NULL REFERENCES works(work_id) ON DELETE CASCADE,
person_id TEXT NOT NULL REFERENCES persons(person_id),
role_id TEXT NOT NULL REFERENCES roles(role_id),
sequence_number INTEGER NOT NULL,
PRIMARY KEY (work_id, person_id, role_id)
);
CREATE TABLE work_instruments (
work_id TEXT NOT NULL REFERENCES works(work_id) ON DELETE CASCADE,
instrument_id TEXT NOT NULL REFERENCES instruments(instrument_id),
sequence_number INTEGER NOT NULL,
PRIMARY KEY (work_id, instrument_id)
);
CREATE TABLE ensembles_new (
ensemble_id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
edited_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_played_at TIMESTAMP
);
CREATE TABLE ensemble_persons (
ensemble_id TEXT NOT NULL REFERENCES ensembles(ensemble_id) ON DELETE CASCADE,
person_id TEXT NOT NULL REFERENCES persons(person_id),
instrument_id TEXT NOT NULL REFERENCES instruments(instrument_id),
sequence_number INTEGER NOT NULL,
PRIMARY KEY (ensemble_id, person_id, instrument_id)
);
CREATE TABLE recordings_new (
recording_id TEXT NOT NULL PRIMARY KEY,
work_id TEXT NOT NULL REFERENCES works(work_id),
year INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
edited_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_played_at TIMESTAMP
);
CREATE TABLE recording_persons (
recording_id TEXT NOT NULL REFERENCES recordings(recording_id) ON DELETE CASCADE,
person_id TEXT NOT NULL REFERENCES persons(person_id),
role_id TEXT NOT NULL REFERENCES roles(role_id),
instrument_id TEXT REFERENCES instruments(instrument_id),
sequence_number INTEGER NOT NULL,
PRIMARY KEY (recording_id, person_id, role_id, instrument_id)
);
CREATE TABLE recording_ensembles (
recording_id TEXT NOT NULL REFERENCES recordings(recording_id) ON DELETE CASCADE,
ensemble_id TEXT NOT NULL REFERENCES ensembles(ensemble_id),
role_id TEXT NOT NULL REFERENCES roles(role_id),
sequence_number INTEGER NOT NULL,
PRIMARY KEY (recording_id, ensemble_id, role_id)
);
CREATE TABLE tracks_new (
track_id TEXT NOT NULL PRIMARY KEY,
recording_id TEXT NOT NULL REFERENCES recordings(recording_id),
sequence_number INTEGER NOT NULL,
path TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
edited_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_played_at TIMESTAMP
);
CREATE TABLE track_works (
track_id TEXT NOT NULL REFERENCES tracks(track_id) ON DELETE CASCADE,
work_id TEXT NOT NULL REFERENCES works(work_id),
sequence_number INTEGER NOT NULL,
PRIMARY KEY (track_id, work_id)
);
INSERT INTO persons_new (person_id, name)
SELECT id, json_set('{}', '$.generic', first_name || ' ' || last_name)
FROM persons;
INSERT INTO roles (role_id, name)
VALUES ('380d7e09eb2f49c1a90db2ba4acb6ffd', json_set('{}', '$.generic', 'Composer'));
INSERT INTO roles (role_id, name)
VALUES ('28ff0aeb11c041a6916d93e9b4884eef', json_set('{}', '$.generic', 'Performer'));
INSERT INTO instruments_new (instrument_id, name)
SELECT id, json_set('{}', '$.generic', name)
FROM instruments;
INSERT INTO works_new (work_id, name)
SELECT id, json_set('{}', '$.generic', title)
FROM works;
INSERT INTO works_new (work_id, parent_work_id, sequence_number, name)
SELECT id, work, part_index, json_set('{}', '$.generic', title)
FROM work_parts;
INSERT INTO work_persons (work_id, person_id, role_id, sequence_number)
SELECT id, composer, '380d7e09eb2f49c1a90db2ba4acb6ffd', 0
FROM works;
INSERT INTO work_instruments (work_id, instrument_id, sequence_number)
SELECT work, instrument, 0
FROM instrumentations;
INSERT INTO ensembles_new (ensemble_id, name)
SELECT id, json_set('{}', '$.generic', name)
FROM ensembles;
INSERT INTO recordings_new (recording_id, work_id, year)
SELECT id, work, CAST(comment as INTEGER)
FROM recordings;
UPDATE recordings_new
SET year = NULL
WHERE year <= 0;
INSERT INTO recording_persons (recording_id, person_id, role_id, instrument_id, sequence_number)
SELECT recording, person, '28ff0aeb11c041a6916d93e9b4884eef', role, 0
FROM performances
WHERE person IS NOT NULL;
INSERT INTO recording_ensembles (recording_id, ensemble_id, role_id, sequence_number)
SELECT recording, ensemble, '28ff0aeb11c041a6916d93e9b4884eef', 0
FROM performances
WHERE ensemble IS NOT NULL;
INSERT INTO tracks_new (track_id, recording_id, sequence_number, path)
SELECT id, recording, "index", path
FROM tracks;
INSERT INTO track_works (track_id, work_id, sequence_number)
SELECT tracks.id, work_parts.id, 0
FROM tracks
JOIN recordings ON tracks.recording = recordings.id
JOIN work_parts ON recordings.work = work_parts.work
AND tracks.work_parts = work_parts.part_index;
DROP TABLE persons;
DROP TABLE instruments;
DROP TABLE works;
DROP TABLE instrumentations;
DROP TABLE work_parts;
DROP TABLE ensembles;
DROP TABLE recordings;
DROP TABLE performances;
DROP TABLE mediums;
DROP TABLE tracks;
ALTER TABLE persons_new RENAME TO persons;
ALTER TABLE instruments_new RENAME TO instruments;
ALTER TABLE works_new RENAME TO works;
ALTER TABLE recordings_new RENAME TO recordings;
ALTER TABLE tracks_new RENAME TO tracks;
ALTER TABLE ensembles_new RENAME TO ensembles;

87
src/db/mod.rs Normal file
View file

@ -0,0 +1,87 @@
pub mod models;
pub mod schema;
pub mod tables;
use std::collections::HashMap;
use anyhow::{anyhow, Result};
use diesel::{
backend::Backend,
deserialize::{self, FromSql, FromSqlRow},
expression::AsExpression,
prelude::*,
serialize::{self, IsNull, Output, ToSql},
sql_types::Text,
sqlite::Sqlite,
};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness};
use serde::{Deserialize, Serialize};
// This makes the SQL migration scripts accessible from the code.
const MIGRATIONS: EmbeddedMigrations = diesel_migrations::embed_migrations!();
/// Connect to a Musicus database and apply any pending migrations.
pub fn connect(file_name: &str) -> Result<SqliteConnection> {
log::info!("Opening database file '{}'", file_name);
let mut connection = SqliteConnection::establish(file_name)?;
log::info!("Running migrations if necessary");
connection
.run_pending_migrations(MIGRATIONS)
.map_err(|e| anyhow!(e))?;
// Enable after running migrations to simplify changes in schema.
diesel::sql_query("PRAGMA foreign_keys = ON").execute(&mut connection)?;
Ok(connection)
}
/// A single translated string value.
#[derive(Serialize, Deserialize, AsExpression, FromSqlRow, Clone, Debug)]
#[diesel(sql_type = Text)]
pub struct TranslatedString(HashMap<String, String>);
impl TranslatedString {
/// Get the best translation for the user's current locale.
///
/// This will fall back to the generic variant if no translation exists. If no
/// generic translation exists (which is a bug in the data), an empty string is
/// returned and a warning is logged.
pub fn get(&self) -> &str {
// TODO: Get language from locale.
let lang = "generic";
match self.0.get(lang) {
Some(s) => s,
None => match self.0.get("generic") {
Some(s) => s,
None => {
log::warn!("No generic variant for TranslatedString: {:?}", self);
""
}
},
}
}
}
impl<DB: Backend> FromSql<Text, DB> for TranslatedString
where
String: FromSql<Text, DB>,
{
fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
let text = String::from_sql(bytes)?;
let translated_string = serde_json::from_str(&text)?;
Ok(translated_string)
}
}
impl ToSql<Text, Sqlite> for TranslatedString
where
String: ToSql<Text, Sqlite>,
{
fn to_sql(&self, out: &mut Output<Sqlite>) -> serialize::Result {
let text = serde_json::to_string(self)?;
out.set_value(text);
Ok(IsNull::No)
}
}

297
src/db/models.rs Normal file
View file

@ -0,0 +1,297 @@
//! This module contains higher-level models combining information from
//! multiple database tables.
use std::{fmt::Display, path::Path};
use anyhow::Result;
use diesel::prelude::*;
use super::{schema::*, tables, TranslatedString};
// Re-exports for tables that don't need additional information.
pub use tables::{Instrument, Person, Role};
#[derive(Clone, Debug)]
pub struct Work {
pub work_id: String,
pub name: TranslatedString,
pub parts: Vec<WorkPart>,
pub persons: Vec<Person>,
pub instruments: Vec<Instrument>,
}
#[derive(Clone, Debug)]
pub struct WorkPart {
pub work_id: String,
pub level: u8,
pub name: TranslatedString,
}
#[derive(Clone, Debug)]
pub struct Ensemble {
pub ensemble_id: String,
pub name: TranslatedString,
pub persons: Vec<(Person, Instrument)>,
}
#[derive(Clone, Debug)]
pub struct Recording {
pub recording_id: String,
pub work: Work,
pub year: Option<i32>,
pub persons: Vec<Performer>,
pub ensembles: Vec<Ensemble>,
pub tracks: Vec<Track>,
}
#[derive(Clone, Debug)]
pub struct Performer {
pub person: Person,
pub role: Role,
pub instrument: Option<Instrument>,
}
#[derive(Clone, Debug)]
pub struct Track {
pub track_id: String,
pub path: String,
pub works: Vec<Work>,
}
impl Eq for Person {}
impl PartialEq for Person {
fn eq(&self, other: &Self) -> bool {
self.person_id == other.person_id
}
}
impl Work {
pub fn from_table(data: tables::Work, connection: &mut SqliteConnection) -> Result<Self> {
fn visit_children(
work_id: &str,
level: u8,
connection: &mut SqliteConnection,
) -> Result<Vec<WorkPart>> {
let mut parts = Vec::new();
let children: Vec<tables::Work> = works::table
.filter(works::parent_work_id.eq(work_id))
.load(connection)?;
for child in children {
let mut grand_children = visit_children(&child.work_id, level + 1, connection)?;
parts.push(WorkPart {
work_id: child.work_id,
level,
name: child.name,
});
parts.append(&mut grand_children);
}
Ok(parts)
}
let parts = visit_children(&data.work_id, 0, connection)?;
let persons: Vec<Person> = persons::table
.inner_join(work_persons::table)
.order(work_persons::sequence_number)
.filter(work_persons::work_id.eq(&data.work_id))
.select(tables::Person::as_select())
.load(connection)?;
let instruments: Vec<Instrument> = instruments::table
.inner_join(work_instruments::table)
.order(work_instruments::sequence_number)
.filter(work_instruments::work_id.eq(&data.work_id))
.select(tables::Instrument::as_select())
.load(connection)?;
Ok(Self {
work_id: data.work_id,
name: data.name,
parts,
persons,
instruments,
})
}
pub fn composers_string(&self) -> String {
self.persons
.iter()
.map(|p| p.name.get().to_string())
.collect::<Vec<String>>()
.join(", ")
}
}
impl Eq for Work {}
impl PartialEq for Work {
fn eq(&self, other: &Self) -> bool {
self.work_id == other.work_id
}
}
impl Ensemble {
pub fn from_table(data: tables::Ensemble, connection: &mut SqliteConnection) -> Result<Self> {
let persons: Vec<(Person, Instrument)> = persons::table
.inner_join(ensemble_persons::table.inner_join(instruments::table))
.order(ensemble_persons::sequence_number)
.filter(ensemble_persons::ensemble_id.eq(&data.ensemble_id))
.select((tables::Person::as_select(), tables::Instrument::as_select()))
.load(connection)?;
Ok(Self {
ensemble_id: data.ensemble_id,
name: data.name,
persons,
})
}
}
impl Eq for Ensemble {}
impl PartialEq for Ensemble {
fn eq(&self, other: &Self) -> bool {
self.ensemble_id == other.ensemble_id
}
}
impl Recording {
pub fn from_table(
data: tables::Recording,
library_path: &str,
connection: &mut SqliteConnection,
) -> Result<Self> {
let work = Work::from_table(
works::table
.filter(works::work_id.eq(&data.work_id))
.first::<tables::Work>(connection)?,
connection,
)?;
let persons = recording_persons::table
.order(recording_persons::sequence_number)
.filter(recording_persons::recording_id.eq(&data.recording_id))
.load::<tables::RecordingPerson>(connection)?
.into_iter()
.map(|r| Performer::from_table(r, connection))
.collect::<Result<Vec<Performer>>>()?;
let ensembles: Vec<Ensemble> = ensembles::table
.inner_join(recording_ensembles::table)
.order(recording_ensembles::sequence_number)
.filter(recording_ensembles::recording_id.eq(&data.recording_id))
.select(tables::Ensemble::as_select())
.load::<tables::Ensemble>(connection)?
.into_iter()
.map(|e| Ensemble::from_table(e, connection))
.collect::<Result<Vec<Ensemble>>>()?;
let tracks: Vec<Track> = tracks::table
.order(tracks::sequence_number)
.filter(tracks::recording_id.eq(&data.recording_id))
.select(tables::Track::as_select())
.load::<tables::Track>(connection)?
.into_iter()
.map(|t| Track::from_table(t, library_path, connection))
.collect::<Result<Vec<Track>>>()?;
Ok(Self {
recording_id: data.recording_id,
work,
year: data.year,
persons,
ensembles,
tracks,
})
}
pub fn performers_string(&self) -> String {
let mut performers = self
.persons
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>();
performers.append(
&mut self
.ensembles
.iter()
.map(|e| e.name.get().to_string())
.collect::<Vec<String>>(),
);
performers.join(", ")
}
}
impl Performer {
pub fn from_table(
data: tables::RecordingPerson,
connection: &mut SqliteConnection,
) -> Result<Self> {
let person: Person = persons::table
.filter(persons::person_id.eq(&data.person_id))
.first(connection)?;
let role: Role = roles::table
.filter(roles::role_id.eq(&data.role_id))
.first(connection)?;
let instrument = match &data.instrument_id {
Some(instrument_id) => Some(
instruments::table
.filter(instruments::instrument_id.eq(instrument_id))
.first::<Instrument>(connection)?,
),
None => None,
};
Ok(Self {
person,
role,
instrument,
})
}
}
impl Display for Performer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.instrument {
Some(instrument) => {
format!("{} ({})", self.person.name.get(), instrument.name.get()).fmt(f)
}
None => self.person.name.get().fmt(f),
}
}
}
impl Track {
pub fn from_table(
data: tables::Track,
library_path: &str,
connection: &mut SqliteConnection,
) -> Result<Self> {
let works: Vec<Work> = works::table
.inner_join(track_works::table)
.order(track_works::sequence_number)
.filter(track_works::track_id.eq(&data.track_id))
.select(tables::Work::as_select())
.load::<tables::Work>(connection)?
.into_iter()
.map(|w| Work::from_table(w, connection))
.collect::<Result<Vec<Work>>>()?;
Ok(Self {
track_id: data.track_id,
path: Path::new(library_path)
.join(&data.path)
.to_str()
.unwrap()
.to_string(),
works,
})
}
}

181
src/db/schema.rs Normal file
View file

@ -0,0 +1,181 @@
// @generated automatically by Diesel CLI.
diesel::table! {
ensemble_persons (ensemble_id, person_id, instrument_id) {
ensemble_id -> Text,
person_id -> Text,
instrument_id -> Text,
sequence_number -> Integer,
}
}
diesel::table! {
ensembles (ensemble_id) {
ensemble_id -> Text,
name -> Text,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
}
}
diesel::table! {
instruments (instrument_id) {
instrument_id -> Text,
name -> Text,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
}
}
diesel::table! {
persons (person_id) {
person_id -> Text,
name -> Text,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
}
}
diesel::table! {
recording_ensembles (recording_id, ensemble_id, role_id) {
recording_id -> Text,
ensemble_id -> Text,
role_id -> Text,
sequence_number -> Integer,
}
}
diesel::table! {
recording_persons (recording_id, person_id, role_id, instrument_id) {
recording_id -> Text,
person_id -> Text,
role_id -> Text,
instrument_id -> Nullable<Text>,
sequence_number -> Integer,
}
}
diesel::table! {
recordings (recording_id) {
recording_id -> Text,
work_id -> Text,
year -> Nullable<Integer>,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
}
}
diesel::table! {
roles (role_id) {
role_id -> Text,
name -> Text,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
}
}
diesel::table! {
track_works (track_id, work_id) {
track_id -> Text,
work_id -> Text,
sequence_number -> Integer,
}
}
diesel::table! {
tracks (track_id) {
track_id -> Text,
recording_id -> Text,
sequence_number -> Integer,
path -> Text,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
}
}
diesel::table! {
work_instruments (work_id, instrument_id) {
work_id -> Text,
instrument_id -> Text,
sequence_number -> Integer,
}
}
diesel::table! {
work_persons (work_id, person_id, role_id) {
work_id -> Text,
person_id -> Text,
role_id -> Text,
sequence_number -> Integer,
}
}
diesel::table! {
work_sections (id) {
id -> BigInt,
work -> Text,
title -> Text,
before_index -> BigInt,
}
}
diesel::table! {
works (work_id) {
work_id -> Text,
parent_work_id -> Nullable<Text>,
sequence_number -> Nullable<Integer>,
name -> Text,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
}
}
diesel::joinable!(ensemble_persons -> ensembles (ensemble_id));
diesel::joinable!(ensemble_persons -> instruments (instrument_id));
diesel::joinable!(ensemble_persons -> persons (person_id));
diesel::joinable!(recording_ensembles -> ensembles (ensemble_id));
diesel::joinable!(recording_ensembles -> recordings (recording_id));
diesel::joinable!(recording_ensembles -> roles (role_id));
diesel::joinable!(recording_persons -> instruments (instrument_id));
diesel::joinable!(recording_persons -> persons (person_id));
diesel::joinable!(recording_persons -> recordings (recording_id));
diesel::joinable!(recording_persons -> roles (role_id));
diesel::joinable!(recordings -> works (work_id));
diesel::joinable!(track_works -> tracks (track_id));
diesel::joinable!(track_works -> works (work_id));
diesel::joinable!(tracks -> recordings (recording_id));
diesel::joinable!(work_instruments -> instruments (instrument_id));
diesel::joinable!(work_instruments -> works (work_id));
diesel::joinable!(work_persons -> persons (person_id));
diesel::joinable!(work_persons -> roles (role_id));
diesel::joinable!(work_persons -> works (work_id));
diesel::allow_tables_to_appear_in_same_query!(
ensemble_persons,
ensembles,
instruments,
persons,
recording_ensembles,
recording_persons,
recordings,
roles,
track_works,
tracks,
work_instruments,
work_persons,
work_sections,
works,
);

142
src/db/tables.rs Normal file
View file

@ -0,0 +1,142 @@
//! This module contains structs that are one-to-one representations of the
//! tables in the database schema.
use chrono::NaiveDateTime;
use diesel::prelude::*;
use diesel::sqlite::Sqlite;
use super::{schema::*, TranslatedString};
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Person {
pub person_id: String,
pub name: TranslatedString,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Role {
pub role_id: String,
pub name: TranslatedString,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Instrument {
pub instrument_id: String,
pub name: TranslatedString,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Work {
pub work_id: String,
pub parent_work_id: Option<String>,
pub sequence_number: Option<i32>,
pub name: TranslatedString,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct WorkPerson {
pub work_id: String,
pub person_id: String,
pub role_id: String,
pub sequence_number: i32,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct WorkInstrument {
pub work_id: String,
pub instrument_id: String,
pub sequence_number: i32,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Ensemble {
pub ensemble_id: String,
pub name: TranslatedString,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct EnsemblePerson {
pub ensemble_id: String,
pub person_id: String,
pub instrument_id: String,
pub sequence_number: i32,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Recording {
pub recording_id: String,
pub work_id: String,
pub year: Option<i32>,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct RecordingPerson {
pub recording_id: String,
pub person_id: String,
pub role_id: String,
pub instrument_id: Option<String>,
pub sequence_number: i32,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct RecordingEnsemble {
pub recording_id: String,
pub ensemble_id: String,
pub role_id: String,
pub sequence_number: i32,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Track {
pub track_id: String,
pub recording_id: String,
pub sequence_number: i32,
pub path: String,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct TrackWork {
pub track_id: String,
pub work_id: String,
pub sequence_number: i32,
}

View file

@ -1,5 +1,6 @@
use crate::{
library::{Ensemble, LibraryQuery, MusicusLibrary, Person, Recording, Track, Work},
db::models::*,
library::{LibraryQuery, MusicusLibrary},
player::MusicusPlayer,
playlist_item::PlaylistItem,
recording_tile::MusicusRecordingTile,
@ -159,7 +160,7 @@ impl MusicusHomePage {
}
fn play_recording(&self, recording: &Recording) {
let tracks = self.library().tracks(recording);
let tracks = &recording.tracks;
if tracks.is_empty() {
log::warn!("Ignoring recording without tracks being added to the playlist.");
@ -168,16 +169,11 @@ impl MusicusHomePage {
let title = format!(
"{}: {}",
recording.work.composer.name_fl(),
recording.work.title
recording.work.composers_string(),
recording.work.name.get(),
);
let performances = self.library().performances(recording);
let performances = if performances.is_empty() {
None
} else {
Some(performances.join(", "))
};
let performances = recording.performers_string();
let mut items = Vec::new();
@ -185,20 +181,19 @@ impl MusicusHomePage {
items.push(PlaylistItem::new(
true,
&title,
performances.as_deref(),
Some(&performances),
None,
&tracks[0].path,
));
} else {
let work_parts = self.library().work_parts(&recording.work);
let mut tracks = tracks.into_iter();
let first_track = tracks.next().unwrap();
let track_title = |track: &Track, number: usize| -> String {
let title = track
.work_parts
.works
.iter()
.map(|w| work_parts[*w].clone())
.map(|w| w.name.get().to_string())
.collect::<Vec<String>>()
.join(", ");
@ -212,7 +207,7 @@ impl MusicusHomePage {
items.push(PlaylistItem::new(
true,
&title,
performances.as_deref(),
Some(&performances),
Some(&track_title(&first_track, 1)),
&first_track.path,
));
@ -221,7 +216,7 @@ impl MusicusHomePage {
items.push(PlaylistItem::new(
false,
&title,
performances.as_deref(),
Some(&performances),
// track number = track index + 1 (first track) + 1 (zero based)
Some(&track_title(&track, index + 2)),
&track.path,
@ -234,7 +229,7 @@ impl MusicusHomePage {
fn query(&self, query: &LibraryQuery) {
let imp = self.imp();
let results = self.library().query(query);
let results = self.library().query(query).unwrap();
for flowbox in [
&imp.composers_flow_box,
@ -284,9 +279,8 @@ impl MusicusHomePage {
}
for recording in &results.recordings {
let performances = self.library().performances(recording);
imp.recordings_flow_box
.append(&MusicusRecordingTile::new(recording, performances));
.append(&MusicusRecordingTile::new(recording));
}
imp.composers.replace(results.composers);

View file

@ -1,11 +1,19 @@
use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*};
use rusqlite::{Connection, Row};
use std::{
cell::OnceCell,
num::ParseIntError,
cell::{OnceCell, RefCell},
path::{Path, PathBuf},
};
use anyhow::Result;
use diesel::{dsl::exists, prelude::*, QueryDsl, SqliteConnection};
use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*};
use crate::db::{self, models::*, schema::*, tables};
diesel::sql_function! {
/// Represents the SQL RANDOM() function.
fn random() -> Integer
}
mod imp {
use super::*;
@ -14,7 +22,7 @@ mod imp {
pub struct MusicusLibrary {
#[property(get, construct_only)]
pub folder: OnceCell<String>,
pub connection: OnceCell<Connection>,
pub connection: RefCell<Option<SqliteConnection>>,
}
#[glib::object_subclass]
@ -27,10 +35,10 @@ mod imp {
impl ObjectImpl for MusicusLibrary {
fn constructed(&self) {
self.parent_constructed();
let db_path = PathBuf::from(self.folder.get().unwrap()).join("musicus.db");
self.connection
.set(Connection::open(db_path).unwrap())
.unwrap();
let db_path = PathBuf::from(&self.folder.get().unwrap()).join("musicus.db");
let connection = db::connect(db_path.to_str().unwrap()).unwrap();
self.connection.set(Some(connection));
}
}
}
@ -46,10 +54,12 @@ impl MusicusLibrary {
.build()
}
pub fn query(&self, query: &LibraryQuery) -> LibraryResults {
pub fn query(&self, query: &LibraryQuery) -> Result<LibraryResults> {
let search = format!("%{}%", query.search);
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
match query {
Ok(match query {
LibraryQuery {
composer: None,
performer: None,
@ -57,59 +67,47 @@ impl MusicusLibrary {
work: None,
..
} => {
let composers = self
.con()
.prepare(
"SELECT DISTINCT persons.id, persons.first_name, persons.last_name \
FROM persons \
JOIN works ON works.composer = persons.id \
WHERE persons.first_name LIKE ?1 OR persons.last_name LIKE ?1 \
LIMIT 9",
let composers: Vec<Person> = persons::table
.filter(
exists(
work_persons::table
.filter(work_persons::person_id.eq(persons::person_id)),
)
.and(persons::name.like(&search)),
)
.unwrap()
.query_map([&search], Person::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Person>>>()
.unwrap();
.limit(9)
.load(connection)?;
let performers = self
.con()
.prepare(
"SELECT DISTINCT persons.id, persons.first_name, persons.last_name \
FROM persons \
JOIN performances ON performances.person = persons.id \
WHERE persons.first_name LIKE ?1 OR persons.last_name LIKE ?1 \
LIMIT 9",
let performers: Vec<Person> = persons::table
.filter(
exists(
recording_persons::table
.filter(recording_persons::person_id.eq(persons::person_id)),
)
.and(persons::name.like(&search)),
)
.unwrap()
.query_map([&search], Person::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Person>>>()
.unwrap();
.limit(9)
.load(connection)?;
let ensembles = self
.con()
.prepare("SELECT id, name FROM ensembles WHERE name LIKE ?1 LIMIT 9")
.unwrap()
.query_map([&search], Ensemble::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Ensemble>>>()
.unwrap();
// TODO: Search ensemble persons as well.
let ensembles: Vec<Ensemble> = ensembles::table
.filter(ensembles::name.like(&search))
.limit(9)
.load::<tables::Ensemble>(connection)?
.into_iter()
.map(|e| Ensemble::from_table(e, connection))
.collect::<Result<Vec<Ensemble>>>()?;
let works = self
.con()
.prepare(
"SELECT works.id, works.title, persons.id, persons.first_name, persons.last_name \
FROM works \
JOIN persons ON works.composer = persons.id \
WHERE title LIKE ?1 \
LIMIT 9"
)
.unwrap()
.query_map([&search], Work::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Work>>>()
.unwrap();
let works: Vec<Work> = works::table
.inner_join(work_persons::table.inner_join(persons::table))
.filter(works::name.like(&search).or(persons::name.like(&search)))
.limit(9)
.select(works::all_columns)
.distinct()
.load::<tables::Work>(connection)?
.into_iter()
.map(|w| Work::from_table(w, connection))
.collect::<Result<Vec<Work>>>()?;
LibraryResults {
composers,
@ -126,54 +124,51 @@ impl MusicusLibrary {
work: None,
..
} => {
let performers = self
.con()
.prepare(
"SELECT DISTINCT persons.id, persons.first_name, persons.last_name \
FROM persons \
JOIN performances ON performances.person = persons.id \
JOIN recordings ON recordings.id = performances.recording \
JOIN works ON works.id = recordings.work \
WHERE works.composer IS ?1 \
AND (persons.first_name LIKE ?2 OR persons.last_name LIKE ?2) \
LIMIT 9",
let performers: Vec<Person> = persons::table
.inner_join(recording_persons::table.inner_join(
recordings::table.inner_join(works::table.inner_join(work_persons::table)),
))
.filter(
work_persons::person_id
.eq(&composer.person_id)
.and(persons::name.like(&search)),
)
.unwrap()
.query_map([&composer.id, &search], Person::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Person>>>()
.unwrap();
.limit(9)
.select(persons::all_columns)
.distinct()
.load(connection)?;
let ensembles = self
.con()
.prepare(
"SELECT DISTINCT ensembles.id, ensembles.name \
FROM ensembles \
JOIN performances ON performances.ensemble = ensembles.id \
JOIN recordings ON recordings.id = performances.recording \
JOIN works ON works.id = recordings.work \
WHERE works.composer IS ?1 AND ensembles.name LIKE ?2 \
LIMIT 9",
let ensembles: Vec<Ensemble> = ensembles::table
.inner_join(recording_ensembles::table.inner_join(
recordings::table.inner_join(works::table.inner_join(work_persons::table)),
))
.filter(
work_persons::person_id
.eq(&composer.person_id)
.and(ensembles::name.like(&search)),
)
.unwrap()
.query_map([&composer.id, &search], Ensemble::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Ensemble>>>()
.unwrap();
.limit(9)
.select(ensembles::all_columns)
.distinct()
.load::<tables::Ensemble>(connection)?
.into_iter()
.map(|e| Ensemble::from_table(e, connection))
.collect::<Result<Vec<Ensemble>>>()?;
let works = self
.con()
.prepare(
"SELECT DISTINCT works.id, works.title, persons.id, persons.first_name, persons.last_name \
FROM works \
JOIN persons ON works.composer = persons.id \
WHERE works.composer = ?1 AND title LIKE ?2 \
LIMIT 9")
.unwrap()
.query_map([&composer.id, &search], Work::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Work>>>()
.unwrap();
let works: Vec<Work> = works::table
.inner_join(work_persons::table)
.filter(
work_persons::person_id
.eq(&composer.person_id)
.and(works::name.like(&search)),
)
.limit(9)
.select(works::all_columns)
.distinct()
.load::<tables::Work>(connection)?
.into_iter()
.map(|w| Work::from_table(w, connection))
.collect::<Result<Vec<Work>>>()?;
LibraryResults {
performers,
@ -189,40 +184,40 @@ impl MusicusLibrary {
work: None,
..
} => {
let composers = self
.con()
.prepare(
"SELECT DISTINCT persons.id, persons.first_name, persons.last_name \
FROM persons \
JOIN works ON works.composer = persons.id \
JOIN recordings ON recordings.work = works.id \
JOIN performances ON performances.recording = recordings.id \
WHERE performances.ensemble IS ?1 \
AND (persons.first_name LIKE ?2 OR persons.last_name LIKE ?2) \
LIMIT 9",
)
.unwrap()
.query_map([&ensemble.id, &search], Person::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Person>>>()
.unwrap();
let composers: Vec<Person> =
persons::table
.inner_join(work_persons::table.inner_join(
works::table.inner_join(
recordings::table.inner_join(recording_ensembles::table),
),
))
.filter(
recording_ensembles::ensemble_id
.eq(&ensemble.ensemble_id)
.and(persons::name.like(&search)),
)
.limit(9)
.select(persons::all_columns)
.distinct()
.load(connection)?;
let recordings = self
.con()
.prepare(
"SELECT DISTINCT recordings.id, works.id, works.title, persons.id, persons.first_name, persons.last_name \
FROM recordings \
JOIN works ON recordings.work = works.id \
JOIN persons ON works.composer = persons.id \
JOIN performances ON recordings.id = performances.recording \
WHERE performances.ensemble IS ?1 \
AND (works.title LIKE ?2 OR persons.first_name LIKE ?2 OR persons.last_name LIKE ?2) \
LIMIT 9")
.unwrap()
.query_map([&ensemble.id, &search], Recording::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Recording>>>()
.unwrap();
let recordings = recordings::table
.inner_join(
works::table.inner_join(work_persons::table.inner_join(persons::table)),
)
// .inner_join(recording_persons::table.inner_join(persons::table))
.inner_join(recording_ensembles::table)
.filter(
recording_ensembles::ensemble_id
.eq(&ensemble.ensemble_id)
.and(works::name.like(&search).or(persons::name.like(&search))),
)
.select(recordings::all_columns)
.distinct()
.load::<tables::Recording>(connection)?
.into_iter()
.map(|r| Recording::from_table(r, &&self.folder(), connection))
.collect::<Result<Vec<Recording>>>()?;
LibraryResults {
composers,
@ -236,40 +231,39 @@ impl MusicusLibrary {
work: None,
..
} => {
let composers = self
.con()
.prepare(
"SELECT DISTINCT persons.id, persons.first_name, persons.last_name \
FROM persons \
JOIN works ON works.composer = persons.id \
JOIN recordings ON recordings.work = works.id \
JOIN performances ON performances.recording = recordings.id \
WHERE performances.person IS ?1 \
AND (persons.first_name LIKE ?2 OR persons.last_name LIKE ?2) \
LIMIT 9",
let composers: Vec<Person> = persons::table
.inner_join(
work_persons::table
.inner_join(works::table.inner_join(
recordings::table.inner_join(recording_persons::table),
)),
)
.unwrap()
.query_map([&performer.id, &search], Person::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Person>>>()
.unwrap();
.filter(
recording_persons::person_id
.eq(&performer.person_id)
.and(persons::name.like(&search)),
)
.limit(9)
.select(persons::all_columns)
.distinct()
.load(connection)?;
let recordings = self
.con()
.prepare(
"SELECT DISTINCT recordings.id, works.id, works.title, persons.id, persons.first_name, persons.last_name \
FROM recordings \
JOIN works ON recordings.work = works.id \
JOIN persons ON works.composer = persons.id \
JOIN performances ON recordings.id = performances.recording \
WHERE performances.person IS ?1 \
AND (works.title LIKE ?2 OR persons.first_name LIKE ?2 OR persons.last_name LIKE ?2) \
LIMIT 9")
.unwrap()
.query_map([&performer.id, &search], Recording::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Recording>>>()
.unwrap();
let recordings = recordings::table
.inner_join(
works::table.inner_join(work_persons::table.inner_join(persons::table)),
)
.inner_join(recording_persons::table)
.filter(
recording_persons::person_id
.eq(&performer.person_id)
.and(works::name.like(&search).or(persons::name.like(&search))),
)
.select(recordings::all_columns)
.distinct()
.load::<tables::Recording>(connection)?
.into_iter()
.map(|r| Recording::from_table(r, &self.folder(), connection))
.collect::<Result<Vec<Recording>>>()?;
LibraryResults {
composers,
@ -283,23 +277,21 @@ impl MusicusLibrary {
work: None,
..
} => {
let recordings = self
.con()
.prepare(
"SELECT DISTINCT recordings.id, works.id, works.title, persons.id, persons.first_name, persons.last_name \
FROM recordings \
JOIN works ON recordings.work = works.id \
JOIN persons ON works.composer = persons.id \
JOIN performances ON recordings.id = performances.recording \
WHERE works.composer IS ?1 \
AND performances.ensemble IS ?2 \
AND works.title LIKE ?3 \
LIMIT 9")
.unwrap()
.query_map([&composer.id, &ensemble.id, &search], Recording::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Recording>>>()
.unwrap();
let recordings = recordings::table
.inner_join(works::table.inner_join(work_persons::table))
.inner_join(recording_ensembles::table)
.filter(
work_persons::person_id
.eq(&composer.person_id)
.and(recording_ensembles::ensemble_id.eq(&ensemble.ensemble_id))
.and(works::name.like(search)),
)
.select(recordings::all_columns)
.distinct()
.load::<tables::Recording>(connection)?
.into_iter()
.map(|r| Recording::from_table(r, &self.folder(), connection))
.collect::<Result<Vec<Recording>>>()?;
LibraryResults {
recordings,
@ -312,23 +304,21 @@ impl MusicusLibrary {
work: None,
..
} => {
let recordings = self
.con()
.prepare(
"SELECT DISTINCT recordings.id, works.id, works.title, persons.id, persons.first_name, persons.last_name \
FROM recordings \
JOIN works ON recordings.work = works.id \
JOIN persons ON works.composer = persons.id \
JOIN performances ON recordings.id = performances.recording \
WHERE works.composer IS ?1 \
AND performances.person IS ?2 \
AND works.title LIKE ?3 \
LIMIT 9")
.unwrap()
.query_map([&composer.id, &performer.id, &search], Recording::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Recording>>>()
.unwrap();
let recordings = recordings::table
.inner_join(works::table.inner_join(work_persons::table))
.inner_join(recording_persons::table)
.filter(
work_persons::person_id
.eq(&composer.person_id)
.and(recording_persons::person_id.eq(&performer.person_id))
.and(works::name.like(search)),
)
.select(recordings::all_columns)
.distinct()
.load::<tables::Recording>(connection)?
.into_iter()
.map(|r| Recording::from_table(r, &self.folder(), connection))
.collect::<Result<Vec<Recording>>>()?;
LibraryResults {
recordings,
@ -338,130 +328,35 @@ impl MusicusLibrary {
LibraryQuery {
work: Some(work), ..
} => {
let recordings = self
.con()
.prepare(
"SELECT DISTINCT recordings.id, works.id, works.title, persons.id, persons.first_name, persons.last_name \
FROM recordings \
JOIN works ON recordings.work = works.id \
JOIN persons ON works.composer IS persons.id \
WHERE works.id IS ?1")
.unwrap()
.query_map([&work.id], Recording::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Recording>>>()
.unwrap();
let recordings = recordings::table
.filter(recordings::work_id.eq(&work.work_id))
.load::<tables::Recording>(connection)?
.into_iter()
.map(|r| Recording::from_table(r, &self.folder(), connection))
.collect::<Result<Vec<Recording>>>()?;
LibraryResults {
recordings,
..Default::default()
}
}
}
})
}
pub fn work_parts(&self, work: &Work) -> Vec<String> {
self.con()
.prepare("SELECT * FROM work_parts WHERE work IS ?1 ORDER BY part_index")
.unwrap()
.query_map([&work.id], |row| row.get::<_, String>(3))
.unwrap()
.collect::<rusqlite::Result<Vec<String>>>()
.unwrap()
}
pub fn random_recording(&self, query: &LibraryQuery) -> Result<Recording> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
pub fn tracks(&self, recording: &Recording) -> Vec<Track> {
self.con()
.prepare("SELECT * FROM tracks WHERE recording IS ?1 ORDER BY \"index\"")
.unwrap()
.query_map([&recording.id], |row| {
Ok(Track {
work_parts: row
.get::<_, String>(4)?
.split(',')
.filter(|s| !s.is_empty())
.map(str::parse::<usize>)
.collect::<Result<Vec<usize>, ParseIntError>>()
.expect("work part IDs should be valid integers"),
path: PathBuf::from(self.folder()).join(row.get::<_, String>(6)?),
})
})
.unwrap()
.collect::<rusqlite::Result<Vec<Track>>>()
.unwrap()
}
pub fn random_recording(&self, query: &LibraryQuery) -> Option<Recording> {
match query {
LibraryQuery { .. } => self
.con()
.prepare("SELECT * FROM recordings ORDER BY RANDOM() LIMIT 1")
.unwrap()
.query_map([], Recording::from_row)
.unwrap()
.next()
.map(|r| r.unwrap()),
LibraryQuery { .. } => Recording::from_table(
recordings::table
.order(random())
.first::<tables::Recording>(connection)?,
&self.folder(),
connection,
),
}
}
pub fn performances(&self, recording: &Recording) -> Vec<String> {
let mut performances = self
.con()
.prepare(
"SELECT persons.id, persons.first_name, persons.last_name, instruments.id, instruments.name \
FROM performances \
INNER JOIN persons ON persons.id = performances.person \
LEFT JOIN instruments ON instruments.id = performances.role \
INNER JOIN recordings ON performances.recording = recordings.id \
WHERE recordings.id IS ?1")
.unwrap()
.query_map([&recording.id], Performance::from_person_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Performance>>>()
.unwrap();
performances.append(
&mut self
.con()
.prepare(
"SELECT ensembles.id, ensembles.name, instruments.id, instruments.name \
FROM performances \
INNER JOIN ensembles ON ensembles.id = performances.ensemble \
LEFT JOIN instruments ON instruments.id = performances.role \
INNER JOIN recordings ON performances.recording = recordings.id \
WHERE recordings.id IS ?1",
)
.unwrap()
.query_map([&recording.id], Performance::from_ensemble_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Performance>>>()
.unwrap(),
);
performances
.into_iter()
.map(|performance| match performance {
Performance::Person(person, role) => {
let mut result = person.name_fl();
if let Some(role) = role {
result.push_str(&format!(" ({})", role.name));
}
result
}
Performance::Ensemble(ensemble, role) => {
let mut result = ensemble.name;
if let Some(role) = role {
result.push_str(&format!(" ({})", role.name));
}
result
}
})
.collect::<Vec<String>>()
}
fn con(&self) -> &Connection {
self.imp().connection.get().unwrap()
}
}
#[derive(Default, Debug)]
@ -491,170 +386,3 @@ impl LibraryResults {
&& self.recordings.is_empty()
}
}
#[derive(Debug, Clone, Eq)]
pub struct Person {
pub id: String,
pub first_name: String,
pub last_name: String,
}
impl PartialEq for Person {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Person {
pub fn from_row(row: &Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
first_name: row.get(1)?,
last_name: row.get(2)?,
})
}
pub fn name_fl(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}
#[derive(Debug, Clone, Eq)]
pub struct Ensemble {
pub id: String,
pub name: String,
}
impl PartialEq for Ensemble {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Ensemble {
pub fn from_row(row: &Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
name: row.get(1)?,
})
}
}
#[derive(Debug, Clone, Eq)]
pub struct Work {
pub id: String,
pub title: String,
pub composer: Person,
}
impl PartialEq for Work {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Work {
pub fn from_row(row: &Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
title: row.get(1)?,
composer: Person {
id: row.get(2)?,
first_name: row.get(3)?,
last_name: row.get(4)?,
},
})
}
}
#[derive(Debug, Clone, Eq)]
pub struct Recording {
pub id: String,
pub work: Work,
}
impl PartialEq for Recording {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Recording {
pub fn from_row(row: &Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
work: Work {
id: row.get(1)?,
title: row.get(2)?,
composer: Person {
id: row.get(3)?,
first_name: row.get(4)?,
last_name: row.get(5)?,
},
},
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Performance {
Person(Person, Option<Role>),
Ensemble(Ensemble, Option<Role>),
}
impl Performance {
pub fn from_person_row(row: &Row) -> rusqlite::Result<Self> {
let person = Person {
id: row.get(0)?,
first_name: row.get(1)?,
last_name: row.get(2)?,
};
Ok(match row.get::<_, Option<String>>(3)? {
None => Self::Person(person, None),
Some(role_id) => Self::Person(
person,
Some(Role {
id: role_id,
name: row.get(4)?,
}),
),
})
}
pub fn from_ensemble_row(row: &Row) -> rusqlite::Result<Self> {
let ensemble = Ensemble {
id: row.get(0)?,
name: row.get(1)?,
};
Ok(match row.get::<_, Option<String>>(2)? {
None => Self::Ensemble(ensemble, None),
Some(role_id) => Self::Ensemble(
ensemble,
Some(Role {
id: role_id,
name: row.get(3)?,
}),
),
})
}
}
#[derive(Debug, Clone, Eq)]
pub struct Role {
pub id: String,
pub name: String,
}
impl PartialEq for Role {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
#[derive(Debug, Clone)]
pub struct Track {
pub work_parts: Vec<usize>,
pub path: PathBuf,
}

View file

@ -1,5 +1,6 @@
mod application;
mod config;
mod db;
mod home_page;
mod library_manager;
mod library;

View file

@ -1,7 +1,8 @@
use crate::library::Recording;
use gtk::{glib, subclass::prelude::*};
use std::cell::OnceCell;
use crate::db::models::Recording;
mod imp {
use super::*;
@ -44,14 +45,14 @@ glib::wrapper! {
}
impl MusicusRecordingTile {
pub fn new(recording: &Recording, performances: Vec<String>) -> Self {
pub fn new(recording: &Recording) -> Self {
let obj: Self = glib::Object::new();
let imp = obj.imp();
imp.work_label.set_label(&recording.work.title);
imp.composer_label
.set_label(&recording.work.composer.name_fl());
imp.performances_label.set_label(&performances.join(", "));
imp.work_label.set_label(&recording.work.name.get());
imp.composer_label.set_label(&recording.work.composers_string());
imp.performances_label.set_label(&recording.performers_string());
imp.recording.set(recording.clone()).unwrap();
obj

View file

@ -1,8 +1,9 @@
use crate::library::{Ensemble, Person, Work};
use adw::{glib, glib::subclass::Signal, prelude::*, subclass::prelude::*};
use once_cell::sync::Lazy;
use std::cell::OnceCell;
use crate::db::models::{Ensemble, Person, Work};
mod imp {
use super::*;
@ -53,11 +54,11 @@ impl MusicusSearchTag {
pub fn new(tag: Tag) -> Self {
let obj: MusicusSearchTag = glib::Object::new();
obj.imp().label.set_label(&match &tag {
Tag::Composer(person) => person.name_fl(),
Tag::Performer(person) => person.name_fl(),
Tag::Ensemble(ensemble) => ensemble.name.clone(),
Tag::Work(work) => work.title.clone(),
obj.imp().label.set_label(match &tag {
Tag::Composer(person) => person.name.get(),
Tag::Performer(person) => person.name.get(),
Tag::Ensemble(ensemble) => ensemble.name.get(),
Tag::Work(work) => work.name.get(),
});
obj.imp().tag.set(tag).unwrap();

View file

@ -48,14 +48,14 @@ impl MusicusTagTile {
match &tag {
Tag::Composer(person) | Tag::Performer(person) => {
imp.title_label.set_label(&person.name_fl());
imp.title_label.set_label(person.name.get());
}
Tag::Ensemble(ensemble) => {
imp.title_label.set_label(&ensemble.name);
imp.title_label.set_label(ensemble.name.get());
}
Tag::Work(work) => {
imp.title_label.set_label(&work.title);
imp.subtitle_label.set_label(&work.composer.name_fl());
imp.title_label.set_label(work.name.get());
imp.subtitle_label.set_label(&work.composers_string());
imp.subtitle_label.set_visible(true);
}
}