database: Store access times

This commit is contained in:
Elias Projahn 2022-04-10 13:43:31 +02:00
parent 5c64bdef7e
commit a0554a478f
20 changed files with 315 additions and 104 deletions

31
Cargo.lock generated
View file

@ -121,6 +121,19 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"time",
"winapi",
]
[[package]]
name = "cpufeatures"
version = "0.2.2"
@ -353,7 +366,7 @@ checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
dependencies = [
"cfg-if",
"libc",
"wasi 0.10.2+wasi-snapshot-preview1",
"wasi 0.10.0+wasi-snapshot-preview1",
]
[[package]]
@ -1016,6 +1029,7 @@ dependencies = [
name = "musicus_database"
version = "0.1.0"
dependencies = [
"chrono",
"diesel",
"diesel_migrations",
"log",
@ -1481,6 +1495,17 @@ dependencies = [
"syn",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "tokio"
version = "1.17.0"
@ -1564,9 +1589,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "winapi"

View file

@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
diesel = { version = "1.4.5", features = ["sqlite"] }
diesel_migrations = "1.4.0"
chrono = "0.4.19"
log = "0.4.14"
rand = "0.7.3"
thiserror = "1.0.23"

View file

@ -0,0 +1,20 @@
ALTER TABLE "persons" DROP COLUMN "last_used";
ALTER TABLE "persons" DROP COLUMN "last_played";
ALTER TABLE "instruments" DROP COLUMN "last_used";
ALTER TABLE "instruments" DROP COLUMN "last_played";
ALTER TABLE "works" DROP COLUMN "last_used";
ALTER TABLE "works" DROP COLUMN "last_played";
ALTER TABLE "ensembles" DROP COLUMN "last_used";
ALTER TABLE "ensembles" DROP COLUMN "last_played";
ALTER TABLE "recordings" DROP COLUMN "last_used";
ALTER TABLE "recordings" DROP COLUMN "last_played";
ALTER TABLE "mediums" DROP COLUMN "last_used";
ALTER TABLE "mediums" DROP COLUMN "last_played";
ALTER TABLE "tracks" DROP COLUMN "last_used";
ALTER TABLE "tracks" DROP COLUMN "last_played";

View file

@ -0,0 +1,21 @@
ALTER TABLE "persons" ADD COLUMN "last_used" BIGINT;
ALTER TABLE "persons" ADD COLUMN "last_played" BIGINT;
ALTER TABLE "instruments" ADD COLUMN "last_used" BIGINT;
ALTER TABLE "instruments" ADD COLUMN "last_played" BIGINT;
ALTER TABLE "works" ADD COLUMN "last_used" BIGINT;
ALTER TABLE "works" ADD COLUMN "last_played" BIGINT;
ALTER TABLE "ensembles" ADD COLUMN "last_used" BIGINT;
ALTER TABLE "ensembles" ADD COLUMN "last_played" BIGINT;
ALTER TABLE "recordings" ADD COLUMN "last_used" BIGINT;
ALTER TABLE "recordings" ADD COLUMN "last_played" BIGINT;
ALTER TABLE "mediums" ADD COLUMN "last_used" BIGINT;
ALTER TABLE "mediums" ADD COLUMN "last_played" BIGINT;
ALTER TABLE "tracks" ADD COLUMN "last_used" BIGINT;
ALTER TABLE "tracks" ADD COLUMN "last_played" BIGINT;

View file

@ -1,5 +1,6 @@
use super::schema::ensembles;
use super::{Database, Result};
use chrono::Utc;
use diesel::prelude::*;
use log::info;
@ -8,14 +9,29 @@ use log::info;
pub struct Ensemble {
pub id: String,
pub name: String,
pub last_used: Option<i64>,
pub last_played: Option<i64>,
}
impl Ensemble {
pub fn new(id: String, name: String) -> Self {
Self {
id,
name,
last_used: Some(Utc::now().timestamp()),
last_played: None,
}
}
}
impl Database {
/// Update an existing ensemble or insert a new one.
pub fn update_ensemble(&self, ensemble: Ensemble) -> Result<()> {
pub fn update_ensemble(&self, mut ensemble: Ensemble) -> Result<()> {
info!("Updating ensemble {:?}", ensemble);
self.defer_foreign_keys()?;
ensemble.last_used = Some(Utc::now().timestamp());
self.connection.transaction(|| {
diesel::replace_into(ensembles::table)
.values(ensemble)

View file

@ -1,5 +1,6 @@
use super::schema::instruments;
use super::{Database, Result};
use chrono::Utc;
use diesel::prelude::*;
use log::info;
@ -8,14 +9,29 @@ use log::info;
pub struct Instrument {
pub id: String,
pub name: String,
pub last_used: Option<i64>,
pub last_played: Option<i64>,
}
impl Instrument {
pub fn new(id: String, name: String) -> Self {
Self {
id,
name,
last_used: Some(Utc::now().timestamp()),
last_played: None,
}
}
}
impl Database {
/// Update an existing instrument or insert a new one.
pub fn update_instrument(&self, instrument: Instrument) -> Result<()> {
pub fn update_instrument(&self, mut instrument: Instrument) -> Result<()> {
info!("Updating instrument {:?}", instrument);
self.defer_foreign_keys()?;
instrument.last_used = Some(Utc::now().timestamp());
self.connection.transaction(|| {
diesel::replace_into(instruments::table)
.values(instrument)

View file

@ -1,6 +1,7 @@
use super::generate_id;
use super::schema::{ensembles, mediums, performances, persons, recordings, tracks};
use super::{Database, Error, Recording, Result};
use chrono::{DateTime, TimeZone, Utc};
use diesel::prelude::*;
use log::info;
@ -19,6 +20,22 @@ pub struct Medium {
/// The tracks of the medium.
pub tracks: Vec<Track>,
pub last_used: Option<DateTime<Utc>>,
pub last_played: Option<DateTime<Utc>>,
}
impl Medium {
pub fn new(id: String, name: String, discid: Option<String>, tracks: Vec<Track>) -> Self {
Self {
id,
name,
discid,
tracks,
last_used: Some(Utc::now()),
last_played: None,
}
}
}
/// A track on a medium.
@ -37,6 +54,22 @@ pub struct Track {
/// The path to the audio file containing this track.
pub path: String,
pub last_used: Option<DateTime<Utc>>,
pub last_played: Option<DateTime<Utc>>,
}
impl Track {
pub fn new(recording: Recording, work_parts: Vec<usize>, source_index: usize, path: String) -> Self {
Self {
recording,
work_parts,
source_index,
path,
last_used: Some(Utc::now()),
last_played: None,
}
}
}
/// Table data for a [`Medium`].
@ -46,6 +79,8 @@ struct MediumRow {
pub id: String,
pub name: String,
pub discid: Option<String>,
pub last_used: Option<i64>,
pub last_played: Option<i64>,
}
/// Table data for a [`Track`].
@ -59,6 +94,8 @@ struct TrackRow {
pub work_parts: String,
pub source_index: i32,
pub path: String,
pub last_used: Option<i64>,
pub last_played: Option<i64>,
}
impl Database {
@ -79,6 +116,8 @@ impl Database {
id: medium_id.to_owned(),
name: medium.name.clone(),
discid: medium.discid.clone(),
last_used: Some(Utc::now().timestamp()),
last_played: medium.last_played.map(|t| t.timestamp()),
};
diesel::insert_into(mediums::table)
@ -109,6 +148,8 @@ impl Database {
work_parts,
source_index: track.source_index as i32,
path: track.path.clone(),
last_used: Some(Utc::now().timestamp()),
last_played: track.last_played.map(|t| t.timestamp()),
};
diesel::insert_into(tracks::table)
@ -254,6 +295,8 @@ impl Database {
name: row.name,
discid: row.discid,
tracks,
last_used: row.last_used.map(|t| Utc.timestamp(t, 0)),
last_played: row.last_played.map(|t| Utc.timestamp(t, 0)),
};
Ok(medium)
@ -285,6 +328,8 @@ impl Database {
work_parts: part_indices,
source_index: row.source_index as usize,
path: row.path,
last_used: row.last_used.map(|t| Utc.timestamp(t, 0)),
last_played: row.last_played.map(|t| Utc.timestamp(t, 0)),
};
Ok(track)

View file

@ -1,5 +1,6 @@
use super::schema::persons;
use super::{Database, Result};
use chrono::Utc;
use diesel::prelude::*;
use log::info;
@ -9,9 +10,21 @@ pub struct Person {
pub id: String,
pub first_name: String,
pub last_name: String,
pub last_used: Option<i64>,
pub last_played: Option<i64>,
}
impl Person {
pub fn new(id: String, first_name: String, last_name: String) -> Self {
Self {
id,
first_name,
last_name,
last_used: Some(Utc::now().timestamp()),
last_played: None,
}
}
/// Get the full name in the form "First Last".
pub fn name_fl(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
@ -25,10 +38,12 @@ impl Person {
impl Database {
/// Update an existing person or insert a new one.
pub fn update_person(&self, person: Person) -> Result<()> {
pub fn update_person(&self, mut person: Person) -> Result<()> {
info!("Updating person {:?}", person);
self.defer_foreign_keys()?;
person.last_used = Some(Utc::now().timestamp());
self.connection.transaction(|| {
diesel::replace_into(persons::table)
.values(person)

View file

@ -1,6 +1,7 @@
use super::generate_id;
use super::schema::{ensembles, performances, persons, recordings};
use super::{Database, Ensemble, Error, Instrument, Person, Result, Work};
use chrono::{DateTime, TimeZone, Utc};
use diesel::prelude::*;
use log::info;
@ -11,16 +12,31 @@ pub struct Recording {
pub work: Work,
pub comment: String,
pub performances: Vec<Performance>,
pub last_used: Option<DateTime<Utc>>,
pub last_played: Option<DateTime<Utc>>,
}
impl Recording {
pub fn new(id: String, work: Work, comment: String, performances: Vec<Performance>) -> Self {
Self {
id,
work,
comment,
performances,
last_used: Some(Utc::now()),
last_played: None,
}
}
/// Initialize a new recording with a work.
pub fn new(work: Work) -> Self {
pub fn from_work(work: Work) -> Self {
Self {
id: generate_id(),
work,
comment: String::new(),
performances: Vec::new(),
last_used: Some(Utc::now()),
last_played: None,
}
}
@ -82,6 +98,8 @@ struct RecordingRow {
pub id: String,
pub work: String,
pub comment: String,
pub last_used: Option<i64>,
pub last_played: Option<i64>,
}
impl From<Recording> for RecordingRow {
@ -90,6 +108,8 @@ impl From<Recording> for RecordingRow {
id: recording.id,
work: recording.work.id,
comment: recording.comment,
last_used: Some(Utc::now().timestamp()),
last_played: recording.last_played.map(|t| t.timestamp()),
}
}
}
@ -255,6 +275,8 @@ impl Database {
work,
comment: row.comment,
performances: performance_descriptions,
last_used: row.last_used.map(|t| Utc.timestamp(t, 0)),
last_played: row.last_played.map(|t| Utc.timestamp(t, 0)),
};
Ok(recording_description)

View file

@ -2,6 +2,8 @@ table! {
ensembles (id) {
id -> Text,
name -> Text,
last_used -> Nullable<BigInt>,
last_played -> Nullable<BigInt>,
}
}
@ -17,6 +19,8 @@ table! {
instruments (id) {
id -> Text,
name -> Text,
last_used -> Nullable<BigInt>,
last_played -> Nullable<BigInt>,
}
}
@ -25,6 +29,8 @@ table! {
id -> Text,
name -> Text,
discid -> Nullable<Text>,
last_used -> Nullable<BigInt>,
last_played -> Nullable<BigInt>,
}
}
@ -43,6 +49,8 @@ table! {
id -> Text,
first_name -> Text,
last_name -> Text,
last_used -> Nullable<BigInt>,
last_played -> Nullable<BigInt>,
}
}
@ -51,6 +59,8 @@ table! {
id -> Text,
work -> Text,
comment -> Text,
last_used -> Nullable<BigInt>,
last_played -> Nullable<BigInt>,
}
}
@ -63,6 +73,8 @@ table! {
work_parts -> Text,
source_index -> Integer,
path -> Text,
last_used -> Nullable<BigInt>,
last_played -> Nullable<BigInt>,
}
}
@ -80,6 +92,8 @@ table! {
id -> Text,
composer -> Text,
title -> Text,
last_used -> Nullable<BigInt>,
last_played -> Nullable<BigInt>,
}
}

View file

@ -1,6 +1,7 @@
use super::generate_id;
use super::schema::{instrumentations, work_parts, works};
use super::{Database, Error, Instrument, Person, Result};
use chrono::{DateTime, TimeZone, Utc};
use diesel::prelude::*;
use diesel::{Insertable, Queryable};
use log::info;
@ -12,6 +13,8 @@ struct WorkRow {
pub id: String,
pub composer: String,
pub title: String,
pub last_used: Option<i64>,
pub last_played: Option<i64>,
}
impl From<Work> for WorkRow {
@ -20,6 +23,8 @@ impl From<Work> for WorkRow {
id: work.id,
composer: work.composer.id,
title: work.title,
last_used: Some(Utc::now().timestamp()),
last_played: work.last_played.map(|t| t.timestamp()),
}
}
}
@ -57,17 +62,33 @@ pub struct Work {
pub composer: Person,
pub instruments: Vec<Instrument>,
pub parts: Vec<WorkPart>,
pub last_used: Option<DateTime<Utc>>,
pub last_played: Option<DateTime<Utc>>,
}
impl Work {
pub fn new(id: String, title: String, composer: Person, instruments: Vec<Instrument>, parts: Vec<WorkPart>) -> Self {
Self {
id,
title,
composer,
instruments,
parts,
last_used: Some(Utc::now()),
last_played: None,
}
}
/// Initialize a new work with a composer.
pub fn new(composer: Person) -> Self {
pub fn from_composer(composer: Person) -> Self {
Self {
id: generate_id(),
title: String::new(),
composer,
instruments: Vec::new(),
parts: Vec::new(),
last_used: Some(Utc::now()),
last_played: None,
}
}
@ -109,9 +130,7 @@ impl Database {
.execute(&self.connection)?;
let Work {
instruments,
parts,
..
instruments, parts, ..
} = work;
for instrument in instruments {
@ -200,6 +219,8 @@ impl Database {
title: row.title,
instruments,
parts,
last_used: row.last_used.map(|t| Utc.timestamp(t, 0)),
last_played: row.last_played.map(|t| Utc.timestamp(t, 0)),
})
}

View file

@ -88,10 +88,7 @@ impl EnsembleEditor {
fn save(&self) -> Result<Ensemble> {
let name = self.name.get_text();
let ensemble = Ensemble {
id: self.id.clone(),
name,
};
let ensemble = Ensemble::new(self.id.clone(), name);
self.handle.backend.db().update_ensemble(ensemble.clone())?;
self.handle.backend.library_changed();

View file

@ -88,12 +88,12 @@ impl InstrumentEditor {
fn save(&self) -> Result<Instrument> {
let name = self.name.get_text();
let instrument = Instrument {
id: self.id.clone(),
name,
};
let instrument = Instrument::new(self.id.clone(), name);
self.handle.backend.db().update_instrument(instrument.clone())?;
self.handle
.backend
.db()
.update_instrument(instrument.clone())?;
self.handle.backend.library_changed();
Ok(instrument)

View file

@ -102,11 +102,7 @@ impl PersonEditor {
let first_name = self.first_name.get_text();
let last_name = self.last_name.get_text();
let person = Person {
id: self.id.clone(),
first_name,
last_name,
};
let person = Person::new(self.id.clone(), first_name, last_name);
self.handle.backend.db().update_person(person.clone())?;
self.handle.backend.library_changed();

View file

@ -177,18 +177,20 @@ impl RecordingEditor {
/// Save the recording.
fn save(self: &Rc<Self>) -> Result<Recording> {
let recording = Recording {
id: self.id.clone(),
work: self
.work
let recording = Recording::new(
self.id.clone(),
self.work
.borrow()
.clone()
.expect("Tried to create recording without work!"),
comment: self.comment_entry.text().to_string(),
performances: self.performances.borrow().clone(),
};
self.comment_entry.text().to_string(),
self.performances.borrow().clone(),
);
self.handle.backend.db().update_recording(recording.clone())?;
self.handle
.backend
.db()
.update_recording(recording.clone())?;
self.handle.backend.library_changed();
Ok(recording)

View file

@ -148,7 +148,8 @@ impl Screen<Option<Work>, Work> for WorkEditor {
});
}));
this.part_list.set_make_widget_cb(clone!(@weak this => @default-panic, move |index| {
this.part_list
.set_make_widget_cb(clone!(@weak this => @default-panic, move |index| {
let part = &this.parts.borrow()[index];
let delete_button = gtk::Button::from_icon_name(Some("user-trash-symbolic"));
@ -248,17 +249,16 @@ impl WorkEditor {
/// Save the work.
fn save(self: &Rc<Self>) -> Result<Work> {
let work = Work {
id: self.id.clone(),
title: self.title_entry.text().to_string(),
composer: self
.composer
let work = Work::new(
self.id.clone(),
self.title_entry.text().to_string(),
self.composer
.borrow()
.clone()
.expect("Tried to create work without composer!"),
instruments: self.instruments.borrow().clone(),
parts: self.parts.borrow().clone(),
};
self.instruments.borrow().clone(),
self.parts.borrow().clone(),
);
self.handle.backend.db().update_work(work.clone())?;
self.handle.backend.library_changed();

View file

@ -186,23 +186,23 @@ impl MediumEditor {
for track_set_data in &*self.track_sets.borrow() {
for track_data in &track_set_data.tracks {
let track = Track {
recording: track_set_data.recording.clone(),
work_parts: track_data.work_parts.clone(),
source_index: track_data.track_source,
path: String::new(),
};
let track = Track::new(
track_set_data.recording.clone(),
track_data.work_parts.clone(),
track_data.track_source,
String::new(),
);
tracks.push(track);
}
}
let medium = Medium {
id: generate_id(),
name: self.name_entry.text().to_string(),
discid: Some(self.session.source_id().to_owned()),
let medium = Medium::new(
generate_id(),
self.name_entry.text().to_string(),
Some(self.session.source_id().to_owned()),
tracks,
};
);
// The medium is not added to the database, because the track paths are not known until the
// medium is actually imported into the music library. This step will be handled by the

View file

@ -248,12 +248,12 @@ impl MediumPreview {
// Add the modified medium to the database.
let medium = Medium {
id: medium.id.clone(),
name: medium.name.clone(),
discid: medium.discid.clone(),
let medium = Medium::new(
medium.id.clone(),
medium.name.clone(),
medium.discid.clone(),
tracks,
};
);
self.handle.backend.db().update_medium(medium)?;
self.handle.backend.library_changed();

View file

@ -36,12 +36,12 @@ impl Screen<(), Recording> for RecordingSelector {
// immediately show the work editor. Going back from the work editor will
// correctly show the person selector again.
let work = Work::new(person);
let work = Work::from_composer(person);
if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await {
// There will also be no existing recordings, so we show the recording
// editor next.
let recording = Recording::new(work);
let recording = Recording::from_work(work);
if let Some(recording) = push!(this.handle, RecordingEditor, Some(recording)).await {
this.handle.pop(Some(recording));
}
@ -117,7 +117,7 @@ impl Screen<Person, Work> for RecordingSelectorWorkScreen {
this.selector.set_add_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
let work = Work::new(this.person.clone());
let work = Work::from_composer(this.person.clone());
if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await {
this.handle.pop(Some(work));
}
@ -180,7 +180,7 @@ impl Screen<Work, Recording> for RecordingSelectorRecordingScreen {
this.selector.set_add_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
let recording = Recording::new(this.work.clone());
let recording = Recording::from_work(this.work.clone());
if let Some(recording) = push!(this.handle, RecordingEditor, Some(recording)).await {
this.handle.pop(Some(recording));
}

View file

@ -36,7 +36,7 @@ impl Screen<(), Work> for WorkSelector {
// immediately show the work editor. Going back from the work editor will
// correctly show the person selector again.
let work = Work::new(person);
let work = Work::from_composer(person);
if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await {
this.handle.pop(Some(work));
}
@ -107,7 +107,7 @@ impl Screen<Person, Work> for WorkSelectorWorkScreen {
this.selector.set_add_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
let work = Work::new(this.person.clone());
let work = Work::from_composer(this.person.clone());
if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await {
this.handle.pop(Some(work));
}