mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-25 20:37:24 +02:00
Support metadata updates
This commit is contained in:
parent
cb90f02073
commit
456af4a1df
31 changed files with 2930 additions and 2161 deletions
|
|
@ -52,12 +52,24 @@
|
|||
<default l10n="messages">'{"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>
|
||||
<summary>Default settings for program 3</summary>
|
||||
</key>
|
||||
<key name="enable-automatic-metadata-updates" type="b">
|
||||
<default>true</default>
|
||||
<summary>Automatically download metadata updates</summary>
|
||||
</key>
|
||||
<key name="use-custom-metadata-url" type="b">
|
||||
<default>false</default>
|
||||
<summary>Use a custom URL for metadata downloads</summary>
|
||||
</key>
|
||||
<key name="custom-metadata-url" type="s">
|
||||
<default>'https://musicus.johrpan.de/musicus_metadata_latest.musdb'</default>
|
||||
<summary>Custom URL for metadata downloads</summary>
|
||||
</key>
|
||||
<key name="use-custom-library-url" type="b">
|
||||
<default>false</default>
|
||||
<summary>Use a custom URL for library downloads</summary>
|
||||
</key>
|
||||
<key name="custom-library-url" type="s">
|
||||
<default>'https://musicus.johrpan.de/musicus_library_latest.zip'</default>
|
||||
<default>'https://musicus.johrpan.de/musicus_library_latest.muslib'</default>
|
||||
<summary>Custom URL for library downloads</summary>
|
||||
</key>
|
||||
</schema>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
173
migrations/2025-03-30-122451_updates/down.sql
Normal file
173
migrations/2025-03-30-122451_updates/down.sql
Normal file
|
|
@ -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;
|
||||
179
migrations/2025-03-30-122451_updates/up.sql
Normal file
179
migrations/2025-03-30-122451_updates/up.sql
Normal file
|
|
@ -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;
|
||||
|
|
@ -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@;
|
||||
|
|
@ -19,6 +19,7 @@ pub struct Work {
|
|||
pub parts: Vec<Work>,
|
||||
pub persons: Vec<Composer>,
|
||||
pub instruments: Vec<Instrument>,
|
||||
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<i32>,
|
||||
pub persons: Vec<Performer>,
|
||||
pub ensembles: Vec<EnsemblePerformer>,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ diesel::table! {
|
|||
edited_at -> Timestamp,
|
||||
last_used_at -> Timestamp,
|
||||
last_played_at -> Nullable<Timestamp>,
|
||||
enable_updates -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +56,7 @@ diesel::table! {
|
|||
edited_at -> Timestamp,
|
||||
last_used_at -> Timestamp,
|
||||
last_played_at -> Nullable<Timestamp>,
|
||||
enable_updates -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,11 +79,12 @@ diesel::table! {
|
|||
edited_at -> Timestamp,
|
||||
last_used_at -> Timestamp,
|
||||
last_played_at -> Nullable<Timestamp>,
|
||||
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<Text>,
|
||||
|
|
@ -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<Text>,
|
||||
|
|
@ -108,6 +111,7 @@ diesel::table! {
|
|||
edited_at -> Timestamp,
|
||||
last_used_at -> Timestamp,
|
||||
last_played_at -> Nullable<Timestamp>,
|
||||
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<Text>,
|
||||
|
|
@ -171,6 +176,7 @@ diesel::table! {
|
|||
edited_at -> Timestamp,
|
||||
last_used_at -> Timestamp,
|
||||
last_played_at -> Nullable<Timestamp>,
|
||||
enable_updates -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ pub struct Person {
|
|||
pub edited_at: NaiveDateTime,
|
||||
pub last_used_at: NaiveDateTime,
|
||||
pub last_played_at: Option<NaiveDateTime>,
|
||||
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<NaiveDateTime>,
|
||||
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<NaiveDateTime>,
|
||||
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<NaiveDateTime>,
|
||||
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<NaiveDateTime>,
|
||||
pub enable_updates: bool,
|
||||
}
|
||||
|
||||
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ mod imp {
|
|||
#[template_child]
|
||||
pub name_editor: TemplateChild<TranslationEditor>,
|
||||
#[template_child]
|
||||
pub enable_updates_row: TemplateChild<adw::SwitchRow>,
|
||||
#[template_child]
|
||||
pub save_row: TemplateChild<adw::ButtonRow>,
|
||||
}
|
||||
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ mod imp {
|
|||
#[template_child]
|
||||
pub name_editor: TemplateChild<TranslationEditor>,
|
||||
#[template_child]
|
||||
pub enable_updates_row: TemplateChild<adw::SwitchRow>,
|
||||
#[template_child]
|
||||
pub save_row: TemplateChild<adw::ButtonRow>,
|
||||
}
|
||||
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ mod imp {
|
|||
#[template_child]
|
||||
pub name_editor: TemplateChild<TranslationEditor>,
|
||||
#[template_child]
|
||||
pub enable_updates_row: TemplateChild<adw::SwitchRow>,
|
||||
#[template_child]
|
||||
pub save_row: TemplateChild<adw::ButtonRow>,
|
||||
}
|
||||
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ mod imp {
|
|||
#[template_child]
|
||||
pub ensemble_list: TemplateChild<gtk::ListBox>,
|
||||
#[template_child]
|
||||
pub enable_updates_row: TemplateChild<adw::SwitchRow>,
|
||||
#[template_child]
|
||||
pub save_row: TemplateChild<adw::ButtonRow>,
|
||||
}
|
||||
|
||||
|
|
@ -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::<Vec<EnsemblePerformer>>();
|
||||
|
||||
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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ mod imp {
|
|||
#[template_child]
|
||||
pub name_editor: TemplateChild<TranslationEditor>,
|
||||
#[template_child]
|
||||
pub enable_updates_row: TemplateChild<adw::SwitchRow>,
|
||||
#[template_child]
|
||||
pub save_row: TemplateChild<adw::ButtonRow>,
|
||||
}
|
||||
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ mod imp {
|
|||
#[template_child]
|
||||
pub instrument_list: TemplateChild<gtk::ListBox>,
|
||||
#[template_child]
|
||||
pub enable_updates_row: TemplateChild<adw::SwitchRow>,
|
||||
#[template_child]
|
||||
pub save_row: TemplateChild<adw::ButtonRow>,
|
||||
}
|
||||
|
||||
|
|
@ -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::<Vec<Instrument>>();
|
||||
|
||||
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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
2120
src/library.rs
2120
src/library.rs
File diff suppressed because it is too large
Load diff
904
src/library/edit.rs
Normal file
904
src/library/edit.rs
Normal file
|
|
@ -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<Person> {
|
||||
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<Instrument> {
|
||||
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<Role> {
|
||||
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<Work>,
|
||||
persons: Vec<Composer>,
|
||||
instruments: Vec<Instrument>,
|
||||
enable_updates: bool,
|
||||
) -> Result<Work> {
|
||||
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<Work>,
|
||||
persons: Vec<Composer>,
|
||||
instruments: Vec<Instrument>,
|
||||
parent_work_id: Option<&str>,
|
||||
sequence_number: Option<i32>,
|
||||
enable_updates: bool,
|
||||
) -> Result<Work> {
|
||||
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<Work>,
|
||||
persons: Vec<Composer>,
|
||||
instruments: Vec<Instrument>,
|
||||
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<Work>,
|
||||
persons: Vec<Composer>,
|
||||
instruments: Vec<Instrument>,
|
||||
parent_work_id: Option<&str>,
|
||||
sequence_number: Option<i32>,
|
||||
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::<tables::Work>(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<Ensemble> {
|
||||
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<i32>,
|
||||
performers: Vec<Performer>,
|
||||
ensembles: Vec<EnsemblePerformer>,
|
||||
enable_updates: bool,
|
||||
) -> Result<Recording> {
|
||||
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<i32>,
|
||||
performers: Vec<Performer>,
|
||||
ensembles: Vec<EnsemblePerformer>,
|
||||
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::<tables::Track>(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<Recording>,
|
||||
) -> Result<Album> {
|
||||
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<Recording>,
|
||||
) -> 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<Path>,
|
||||
recording_id: &str,
|
||||
recording_index: i32,
|
||||
works: Vec<Work>,
|
||||
) -> 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<Work>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
551
src/library/exchange.rs
Normal file
551
src/library/exchange.rs
Normal file
|
|
@ -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<Path>,
|
||||
) -> Result<async_channel::Receiver<ProcessMsg>> {
|
||||
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::<ProcessMsg>();
|
||||
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<Path>,
|
||||
) -> Result<async_channel::Receiver<ProcessMsg>> {
|
||||
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::<tables::Track>(connection)?;
|
||||
|
||||
let (sender, receiver) = async_channel::unbounded::<ProcessMsg>();
|
||||
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<async_channel::Receiver<ProcessMsg>> {
|
||||
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::<ProcessMsg>();
|
||||
|
||||
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<async_channel::Receiver<ProcessMsg>> {
|
||||
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::<ProcessMsg>();
|
||||
|
||||
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<Path>,
|
||||
library_folder: impl AsRef<Path>,
|
||||
this_connection: Arc<Mutex<SqliteConnection>>,
|
||||
sender: &async_channel::Sender<ProcessMsg>,
|
||||
) -> 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<Path>,
|
||||
library_folder: impl AsRef<Path>,
|
||||
tracks: Vec<tables::Track>,
|
||||
sender: &async_channel::Sender<ProcessMsg>,
|
||||
) -> 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<BufWriter<File>>,
|
||||
library_folder: impl AsRef<Path>,
|
||||
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<Mutex<SqliteConnection>>,
|
||||
sender: &async_channel::Sender<ProcessMsg>,
|
||||
) -> 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<Path>,
|
||||
this_connection: Arc<Mutex<SqliteConnection>>,
|
||||
sender: &async_channel::Sender<ProcessMsg>,
|
||||
) -> 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<Path>,
|
||||
this_connection: Arc<Mutex<SqliteConnection>>,
|
||||
ignore_tracks: bool,
|
||||
) -> Result<Vec<tables::Track>> {
|
||||
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::<tables::Person>(&mut other_connection)?;
|
||||
let roles = roles::table.load::<tables::Role>(&mut other_connection)?;
|
||||
let instruments = instruments::table.load::<tables::Instrument>(&mut other_connection)?;
|
||||
let works = works::table.load::<tables::Work>(&mut other_connection)?;
|
||||
let work_persons = work_persons::table.load::<tables::WorkPerson>(&mut other_connection)?;
|
||||
let work_instruments =
|
||||
work_instruments::table.load::<tables::WorkInstrument>(&mut other_connection)?;
|
||||
let ensembles = ensembles::table.load::<tables::Ensemble>(&mut other_connection)?;
|
||||
let ensemble_persons =
|
||||
ensemble_persons::table.load::<tables::EnsemblePerson>(&mut other_connection)?;
|
||||
let recordings = recordings::table.load::<tables::Recording>(&mut other_connection)?;
|
||||
let recording_persons =
|
||||
recording_persons::table.load::<tables::RecordingPerson>(&mut other_connection)?;
|
||||
let recording_ensembles =
|
||||
recording_ensembles::table.load::<tables::RecordingEnsemble>(&mut other_connection)?;
|
||||
let tracks = tracks::table.load::<tables::Track>(&mut other_connection)?;
|
||||
let track_works = track_works::table.load::<tables::TrackWork>(&mut other_connection)?;
|
||||
let mediums = mediums::table.load::<tables::Medium>(&mut other_connection)?;
|
||||
let albums = albums::table.load::<tables::Album>(&mut other_connection)?;
|
||||
let album_recordings =
|
||||
album_recordings::table.load::<tables::AlbumRecording>(&mut other_connection)?;
|
||||
let album_mediums = album_mediums::table.load::<tables::AlbumMedium>(&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<ProcessMsg>,
|
||||
) -> Result<NamedTempFile> {
|
||||
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<u8> = 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<Path>) -> Result<String> {
|
||||
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::<Result<Vec<String>>>()?
|
||||
.join("/"))
|
||||
}
|
||||
827
src/library/query.rs
Normal file
827
src/library/query.rs
Normal file
|
|
@ -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<Person>,
|
||||
pub performer: Option<Person>,
|
||||
pub ensemble: Option<Ensemble>,
|
||||
pub instrument: Option<Instrument>,
|
||||
pub work: Option<Work>,
|
||||
}
|
||||
|
||||
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<Person>,
|
||||
pub performers: Vec<Person>,
|
||||
pub ensembles: Vec<Ensemble>,
|
||||
pub instruments: Vec<Instrument>,
|
||||
pub works: Vec<Work>,
|
||||
pub recordings: Vec<Recording>,
|
||||
pub albums: Vec<Album>,
|
||||
}
|
||||
|
||||
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<LibraryResults> {
|
||||
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::<Person>(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::<Person>(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::<tables::Ensemble>(connection)?
|
||||
.into_iter()
|
||||
.map(|e| Ensemble::from_table(e, connection))
|
||||
.collect::<Result<Vec<Ensemble>>>()?
|
||||
} 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::<Instrument>(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::<tables::Work>(connection)?
|
||||
.into_iter()
|
||||
.map(|w| Work::from_table(w, connection))
|
||||
.collect::<Result<Vec<Work>>>()?
|
||||
} 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::<tables::Recording>(connection)?
|
||||
.into_iter()
|
||||
.map(|r| Recording::from_table(r, connection))
|
||||
.collect::<Result<Vec<Recording>>>()?
|
||||
} 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::<tables::Album>(connection)?
|
||||
.into_iter()
|
||||
.map(|r| Album::from_table(r, connection))
|
||||
.collect::<Result<Vec<Album>>>()?;
|
||||
|
||||
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::<tables::Recording>(connection)?
|
||||
.into_iter()
|
||||
.map(|r| Recording::from_table(r, connection))
|
||||
.collect::<Result<Vec<Recording>>>()?;
|
||||
|
||||
LibraryResults {
|
||||
recordings,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_recording(&self, program: &Program) -> Result<Recording> {
|
||||
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::<sql_types::Untyped>("( \
|
||||
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::<sql_types::Double, _>(program.prefer_least_recently_played())
|
||||
.sql(" * (least_recently_played - 1.0)) + EXP(10.0 * ")
|
||||
.bind::<sql_types::Double, _>(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::<sql_types::Integer, _>(program.avoid_repeated_instruments())
|
||||
.sql(",
|
||||
1.0
|
||||
),
|
||||
IFNULL(
|
||||
(
|
||||
UNIXEPOCH('now', 'localtime') - UNIXEPOCH(persons.last_played_at)
|
||||
) * 1.0 / ").bind::<sql_types::Integer, _>(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::<tables::Recording>(connection)?;
|
||||
|
||||
Recording::from_table(row, connection)
|
||||
}
|
||||
|
||||
pub fn tracks_for_recording(&self, recording_id: &str) -> Result<Vec<Track>> {
|
||||
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::<tables::Track>(connection)?
|
||||
.into_iter()
|
||||
.map(|t| Track::from_table(t, connection))
|
||||
.collect::<Result<Vec<Track>>>()?;
|
||||
|
||||
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<Vec<Person>> {
|
||||
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<Vec<Role>> {
|
||||
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<Vec<Instrument>> {
|
||||
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<Vec<Work>> {
|
||||
let search = format!("%{}%", search);
|
||||
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
|
||||
|
||||
let works: Vec<Work> = 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::<tables::Work>(connection)?
|
||||
.into_iter()
|
||||
.map(|w| Work::from_table(w, connection))
|
||||
.collect::<Result<Vec<Work>>>()?;
|
||||
|
||||
Ok(works)
|
||||
}
|
||||
|
||||
pub fn search_recordings(&self, work: &Work, search: &str) -> Result<Vec<Recording>> {
|
||||
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::<tables::Recording>(connection)?
|
||||
.into_iter()
|
||||
.map(|r| Recording::from_table(r, connection))
|
||||
.collect::<Result<Vec<Recording>>>()?;
|
||||
|
||||
Ok(recordings)
|
||||
}
|
||||
|
||||
pub fn search_ensembles(&self, search: &str) -> Result<Vec<Ensemble>> {
|
||||
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::<tables::Ensemble>(connection)?
|
||||
.into_iter()
|
||||
.map(|e| Ensemble::from_table(e, connection))
|
||||
.collect::<Result<Vec<Ensemble>>>()?;
|
||||
|
||||
Ok(ensembles)
|
||||
}
|
||||
}
|
||||
|
|
@ -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:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -20,9 +20,15 @@ mod imp {
|
|||
#[template_child]
|
||||
pub play_full_recordings_row: TemplateChild<adw::SwitchRow>,
|
||||
#[template_child]
|
||||
pub use_custom_url_row: TemplateChild<adw::SwitchRow>,
|
||||
pub enable_automatic_metadata_updates_row: TemplateChild<adw::SwitchRow>,
|
||||
#[template_child]
|
||||
pub custom_url_row: TemplateChild<adw::EntryRow>,
|
||||
pub use_custom_metadata_url_row: TemplateChild<adw::SwitchRow>,
|
||||
#[template_child]
|
||||
pub custom_metadata_url_row: TemplateChild<adw::EntryRow>,
|
||||
#[template_child]
|
||||
pub use_custom_library_url_row: TemplateChild<adw::SwitchRow>,
|
||||
#[template_child]
|
||||
pub custom_library_url_row: TemplateChild<adw::EntryRow>,
|
||||
}
|
||||
|
||||
#[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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue