diff --git a/data/de.johrpan.Musicus.gschema.xml.in b/data/de.johrpan.Musicus.gschema.xml.in
index 27f4f52..b588ac0 100644
--- a/data/de.johrpan.Musicus.gschema.xml.in
+++ b/data/de.johrpan.Musicus.gschema.xml.in
@@ -52,12 +52,24 @@
'{"title":"A long time ago","description":"Works that you haven\'t listened to for a long time.","design":"Purple","prefer_recently_added":0.0,"prefer_least_recently_played":1.0,"avoid_repeated_composers":60,"avoid_repeated_instruments":60,"play_full_recordings":true}'
Default settings for program 3
+
+ true
+ Automatically download metadata updates
+
+
+ false
+ Use a custom URL for metadata downloads
+
+
+ 'https://musicus.johrpan.de/musicus_metadata_latest.musdb'
+ Custom URL for metadata downloads
+
false
Use a custom URL for library downloads
- 'https://musicus.johrpan.de/musicus_library_latest.zip'
+ 'https://musicus.johrpan.de/musicus_library_latest.muslib'
Custom URL for library downloads
diff --git a/data/ui/editor/ensemble.blp b/data/ui/editor/ensemble.blp
index 5f1df80..e45d913 100644
--- a/data/ui/editor/ensemble.blp
+++ b/data/ui/editor/ensemble.blp
@@ -35,9 +35,15 @@ template $MusicusEnsembleEditor: Adw.NavigationPage {
margin-top: 24;
styles [
- "boxed-list",
+ "boxed-list-separate",
]
+ Adw.SwitchRow enable_updates_row {
+ title: _("Enable updates");
+ subtitle: _("Keep this item up to date with the online metadata library");
+ active: true;
+ }
+
Adw.ButtonRow save_row {
title: _("_Create ensemble");
use-underline: true;
diff --git a/data/ui/editor/instrument.blp b/data/ui/editor/instrument.blp
index 7502cf7..672b058 100644
--- a/data/ui/editor/instrument.blp
+++ b/data/ui/editor/instrument.blp
@@ -35,9 +35,15 @@ template $MusicusInstrumentEditor: Adw.NavigationPage {
margin-top: 24;
styles [
- "boxed-list",
+ "boxed-list-separate",
]
+ Adw.SwitchRow enable_updates_row {
+ title: _("Enable updates");
+ subtitle: _("Keep this item up to date with the online metadata library");
+ active: true;
+ }
+
Adw.ButtonRow save_row {
title: _("_Create instrument");
use-underline: true;
diff --git a/data/ui/editor/person.blp b/data/ui/editor/person.blp
index bb87451..01d2b6b 100644
--- a/data/ui/editor/person.blp
+++ b/data/ui/editor/person.blp
@@ -35,9 +35,15 @@ template $MusicusPersonEditor: Adw.NavigationPage {
margin-top: 24;
styles [
- "boxed-list",
+ "boxed-list-separate",
]
+ Adw.SwitchRow enable_updates_row {
+ title: _("Enable updates");
+ subtitle: _("Keep this item up to date with the online metadata library");
+ active: true;
+ }
+
Adw.ButtonRow save_row {
title: _("_Create person");
use-underline: true;
diff --git a/data/ui/editor/recording.blp b/data/ui/editor/recording.blp
index 9c12c4a..43c28ac 100644
--- a/data/ui/editor/recording.blp
+++ b/data/ui/editor/recording.blp
@@ -125,9 +125,15 @@ template $MusicusRecordingEditor: Adw.NavigationPage {
margin-top: 24;
styles [
- "boxed-list",
+ "boxed-list-separate",
]
+ Adw.SwitchRow enable_updates_row {
+ title: _("Enable updates");
+ subtitle: _("Keep this item up to date with the online metadata library");
+ active: true;
+ }
+
Adw.ButtonRow save_row {
title: _("_Create recording");
use-underline: true;
diff --git a/data/ui/editor/role.blp b/data/ui/editor/role.blp
index fb6d442..064868f 100644
--- a/data/ui/editor/role.blp
+++ b/data/ui/editor/role.blp
@@ -35,9 +35,15 @@ template $MusicusRoleEditor: Adw.NavigationPage {
margin-top: 24;
styles [
- "boxed-list",
+ "boxed-list-separate",
]
+ Adw.SwitchRow enable_updates_row {
+ title: _("Enable updates");
+ subtitle: _("Keep this item up to date with the online metadata library");
+ active: true;
+ }
+
Adw.ButtonRow save_row {
title: _("_Create role");
use-underline: true;
diff --git a/data/ui/editor/work.blp b/data/ui/editor/work.blp
index e245fd9..8c544ef 100644
--- a/data/ui/editor/work.blp
+++ b/data/ui/editor/work.blp
@@ -119,9 +119,15 @@ template $MusicusWorkEditor: Adw.NavigationPage {
margin-top: 24;
styles [
- "boxed-list",
+ "boxed-list-separate",
]
+ Adw.SwitchRow enable_updates_row {
+ title: _("Enable updates");
+ subtitle: _("Keep this item up to date with the online metadata library");
+ active: true;
+ }
+
Adw.ButtonRow save_row {
title: _("_Create work");
use-underline: true;
diff --git a/data/ui/library_manager.blp b/data/ui/library_manager.blp
index e7e85c1..c3507d3 100644
--- a/data/ui/library_manager.blp
+++ b/data/ui/library_manager.blp
@@ -63,9 +63,15 @@ template $MusicusLibraryManager: Adw.NavigationPage {
}
Adw.ButtonRow {
- title: _("Update default library");
+ title: _("Update metadata");
end-icon-name: "go-next-symbolic";
- activated => $update_default_library() swapped;
+ activated => $update_metadata() swapped;
+ }
+
+ Adw.ButtonRow {
+ title: _("Update library");
+ end-icon-name: "go-next-symbolic";
+ activated => $update_library() swapped;
}
}
diff --git a/data/ui/preferences_dialog.blp b/data/ui/preferences_dialog.blp
index d771b59..eac4881 100644
--- a/data/ui/preferences_dialog.blp
+++ b/data/ui/preferences_dialog.blp
@@ -69,15 +69,33 @@ template $MusicusPreferencesDialog: Adw.PreferencesDialog {
icon-name: "library-symbolic";
Adw.PreferencesGroup {
- title: _("Library download");
+ title: _("Metadata updates");
- Adw.SwitchRow use_custom_url_row {
- title: _("Use custom download URL");
+ Adw.SwitchRow enable_automatic_metadata_updates_row {
+ title: _("Enable automatic metadata updates");
+ }
+
+ Adw.SwitchRow use_custom_metadata_url_row {
+ title: _("Use custom metadata URL");
active: false;
}
- Adw.EntryRow custom_url_row {
- title: _("Download URL");
+ Adw.EntryRow custom_metadata_url_row {
+ title: _("Metadata download URL");
+ show-apply-button: true;
+ }
+ }
+
+ Adw.PreferencesGroup {
+ title: _("Library updates");
+
+ Adw.SwitchRow use_custom_library_url_row {
+ title: _("Use custom library URL");
+ active: false;
+ }
+
+ Adw.EntryRow custom_library_url_row {
+ title: _("Library download URL");
show-apply-button: true;
}
}
diff --git a/meson.build b/meson.build
index 382f24a..c6ea161 100644
--- a/meson.build
+++ b/meson.build
@@ -19,7 +19,8 @@ dependency('openssl', version: '>= 1.0')
name = 'Musicus'
base_id = 'de.johrpan.Musicus'
-library_url = 'https://musicus.johrpan.de/musicus_library_latest.zip'
+metadata_url = 'https://musicus.johrpan.de/musicus_metadata_latest.musdb'
+library_url = 'https://musicus.johrpan.de/musicus_library_latest.muslib'
app_id = base_id
path_id = '/de/johrpan/Musicus'
profile = get_option('profile')
diff --git a/migrations/2025-03-30-122451_updates/down.sql b/migrations/2025-03-30-122451_updates/down.sql
new file mode 100644
index 0000000..4dba8b5
--- /dev/null
+++ b/migrations/2025-03-30-122451_updates/down.sql
@@ -0,0 +1,173 @@
+CREATE TABLE persons_old (
+ 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_old (
+ 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_old (
+ 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_old (
+ 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 ensembles_old (
+ 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 recordings_old (
+ 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
+);
+
+INSERT INTO persons_old (
+ person_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+ )
+SELECT person_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+FROM persons;
+DROP TABLE persons;
+ALTER TABLE persons_old
+ RENAME TO persons;
+
+INSERT INTO roles_old (
+ role_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at
+ )
+SELECT role_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at
+FROM roles;
+DROP TABLE roles;
+ALTER TABLE roles_old
+ RENAME TO roles;
+
+INSERT INTO instruments_old (
+ instrument_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+ )
+SELECT instrument_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+FROM instruments;
+DROP TABLE instruments;
+ALTER TABLE instruments_old
+ RENAME TO instruments;
+
+INSERT INTO works_old (
+ work_id,
+ parent_work_id,
+ sequence_number,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+ )
+SELECT work_id,
+ parent_work_id,
+ sequence_number,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+FROM works;
+DROP TABLE works;
+ALTER TABLE works_old
+ RENAME TO works;
+
+INSERT INTO ensembles_old (
+ ensemble_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+ )
+SELECT ensemble_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+FROM ensembles;
+DROP TABLE ensembles;
+ALTER TABLE ensembles_old
+ RENAME TO ensembles;
+
+INSERT INTO recordings_old (
+ recording_id,
+ work_id,
+ year,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+ )
+SELECT recording_id,
+ work_id,
+ year,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+FROM recordings;
+DROP TABLE recordings;
+ALTER TABLE recordings_old
+ RENAME TO recordings;
\ No newline at end of file
diff --git a/migrations/2025-03-30-122451_updates/up.sql b/migrations/2025-03-30-122451_updates/up.sql
new file mode 100644
index 0000000..3298289
--- /dev/null
+++ b/migrations/2025-03-30-122451_updates/up.sql
@@ -0,0 +1,179 @@
+CREATE TABLE persons_new (
+ person_id TEXT NOT NULL PRIMARY KEY,
+ name TEXT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ last_played_at TIMESTAMP,
+ enable_updates BOOLEAN NOT NULL DEFAULT TRUE
+);
+
+CREATE TABLE roles_new (
+ role_id TEXT NOT NULL PRIMARY KEY,
+ name TEXT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ enable_updates BOOLEAN NOT NULL DEFAULT TRUE
+);
+
+CREATE TABLE instruments_new (
+ instrument_id TEXT NOT NULL PRIMARY KEY,
+ name TEXT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ last_played_at TIMESTAMP,
+ enable_updates BOOLEAN NOT NULL DEFAULT TRUE
+);
+
+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 (DATETIME('now', 'localtime')),
+ edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ last_played_at TIMESTAMP,
+ enable_updates BOOLEAN NOT NULL DEFAULT TRUE
+);
+
+CREATE TABLE ensembles_new (
+ ensemble_id TEXT NOT NULL PRIMARY KEY,
+ name TEXT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ last_played_at TIMESTAMP,
+ enable_updates BOOLEAN NOT NULL DEFAULT TRUE
+);
+
+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 (DATETIME('now', 'localtime')),
+ edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
+ last_played_at TIMESTAMP,
+ enable_updates BOOLEAN NOT NULL DEFAULT TRUE
+);
+
+INSERT INTO persons_new (
+ person_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+ )
+SELECT person_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+FROM persons;
+DROP TABLE persons;
+ALTER TABLE persons_new
+ RENAME TO persons;
+
+INSERT INTO roles_new (
+ role_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at
+ )
+SELECT role_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at
+FROM roles;
+DROP TABLE roles;
+ALTER TABLE roles_new
+ RENAME TO roles;
+
+INSERT INTO instruments_new (
+ instrument_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+ )
+SELECT instrument_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+FROM instruments;
+DROP TABLE instruments;
+ALTER TABLE instruments_new
+ RENAME TO instruments;
+
+INSERT INTO works_new (
+ work_id,
+ parent_work_id,
+ sequence_number,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+ )
+SELECT work_id,
+ parent_work_id,
+ sequence_number,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+FROM works;
+DROP TABLE works;
+ALTER TABLE works_new
+ RENAME TO works;
+
+INSERT INTO ensembles_new (
+ ensemble_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+ )
+SELECT ensemble_id,
+ name,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+FROM ensembles;
+DROP TABLE ensembles;
+ALTER TABLE ensembles_new
+ RENAME TO ensembles;
+
+INSERT INTO recordings_new (
+ recording_id,
+ work_id,
+ year,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+ )
+SELECT recording_id,
+ work_id,
+ year,
+ created_at,
+ edited_at,
+ last_used_at,
+ last_played_at
+FROM recordings;
+DROP TABLE recordings;
+ALTER TABLE recordings_new
+ RENAME TO recordings;
\ No newline at end of file
diff --git a/src/config.rs.in b/src/config.rs.in
index d4e31c8..00b5b25 100644
--- a/src/config.rs.in
+++ b/src/config.rs.in
@@ -6,4 +6,5 @@ pub static VERSION: &str = @VERSION@;
pub static PROFILE: &str = @PROFILE@;
pub static LOCALEDIR: &str = @LOCALEDIR@;
pub static DATADIR: &str = @DATADIR@;
-pub static LIBRARY_URL: &str = @LIBRARY_URL@;
+pub static METADATA_URL: &str = @METADATA_URL@;
+pub static LIBRARY_URL: &str = @LIBRARY_URL@;
\ No newline at end of file
diff --git a/src/db/models.rs b/src/db/models.rs
index a33cc49..729801c 100644
--- a/src/db/models.rs
+++ b/src/db/models.rs
@@ -19,6 +19,7 @@ pub struct Work {
pub parts: Vec,
pub persons: Vec,
pub instruments: Vec,
+ pub enable_updates: bool,
}
#[derive(Clone, Debug)]
@@ -33,6 +34,7 @@ pub struct Ensemble {
pub ensemble_id: String,
pub name: TranslatedString,
pub persons: Vec<(Person, Instrument)>,
+ pub enable_updates: bool,
}
#[derive(Boxed, Clone, Debug)]
@@ -43,6 +45,7 @@ pub struct Recording {
pub year: Option,
pub persons: Vec,
pub ensembles: Vec,
+ pub enable_updates: bool,
}
#[derive(Clone, Debug)]
@@ -152,6 +155,7 @@ impl Work {
parts,
persons,
instruments,
+ enable_updates: data.enable_updates,
})
}
@@ -229,6 +233,7 @@ impl Ensemble {
ensemble_id: data.ensemble_id,
name: data.name,
persons,
+ enable_updates: data.enable_updates,
})
}
}
@@ -279,6 +284,7 @@ impl Recording {
year: data.year,
persons,
ensembles,
+ enable_updates: data.enable_updates,
})
}
diff --git a/src/db/schema.rs b/src/db/schema.rs
index 6d90ece..eace9ab 100644
--- a/src/db/schema.rs
+++ b/src/db/schema.rs
@@ -44,6 +44,7 @@ diesel::table! {
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable,
+ enable_updates -> Bool,
}
}
@@ -55,6 +56,7 @@ diesel::table! {
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable,
+ enable_updates -> Bool,
}
}
@@ -77,11 +79,12 @@ diesel::table! {
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable,
+ enable_updates -> Bool,
}
}
diesel::table! {
- recording_ensembles (recording_id, ensemble_id, sequence_number) {
+ recording_ensembles (recording_id, ensemble_id) {
recording_id -> Text,
ensemble_id -> Text,
role_id -> Nullable,
@@ -90,7 +93,7 @@ diesel::table! {
}
diesel::table! {
- recording_persons (recording_id, person_id, sequence_number) {
+ recording_persons (recording_id, person_id) {
recording_id -> Text,
person_id -> Text,
role_id -> Nullable,
@@ -108,6 +111,7 @@ diesel::table! {
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable,
+ enable_updates -> Bool,
}
}
@@ -118,6 +122,7 @@ diesel::table! {
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
+ enable_updates -> Bool,
}
}
@@ -153,7 +158,7 @@ diesel::table! {
}
diesel::table! {
- work_persons (work_id, person_id, sequence_number) {
+ work_persons (work_id, person_id) {
work_id -> Text,
person_id -> Text,
role_id -> Nullable,
@@ -171,6 +176,7 @@ diesel::table! {
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable,
+ enable_updates -> Bool,
}
}
diff --git a/src/db/tables.rs b/src/db/tables.rs
index 42bb8ec..223b24b 100644
--- a/src/db/tables.rs
+++ b/src/db/tables.rs
@@ -28,6 +28,7 @@ pub struct Person {
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option,
+ pub enable_updates: bool,
}
#[derive(Boxed, Insertable, Queryable, Selectable, Clone, Debug)]
@@ -39,6 +40,7 @@ pub struct Role {
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
+ pub enable_updates: bool,
}
#[derive(Boxed, Insertable, Queryable, Selectable, Clone, Debug)]
@@ -51,6 +53,7 @@ pub struct Instrument {
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option,
+ pub enable_updates: bool,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
@@ -64,6 +67,7 @@ pub struct Work {
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option,
+ pub enable_updates: bool,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
@@ -92,6 +96,7 @@ pub struct Ensemble {
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option,
+ pub enable_updates: bool,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
@@ -113,6 +118,7 @@ pub struct Recording {
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option,
+ pub enable_updates: bool,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
diff --git a/src/editor/ensemble.rs b/src/editor/ensemble.rs
index d7ce46f..df055d9 100644
--- a/src/editor/ensemble.rs
+++ b/src/editor/ensemble.rs
@@ -21,6 +21,8 @@ mod imp {
#[template_child]
pub name_editor: TemplateChild,
#[template_child]
+ pub enable_updates_row: TemplateChild,
+ #[template_child]
pub save_row: TemplateChild,
}
@@ -81,6 +83,9 @@ impl EnsembleEditor {
.set(ensemble.ensemble_id.clone())
.unwrap();
obj.imp().name_editor.set_translation(&ensemble.name);
+ obj.imp()
+ .enable_updates_row
+ .set_active(ensemble.enable_updates);
}
obj
@@ -99,11 +104,14 @@ impl EnsembleEditor {
fn save(&self) {
let library = self.imp().library.get().unwrap();
let name = self.imp().name_editor.translation();
+ let enable_updates = self.imp().enable_updates_row.is_active();
if let Some(ensemble_id) = self.imp().ensemble_id.get() {
- library.update_ensemble(ensemble_id, name).unwrap();
+ library
+ .update_ensemble(ensemble_id, name, enable_updates)
+ .unwrap();
} else {
- let ensemble = library.create_ensemble(name).unwrap();
+ let ensemble = library.create_ensemble(name, enable_updates).unwrap();
self.emit_by_name::<()>("created", &[&ensemble]);
}
diff --git a/src/editor/instrument.rs b/src/editor/instrument.rs
index 3f0c561..0d928c8 100644
--- a/src/editor/instrument.rs
+++ b/src/editor/instrument.rs
@@ -21,6 +21,8 @@ mod imp {
#[template_child]
pub name_editor: TemplateChild,
#[template_child]
+ pub enable_updates_row: TemplateChild,
+ #[template_child]
pub save_row: TemplateChild,
}
@@ -81,6 +83,9 @@ impl InstrumentEditor {
.set(instrument.instrument_id.clone())
.unwrap();
obj.imp().name_editor.set_translation(&instrument.name);
+ obj.imp()
+ .enable_updates_row
+ .set_active(instrument.enable_updates);
}
obj
@@ -102,11 +107,14 @@ impl InstrumentEditor {
fn save(&self) {
let library = self.imp().library.get().unwrap();
let name = self.imp().name_editor.translation();
+ let enable_updates = self.imp().enable_updates_row.is_active();
if let Some(instrument_id) = self.imp().instrument_id.get() {
- library.update_instrument(instrument_id, name).unwrap();
+ library
+ .update_instrument(instrument_id, name, enable_updates)
+ .unwrap();
} else {
- let instrument = library.create_instrument(name).unwrap();
+ let instrument = library.create_instrument(name, enable_updates).unwrap();
self.emit_by_name::<()>("created", &[&instrument]);
}
diff --git a/src/editor/person.rs b/src/editor/person.rs
index fbedb04..bdcd961 100644
--- a/src/editor/person.rs
+++ b/src/editor/person.rs
@@ -21,6 +21,8 @@ mod imp {
#[template_child]
pub name_editor: TemplateChild,
#[template_child]
+ pub enable_updates_row: TemplateChild,
+ #[template_child]
pub save_row: TemplateChild,
}
@@ -78,6 +80,9 @@ impl PersonEditor {
obj.imp().save_row.set_title(&gettext("_Save changes"));
obj.imp().person_id.set(person.person_id.clone()).unwrap();
obj.imp().name_editor.set_translation(&person.name);
+ obj.imp()
+ .enable_updates_row
+ .set_active(person.enable_updates);
}
obj
@@ -96,11 +101,14 @@ impl PersonEditor {
fn save(&self) {
let library = self.imp().library.get().unwrap();
let name = self.imp().name_editor.translation();
+ let enable_updates = self.imp().enable_updates_row.is_active();
if let Some(person_id) = self.imp().person_id.get() {
- library.update_person(person_id, name).unwrap();
+ library
+ .update_person(person_id, name, enable_updates)
+ .unwrap();
} else {
- let person = library.create_person(name).unwrap();
+ let person = library.create_person(name, enable_updates).unwrap();
self.emit_by_name::<()>("created", &[&person]);
}
diff --git a/src/editor/recording.rs b/src/editor/recording.rs
index b4e6e45..4d60e66 100644
--- a/src/editor/recording.rs
+++ b/src/editor/recording.rs
@@ -57,6 +57,8 @@ mod imp {
#[template_child]
pub ensemble_list: TemplateChild,
#[template_child]
+ pub enable_updates_row: TemplateChild,
+ #[template_child]
pub save_row: TemplateChild,
}
@@ -250,6 +252,11 @@ impl RecordingEditor {
.composers_string()
.unwrap_or_else(|| gettext("No composers")),
);
+
+ self.imp()
+ .enable_updates_row
+ .set_active(work.enable_updates);
+
self.imp().save_row.set_sensitive(true);
self.imp().work.replace(Some(work));
}
@@ -367,13 +374,22 @@ impl RecordingEditor {
.map(|e| e.ensemble())
.collect::>();
+ let enable_updates = self.imp().enable_updates_row.is_active();
+
if let Some(recording_id) = self.imp().recording_id.get() {
library
- .update_recording(recording_id, work, Some(year), performers, ensembles)
+ .update_recording(
+ recording_id,
+ work,
+ Some(year),
+ performers,
+ ensembles,
+ enable_updates,
+ )
.unwrap();
} else {
let recording = library
- .create_recording(work, Some(year), performers, ensembles)
+ .create_recording(work, Some(year), performers, ensembles, enable_updates)
.unwrap();
self.emit_by_name::<()>("created", &[&recording]);
}
diff --git a/src/editor/role.rs b/src/editor/role.rs
index 0269ed8..f0f050e 100644
--- a/src/editor/role.rs
+++ b/src/editor/role.rs
@@ -20,6 +20,8 @@ mod imp {
#[template_child]
pub name_editor: TemplateChild,
#[template_child]
+ pub enable_updates_row: TemplateChild,
+ #[template_child]
pub save_row: TemplateChild,
}
@@ -73,6 +75,7 @@ impl RoleEditor {
obj.imp().save_row.set_title(&gettext("_Save changes"));
obj.imp().role_id.set(role.role_id.clone()).unwrap();
obj.imp().name_editor.set_translation(&role.name);
+ obj.imp().enable_updates_row.set_active(role.enable_updates);
}
obj
@@ -91,11 +94,12 @@ impl RoleEditor {
fn save(&self) {
let library = self.imp().library.get().unwrap();
let name = self.imp().name_editor.translation();
+ let enable_updates = self.imp().enable_updates_row.is_active();
if let Some(role_id) = self.imp().role_id.get() {
- library.update_role(role_id, name).unwrap();
+ library.update_role(role_id, name, enable_updates).unwrap();
} else {
- let role = library.create_role(name).unwrap();
+ let role = library.create_role(name, enable_updates).unwrap();
self.emit_by_name::<()>("created", &[&role]);
}
diff --git a/src/editor/work.rs b/src/editor/work.rs
index b0560cf..18191c7 100644
--- a/src/editor/work.rs
+++ b/src/editor/work.rs
@@ -61,6 +61,8 @@ mod imp {
#[template_child]
pub instrument_list: TemplateChild,
#[template_child]
+ pub enable_updates_row: TemplateChild,
+ #[template_child]
pub save_row: TemplateChild,
}
@@ -193,6 +195,8 @@ impl WorkEditor {
for instrument in &work.instruments {
obj.add_instrument_row(instrument.clone());
}
+
+ obj.imp().enable_updates_row.set_active(work.enable_updates);
}
obj
@@ -366,6 +370,8 @@ impl WorkEditor {
.map(|r| r.instrument())
.collect::>();
+ let enable_updates = self.imp().enable_updates_row.is_active();
+
if self.imp().is_part_editor.get() {
let work_id = self
.imp()
@@ -380,17 +386,18 @@ impl WorkEditor {
parts,
persons: composers,
instruments,
+ enable_updates,
};
self.emit_by_name::<()>("created", &[&part]);
} else {
if let Some(work_id) = self.imp().work_id.get() {
library
- .update_work(work_id, name, parts, composers, instruments)
+ .update_work(work_id, name, parts, composers, instruments, enable_updates)
.unwrap();
} else {
let work = library
- .create_work(name, parts, composers, instruments)
+ .create_work(name, parts, composers, instruments, enable_updates)
.unwrap();
self.emit_by_name::<()>("created", &[&work]);
}
diff --git a/src/empty_page.rs b/src/empty_page.rs
index a44136a..1cf2ffb 100644
--- a/src/empty_page.rs
+++ b/src/empty_page.rs
@@ -114,7 +114,13 @@ impl EmptyPage {
config::LIBRARY_URL.to_string()
};
- match obj.imp().library.get().unwrap().import_url(&url) {
+ match obj
+ .imp()
+ .library
+ .get()
+ .unwrap()
+ .import_library_from_url(&url)
+ {
Ok(receiver) => {
let process = Process::new(&gettext("Downloading music library"), receiver);
diff --git a/src/library.rs b/src/library.rs
index 6bba536..864ac52 100644
--- a/src/library.rs
+++ b/src/library.rs
@@ -1,11 +1,7 @@
use std::{
cell::OnceCell,
- ffi::OsString,
- fs::{self, File},
- io::{BufReader, BufWriter, Read, Write},
path::{Path, PathBuf},
sync::{Arc, Mutex},
- thread,
};
use adw::{
@@ -13,27 +9,16 @@ use adw::{
prelude::*,
subclass::prelude::*,
};
-use anyhow::{anyhow, Context, Error, Result};
-use chrono::prelude::*;
-use diesel::{dsl::exists, prelude::*, sql_types, QueryDsl, SqliteConnection};
-use formatx::formatx;
-use futures_util::StreamExt;
-use gettextrs::gettext;
+use anyhow::{anyhow, Context, Result};
+use diesel::{prelude::*, SqliteConnection};
use once_cell::sync::Lazy;
-use tempfile::NamedTempFile;
-use tokio::io::AsyncWriteExt;
-use zip::{write::SimpleFileOptions, ZipWriter};
-use crate::{
- db::{self, models::*, schema::*, tables, TranslatedString},
- process::ProcessMsg,
- program::Program,
-};
+use crate::db::{self, schema::*, tables};
+pub use query::LibraryQuery;
-diesel::define_sql_function! {
- /// Represents the SQL RANDOM() function.
- fn random() -> Integer
-}
+pub mod edit;
+pub mod exchange;
+pub mod query;
mod imp {
use super::*;
@@ -87,1683 +72,6 @@ impl Library {
.is_none())
}
- /// Import from a library archive.
- pub fn import_archive(
- &self,
- path: impl AsRef,
- ) -> Result> {
- let path = path.as_ref().to_owned();
- let library_folder = PathBuf::from(&self.folder());
- let this_connection = self.imp().connection.get().unwrap().clone();
-
- let (sender, receiver) = async_channel::unbounded::();
- thread::spawn(move || {
- if let Err(err) = sender.send_blocking(ProcessMsg::Result(import_from_zip(
- path,
- library_folder,
- this_connection,
- &sender,
- ))) {
- log::error!("Failed to send library action result: {err:?}");
- }
- });
-
- Ok(receiver)
- }
-
- /// Import from a library archive at `url`.
- pub fn import_url(&self, url: &str) -> Result> {
- let url = url.to_owned();
- let library_folder = PathBuf::from(&self.folder());
- let this_connection = self.imp().connection.get().unwrap().clone();
-
- let (sender, receiver) = async_channel::unbounded::();
-
- thread::spawn(move || {
- if let Err(err) = sender.send_blocking(ProcessMsg::Result(import_from_url(
- url,
- library_folder,
- this_connection,
- &sender,
- ))) {
- log::error!("Failed to send library action result: {err:?}");
- }
- });
-
- Ok(receiver)
- }
-
- /// Export the whole music library to an archive at `path`. If `path` already exists, it will
- /// be overwritten. The work will be done in a background thread.
- pub fn export_archive(
- &self,
- path: impl AsRef,
- ) -> Result> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let path = path.as_ref().to_owned();
- let library_folder = PathBuf::from(&self.folder());
- let tracks = tracks::table.load::(connection)?;
-
- let (sender, receiver) = async_channel::unbounded::();
- thread::spawn(move || {
- if let Err(err) = sender.send_blocking(ProcessMsg::Result(write_zip(
- path,
- library_folder,
- tracks,
- &sender,
- ))) {
- log::error!("Failed to send library action result: {err:?}");
- }
- });
-
- Ok(receiver)
- }
-
- pub fn search(&self, query: &LibraryQuery, search: &str) -> Result {
- let search = format!("%{}%", search);
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- Ok(match query {
- LibraryQuery { work: None, .. } => {
- let composers = if query.composer.is_none() {
- let mut statement = persons::table
- .inner_join(
- work_persons::table.inner_join(
- works::table
- .inner_join(
- recordings::table
- .left_join(recording_ensembles::table.inner_join(
- ensembles::table.left_join(ensemble_persons::table),
- ))
- .left_join(recording_persons::table),
- )
- .left_join(work_instruments::table),
- ),
- )
- .filter(persons::name.like(&search))
- .into_boxed();
-
- if let Some(person) = &query.performer {
- statement = statement.filter(
- recording_persons::person_id
- .eq(&person.person_id)
- .or(ensemble_persons::person_id.eq(&person.person_id)),
- );
- }
-
- if let Some(ensemble) = &query.ensemble {
- statement = statement
- .filter(recording_ensembles::ensemble_id.eq(&ensemble.ensemble_id));
- }
-
- if let Some(instrument) = &query.instrument {
- statement = statement.filter(
- work_instruments::instrument_id
- .eq(&instrument.instrument_id)
- .or(recording_persons::instrument_id.eq(&instrument.instrument_id)),
- );
- }
-
- statement
- .order_by(persons::last_played_at.desc())
- .limit(9)
- .select(persons::all_columns)
- .distinct()
- .load::(connection)?
- } else {
- Vec::new()
- };
-
- let performers = if query.performer.is_none() {
- let mut statement = persons::table
- .inner_join(
- recording_persons::table.inner_join(
- recordings::table
- .inner_join(
- works::table
- .left_join(work_persons::table)
- .left_join(work_instruments::table),
- )
- .left_join(recording_ensembles::table),
- ),
- )
- .filter(persons::name.like(&search))
- .into_boxed();
-
- if let Some(person) = &query.composer {
- statement = statement.filter(work_persons::person_id.eq(&person.person_id));
- }
-
- if let Some(ensemble) = &query.ensemble {
- statement = statement
- .filter(recording_ensembles::ensemble_id.eq(&ensemble.ensemble_id));
- }
-
- if let Some(instrument) = &query.instrument {
- statement = statement.filter(
- work_instruments::instrument_id
- .eq(&instrument.instrument_id)
- .or(recording_persons::instrument_id.eq(&instrument.instrument_id)),
- );
- }
-
- statement
- .order_by(persons::last_played_at.desc())
- .limit(9)
- .select(persons::all_columns)
- .distinct()
- .load::(connection)?
- } else {
- Vec::new()
- };
-
- let ensembles = if query.ensemble.is_none() {
- let mut statement = ensembles::table
- .inner_join(
- recording_ensembles::table.inner_join(
- recordings::table
- .inner_join(
- works::table
- .left_join(work_persons::table)
- .left_join(work_instruments::table),
- )
- .left_join(recording_persons::table),
- ),
- )
- .left_join(ensemble_persons::table.inner_join(persons::table))
- .filter(
- ensembles::name
- .like(&search)
- .or(persons::name.like(&search)),
- )
- .into_boxed();
-
- if let Some(person) = &query.composer {
- statement = statement.filter(work_persons::person_id.eq(&person.person_id));
- }
-
- if let Some(person) = &query.performer {
- statement = statement.filter(
- recording_persons::person_id
- .eq(&person.person_id)
- .or(ensemble_persons::person_id.eq(&person.person_id)),
- );
- }
-
- if let Some(instrument) = &query.instrument {
- statement = statement.filter(
- work_instruments::instrument_id
- .eq(&instrument.instrument_id)
- .or(ensemble_persons::instrument_id.eq(&instrument.instrument_id)),
- );
- }
-
- statement
- .order_by(ensembles::last_played_at.desc())
- .limit(9)
- .select(ensembles::all_columns)
- .distinct()
- .load::(connection)?
- .into_iter()
- .map(|e| Ensemble::from_table(e, connection))
- .collect::>>()?
- } else {
- Vec::new()
- };
-
- let instruments = if query.instrument.is_none() {
- let mut statement = instruments::table
- .left_join(
- work_instruments::table
- .inner_join(works::table.left_join(work_persons::table)),
- )
- .left_join(recording_persons::table)
- .left_join(ensemble_persons::table)
- .filter(instruments::name.like(&search))
- .into_boxed();
-
- if let Some(person) = &query.composer {
- statement = statement.filter(work_persons::person_id.eq(&person.person_id));
- }
-
- if let Some(person) = &query.performer {
- statement = statement.filter(
- recording_persons::person_id
- .eq(&person.person_id)
- .or(ensemble_persons::person_id.eq(&person.person_id)),
- );
- }
-
- if let Some(ensemble) = &query.ensemble {
- statement = statement
- .filter(ensemble_persons::ensemble_id.eq(&ensemble.ensemble_id));
- }
-
- statement
- .order_by(instruments::last_played_at.desc())
- .limit(9)
- .select(instruments::all_columns)
- .distinct()
- .load::(connection)?
- } else {
- Vec::new()
- };
-
- let works = if query.work.is_none() {
- let mut statement = works::table
- .left_join(work_persons::table)
- .inner_join(
- recordings::table
- .left_join(recording_persons::table)
- .left_join(recording_ensembles::table.left_join(
- ensembles::table.inner_join(ensemble_persons::table),
- )),
- )
- .left_join(work_instruments::table)
- .filter(works::name.like(&search))
- .into_boxed();
-
- if let Some(person) = &query.composer {
- statement = statement.filter(work_persons::person_id.eq(&person.person_id));
- }
-
- if let Some(person) = &query.performer {
- statement = statement.filter(
- recording_persons::person_id
- .eq(&person.person_id)
- .or(ensemble_persons::person_id.eq(&person.person_id)),
- );
- }
-
- if let Some(instrument) = &query.instrument {
- statement = statement.filter(
- work_instruments::instrument_id
- .eq(&instrument.instrument_id)
- .or(recording_persons::instrument_id.eq(&instrument.instrument_id))
- .or(ensemble_persons::instrument_id.eq(&instrument.instrument_id)),
- );
- }
-
- if let Some(ensemble) = &query.ensemble {
- statement = statement
- .filter(recording_ensembles::ensemble_id.eq(&ensemble.ensemble_id));
- }
-
- statement
- .order_by(works::last_played_at.desc())
- .limit(9)
- .select(works::all_columns)
- .distinct()
- .load::(connection)?
- .into_iter()
- .map(|w| Work::from_table(w, connection))
- .collect::>>()?
- } else {
- Vec::new()
- };
-
- // Only search recordings in special cases. Works will always be searched and
- // directly lead to recordings. The special case of a work in the query is already
- // handled in another branch of the top-level match expression.
- let recordings = if query.performer.is_some() || query.ensemble.is_some() {
- let mut statement = recordings::table
- .inner_join(
- works::table
- .left_join(work_persons::table)
- .left_join(work_instruments::table),
- )
- .left_join(recording_persons::table)
- .left_join(
- recording_ensembles::table
- .inner_join(ensembles::table.left_join(ensemble_persons::table)),
- )
- .filter(works::name.like(&search))
- .into_boxed();
-
- if let Some(person) = &query.composer {
- statement = statement.filter(work_persons::person_id.eq(&person.person_id));
- }
-
- if let Some(person) = &query.performer {
- statement = statement.filter(
- recording_persons::person_id
- .eq(&person.person_id)
- .or(ensemble_persons::person_id.eq(&person.person_id)),
- );
- }
-
- if let Some(instrument) = &query.instrument {
- statement = statement.filter(
- work_instruments::instrument_id
- .eq(&instrument.instrument_id)
- .or(recording_persons::instrument_id.eq(&instrument.instrument_id))
- .or(ensemble_persons::instrument_id.eq(&instrument.instrument_id)),
- );
- }
-
- if let Some(ensemble) = &query.ensemble {
- statement = statement
- .filter(recording_ensembles::ensemble_id.eq(&ensemble.ensemble_id));
- }
-
- statement
- .order_by(recordings::last_played_at.desc())
- .limit(9)
- .select(recordings::all_columns)
- .distinct()
- .load::(connection)?
- .into_iter()
- .map(|r| Recording::from_table(r, connection))
- .collect::>>()?
- } else {
- Vec::new()
- };
-
- let mut statement = albums::table
- .inner_join(
- album_recordings::table.inner_join(
- recordings::table
- .inner_join(
- works::table
- .left_join(work_persons::table)
- .left_join(work_instruments::table),
- )
- .left_join(recording_persons::table)
- .left_join(recording_ensembles::table.inner_join(
- ensembles::table.left_join(ensemble_persons::table),
- )),
- ),
- )
- .filter(albums::name.like(&search))
- .into_boxed();
-
- if let Some(person) = &query.composer {
- statement = statement.filter(work_persons::person_id.eq(&person.person_id));
- }
-
- if let Some(person) = &query.performer {
- statement = statement.filter(
- recording_persons::person_id
- .eq(&person.person_id)
- .or(ensemble_persons::person_id.eq(&person.person_id)),
- );
- }
-
- if let Some(instrument) = &query.instrument {
- statement = statement.filter(
- work_instruments::instrument_id
- .eq(&instrument.instrument_id)
- .or(recording_persons::instrument_id.eq(&instrument.instrument_id))
- .or(ensemble_persons::instrument_id.eq(&instrument.instrument_id)),
- );
- }
-
- if let Some(ensemble) = &query.ensemble {
- statement = statement
- .filter(recording_ensembles::ensemble_id.eq(&ensemble.ensemble_id));
- }
-
- let albums = statement
- .order_by(albums::last_played_at.desc())
- .limit(9)
- .select(albums::all_columns)
- .distinct()
- .load::(connection)?
- .into_iter()
- .map(|r| Album::from_table(r, connection))
- .collect::>>()?;
-
- LibraryResults {
- composers,
- performers,
- ensembles,
- instruments,
- works,
- recordings,
- albums,
- ..Default::default()
- }
- }
- LibraryQuery {
- work: Some(work), ..
- } => {
- let recordings = recordings::table
- .filter(recordings::work_id.eq(&work.work_id))
- .order_by(recordings::last_played_at.desc())
- .load::(connection)?
- .into_iter()
- .map(|r| Recording::from_table(r, connection))
- .collect::>>()?;
-
- LibraryResults {
- recordings,
- ..Default::default()
- }
- }
- })
- }
-
- pub fn generate_recording(&self, program: &Program) -> Result {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let composer_id = program.composer_id();
- let performer_id = program.performer_id();
- let ensemble_id = program.ensemble_id();
- let instrument_id = program.instrument_id();
- let work_id = program.work_id();
- let album_id = program.album_id();
-
- let mut query = recordings::table
- .inner_join(
- works::table
- .left_join(work_persons::table.inner_join(persons::table))
- .left_join(work_instruments::table.inner_join(instruments::table)),
- )
- .left_join(recording_persons::table)
- .left_join(
- recording_ensembles::table
- .left_join(ensembles::table.inner_join(ensemble_persons::table)),
- )
- .left_join(album_recordings::table)
- .into_boxed();
-
- if let Some(composer_id) = &composer_id {
- query = query.filter(work_persons::person_id.eq(composer_id));
- }
-
- if let Some(performer_id) = &performer_id {
- query = query.filter(
- recording_persons::person_id
- .eq(performer_id)
- .or(ensemble_persons::person_id.eq(performer_id)),
- );
- }
-
- if let Some(ensemble_id) = &ensemble_id {
- query = query.filter(recording_ensembles::ensemble_id.eq(ensemble_id));
- }
-
- if let Some(instrument_id) = &instrument_id {
- query = query.filter(
- work_instruments::instrument_id
- .eq(instrument_id)
- .or(recording_persons::instrument_id.eq(instrument_id))
- .or(ensemble_persons::instrument_id.eq(instrument_id)),
- );
- }
-
- if let Some(work_id) = &work_id {
- query = query.filter(recordings::work_id.eq(work_id));
- }
-
- if let Some(album_id) = &album_id {
- query = query.filter(album_recordings::album_id.eq(album_id));
- }
-
- // Orders recordings using a dynamically calculated priority score that includes:
- // - a random base value between 0.0 and 1.0 giving equal probability to each recording
- // - weighted by the average of two scores between 0.0 and 1.0 based on
- // 1. how long ago the last playback is
- // 2. how recently the recording was added to the library
- // Both scores are individually modified based on the following formula:
- // e^(10 * a * (score - 1))
- // This assigns a new score between 0.0 and 1.0 that favors higher scores with "a" being
- // a user defined constant to determine the bias.
- query = query.order(
- diesel::dsl::sql::("( \
- WITH global_bounds AS (
- SELECT MIN(UNIXEPOCH(last_played_at)) AS min_last_played_at,
- NULLIF(
- MAX(UNIXEPOCH(last_played_at)) - MIN(UNIXEPOCH(last_played_at)),
- 0.0
- ) AS last_played_at_range,
- MIN(UNIXEPOCH(created_at)) AS min_created_at,
- NULLIF(
- MAX(UNIXEPOCH(created_at)) - MIN(UNIXEPOCH(created_at)),
- 0.0
- ) AS created_at_range
- FROM recordings
- ),
- normalized AS (
- SELECT IFNULL(
- 1.0 - (
- UNIXEPOCH(recordings.last_played_at) - min_last_played_at
- ) * 1.0 / last_played_at_range,
- 1.0
- ) AS least_recently_played,
- IFNULL(
- (
- UNIXEPOCH(recordings.created_at) - min_created_at
- ) * 1.0 / created_at_range,
- 1.0
- ) AS recently_created
- FROM global_bounds
- )
- SELECT (RANDOM() / 9223372036854775808.0 + 1.0) / 2.0 * MIN(
- (
- EXP(10.0 * ")
- .bind::(program.prefer_least_recently_played())
- .sql(" * (least_recently_played - 1.0)) + EXP(10.0 * ")
- .bind::(program.prefer_recently_added())
- .sql(" * (recently_created - 1.0))
- ) / 2.0,
- FIRST_VALUE(
- MIN(
- IFNULL(
- (
- UNIXEPOCH('now', 'localtime') - UNIXEPOCH(instruments.last_played_at)
- ) * 1.0 / ")
- .bind::(program.avoid_repeated_instruments())
- .sql(",
- 1.0
- ),
- IFNULL(
- (
- UNIXEPOCH('now', 'localtime') - UNIXEPOCH(persons.last_played_at)
- ) * 1.0 / ").bind::(program.avoid_repeated_composers()).sql(",
- 1.0
- ),
- 1.0
- )
- ) OVER (
- PARTITION BY recordings.recording_id
- ORDER BY MAX(
- IFNULL(instruments.last_played_at, 0),
- IFNULL(persons.last_played_at, 0)
- )
- )
- )
- FROM normalized
- ) DESC")
- );
-
- let row = query
- .select(tables::Recording::as_select())
- .distinct()
- .first::(connection)?;
-
- Recording::from_table(row, connection)
- }
-
- pub fn tracks_for_recording(&self, recording_id: &str) -> Result> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let tracks = tracks::table
- .order(tracks::recording_index)
- .filter(tracks::recording_id.eq(&recording_id))
- .select(tables::Track::as_select())
- .load::(connection)?
- .into_iter()
- .map(|t| Track::from_table(t, connection))
- .collect::>>()?;
-
- Ok(tracks)
- }
-
- pub fn track_played(&self, track_id: &str) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let now = Local::now().naive_local();
-
- diesel::update(tracks::table)
- .filter(tracks::track_id.eq(track_id))
- .set(tracks::last_played_at.eq(now))
- .execute(connection)?;
-
- diesel::update(recordings::table)
- .filter(exists(
- tracks::table.filter(
- tracks::track_id
- .eq(track_id)
- .and(tracks::recording_id.eq(recordings::recording_id)),
- ),
- ))
- .set(recordings::last_played_at.eq(now))
- .execute(connection)?;
-
- diesel::update(works::table)
- .filter(exists(
- recordings::table.inner_join(tracks::table).filter(
- tracks::track_id
- .eq(track_id)
- .and(recordings::work_id.eq(works::work_id)),
- ),
- ))
- .set(works::last_played_at.eq(now))
- .execute(connection)?;
-
- diesel::update(instruments::table)
- .filter(exists(
- work_instruments::table
- .inner_join(
- works::table.inner_join(recordings::table.inner_join(tracks::table)),
- )
- .filter(
- tracks::track_id
- .eq(track_id)
- .and(work_instruments::instrument_id.eq(instruments::instrument_id)),
- ),
- ))
- .set(instruments::last_played_at.eq(now))
- .execute(connection)?;
-
- diesel::update(persons::table)
- .filter(
- exists(
- work_persons::table
- .inner_join(
- works::table.inner_join(recordings::table.inner_join(tracks::table)),
- )
- .filter(
- tracks::track_id
- .eq(track_id)
- .and(work_persons::person_id.eq(persons::person_id)),
- ),
- )
- .or(exists(
- recording_persons::table
- .inner_join(recordings::table.inner_join(tracks::table))
- .filter(
- tracks::track_id
- .eq(track_id)
- .and(recording_persons::person_id.eq(persons::person_id)),
- ),
- )),
- )
- .set(persons::last_played_at.eq(now))
- .execute(connection)?;
-
- diesel::update(ensembles::table)
- .filter(exists(
- recording_ensembles::table
- .inner_join(recordings::table.inner_join(tracks::table))
- .filter(
- tracks::track_id
- .eq(track_id)
- .and(recording_ensembles::ensemble_id.eq(ensembles::ensemble_id)),
- ),
- ))
- .set(ensembles::last_played_at.eq(now))
- .execute(connection)?;
-
- diesel::update(mediums::table)
- .filter(exists(
- tracks::table.filter(
- tracks::track_id
- .eq(track_id)
- .and(tracks::medium_id.eq(mediums::medium_id.nullable())),
- ),
- ))
- .set(mediums::last_played_at.eq(now))
- .execute(connection)?;
-
- diesel::update(albums::table)
- .filter(
- exists(
- album_recordings::table
- .inner_join(recordings::table.inner_join(tracks::table))
- .filter(
- tracks::track_id
- .eq(track_id)
- .and(album_recordings::album_id.eq(albums::album_id)),
- ),
- )
- .or(exists(
- album_mediums::table
- .inner_join(mediums::table.inner_join(tracks::table))
- .filter(
- tracks::track_id
- .eq(track_id)
- .and(album_mediums::album_id.eq(albums::album_id)),
- ),
- )),
- )
- .set(albums::last_played_at.eq(now))
- .execute(connection)?;
-
- Ok(())
- }
-
- pub fn search_persons(&self, search: &str) -> Result> {
- let search = format!("%{}%", search);
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let persons = persons::table
- .order(persons::last_used_at.desc())
- .filter(persons::name.like(&search))
- .limit(20)
- .load(connection)?;
-
- Ok(persons)
- }
-
- pub fn search_roles(&self, search: &str) -> Result> {
- let search = format!("%{}%", search);
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let roles = roles::table
- .order(roles::last_used_at.desc())
- .filter(roles::name.like(&search))
- .limit(20)
- .load(connection)?;
-
- Ok(roles)
- }
-
- pub fn search_instruments(&self, search: &str) -> Result> {
- let search = format!("%{}%", search);
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let instruments = instruments::table
- .order(instruments::last_used_at.desc())
- .filter(instruments::name.like(&search))
- .limit(20)
- .load(connection)?;
-
- Ok(instruments)
- }
-
- pub fn search_works(&self, composer: &Person, search: &str) -> Result> {
- let search = format!("%{}%", search);
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let works: Vec = works::table
- .left_join(work_persons::table)
- .filter(
- works::name
- .like(&search)
- .and(work_persons::person_id.eq(&composer.person_id)),
- )
- .limit(9)
- .select(works::all_columns)
- .distinct()
- .load::(connection)?
- .into_iter()
- .map(|w| Work::from_table(w, connection))
- .collect::>>()?;
-
- Ok(works)
- }
-
- pub fn search_recordings(&self, work: &Work, search: &str) -> Result> {
- let search = format!("%{}%", search);
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let recordings = recordings::table
- .left_join(recording_persons::table.inner_join(persons::table))
- .left_join(recording_ensembles::table.inner_join(ensembles::table))
- .filter(
- recordings::work_id.eq(&work.work_id).and(
- persons::name
- .like(&search)
- .or(ensembles::name.like(&search)),
- ),
- )
- .limit(9)
- .select(recordings::all_columns)
- .distinct()
- .load::(connection)?
- .into_iter()
- .map(|r| Recording::from_table(r, connection))
- .collect::>>()?;
-
- Ok(recordings)
- }
-
- pub fn search_ensembles(&self, search: &str) -> Result> {
- let search = format!("%{}%", search);
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let ensembles = ensembles::table
- .order(ensembles::last_used_at.desc())
- .left_join(ensemble_persons::table.inner_join(persons::table))
- .filter(
- ensembles::name
- .like(&search)
- .or(persons::name.like(&search)),
- )
- .limit(20)
- .select(ensembles::all_columns)
- .load::(connection)?
- .into_iter()
- .map(|e| Ensemble::from_table(e, connection))
- .collect::>>()?;
-
- Ok(ensembles)
- }
-
- pub fn create_person(&self, name: TranslatedString) -> Result {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let now = Local::now().naive_local();
-
- let person = Person {
- person_id: db::generate_id(),
- name,
- created_at: now,
- edited_at: now,
- last_used_at: now,
- last_played_at: None,
- };
-
- diesel::insert_into(persons::table)
- .values(&person)
- .execute(connection)?;
-
- self.changed();
-
- Ok(person)
- }
-
- pub fn update_person(&self, id: &str, name: TranslatedString) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let now = Local::now().naive_local();
-
- diesel::update(persons::table)
- .filter(persons::person_id.eq(id))
- .set((
- persons::name.eq(name),
- persons::edited_at.eq(now),
- persons::last_used_at.eq(now),
- ))
- .execute(connection)?;
-
- self.changed();
-
- Ok(())
- }
-
- pub fn delete_person(&self, person_id: &str) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- diesel::delete(persons::table)
- .filter(persons::person_id.eq(person_id))
- .execute(connection)?;
-
- self.changed();
-
- Ok(())
- }
-
- pub fn create_instrument(&self, name: TranslatedString) -> Result {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let now = Local::now().naive_local();
-
- let instrument = Instrument {
- instrument_id: db::generate_id(),
- name,
- created_at: now,
- edited_at: now,
- last_used_at: now,
- last_played_at: None,
- };
-
- diesel::insert_into(instruments::table)
- .values(&instrument)
- .execute(connection)?;
-
- self.changed();
-
- Ok(instrument)
- }
-
- pub fn update_instrument(&self, id: &str, name: TranslatedString) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let now = Local::now().naive_local();
-
- diesel::update(instruments::table)
- .filter(instruments::instrument_id.eq(id))
- .set((
- instruments::name.eq(name),
- instruments::edited_at.eq(now),
- instruments::last_used_at.eq(now),
- ))
- .execute(connection)?;
-
- self.changed();
-
- Ok(())
- }
-
- pub fn delete_instrument(&self, instrument_id: &str) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- diesel::delete(instruments::table)
- .filter(instruments::instrument_id.eq(instrument_id))
- .execute(connection)?;
-
- self.changed();
-
- Ok(())
- }
-
- pub fn create_role(&self, name: TranslatedString) -> Result {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let now = Local::now().naive_local();
-
- let role = Role {
- role_id: db::generate_id(),
- name,
- created_at: now,
- edited_at: now,
- last_used_at: now,
- };
-
- diesel::insert_into(roles::table)
- .values(&role)
- .execute(connection)?;
-
- self.changed();
-
- Ok(role)
- }
-
- pub fn update_role(&self, id: &str, name: TranslatedString) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let now = Local::now().naive_local();
-
- diesel::update(roles::table)
- .filter(roles::role_id.eq(id))
- .set((
- roles::name.eq(name),
- roles::edited_at.eq(now),
- roles::last_used_at.eq(now),
- ))
- .execute(connection)?;
-
- self.changed();
-
- Ok(())
- }
-
- pub fn delete_role(&self, role_id: &str) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- diesel::delete(roles::table)
- .filter(roles::role_id.eq(role_id))
- .execute(connection)?;
-
- self.changed();
-
- Ok(())
- }
-
- pub fn create_work(
- &self,
- name: TranslatedString,
- parts: Vec,
- persons: Vec,
- instruments: Vec,
- ) -> Result {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let work =
- self.create_work_priv(connection, name, parts, persons, instruments, None, None)?;
-
- self.changed();
-
- Ok(work)
- }
-
- fn create_work_priv(
- &self,
- connection: &mut SqliteConnection,
- name: TranslatedString,
- parts: Vec,
- persons: Vec,
- instruments: Vec,
- parent_work_id: Option<&str>,
- sequence_number: Option,
- ) -> Result {
- let work_id = db::generate_id();
- let now = Local::now().naive_local();
-
- let work_data = tables::Work {
- work_id: work_id.clone(),
- parent_work_id: parent_work_id.map(|w| w.to_string()),
- sequence_number: sequence_number,
- name,
- created_at: now,
- edited_at: now,
- last_used_at: now,
- last_played_at: None,
- };
-
- diesel::insert_into(works::table)
- .values(&work_data)
- .execute(connection)?;
-
- for (index, part) in parts.into_iter().enumerate() {
- self.create_work_priv(
- connection,
- part.name,
- part.parts,
- part.persons,
- part.instruments,
- Some(&work_id),
- Some(index as i32),
- )?;
- }
-
- for (index, composer) in persons.into_iter().enumerate() {
- let composer_data = tables::WorkPerson {
- work_id: work_id.clone(),
- person_id: composer.person.person_id,
- role_id: composer.role.map(|r| r.role_id),
- sequence_number: index as i32,
- };
-
- diesel::insert_into(work_persons::table)
- .values(composer_data)
- .execute(connection)?;
- }
-
- for (index, instrument) in instruments.into_iter().enumerate() {
- let instrument_data = tables::WorkInstrument {
- work_id: work_id.clone(),
- instrument_id: instrument.instrument_id,
- sequence_number: index as i32,
- };
-
- diesel::insert_into(work_instruments::table)
- .values(instrument_data)
- .execute(connection)?;
- }
-
- let work = Work::from_table(work_data, connection)?;
-
- Ok(work)
- }
-
- pub fn update_work(
- &self,
- work_id: &str,
- name: TranslatedString,
- parts: Vec,
- persons: Vec,
- instruments: Vec,
- ) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- self.update_work_priv(
- connection,
- work_id,
- name,
- parts,
- persons,
- instruments,
- None,
- None,
- )?;
-
- self.changed();
-
- Ok(())
- }
-
- fn update_work_priv(
- &self,
- connection: &mut SqliteConnection,
- work_id: &str,
- name: TranslatedString,
- parts: Vec,
- persons: Vec,
- instruments: Vec,
- parent_work_id: Option<&str>,
- sequence_number: Option,
- ) -> Result<()> {
- let now = Local::now().naive_local();
-
- diesel::update(works::table)
- .filter(works::work_id.eq(work_id))
- .set((
- works::parent_work_id.eq(parent_work_id),
- works::sequence_number.eq(sequence_number),
- works::name.eq(name),
- works::edited_at.eq(now),
- works::last_used_at.eq(now),
- ))
- .execute(connection)?;
-
- diesel::delete(works::table)
- .filter(
- works::parent_work_id
- .eq(work_id)
- .and(works::work_id.ne_all(parts.iter().map(|p| p.work_id.clone()))),
- )
- .execute(connection)?;
-
- for (index, part) in parts.into_iter().enumerate() {
- if works::table
- .filter(works::work_id.eq(&part.work_id))
- .first::(connection)
- .optional()?
- .is_some()
- {
- self.update_work_priv(
- connection,
- &part.work_id,
- part.name,
- part.parts,
- part.persons,
- part.instruments,
- Some(work_id),
- Some(index as i32),
- )?;
- } else {
- // Note: The previously used ID is discarded. This should be OK, because
- // at this point, the part ID should not have been used anywhere.
- self.create_work_priv(
- connection,
- part.name,
- part.parts,
- part.persons,
- part.instruments,
- Some(work_id),
- Some(index as i32),
- )?;
- }
- }
-
- diesel::delete(work_persons::table)
- .filter(work_persons::work_id.eq(work_id))
- .execute(connection)?;
-
- for (index, composer) in persons.into_iter().enumerate() {
- let composer_data = tables::WorkPerson {
- work_id: work_id.to_string(),
- person_id: composer.person.person_id,
- role_id: composer.role.map(|r| r.role_id),
- sequence_number: index as i32,
- };
-
- diesel::insert_into(work_persons::table)
- .values(composer_data)
- .execute(connection)?;
- }
-
- diesel::delete(work_instruments::table)
- .filter(work_instruments::work_id.eq(work_id))
- .execute(connection)?;
-
- for (index, instrument) in instruments.into_iter().enumerate() {
- let instrument_data = tables::WorkInstrument {
- work_id: work_id.to_string(),
- instrument_id: instrument.instrument_id,
- sequence_number: index as i32,
- };
-
- diesel::insert_into(work_instruments::table)
- .values(instrument_data)
- .execute(connection)?;
- }
-
- Ok(())
- }
-
- pub fn delete_work(&self, work_id: &str) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- diesel::delete(works::table)
- .filter(works::work_id.eq(work_id))
- .execute(connection)?;
-
- self.changed();
-
- Ok(())
- }
-
- pub fn create_ensemble(&self, name: TranslatedString) -> Result {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let now = Local::now().naive_local();
-
- let ensemble_data = tables::Ensemble {
- ensemble_id: db::generate_id(),
- name,
- created_at: now,
- edited_at: now,
- last_used_at: now,
- last_played_at: None,
- };
-
- // TODO: Add persons.
-
- diesel::insert_into(ensembles::table)
- .values(&ensemble_data)
- .execute(connection)?;
-
- let ensemble = Ensemble::from_table(ensemble_data, connection)?;
-
- self.changed();
-
- Ok(ensemble)
- }
-
- pub fn update_ensemble(&self, id: &str, name: TranslatedString) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let now = Local::now().naive_local();
-
- diesel::update(ensembles::table)
- .filter(ensembles::ensemble_id.eq(id))
- .set((
- ensembles::name.eq(name),
- ensembles::edited_at.eq(now),
- ensembles::last_used_at.eq(now),
- ))
- .execute(connection)?;
-
- // TODO: Support updating persons.
-
- self.changed();
-
- Ok(())
- }
-
- pub fn delete_ensemble(&self, ensemble_id: &str) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- diesel::delete(ensembles::table)
- .filter(ensembles::ensemble_id.eq(ensemble_id))
- .execute(connection)?;
-
- self.changed();
-
- Ok(())
- }
-
- pub fn create_recording(
- &self,
- work: Work,
- year: Option,
- performers: Vec,
- ensembles: Vec,
- ) -> Result {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let recording_id = db::generate_id();
- let now = Local::now().naive_local();
-
- let recording_data = tables::Recording {
- recording_id: recording_id.clone(),
- work_id: work.work_id.clone(),
- year,
- created_at: now,
- edited_at: now,
- last_used_at: now,
- last_played_at: None,
- };
-
- diesel::insert_into(recordings::table)
- .values(&recording_data)
- .execute(connection)?;
-
- for (index, performer) in performers.into_iter().enumerate() {
- let recording_person_data = tables::RecordingPerson {
- recording_id: recording_id.clone(),
- person_id: performer.person.person_id,
- role_id: performer.role.map(|r| r.role_id),
- instrument_id: performer.instrument.map(|i| i.instrument_id),
- sequence_number: index as i32,
- };
-
- diesel::insert_into(recording_persons::table)
- .values(&recording_person_data)
- .execute(connection)?;
- }
-
- for (index, ensemble) in ensembles.into_iter().enumerate() {
- let recording_ensemble_data = tables::RecordingEnsemble {
- recording_id: recording_id.clone(),
- ensemble_id: ensemble.ensemble.ensemble_id,
- role_id: ensemble.role.map(|r| r.role_id),
- sequence_number: index as i32,
- };
-
- diesel::insert_into(recording_ensembles::table)
- .values(&recording_ensemble_data)
- .execute(connection)?;
- }
-
- let recording = Recording::from_table(recording_data, connection)?;
-
- self.changed();
-
- Ok(recording)
- }
-
- pub fn update_recording(
- &self,
- recording_id: &str,
- work: Work,
- year: Option,
- performers: Vec,
- ensembles: Vec,
- ) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let now = Local::now().naive_local();
-
- diesel::update(recordings::table)
- .filter(recordings::recording_id.eq(recording_id))
- .set((
- recordings::work_id.eq(work.work_id),
- recordings::year.eq(year),
- recordings::edited_at.eq(now),
- recordings::last_used_at.eq(now),
- ))
- .execute(connection)?;
-
- diesel::delete(recording_persons::table)
- .filter(recording_persons::recording_id.eq(recording_id))
- .execute(connection)?;
-
- for (index, performer) in performers.into_iter().enumerate() {
- let recording_person_data = tables::RecordingPerson {
- recording_id: recording_id.to_string(),
- person_id: performer.person.person_id,
- role_id: performer.role.map(|r| r.role_id),
- instrument_id: performer.instrument.map(|i| i.instrument_id),
- sequence_number: index as i32,
- };
-
- diesel::insert_into(recording_persons::table)
- .values(&recording_person_data)
- .execute(connection)?;
- }
-
- diesel::delete(recording_ensembles::table)
- .filter(recording_ensembles::recording_id.eq(recording_id))
- .execute(connection)?;
-
- for (index, ensemble) in ensembles.into_iter().enumerate() {
- let recording_ensemble_data = tables::RecordingEnsemble {
- recording_id: recording_id.to_string(),
- ensemble_id: ensemble.ensemble.ensemble_id,
- role_id: ensemble.role.map(|r| r.role_id),
- sequence_number: index as i32,
- };
-
- diesel::insert_into(recording_ensembles::table)
- .values(&recording_ensemble_data)
- .execute(connection)?;
- }
-
- self.changed();
-
- Ok(())
- }
-
- pub fn delete_recording(&self, recording_id: &str) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- diesel::delete(recordings::table)
- .filter(recordings::recording_id.eq(recording_id))
- .execute(connection)?;
-
- self.changed();
-
- Ok(())
- }
-
- pub fn delete_recording_and_tracks(&self, recording_id: &str) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let tracks = tracks::table
- .filter(tracks::recording_id.eq(recording_id))
- .load::(connection)?;
-
- // Delete from library first to avoid orphan tracks in case of file
- // system related errors.
-
- connection.transaction::<(), Error, _>(|connection| {
- for track in &tracks {
- diesel::delete(track_works::table)
- .filter(track_works::track_id.eq(&track.track_id))
- .execute(connection)?;
-
- diesel::delete(tracks::table)
- .filter(tracks::track_id.eq(&track.track_id))
- .execute(connection)?;
- }
-
- diesel::delete(recordings::table)
- .filter(recordings::recording_id.eq(recording_id))
- .execute(connection)?;
-
- Ok(())
- })?;
-
- let library_path = PathBuf::from(self.folder());
- for track in tracks {
- fs::remove_file(library_path.join(&track.path))?;
- }
-
- self.changed();
-
- Ok(())
- }
-
- pub fn create_album(
- &self,
- name: TranslatedString,
- recordings: Vec,
- ) -> Result {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let album_id = db::generate_id();
- let now = Local::now().naive_local();
-
- let album_data = tables::Album {
- album_id: album_id.clone(),
- name,
- created_at: now,
- edited_at: now,
- last_used_at: now,
- last_played_at: None,
- };
-
- diesel::insert_into(albums::table)
- .values(&album_data)
- .execute(connection)?;
-
- for (index, recording) in recordings.into_iter().enumerate() {
- let album_recording_data = tables::AlbumRecording {
- album_id: album_id.clone(),
- recording_id: recording.recording_id,
- sequence_number: index as i32,
- };
-
- diesel::insert_into(album_recordings::table)
- .values(&album_recording_data)
- .execute(connection)?;
- }
-
- let album = Album::from_table(album_data, connection)?;
-
- self.changed();
-
- Ok(album)
- }
-
- pub fn update_album(
- &self,
- album_id: &str,
- name: TranslatedString,
- recordings: Vec,
- ) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let now = Local::now().naive_local();
-
- diesel::update(albums::table)
- .filter(albums::album_id.eq(album_id))
- .set((
- albums::name.eq(name),
- albums::edited_at.eq(now),
- albums::last_used_at.eq(now),
- ))
- .execute(connection)?;
-
- diesel::delete(album_recordings::table)
- .filter(album_recordings::album_id.eq(album_id))
- .execute(connection)?;
-
- for (index, recording) in recordings.into_iter().enumerate() {
- let album_recording_data = tables::AlbumRecording {
- album_id: album_id.to_owned(),
- recording_id: recording.recording_id,
- sequence_number: index as i32,
- };
-
- diesel::insert_into(album_recordings::table)
- .values(&album_recording_data)
- .execute(connection)?;
- }
-
- self.changed();
-
- Ok(())
- }
-
- pub fn delete_album(&self, album_id: &str) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- diesel::delete(albums::table)
- .filter(albums::album_id.eq(album_id))
- .execute(connection)?;
-
- self.changed();
-
- Ok(())
- }
-
- /// Import a track into the music library.
- // TODO: Support mediums.
- pub fn import_track(
- &self,
- path: impl AsRef,
- recording_id: &str,
- recording_index: i32,
- works: Vec,
- ) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let track_id = db::generate_id();
- let now = Local::now().naive_local();
-
- // TODO: Human interpretable filenames?
- let mut filename = OsString::from(recording_id);
- filename.push("_");
- filename.push(OsString::from(format!("{recording_index:02}")));
- if let Some(extension) = path.as_ref().extension() {
- filename.push(".");
- filename.push(extension);
- };
-
- let mut to_path = PathBuf::from(self.folder());
- to_path.push(&filename);
- let library_path = PathBuf::from(filename);
-
- fs::copy(path, to_path)?;
-
- let track_data = tables::Track {
- track_id: track_id.clone(),
- recording_id: recording_id.to_owned(),
- recording_index,
- medium_id: None,
- medium_index: None,
- path: library_path.into(),
- created_at: now,
- edited_at: now,
- last_used_at: now,
- last_played_at: None,
- };
-
- diesel::insert_into(tracks::table)
- .values(&track_data)
- .execute(connection)?;
-
- for (index, work) in works.into_iter().enumerate() {
- let track_work_data = tables::TrackWork {
- track_id: track_id.clone(),
- work_id: work.work_id,
- sequence_number: index as i32,
- };
-
- diesel::insert_into(track_works::table)
- .values(&track_work_data)
- .execute(connection)?;
- }
-
- Ok(())
- }
-
- // TODO: Support mediums, think about albums.
- pub fn delete_track(&self, track: &Track) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- diesel::delete(track_works::table)
- .filter(track_works::track_id.eq(&track.track_id))
- .execute(connection)?;
-
- diesel::delete(tracks::table)
- .filter(tracks::track_id.eq(&track.track_id))
- .execute(connection)?;
-
- let mut path = PathBuf::from(self.folder());
- path.push(&track.path);
- fs::remove_file(path)?;
-
- Ok(())
- }
-
- // TODO: Support mediums, think about albums.
- pub fn update_track(
- &self,
- track_id: &str,
- recording_index: i32,
- works: Vec,
- ) -> Result<()> {
- let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
-
- let now = Local::now().naive_local();
-
- diesel::update(tracks::table)
- .filter(tracks::track_id.eq(track_id.to_owned()))
- .set((
- tracks::recording_index.eq(recording_index),
- tracks::edited_at.eq(now),
- tracks::last_used_at.eq(now),
- ))
- .execute(connection)?;
-
- diesel::delete(track_works::table)
- .filter(track_works::track_id.eq(track_id))
- .execute(connection)?;
-
- for (index, work) in works.into_iter().enumerate() {
- let track_work_data = tables::TrackWork {
- track_id: track_id.to_owned(),
- work_id: work.work_id,
- sequence_number: index as i32,
- };
-
- diesel::insert_into(track_works::table)
- .values(&track_work_data)
- .execute(connection)?;
- }
-
- Ok(())
- }
-
pub fn connect_changed(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("changed", true, move |values| {
let obj = values[0].get::().unwrap();
@@ -1800,417 +108,3 @@ impl Library {
Ok(())
}
}
-
-#[derive(Clone, Default, Debug)]
-pub struct LibraryQuery {
- pub composer: Option,
- pub performer: Option,
- pub ensemble: Option,
- pub instrument: Option,
- pub work: Option,
-}
-
-impl LibraryQuery {
- pub fn is_empty(&self) -> bool {
- self.composer.is_none()
- && self.performer.is_none()
- && self.ensemble.is_none()
- && self.instrument.is_none()
- && self.work.is_none()
- }
-}
-
-#[derive(Default, Debug)]
-pub struct LibraryResults {
- pub composers: Vec,
- pub performers: Vec,
- pub ensembles: Vec,
- pub instruments: Vec,
- pub works: Vec,
- pub recordings: Vec,
- pub albums: Vec,
-}
-
-impl LibraryResults {
- pub fn is_empty(&self) -> bool {
- self.composers.is_empty()
- && self.performers.is_empty()
- && self.ensembles.is_empty()
- && self.instruments.is_empty()
- && self.works.is_empty()
- && self.recordings.is_empty()
- && self.albums.is_empty()
- }
-}
-
-fn write_zip(
- zip_path: impl AsRef,
- library_folder: impl AsRef,
- tracks: Vec,
- sender: &async_channel::Sender,
-) -> Result<()> {
- let mut zip = zip::ZipWriter::new(BufWriter::new(fs::File::create(zip_path)?));
-
- // Start with the database:
- add_file_to_zip(&mut zip, &library_folder, "musicus.db")?;
-
- let n_tracks = tracks.len();
-
- // Include all tracks that are part of the library.
- for (index, track) in tracks.into_iter().enumerate() {
- add_file_to_zip(&mut zip, &library_folder, &path_to_zip(&track.path)?)?;
-
- // Ignore if the reveiver has been dropped.
- let _ = sender.send_blocking(ProcessMsg::Progress((index + 1) as f64 / n_tracks as f64));
- }
-
- zip.finish()?;
-
- Ok(())
-}
-
-fn add_file_to_zip(
- zip: &mut ZipWriter>,
- library_folder: impl AsRef,
- library_path: &str,
-) -> Result<()> {
- let file_path = library_folder.as_ref().join(PathBuf::from(library_path));
-
- let mut file = File::open(file_path)?;
- let mut buffer = Vec::new();
- file.read_to_end(&mut buffer)?;
-
- zip.start_file(library_path, SimpleFileOptions::default())?;
- zip.write_all(&buffer)?;
-
- Ok(())
-}
-
-// TODO: Add options whether to keep stats.
-fn import_from_zip(
- zip_path: impl AsRef,
- library_folder: impl AsRef,
- this_connection: Arc>,
- sender: &async_channel::Sender,
-) -> Result<()> {
- let now = Local::now().naive_local();
-
- let mut archive = zip::ZipArchive::new(BufReader::new(fs::File::open(zip_path)?))?;
-
- let archive_db_file = archive.by_name("musicus.db")?;
- let tmp_db_file = NamedTempFile::new()?;
- std::io::copy(
- &mut BufReader::new(archive_db_file),
- &mut BufWriter::new(tmp_db_file.as_file()),
- )?;
-
- let mut other_connection = db::connect(tmp_db_file.path().to_str().unwrap())?;
-
- // Load all metadata from the archive.
- let persons = persons::table.load::(&mut other_connection)?;
- let roles = roles::table.load::(&mut other_connection)?;
- let instruments = instruments::table.load::(&mut other_connection)?;
- let works = works::table.load::(&mut other_connection)?;
- let work_persons = work_persons::table.load::(&mut other_connection)?;
- let work_instruments =
- work_instruments::table.load::(&mut other_connection)?;
- let ensembles = ensembles::table.load::(&mut other_connection)?;
- let ensemble_persons =
- ensemble_persons::table.load::(&mut other_connection)?;
- let recordings = recordings::table.load::(&mut other_connection)?;
- let recording_persons =
- recording_persons::table.load::(&mut other_connection)?;
- let recording_ensembles =
- recording_ensembles::table.load::(&mut other_connection)?;
- let tracks = tracks::table.load::(&mut other_connection)?;
- let track_works = track_works::table.load::(&mut other_connection)?;
- let mediums = mediums::table.load::(&mut other_connection)?;
- let albums = albums::table.load::(&mut other_connection)?;
- let album_recordings =
- album_recordings::table.load::(&mut other_connection)?;
- let album_mediums = album_mediums::table.load::(&mut other_connection)?;
-
- // Import metadata that is not already present.
-
- for mut person in persons {
- person.created_at = now;
- person.edited_at = now;
- person.last_used_at = now;
- person.last_played_at = None;
-
- diesel::insert_into(persons::table)
- .values(person)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for mut role in roles {
- role.created_at = now;
- role.edited_at = now;
- role.last_used_at = now;
-
- diesel::insert_into(roles::table)
- .values(role)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for mut instrument in instruments {
- instrument.created_at = now;
- instrument.edited_at = now;
- instrument.last_used_at = now;
- instrument.last_played_at = None;
-
- diesel::insert_into(instruments::table)
- .values(instrument)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for mut work in works {
- work.created_at = now;
- work.edited_at = now;
- work.last_used_at = now;
- work.last_played_at = None;
-
- diesel::insert_into(works::table)
- .values(work)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for work_person in work_persons {
- diesel::insert_into(work_persons::table)
- .values(work_person)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for work_instrument in work_instruments {
- diesel::insert_into(work_instruments::table)
- .values(work_instrument)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for mut ensemble in ensembles {
- ensemble.created_at = now;
- ensemble.edited_at = now;
- ensemble.last_used_at = now;
- ensemble.last_played_at = None;
-
- diesel::insert_into(ensembles::table)
- .values(ensemble)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for ensemble_person in ensemble_persons {
- diesel::insert_into(ensemble_persons::table)
- .values(ensemble_person)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for mut recording in recordings {
- recording.created_at = now;
- recording.edited_at = now;
- recording.last_used_at = now;
- recording.last_played_at = None;
-
- diesel::insert_into(recordings::table)
- .values(recording)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for recording_person in recording_persons {
- diesel::insert_into(recording_persons::table)
- .values(recording_person)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for recording_ensemble in recording_ensembles {
- diesel::insert_into(recording_ensembles::table)
- .values(recording_ensemble)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for mut track in tracks.clone() {
- track.created_at = now;
- track.edited_at = now;
- track.last_used_at = now;
- track.last_played_at = None;
-
- diesel::insert_into(tracks::table)
- .values(track)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for track_work in track_works {
- diesel::insert_into(track_works::table)
- .values(track_work)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for mut medium in mediums {
- medium.created_at = now;
- medium.edited_at = now;
- medium.last_used_at = now;
- medium.last_played_at = None;
-
- diesel::insert_into(mediums::table)
- .values(medium)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for mut album in albums {
- album.created_at = now;
- album.edited_at = now;
- album.last_used_at = now;
- album.last_played_at = None;
-
- diesel::insert_into(albums::table)
- .values(album)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for album_recording in album_recordings {
- diesel::insert_into(album_recordings::table)
- .values(album_recording)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- for album_medium in album_mediums {
- diesel::insert_into(album_mediums::table)
- .values(album_medium)
- .on_conflict_do_nothing()
- .execute(&mut *this_connection.lock().unwrap())?;
- }
-
- // Import audio files.
-
- let n_tracks = tracks.len();
-
- for (index, track) in tracks.into_iter().enumerate() {
- let library_track_file_path = library_folder.as_ref().join(&track.path);
-
- // Skip tracks that are already present.
- if !fs::exists(&library_track_file_path)? {
- if let Some(parent) = library_track_file_path.parent() {
- fs::create_dir_all(parent)?;
- }
-
- let archive_track_file = archive.by_name(&path_to_zip(&track.path)?)?;
- let library_track_file = File::create(library_track_file_path)?;
-
- std::io::copy(
- &mut BufReader::new(archive_track_file),
- &mut BufWriter::new(library_track_file),
- )?;
- }
-
- // Ignore if the reveiver has been dropped.
- let _ = sender.send_blocking(ProcessMsg::Progress((index + 1) as f64 / n_tracks as f64));
- }
-
- Ok(())
-}
-
-fn import_from_url(
- url: String,
- library_folder: impl AsRef,
- this_connection: Arc>,
- sender: &async_channel::Sender,
-) -> Result<()> {
- let runtime = tokio::runtime::Builder::new_current_thread()
- .enable_all()
- .build()?;
-
- let _ = sender.send_blocking(ProcessMsg::Message(
- formatx!(gettext("Downloading {}"), &url).unwrap(),
- ));
-
- let archive_file = runtime.block_on(download_tmp_file(&url, &sender));
-
- match archive_file {
- Ok(archive_file) => {
- let _ = sender.send_blocking(ProcessMsg::Message(
- formatx!(gettext("Importing downloaded library"), &url).unwrap(),
- ));
-
- let _ = sender.send_blocking(ProcessMsg::Result(import_from_zip(
- archive_file.path(),
- library_folder,
- this_connection,
- &sender,
- )));
- }
- Err(err) => {
- let _ = sender.send_blocking(ProcessMsg::Result(Err(err)));
- }
- }
-
- Ok(())
-}
-
-async fn download_tmp_file(
- url: &str,
- sender: &async_channel::Sender,
-) -> Result {
- let client = reqwest::Client::builder()
- .connect_timeout(std::time::Duration::from_secs(10))
- .build()?;
-
- let response = client.get(url).send().await?;
- let total_size = response.content_length();
- let mut body_stream = response.bytes_stream();
-
- let file = NamedTempFile::new()?;
- let mut writer =
- tokio::io::BufWriter::new(tokio::fs::File::from_std(file.as_file().try_clone()?));
-
- let mut downloaded = 0;
- while let Some(chunk) = body_stream.next().await {
- let chunk: Vec = chunk?.into();
- let chunk_size = chunk.len();
-
- writer.write_all(&chunk).await?;
-
- if let Some(total_size) = total_size {
- downloaded += chunk_size as u64;
- let _ = sender
- .send(ProcessMsg::Progress(downloaded as f64 / total_size as f64))
- .await;
- }
- }
-
- Ok(file)
-}
-
-/// Convert a path to a ZIP path. ZIP files use "/" as the path separator
-/// regardless of the current platform.
-fn path_to_zip(path: impl AsRef) -> Result {
- Ok(path
- .as_ref()
- .iter()
- .map(|p| {
- p.to_str()
- .ok_or_else(|| {
- anyhow!(
- "Path \"{}\"contains invalid UTF-8",
- path.as_ref().to_string_lossy()
- )
- })
- .map(|s| s.to_owned())
- })
- .collect::>>()?
- .join("/"))
-}
diff --git a/src/library/edit.rs b/src/library/edit.rs
new file mode 100644
index 0000000..1e71da9
--- /dev/null
+++ b/src/library/edit.rs
@@ -0,0 +1,904 @@
+use std::{
+ ffi::OsString,
+ fs::{self},
+ path::{Path, PathBuf},
+};
+
+use adw::subclass::prelude::*;
+use anyhow::{Error, Result};
+use chrono::prelude::*;
+use diesel::{prelude::*, QueryDsl, SqliteConnection};
+
+use super::Library;
+use crate::db::{self, models::*, schema::*, tables, TranslatedString};
+
+impl Library {
+ pub fn create_person(&self, name: TranslatedString, enable_updates: bool) -> Result {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let now = Local::now().naive_local();
+
+ let person = Person {
+ person_id: db::generate_id(),
+ name,
+ created_at: now,
+ edited_at: now,
+ last_used_at: now,
+ last_played_at: None,
+ enable_updates,
+ };
+
+ diesel::insert_into(persons::table)
+ .values(&person)
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(person)
+ }
+
+ pub fn update_person(
+ &self,
+ id: &str,
+ name: TranslatedString,
+ enable_updates: bool,
+ ) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let now = Local::now().naive_local();
+
+ diesel::update(persons::table)
+ .filter(persons::person_id.eq(id))
+ .set((
+ persons::name.eq(name),
+ persons::edited_at.eq(now),
+ persons::last_used_at.eq(now),
+ persons::enable_updates.eq(enable_updates),
+ ))
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn delete_person(&self, person_id: &str) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ diesel::delete(persons::table)
+ .filter(persons::person_id.eq(person_id))
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn create_instrument(
+ &self,
+ name: TranslatedString,
+ enable_updates: bool,
+ ) -> Result {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let now = Local::now().naive_local();
+
+ let instrument = Instrument {
+ instrument_id: db::generate_id(),
+ name,
+ created_at: now,
+ edited_at: now,
+ last_used_at: now,
+ last_played_at: None,
+ enable_updates,
+ };
+
+ diesel::insert_into(instruments::table)
+ .values(&instrument)
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(instrument)
+ }
+
+ pub fn update_instrument(
+ &self,
+ id: &str,
+ name: TranslatedString,
+ enable_updates: bool,
+ ) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let now = Local::now().naive_local();
+
+ diesel::update(instruments::table)
+ .filter(instruments::instrument_id.eq(id))
+ .set((
+ instruments::name.eq(name),
+ instruments::edited_at.eq(now),
+ instruments::last_used_at.eq(now),
+ instruments::enable_updates.eq(enable_updates),
+ ))
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn delete_instrument(&self, instrument_id: &str) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ diesel::delete(instruments::table)
+ .filter(instruments::instrument_id.eq(instrument_id))
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn create_role(&self, name: TranslatedString, enable_updates: bool) -> Result {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let now = Local::now().naive_local();
+
+ let role = Role {
+ role_id: db::generate_id(),
+ name,
+ created_at: now,
+ edited_at: now,
+ last_used_at: now,
+ enable_updates,
+ };
+
+ diesel::insert_into(roles::table)
+ .values(&role)
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(role)
+ }
+
+ pub fn update_role(
+ &self,
+ id: &str,
+ name: TranslatedString,
+ enable_updates: bool,
+ ) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let now = Local::now().naive_local();
+
+ diesel::update(roles::table)
+ .filter(roles::role_id.eq(id))
+ .set((
+ roles::name.eq(name),
+ roles::edited_at.eq(now),
+ roles::last_used_at.eq(now),
+ roles::enable_updates.eq(enable_updates),
+ ))
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn delete_role(&self, role_id: &str) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ diesel::delete(roles::table)
+ .filter(roles::role_id.eq(role_id))
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn create_work(
+ &self,
+ name: TranslatedString,
+ parts: Vec,
+ persons: Vec,
+ instruments: Vec,
+ enable_updates: bool,
+ ) -> Result {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let work = self.create_work_priv(
+ connection,
+ name,
+ parts,
+ persons,
+ instruments,
+ None,
+ None,
+ enable_updates,
+ )?;
+
+ self.changed();
+
+ Ok(work)
+ }
+
+ fn create_work_priv(
+ &self,
+ connection: &mut SqliteConnection,
+ name: TranslatedString,
+ parts: Vec,
+ persons: Vec,
+ instruments: Vec,
+ parent_work_id: Option<&str>,
+ sequence_number: Option,
+ enable_updates: bool,
+ ) -> Result {
+ let work_id = db::generate_id();
+ let now = Local::now().naive_local();
+
+ let work_data = tables::Work {
+ work_id: work_id.clone(),
+ parent_work_id: parent_work_id.map(|w| w.to_string()),
+ sequence_number: sequence_number,
+ name,
+ created_at: now,
+ edited_at: now,
+ last_used_at: now,
+ last_played_at: None,
+ enable_updates,
+ };
+
+ diesel::insert_into(works::table)
+ .values(&work_data)
+ .execute(connection)?;
+
+ for (index, part) in parts.into_iter().enumerate() {
+ self.create_work_priv(
+ connection,
+ part.name,
+ part.parts,
+ part.persons,
+ part.instruments,
+ Some(&work_id),
+ Some(index as i32),
+ enable_updates,
+ )?;
+ }
+
+ for (index, composer) in persons.into_iter().enumerate() {
+ let composer_data = tables::WorkPerson {
+ work_id: work_id.clone(),
+ person_id: composer.person.person_id,
+ role_id: composer.role.map(|r| r.role_id),
+ sequence_number: index as i32,
+ };
+
+ diesel::insert_into(work_persons::table)
+ .values(composer_data)
+ .execute(connection)?;
+ }
+
+ for (index, instrument) in instruments.into_iter().enumerate() {
+ let instrument_data = tables::WorkInstrument {
+ work_id: work_id.clone(),
+ instrument_id: instrument.instrument_id,
+ sequence_number: index as i32,
+ };
+
+ diesel::insert_into(work_instruments::table)
+ .values(instrument_data)
+ .execute(connection)?;
+ }
+
+ let work = Work::from_table(work_data, connection)?;
+
+ Ok(work)
+ }
+
+ pub fn update_work(
+ &self,
+ work_id: &str,
+ name: TranslatedString,
+ parts: Vec,
+ persons: Vec,
+ instruments: Vec,
+ enable_updates: bool,
+ ) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ self.update_work_priv(
+ connection,
+ work_id,
+ name,
+ parts,
+ persons,
+ instruments,
+ None,
+ None,
+ enable_updates,
+ )?;
+
+ self.changed();
+
+ Ok(())
+ }
+
+ fn update_work_priv(
+ &self,
+ connection: &mut SqliteConnection,
+ work_id: &str,
+ name: TranslatedString,
+ parts: Vec,
+ persons: Vec,
+ instruments: Vec,
+ parent_work_id: Option<&str>,
+ sequence_number: Option,
+ enable_updates: bool,
+ ) -> Result<()> {
+ let now = Local::now().naive_local();
+
+ diesel::update(works::table)
+ .filter(works::work_id.eq(work_id))
+ .set((
+ works::parent_work_id.eq(parent_work_id),
+ works::sequence_number.eq(sequence_number),
+ works::name.eq(name),
+ works::edited_at.eq(now),
+ works::last_used_at.eq(now),
+ works::enable_updates.eq(enable_updates),
+ ))
+ .execute(connection)?;
+
+ diesel::delete(works::table)
+ .filter(
+ works::parent_work_id
+ .eq(work_id)
+ .and(works::work_id.ne_all(parts.iter().map(|p| p.work_id.clone()))),
+ )
+ .execute(connection)?;
+
+ for (index, part) in parts.into_iter().enumerate() {
+ if works::table
+ .filter(works::work_id.eq(&part.work_id))
+ .first::(connection)
+ .optional()?
+ .is_some()
+ {
+ self.update_work_priv(
+ connection,
+ &part.work_id,
+ part.name,
+ part.parts,
+ part.persons,
+ part.instruments,
+ Some(work_id),
+ Some(index as i32),
+ enable_updates,
+ )?;
+ } else {
+ // Note: The previously used ID is discarded. This should be OK, because
+ // at this point, the part ID should not have been used anywhere.
+ self.create_work_priv(
+ connection,
+ part.name,
+ part.parts,
+ part.persons,
+ part.instruments,
+ Some(work_id),
+ Some(index as i32),
+ enable_updates,
+ )?;
+ }
+ }
+
+ diesel::delete(work_persons::table)
+ .filter(work_persons::work_id.eq(work_id))
+ .execute(connection)?;
+
+ for (index, composer) in persons.into_iter().enumerate() {
+ let composer_data = tables::WorkPerson {
+ work_id: work_id.to_string(),
+ person_id: composer.person.person_id,
+ role_id: composer.role.map(|r| r.role_id),
+ sequence_number: index as i32,
+ };
+
+ diesel::insert_into(work_persons::table)
+ .values(composer_data)
+ .execute(connection)?;
+ }
+
+ diesel::delete(work_instruments::table)
+ .filter(work_instruments::work_id.eq(work_id))
+ .execute(connection)?;
+
+ for (index, instrument) in instruments.into_iter().enumerate() {
+ let instrument_data = tables::WorkInstrument {
+ work_id: work_id.to_string(),
+ instrument_id: instrument.instrument_id,
+ sequence_number: index as i32,
+ };
+
+ diesel::insert_into(work_instruments::table)
+ .values(instrument_data)
+ .execute(connection)?;
+ }
+
+ Ok(())
+ }
+
+ pub fn delete_work(&self, work_id: &str) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ diesel::delete(works::table)
+ .filter(works::work_id.eq(work_id))
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn create_ensemble(
+ &self,
+ name: TranslatedString,
+ enable_updates: bool,
+ ) -> Result {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let now = Local::now().naive_local();
+
+ let ensemble_data = tables::Ensemble {
+ ensemble_id: db::generate_id(),
+ name,
+ created_at: now,
+ edited_at: now,
+ last_used_at: now,
+ last_played_at: None,
+ enable_updates,
+ };
+
+ // TODO: Add persons.
+
+ diesel::insert_into(ensembles::table)
+ .values(&ensemble_data)
+ .execute(connection)?;
+
+ let ensemble = Ensemble::from_table(ensemble_data, connection)?;
+
+ self.changed();
+
+ Ok(ensemble)
+ }
+
+ pub fn update_ensemble(
+ &self,
+ id: &str,
+ name: TranslatedString,
+ enable_updates: bool,
+ ) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let now = Local::now().naive_local();
+
+ diesel::update(ensembles::table)
+ .filter(ensembles::ensemble_id.eq(id))
+ .set((
+ ensembles::name.eq(name),
+ ensembles::edited_at.eq(now),
+ ensembles::last_used_at.eq(now),
+ ensembles::enable_updates.eq(enable_updates),
+ ))
+ .execute(connection)?;
+
+ // TODO: Support updating persons.
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn delete_ensemble(&self, ensemble_id: &str) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ diesel::delete(ensembles::table)
+ .filter(ensembles::ensemble_id.eq(ensemble_id))
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn create_recording(
+ &self,
+ work: Work,
+ year: Option,
+ performers: Vec,
+ ensembles: Vec,
+ enable_updates: bool,
+ ) -> Result {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let recording_id = db::generate_id();
+ let now = Local::now().naive_local();
+
+ let recording_data = tables::Recording {
+ recording_id: recording_id.clone(),
+ work_id: work.work_id.clone(),
+ year,
+ created_at: now,
+ edited_at: now,
+ last_used_at: now,
+ last_played_at: None,
+ enable_updates,
+ };
+
+ diesel::insert_into(recordings::table)
+ .values(&recording_data)
+ .execute(connection)?;
+
+ for (index, performer) in performers.into_iter().enumerate() {
+ let recording_person_data = tables::RecordingPerson {
+ recording_id: recording_id.clone(),
+ person_id: performer.person.person_id,
+ role_id: performer.role.map(|r| r.role_id),
+ instrument_id: performer.instrument.map(|i| i.instrument_id),
+ sequence_number: index as i32,
+ };
+
+ diesel::insert_into(recording_persons::table)
+ .values(&recording_person_data)
+ .execute(connection)?;
+ }
+
+ for (index, ensemble) in ensembles.into_iter().enumerate() {
+ let recording_ensemble_data = tables::RecordingEnsemble {
+ recording_id: recording_id.clone(),
+ ensemble_id: ensemble.ensemble.ensemble_id,
+ role_id: ensemble.role.map(|r| r.role_id),
+ sequence_number: index as i32,
+ };
+
+ diesel::insert_into(recording_ensembles::table)
+ .values(&recording_ensemble_data)
+ .execute(connection)?;
+ }
+
+ let recording = Recording::from_table(recording_data, connection)?;
+
+ self.changed();
+
+ Ok(recording)
+ }
+
+ pub fn update_recording(
+ &self,
+ recording_id: &str,
+ work: Work,
+ year: Option,
+ performers: Vec,
+ ensembles: Vec,
+ enable_updates: bool,
+ ) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let now = Local::now().naive_local();
+
+ diesel::update(recordings::table)
+ .filter(recordings::recording_id.eq(recording_id))
+ .set((
+ recordings::work_id.eq(work.work_id),
+ recordings::year.eq(year),
+ recordings::edited_at.eq(now),
+ recordings::last_used_at.eq(now),
+ recordings::enable_updates.eq(enable_updates),
+ ))
+ .execute(connection)?;
+
+ diesel::delete(recording_persons::table)
+ .filter(recording_persons::recording_id.eq(recording_id))
+ .execute(connection)?;
+
+ for (index, performer) in performers.into_iter().enumerate() {
+ let recording_person_data = tables::RecordingPerson {
+ recording_id: recording_id.to_string(),
+ person_id: performer.person.person_id,
+ role_id: performer.role.map(|r| r.role_id),
+ instrument_id: performer.instrument.map(|i| i.instrument_id),
+ sequence_number: index as i32,
+ };
+
+ diesel::insert_into(recording_persons::table)
+ .values(&recording_person_data)
+ .execute(connection)?;
+ }
+
+ diesel::delete(recording_ensembles::table)
+ .filter(recording_ensembles::recording_id.eq(recording_id))
+ .execute(connection)?;
+
+ for (index, ensemble) in ensembles.into_iter().enumerate() {
+ let recording_ensemble_data = tables::RecordingEnsemble {
+ recording_id: recording_id.to_string(),
+ ensemble_id: ensemble.ensemble.ensemble_id,
+ role_id: ensemble.role.map(|r| r.role_id),
+ sequence_number: index as i32,
+ };
+
+ diesel::insert_into(recording_ensembles::table)
+ .values(&recording_ensemble_data)
+ .execute(connection)?;
+ }
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn delete_recording(&self, recording_id: &str) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ diesel::delete(recordings::table)
+ .filter(recordings::recording_id.eq(recording_id))
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn delete_recording_and_tracks(&self, recording_id: &str) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let tracks = tracks::table
+ .filter(tracks::recording_id.eq(recording_id))
+ .load::(connection)?;
+
+ // Delete from library first to avoid orphan tracks in case of file
+ // system related errors.
+
+ connection.transaction::<(), Error, _>(|connection| {
+ for track in &tracks {
+ diesel::delete(track_works::table)
+ .filter(track_works::track_id.eq(&track.track_id))
+ .execute(connection)?;
+
+ diesel::delete(tracks::table)
+ .filter(tracks::track_id.eq(&track.track_id))
+ .execute(connection)?;
+ }
+
+ diesel::delete(recordings::table)
+ .filter(recordings::recording_id.eq(recording_id))
+ .execute(connection)?;
+
+ Ok(())
+ })?;
+
+ let library_path = PathBuf::from(self.folder());
+ for track in tracks {
+ fs::remove_file(library_path.join(&track.path))?;
+ }
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn create_album(
+ &self,
+ name: TranslatedString,
+ recordings: Vec,
+ ) -> Result {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let album_id = db::generate_id();
+ let now = Local::now().naive_local();
+
+ let album_data = tables::Album {
+ album_id: album_id.clone(),
+ name,
+ created_at: now,
+ edited_at: now,
+ last_used_at: now,
+ last_played_at: None,
+ };
+
+ diesel::insert_into(albums::table)
+ .values(&album_data)
+ .execute(connection)?;
+
+ for (index, recording) in recordings.into_iter().enumerate() {
+ let album_recording_data = tables::AlbumRecording {
+ album_id: album_id.clone(),
+ recording_id: recording.recording_id,
+ sequence_number: index as i32,
+ };
+
+ diesel::insert_into(album_recordings::table)
+ .values(&album_recording_data)
+ .execute(connection)?;
+ }
+
+ let album = Album::from_table(album_data, connection)?;
+
+ self.changed();
+
+ Ok(album)
+ }
+
+ pub fn update_album(
+ &self,
+ album_id: &str,
+ name: TranslatedString,
+ recordings: Vec,
+ ) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let now = Local::now().naive_local();
+
+ diesel::update(albums::table)
+ .filter(albums::album_id.eq(album_id))
+ .set((
+ albums::name.eq(name),
+ albums::edited_at.eq(now),
+ albums::last_used_at.eq(now),
+ ))
+ .execute(connection)?;
+
+ diesel::delete(album_recordings::table)
+ .filter(album_recordings::album_id.eq(album_id))
+ .execute(connection)?;
+
+ for (index, recording) in recordings.into_iter().enumerate() {
+ let album_recording_data = tables::AlbumRecording {
+ album_id: album_id.to_owned(),
+ recording_id: recording.recording_id,
+ sequence_number: index as i32,
+ };
+
+ diesel::insert_into(album_recordings::table)
+ .values(&album_recording_data)
+ .execute(connection)?;
+ }
+
+ self.changed();
+
+ Ok(())
+ }
+
+ pub fn delete_album(&self, album_id: &str) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ diesel::delete(albums::table)
+ .filter(albums::album_id.eq(album_id))
+ .execute(connection)?;
+
+ self.changed();
+
+ Ok(())
+ }
+
+ /// Import a track into the music library.
+ // TODO: Support mediums.
+ pub fn import_track(
+ &self,
+ path: impl AsRef,
+ recording_id: &str,
+ recording_index: i32,
+ works: Vec,
+ ) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let track_id = db::generate_id();
+ let now = Local::now().naive_local();
+
+ // TODO: Human interpretable filenames?
+ let mut filename = OsString::from(recording_id);
+ filename.push("_");
+ filename.push(OsString::from(format!("{recording_index:02}")));
+ if let Some(extension) = path.as_ref().extension() {
+ filename.push(".");
+ filename.push(extension);
+ };
+
+ let mut to_path = PathBuf::from(self.folder());
+ to_path.push(&filename);
+ let library_path = PathBuf::from(filename);
+
+ fs::copy(path, to_path)?;
+
+ let track_data = tables::Track {
+ track_id: track_id.clone(),
+ recording_id: recording_id.to_owned(),
+ recording_index,
+ medium_id: None,
+ medium_index: None,
+ path: library_path.into(),
+ created_at: now,
+ edited_at: now,
+ last_used_at: now,
+ last_played_at: None,
+ };
+
+ diesel::insert_into(tracks::table)
+ .values(&track_data)
+ .execute(connection)?;
+
+ for (index, work) in works.into_iter().enumerate() {
+ let track_work_data = tables::TrackWork {
+ track_id: track_id.clone(),
+ work_id: work.work_id,
+ sequence_number: index as i32,
+ };
+
+ diesel::insert_into(track_works::table)
+ .values(&track_work_data)
+ .execute(connection)?;
+ }
+
+ Ok(())
+ }
+
+ // TODO: Support mediums, think about albums.
+ pub fn delete_track(&self, track: &Track) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ diesel::delete(track_works::table)
+ .filter(track_works::track_id.eq(&track.track_id))
+ .execute(connection)?;
+
+ diesel::delete(tracks::table)
+ .filter(tracks::track_id.eq(&track.track_id))
+ .execute(connection)?;
+
+ let mut path = PathBuf::from(self.folder());
+ path.push(&track.path);
+ fs::remove_file(path)?;
+
+ Ok(())
+ }
+
+ // TODO: Support mediums, think about albums.
+ pub fn update_track(
+ &self,
+ track_id: &str,
+ recording_index: i32,
+ works: Vec,
+ ) -> Result<()> {
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let now = Local::now().naive_local();
+
+ diesel::update(tracks::table)
+ .filter(tracks::track_id.eq(track_id.to_owned()))
+ .set((
+ tracks::recording_index.eq(recording_index),
+ tracks::edited_at.eq(now),
+ tracks::last_used_at.eq(now),
+ ))
+ .execute(connection)?;
+
+ diesel::delete(track_works::table)
+ .filter(track_works::track_id.eq(track_id))
+ .execute(connection)?;
+
+ for (index, work) in works.into_iter().enumerate() {
+ let track_work_data = tables::TrackWork {
+ track_id: track_id.to_owned(),
+ work_id: work.work_id,
+ sequence_number: index as i32,
+ };
+
+ diesel::insert_into(track_works::table)
+ .values(&track_work_data)
+ .execute(connection)?;
+ }
+
+ Ok(())
+ }
+}
diff --git a/src/library/exchange.rs b/src/library/exchange.rs
new file mode 100644
index 0000000..8cd3063
--- /dev/null
+++ b/src/library/exchange.rs
@@ -0,0 +1,551 @@
+use std::{
+ fs::{self, File},
+ io::{BufReader, BufWriter, Read, Write},
+ path::{Path, PathBuf},
+ sync::{Arc, Mutex},
+ thread,
+};
+
+use adw::subclass::prelude::*;
+use anyhow::{anyhow, Result};
+use chrono::prelude::*;
+use diesel::{prelude::*, SqliteConnection};
+use formatx::formatx;
+use futures_util::StreamExt;
+use gettextrs::gettext;
+use tempfile::NamedTempFile;
+use tokio::io::AsyncWriteExt;
+use zip::{write::SimpleFileOptions, ZipWriter};
+
+use super::Library;
+use crate::{
+ db::{self, schema::*, tables},
+ process::ProcessMsg,
+};
+
+impl Library {
+ /// Import from a music library ZIP archive at `path`.
+ pub fn import_library_from_zip(
+ &self,
+ path: impl AsRef,
+ ) -> Result> {
+ log::info!("Importing library from ZIP at {}", path.as_ref().to_string_lossy());
+ let path = path.as_ref().to_owned();
+ let library_folder = PathBuf::from(&self.folder());
+ let this_connection = self.imp().connection.get().unwrap().clone();
+
+ let (sender, receiver) = async_channel::unbounded::();
+ thread::spawn(move || {
+ if let Err(err) = sender.send_blocking(ProcessMsg::Result(
+ import_library_from_zip_priv(path, library_folder, this_connection, &sender),
+ )) {
+ log::error!("Failed to send library action result: {err:?}");
+ }
+ });
+
+ Ok(receiver)
+ }
+
+ /// Export the whole music library to a ZIP archive at `path`. If `path` already exists, it
+ /// will be overwritten. The work will be done in a background thread.
+ pub fn export_library_to_zip(
+ &self,
+ path: impl AsRef,
+ ) -> Result> {
+ log::info!("Exporting library to ZIP at {}", path.as_ref().to_string_lossy());
+ let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
+
+ let path = path.as_ref().to_owned();
+ let library_folder = PathBuf::from(&self.folder());
+ let tracks = tracks::table.load::(connection)?;
+
+ let (sender, receiver) = async_channel::unbounded::();
+ thread::spawn(move || {
+ if let Err(err) = sender.send_blocking(ProcessMsg::Result(export_library_to_zip_priv(
+ path,
+ library_folder,
+ tracks,
+ &sender,
+ ))) {
+ log::error!("Failed to send library action result: {err:?}");
+ }
+ });
+
+ Ok(receiver)
+ }
+
+ /// Import from a library archive at `url`.
+ pub fn import_library_from_url(
+ &self,
+ url: &str,
+ ) -> Result> {
+ log::info!("Importing library from URL {url}");
+ let url = url.to_owned();
+ let library_folder = PathBuf::from(&self.folder());
+ let this_connection = self.imp().connection.get().unwrap().clone();
+
+ let (sender, receiver) = async_channel::unbounded::();
+
+ thread::spawn(move || {
+ if let Err(err) = sender.send_blocking(ProcessMsg::Result(
+ import_library_from_url_priv(url, library_folder, this_connection, &sender),
+ )) {
+ log::error!("Failed to send library action result: {err:?}");
+ }
+ });
+
+ Ok(receiver)
+ }
+
+ /// Import from metadata from a database file at `url`.
+ pub fn import_metadata_from_url(
+ &self,
+ url: &str,
+ ) -> Result> {
+ log::info!("Importing metadata from URL {url}");
+
+ let url = url.to_owned();
+ let this_connection = self.imp().connection.get().unwrap().clone();
+
+ let (sender, receiver) = async_channel::unbounded::();
+
+ thread::spawn(move || {
+ if let Err(err) = sender.send_blocking(ProcessMsg::Result(
+ import_metadata_from_url_priv(url, this_connection, &sender),
+ )) {
+ log::error!("Failed to send library action result: {err:?}");
+ }
+ });
+
+ Ok(receiver)
+ }
+}
+
+// TODO: Add options whether to keep stats.
+fn import_library_from_zip_priv(
+ zip_path: impl AsRef,
+ library_folder: impl AsRef,
+ this_connection: Arc>,
+ sender: &async_channel::Sender,
+) -> Result<()> {
+ let mut archive = zip::ZipArchive::new(BufReader::new(fs::File::open(zip_path)?))?;
+
+ let archive_db_file = archive.by_name("musicus.db")?;
+ let tmp_db_file = NamedTempFile::new()?;
+ std::io::copy(
+ &mut BufReader::new(archive_db_file),
+ &mut BufWriter::new(tmp_db_file.as_file()),
+ )?;
+
+ // Import metadata.
+ let tracks = import_metadata_from_file(tmp_db_file.path(), this_connection, false)?;
+
+ // Import audio files.
+ let n_tracks = tracks.len();
+ for (index, track) in tracks.into_iter().enumerate() {
+ let library_track_file_path = library_folder.as_ref().join(&track.path);
+
+ // Skip tracks that are already present.
+ if !fs::exists(&library_track_file_path)? {
+ if let Some(parent) = library_track_file_path.parent() {
+ fs::create_dir_all(parent)?;
+ }
+
+ let archive_track_file = archive.by_name(&path_to_zip(&track.path)?)?;
+ let library_track_file = File::create(library_track_file_path)?;
+
+ std::io::copy(
+ &mut BufReader::new(archive_track_file),
+ &mut BufWriter::new(library_track_file),
+ )?;
+ }
+
+ // Ignore if the reveiver has been dropped.
+ let _ = sender.send_blocking(ProcessMsg::Progress((index + 1) as f64 / n_tracks as f64));
+ }
+
+ Ok(())
+}
+
+fn export_library_to_zip_priv(
+ zip_path: impl AsRef,
+ library_folder: impl AsRef,
+ tracks: Vec,
+ sender: &async_channel::Sender,
+) -> Result<()> {
+ let mut zip = zip::ZipWriter::new(BufWriter::new(fs::File::create(zip_path)?));
+
+ // Start with the database:
+ add_file_to_zip(&mut zip, &library_folder, "musicus.db")?;
+
+ let n_tracks = tracks.len();
+
+ // Include all tracks that are part of the library.
+ for (index, track) in tracks.into_iter().enumerate() {
+ add_file_to_zip(&mut zip, &library_folder, &path_to_zip(&track.path)?)?;
+
+ // Ignore if the reveiver has been dropped.
+ let _ = sender.send_blocking(ProcessMsg::Progress((index + 1) as f64 / n_tracks as f64));
+ }
+
+ zip.finish()?;
+
+ Ok(())
+}
+
+fn add_file_to_zip(
+ zip: &mut ZipWriter>,
+ library_folder: impl AsRef,
+ library_path: &str,
+) -> Result<()> {
+ let file_path = library_folder.as_ref().join(PathBuf::from(library_path));
+
+ let mut file = File::open(file_path)?;
+ let mut buffer = Vec::new();
+ file.read_to_end(&mut buffer)?;
+
+ zip.start_file(library_path, SimpleFileOptions::default())?;
+ zip.write_all(&buffer)?;
+
+ Ok(())
+}
+
+fn import_metadata_from_url_priv(
+ url: String,
+ this_connection: Arc>,
+ sender: &async_channel::Sender,
+) -> Result<()> {
+ let runtime = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()?;
+
+ let _ = sender.send_blocking(ProcessMsg::Message(
+ formatx!(gettext("Downloading {}"), &url).unwrap(),
+ ));
+
+ match runtime.block_on(download_tmp_file(&url, &sender)) {
+ Ok(db_file) => {
+ let _ = sender.send_blocking(ProcessMsg::Message(
+ formatx!(gettext("Importing downloaded library"), &url).unwrap(),
+ ));
+
+ let _ = sender.send_blocking(ProcessMsg::Result(
+ import_metadata_from_file(db_file.path(), this_connection, true).and_then(
+ |tracks| {
+ if !tracks.is_empty() {
+ log::warn!("The metadata file at {url} contains tracks.");
+ }
+
+ Ok(())
+ },
+ ),
+ ));
+ }
+ Err(err) => {
+ let _ = sender.send_blocking(ProcessMsg::Result(Err(err)));
+ }
+ }
+
+ Ok(())
+}
+
+fn import_library_from_url_priv(
+ url: String,
+ library_folder: impl AsRef,
+ this_connection: Arc>,
+ sender: &async_channel::Sender,
+) -> Result<()> {
+ let runtime = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()?;
+
+ let _ = sender.send_blocking(ProcessMsg::Message(
+ formatx!(gettext("Downloading {}"), &url).unwrap(),
+ ));
+
+ let archive_file = runtime.block_on(download_tmp_file(&url, &sender));
+
+ match archive_file {
+ Ok(archive_file) => {
+ let _ = sender.send_blocking(ProcessMsg::Message(
+ formatx!(gettext("Importing downloaded library"), &url).unwrap(),
+ ));
+
+ let _ = sender.send_blocking(ProcessMsg::Result(import_library_from_zip_priv(
+ archive_file.path(),
+ library_folder,
+ this_connection,
+ &sender,
+ )));
+ }
+ Err(err) => {
+ let _ = sender.send_blocking(ProcessMsg::Result(Err(err)));
+ }
+ }
+
+ Ok(())
+}
+
+/// Import metadata from the database file at `path`.
+///
+/// If `ignore_tracks` is `true`, tracks and associated items like mediums will not be imported
+/// from the database. In that case, if the database contains tracks, a warning will be logged.
+/// In any case, tracks are returned.
+fn import_metadata_from_file(
+ path: impl AsRef,
+ this_connection: Arc>,
+ ignore_tracks: bool,
+) -> Result> {
+ let now = Local::now().naive_local();
+
+ let mut other_connection = db::connect(path.as_ref().to_str().unwrap())?;
+
+ // Load all metadata from the archive.
+ let persons = persons::table.load::(&mut other_connection)?;
+ let roles = roles::table.load::(&mut other_connection)?;
+ let instruments = instruments::table.load::(&mut other_connection)?;
+ let works = works::table.load::(&mut other_connection)?;
+ let work_persons = work_persons::table.load::(&mut other_connection)?;
+ let work_instruments =
+ work_instruments::table.load::(&mut other_connection)?;
+ let ensembles = ensembles::table.load::(&mut other_connection)?;
+ let ensemble_persons =
+ ensemble_persons::table.load::(&mut other_connection)?;
+ let recordings = recordings::table.load::(&mut other_connection)?;
+ let recording_persons =
+ recording_persons::table.load::(&mut other_connection)?;
+ let recording_ensembles =
+ recording_ensembles::table.load::(&mut other_connection)?;
+ let tracks = tracks::table.load::(&mut other_connection)?;
+ let track_works = track_works::table.load::(&mut other_connection)?;
+ let mediums = mediums::table.load::(&mut other_connection)?;
+ let albums = albums::table.load::(&mut other_connection)?;
+ let album_recordings =
+ album_recordings::table.load::(&mut other_connection)?;
+ let album_mediums = album_mediums::table.load::(&mut other_connection)?;
+
+ // Import metadata that is not already present.
+
+ for mut person in persons {
+ person.created_at = now;
+ person.edited_at = now;
+ person.last_used_at = now;
+ person.last_played_at = None;
+
+ diesel::insert_into(persons::table)
+ .values(person)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for mut role in roles {
+ role.created_at = now;
+ role.edited_at = now;
+ role.last_used_at = now;
+
+ diesel::insert_into(roles::table)
+ .values(role)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for mut instrument in instruments {
+ instrument.created_at = now;
+ instrument.edited_at = now;
+ instrument.last_used_at = now;
+ instrument.last_played_at = None;
+
+ diesel::insert_into(instruments::table)
+ .values(instrument)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for mut work in works {
+ work.created_at = now;
+ work.edited_at = now;
+ work.last_used_at = now;
+ work.last_played_at = None;
+
+ diesel::insert_into(works::table)
+ .values(work)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for work_person in work_persons {
+ diesel::insert_into(work_persons::table)
+ .values(work_person)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for work_instrument in work_instruments {
+ diesel::insert_into(work_instruments::table)
+ .values(work_instrument)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for mut ensemble in ensembles {
+ ensemble.created_at = now;
+ ensemble.edited_at = now;
+ ensemble.last_used_at = now;
+ ensemble.last_played_at = None;
+
+ diesel::insert_into(ensembles::table)
+ .values(ensemble)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for ensemble_person in ensemble_persons {
+ diesel::insert_into(ensemble_persons::table)
+ .values(ensemble_person)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for mut recording in recordings {
+ recording.created_at = now;
+ recording.edited_at = now;
+ recording.last_used_at = now;
+ recording.last_played_at = None;
+
+ diesel::insert_into(recordings::table)
+ .values(recording)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for recording_person in recording_persons {
+ diesel::insert_into(recording_persons::table)
+ .values(recording_person)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for recording_ensemble in recording_ensembles {
+ diesel::insert_into(recording_ensembles::table)
+ .values(recording_ensemble)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ if !ignore_tracks {
+ for mut track in tracks.clone() {
+ track.created_at = now;
+ track.edited_at = now;
+ track.last_used_at = now;
+ track.last_played_at = None;
+
+ diesel::insert_into(tracks::table)
+ .values(track)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for track_work in track_works {
+ diesel::insert_into(track_works::table)
+ .values(track_work)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for mut medium in mediums {
+ medium.created_at = now;
+ medium.edited_at = now;
+ medium.last_used_at = now;
+ medium.last_played_at = None;
+
+ diesel::insert_into(mediums::table)
+ .values(medium)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+ }
+
+ for mut album in albums {
+ album.created_at = now;
+ album.edited_at = now;
+ album.last_used_at = now;
+ album.last_played_at = None;
+
+ diesel::insert_into(albums::table)
+ .values(album)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for album_recording in album_recordings {
+ diesel::insert_into(album_recordings::table)
+ .values(album_recording)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ for album_medium in album_mediums {
+ diesel::insert_into(album_mediums::table)
+ .values(album_medium)
+ .on_conflict_do_nothing()
+ .execute(&mut *this_connection.lock().unwrap())?;
+ }
+
+ Ok(tracks)
+}
+
+async fn download_tmp_file(
+ url: &str,
+ sender: &async_channel::Sender,
+) -> Result {
+ let client = reqwest::Client::builder()
+ .connect_timeout(std::time::Duration::from_secs(10))
+ .build()?;
+
+ let response = client.get(url).send().await?;
+ response.error_for_status_ref()?;
+
+ let total_size = response.content_length();
+ let mut body_stream = response.bytes_stream();
+
+ let file = NamedTempFile::new()?;
+ let mut writer =
+ tokio::io::BufWriter::new(tokio::fs::File::from_std(file.as_file().try_clone()?));
+
+ let mut downloaded = 0;
+ while let Some(chunk) = body_stream.next().await {
+ let chunk: Vec = chunk?.into();
+ let chunk_size = chunk.len();
+
+ writer.write_all(&chunk).await?;
+
+ if let Some(total_size) = total_size {
+ downloaded += chunk_size as u64;
+ let _ = sender
+ .send(ProcessMsg::Progress(downloaded as f64 / total_size as f64))
+ .await;
+ }
+ }
+
+ Ok(file)
+}
+
+/// Convert a path to a ZIP path. ZIP files use "/" as the path separator
+/// regardless of the current platform.
+fn path_to_zip(path: impl AsRef