mirror of
				https://github.com/johrpan/musicus.git
				synced 2025-10-26 03:47:23 +01: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 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