From 456af4a1dfdbf40a2041f97f5f280a3df6bb3f34 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sun, 27 Apr 2025 15:22:04 +0200 Subject: [PATCH] Support metadata updates --- data/de.johrpan.Musicus.gschema.xml.in | 14 +- data/ui/editor/ensemble.blp | 8 +- data/ui/editor/instrument.blp | 8 +- data/ui/editor/person.blp | 8 +- data/ui/editor/recording.blp | 8 +- data/ui/editor/role.blp | 8 +- data/ui/editor/work.blp | 8 +- data/ui/library_manager.blp | 10 +- data/ui/preferences_dialog.blp | 28 +- meson.build | 3 +- migrations/2025-03-30-122451_updates/down.sql | 173 ++ migrations/2025-03-30-122451_updates/up.sql | 179 ++ src/config.rs.in | 3 +- src/db/models.rs | 6 + src/db/schema.rs | 12 +- src/db/tables.rs | 6 + src/editor/ensemble.rs | 12 +- src/editor/instrument.rs | 12 +- src/editor/person.rs | 12 +- src/editor/recording.rs | 20 +- src/editor/role.rs | 8 +- src/editor/work.rs | 11 +- src/empty_page.rs | 8 +- src/library.rs | 2120 +---------------- src/library/edit.rs | 904 +++++++ src/library/exchange.rs | 551 +++++ src/library/query.rs | 827 +++++++ src/library_manager.rs | 55 +- src/meson.build | 1 + src/preferences_dialog.rs | 49 +- src/window.rs | 19 + 31 files changed, 2930 insertions(+), 2161 deletions(-) create mode 100644 migrations/2025-03-30-122451_updates/down.sql create mode 100644 migrations/2025-03-30-122451_updates/up.sql create mode 100644 src/library/edit.rs create mode 100644 src/library/exchange.rs create mode 100644 src/library/query.rs 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) -> 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/query.rs b/src/library/query.rs new file mode 100644 index 0000000..43b685f --- /dev/null +++ b/src/library/query.rs @@ -0,0 +1,827 @@ +use adw::subclass::prelude::*; +use anyhow::Result; +use chrono::prelude::*; +use diesel::{dsl::exists, prelude::*, sql_types, QueryDsl}; + +use super::Library; +use crate::{ + db::{models::*, schema::*, tables}, + program::Program, +}; + +#[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() + } +} + +impl Library { + 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) + } +} diff --git a/src/library_manager.rs b/src/library_manager.rs index 63aa724..5c494f3 100644 --- a/src/library_manager.rs +++ b/src/library_manager.rs @@ -128,7 +128,7 @@ impl LibraryManager { } Ok(path) => { if let Some(path) = path.path() { - match self.imp().library.get().unwrap().import_archive(&path) { + match self.imp().library.get().unwrap().import_library_from_zip(&path) { Ok(receiver) => { let process = Process::new( &formatx!( @@ -186,7 +186,7 @@ impl LibraryManager { } Ok(path) => { if let Some(path) = path.path() { - match self.imp().library.get().unwrap().export_archive(&path) { + match self.imp().library.get().unwrap().export_library_to_zip(&path) { Ok(receiver) => { let process = Process::new( &formatx!( @@ -215,17 +215,23 @@ impl LibraryManager { } #[template_callback] - fn update_default_library(&self) { + fn update_metadata(&self) { let settings = gio::Settings::new(config::APP_ID); - let url = if settings.boolean("use-custom-library-url") { - settings.string("custom-library-url").to_string() + let url = if settings.boolean("use-custom-metadata-url") { + settings.string("custom-metadata-url").to_string() } else { - config::LIBRARY_URL.to_string() + config::METADATA_URL.to_string() }; - match self.imp().library.get().unwrap().import_url(&url) { + match self + .imp() + .library + .get() + .unwrap() + .import_metadata_from_url(&url) + { Ok(receiver) => { - let process = Process::new(&gettext("Downloading music library"), receiver); + let process = Process::new(&gettext("Updating metadata"), receiver); self.imp() .process_manager @@ -235,7 +241,38 @@ impl LibraryManager { self.add_process(&process); } - Err(err) => log::error!("Failed to download library: {err:?}"), + Err(err) => log::error!("Failed to update metadata: {err:?}"), + } + } + + #[template_callback] + fn update_library(&self) { + let settings = gio::Settings::new(config::APP_ID); + let url = if settings.boolean("use-custom-library-url") { + settings.string("custom-library-url").to_string() + } else { + config::LIBRARY_URL.to_string() + }; + + match self + .imp() + .library + .get() + .unwrap() + .import_library_from_url(&url) + { + Ok(receiver) => { + let process = Process::new(&gettext("Updating music library"), receiver); + + self.imp() + .process_manager + .get() + .unwrap() + .add_process(&process); + + self.add_process(&process); + } + Err(err) => log::error!("Failed to update library: {err:?}"), } } diff --git a/src/meson.build b/src/meson.build index 2cbe455..bbf3f68 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,6 +9,7 @@ conf.set_quoted('VERSION', meson.project_version()) conf.set_quoted('PROFILE', profile) conf.set_quoted('LOCALEDIR', localedir) conf.set_quoted('DATADIR', datadir) +conf.set_quoted('METADATA_URL', metadata_url) conf.set_quoted('LIBRARY_URL', library_url) configure_file( diff --git a/src/preferences_dialog.rs b/src/preferences_dialog.rs index be07780..f4821ed 100644 --- a/src/preferences_dialog.rs +++ b/src/preferences_dialog.rs @@ -20,9 +20,15 @@ mod imp { #[template_child] pub play_full_recordings_row: TemplateChild, #[template_child] - pub use_custom_url_row: TemplateChild, + pub enable_automatic_metadata_updates_row: TemplateChild, #[template_child] - pub custom_url_row: TemplateChild, + pub use_custom_metadata_url_row: TemplateChild, + #[template_child] + pub custom_metadata_url_row: TemplateChild, + #[template_child] + pub use_custom_library_url_row: TemplateChild, + #[template_child] + pub custom_library_url_row: TemplateChild, } #[glib::object_subclass] @@ -90,18 +96,47 @@ mod imp { settings .bind( - "use-custom-library-url", - &*self.use_custom_url_row, + "enable-automatic-metadata-updates", + &*self.enable_automatic_metadata_updates_row, "active", ) .build(); settings - .bind("custom-library-url", &*self.custom_url_row, "text") + .bind( + "use-custom-metadata-url", + &*self.use_custom_metadata_url_row, + "active", + ) .build(); - self.use_custom_url_row - .bind_property("active", &*self.custom_url_row, "sensitive") + settings + .bind( + "custom-metadata-url", + &*self.custom_metadata_url_row, + "text", + ) + .build(); + + self.use_custom_metadata_url_row + .bind_property("active", &*self.custom_metadata_url_row, "sensitive") + .sync_create() + .build(); + + settings + .bind( + "use-custom-library-url", + &*self.use_custom_library_url_row, + "active", + ) + .build(); + + settings + .bind("custom-library-url", &*self.custom_library_url_row, "text") + .build(); + + self.use_custom_library_url_row + .bind_property("active", &*self.custom_library_url_row, "sensitive") .sync_create() .build(); } diff --git a/src/window.rs b/src/window.rs index 574975d..cf768f0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -15,6 +15,7 @@ use crate::{ player_bar::PlayerBar, playlist_page::PlaylistPage, preferences_dialog::PreferencesDialog, + process::Process, process_manager::ProcessManager, search_page::SearchPage, util, @@ -267,6 +268,24 @@ impl Window { self.imp().player.set_library(&library); let is_empty = library.is_empty()?; + + let settings = gio::Settings::new(config::APP_ID); + if settings.boolean("enable-automatic-metadata-updates") { + let url = if settings.boolean("use-custom-metadata-url") { + settings.string("custom-metadata-url").to_string() + } else { + config::METADATA_URL.to_string() + }; + + match library.import_metadata_from_url(&url) { + Ok(receiver) => { + let process = Process::new(&gettext("Updating metadata"), receiver); + self.imp().process_manager.add_process(&process); + } + Err(err) => log::error!("Failed to update metadata: {err:?}"), + } + } + self.imp().library.replace(Some(library)); if is_empty {