mirror of
				https://github.com/johrpan/musicus.git
				synced 2025-10-26 03:47:23 +01:00 
			
		
		
		
	Add original database code
This commit is contained in:
		
							parent
							
								
									08be3cb613
								
							
						
					
					
						commit
						7eacfe21f4
					
				
					 26 changed files with 2059 additions and 24 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -2,3 +2,4 @@ | |||
| /.flatpak/ | ||||
| /.vscode/ | ||||
| /target/ | ||||
| /test.sqlite | ||||
|  |  | |||
							
								
								
									
										422
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										422
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							|  | @ -11,6 +11,21 @@ dependencies = [ | |||
|  "memchr", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "android-tzdata" | ||||
| version = "0.1.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "android_system_properties" | ||||
| version = "0.1.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "anyhow" | ||||
| version = "1.0.71" | ||||
|  | @ -35,6 +50,12 @@ version = "0.1.6" | |||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "bumpalo" | ||||
| version = "3.14.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "cairo-rs" | ||||
| version = "0.18.2" | ||||
|  | @ -82,6 +103,75 @@ version = "1.0.0" | |||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "chrono" | ||||
| version = "0.4.31" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" | ||||
| dependencies = [ | ||||
|  "android-tzdata", | ||||
|  "iana-time-zone", | ||||
|  "js-sys", | ||||
|  "num-traits", | ||||
|  "wasm-bindgen", | ||||
|  "windows-targets", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "core-foundation-sys" | ||||
| version = "0.8.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "deranged" | ||||
| version = "0.3.8" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "diesel" | ||||
| version = "2.1.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "53c8a2cb22327206568569e5a45bb5a2c946455efdd76e24d15b7e82171af95e" | ||||
| dependencies = [ | ||||
|  "diesel_derives", | ||||
|  "libsqlite3-sys", | ||||
|  "time", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "diesel_derives" | ||||
| version = "2.1.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ef8337737574f55a468005a83499da720f20c65586241ffea339db9ecdfd2b44" | ||||
| dependencies = [ | ||||
|  "diesel_table_macro_syntax", | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn 2.0.37", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "diesel_migrations" | ||||
| version = "2.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" | ||||
| dependencies = [ | ||||
|  "diesel", | ||||
|  "migrations_internals", | ||||
|  "migrations_macros", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "diesel_table_macro_syntax" | ||||
| version = "0.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" | ||||
| dependencies = [ | ||||
|  "syn 2.0.37", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "field-offset" | ||||
| version = "0.3.6" | ||||
|  | @ -132,7 +222,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" | |||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn 2.0.18", | ||||
|  "syn 2.0.37", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
|  | @ -213,6 +303,17 @@ dependencies = [ | |||
|  "system-deps", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "getrandom" | ||||
| version = "0.2.10" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "libc", | ||||
|  "wasi", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "gettext-rs" | ||||
| version = "0.7.0" | ||||
|  | @ -299,7 +400,7 @@ dependencies = [ | |||
|  "proc-macro-error", | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn 2.0.18", | ||||
|  "syn 2.0.37", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
|  | @ -443,6 +544,29 @@ version = "0.4.1" | |||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "iana-time-zone" | ||||
| version = "0.1.57" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" | ||||
| dependencies = [ | ||||
|  "android_system_properties", | ||||
|  "core-foundation-sys", | ||||
|  "iana-time-zone-haiku", | ||||
|  "js-sys", | ||||
|  "wasm-bindgen", | ||||
|  "windows", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "iana-time-zone-haiku" | ||||
| version = "0.1.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" | ||||
| dependencies = [ | ||||
|  "cc", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "indexmap" | ||||
| version = "1.9.3" | ||||
|  | @ -453,6 +577,21 @@ dependencies = [ | |||
|  "hashbrown", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "itoa" | ||||
| version = "1.0.9" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "js-sys" | ||||
| version = "0.3.64" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" | ||||
| dependencies = [ | ||||
|  "wasm-bindgen", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "lazy_static" | ||||
| version = "1.4.0" | ||||
|  | @ -497,6 +636,16 @@ version = "0.2.146" | |||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "libsqlite3-sys" | ||||
| version = "0.26.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" | ||||
| dependencies = [ | ||||
|  "pkg-config", | ||||
|  "vcpkg", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "locale_config" | ||||
| version = "0.3.0" | ||||
|  | @ -540,16 +689,43 @@ dependencies = [ | |||
|  "autocfg", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "migrations_internals" | ||||
| version = "2.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" | ||||
| dependencies = [ | ||||
|  "serde", | ||||
|  "toml", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "migrations_macros" | ||||
| version = "2.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" | ||||
| dependencies = [ | ||||
|  "migrations_internals", | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "musicus" | ||||
| version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "chrono", | ||||
|  "diesel", | ||||
|  "diesel_migrations", | ||||
|  "gettext-rs", | ||||
|  "gtk4", | ||||
|  "libadwaita", | ||||
|  "log", | ||||
|  "once_cell", | ||||
|  "rand", | ||||
|  "thiserror", | ||||
|  "tracing-subscriber", | ||||
|  "uuid", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
|  | @ -562,6 +738,15 @@ dependencies = [ | |||
|  "winapi", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "num-traits" | ||||
| version = "0.2.16" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" | ||||
| dependencies = [ | ||||
|  "autocfg", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "objc" | ||||
| version = "0.2.7" | ||||
|  | @ -646,6 +831,12 @@ version = "0.3.27" | |||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "ppv-lite86" | ||||
| version = "0.2.17" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "proc-macro-crate" | ||||
| version = "1.3.1" | ||||
|  | @ -682,9 +873,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "proc-macro2" | ||||
| version = "1.0.60" | ||||
| version = "1.0.67" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" | ||||
| checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" | ||||
| dependencies = [ | ||||
|  "unicode-ident", | ||||
| ] | ||||
|  | @ -698,6 +889,36 @@ dependencies = [ | |||
|  "proc-macro2", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rand" | ||||
| version = "0.8.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
|  "rand_chacha", | ||||
|  "rand_core", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rand_chacha" | ||||
| version = "0.3.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" | ||||
| dependencies = [ | ||||
|  "ppv-lite86", | ||||
|  "rand_core", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rand_core" | ||||
| version = "0.6.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" | ||||
| dependencies = [ | ||||
|  "getrandom", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "regex" | ||||
| version = "1.8.4" | ||||
|  | @ -732,9 +953,23 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" | |||
| 
 | ||||
| [[package]] | ||||
| name = "serde" | ||||
| version = "1.0.164" | ||||
| version = "1.0.188" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" | ||||
| checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" | ||||
| dependencies = [ | ||||
|  "serde_derive", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "serde_derive" | ||||
| version = "1.0.188" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn 2.0.37", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "serde_spanned" | ||||
|  | @ -782,9 +1017,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "syn" | ||||
| version = "2.0.18" | ||||
| version = "2.0.37" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" | ||||
| checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  | @ -833,7 +1068,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" | |||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn 2.0.18", | ||||
|  "syn 2.0.37", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
|  | @ -846,6 +1081,34 @@ dependencies = [ | |||
|  "once_cell", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "time" | ||||
| version = "0.3.29" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" | ||||
| dependencies = [ | ||||
|  "deranged", | ||||
|  "itoa", | ||||
|  "serde", | ||||
|  "time-core", | ||||
|  "time-macros", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "time-core" | ||||
| version = "0.1.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "time-macros" | ||||
| version = "0.2.15" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" | ||||
| dependencies = [ | ||||
|  "time-core", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "toml" | ||||
| version = "0.7.4" | ||||
|  | @ -921,12 +1184,27 @@ version = "1.0.9" | |||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "uuid" | ||||
| version = "1.4.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" | ||||
| dependencies = [ | ||||
|  "getrandom", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "valuable" | ||||
| version = "0.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "vcpkg" | ||||
| version = "0.2.15" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "version-compare" | ||||
| version = "0.1.1" | ||||
|  | @ -939,6 +1217,66 @@ version = "0.9.4" | |||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasi" | ||||
| version = "0.11.0+wasi-snapshot-preview1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasm-bindgen" | ||||
| version = "0.2.87" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "wasm-bindgen-macro", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasm-bindgen-backend" | ||||
| version = "0.2.87" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" | ||||
| dependencies = [ | ||||
|  "bumpalo", | ||||
|  "log", | ||||
|  "once_cell", | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn 2.0.37", | ||||
|  "wasm-bindgen-shared", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasm-bindgen-macro" | ||||
| version = "0.2.87" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" | ||||
| dependencies = [ | ||||
|  "quote", | ||||
|  "wasm-bindgen-macro-support", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasm-bindgen-macro-support" | ||||
| version = "0.2.87" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn 2.0.37", | ||||
|  "wasm-bindgen-backend", | ||||
|  "wasm-bindgen-shared", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasm-bindgen-shared" | ||||
| version = "0.2.87" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "winapi" | ||||
| version = "0.3.9" | ||||
|  | @ -961,6 +1299,72 @@ version = "0.4.0" | |||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "windows" | ||||
| version = "0.48.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" | ||||
| dependencies = [ | ||||
|  "windows-targets", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "windows-targets" | ||||
| version = "0.48.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" | ||||
| dependencies = [ | ||||
|  "windows_aarch64_gnullvm", | ||||
|  "windows_aarch64_msvc", | ||||
|  "windows_i686_gnu", | ||||
|  "windows_i686_msvc", | ||||
|  "windows_x86_64_gnu", | ||||
|  "windows_x86_64_gnullvm", | ||||
|  "windows_x86_64_msvc", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "windows_aarch64_gnullvm" | ||||
| version = "0.48.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "windows_aarch64_msvc" | ||||
| version = "0.48.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "windows_i686_gnu" | ||||
| version = "0.48.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "windows_i686_msvc" | ||||
| version = "0.48.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "windows_x86_64_gnu" | ||||
| version = "0.48.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "windows_x86_64_gnullvm" | ||||
| version = "0.48.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "windows_x86_64_msvc" | ||||
| version = "0.48.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "winnow" | ||||
| version = "0.4.7" | ||||
|  |  | |||
|  | @ -5,8 +5,14 @@ edition = "2021" | |||
| 
 | ||||
| [dependencies] | ||||
| adw = { package = "libadwaita", version = "0.5", features = ["v1_4"]} | ||||
| chrono = "0.4" | ||||
| diesel = { version = "2", features = ["sqlite"] } | ||||
| diesel_migrations = "2" | ||||
| gettext-rs = { version = "0.7", features = ["gettext-system"] } | ||||
| gtk = { package = "gtk4", version = "0.7", features = ["v4_10", "blueprint"]} | ||||
| log = "0.4" | ||||
| once_cell = "1" | ||||
| rand = "0.8" | ||||
| thiserror = "1" | ||||
| tracing-subscriber = "0.3" | ||||
| uuid = { version = "1", features = ["v4"] } | ||||
							
								
								
									
										18
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								README.md
									
										
									
									
									
								
							|  | @ -4,6 +4,24 @@ The classical music player and organizer. | |||
| 
 | ||||
| ## Hacking | ||||
| 
 | ||||
| ### ORM | ||||
| 
 | ||||
| This program uses [Diesel](https://diesel.rs) as its ORM. After installing | ||||
| the Diesel command line utility, you will be able to create a new schema | ||||
| migration using the following command: | ||||
| 
 | ||||
| ``` | ||||
| $ diesel migration generate [change_description] | ||||
| ``` | ||||
| 
 | ||||
| To update the `src/db/schema.rs` file, you should use the following command: | ||||
| 
 | ||||
| ``` | ||||
| $ diesel migration run --database-url test.sqlite | ||||
| ``` | ||||
| 
 | ||||
| This file should never be edited manually. | ||||
| 
 | ||||
| ### Internationalization | ||||
| 
 | ||||
| Execute the following commands from the project root directory to update | ||||
|  |  | |||
|  | @ -12,7 +12,8 @@ | |||
|         "--share=ipc", | ||||
|         "--socket=fallback-x11", | ||||
|         "--device=dri", | ||||
|         "--socket=wayland" | ||||
|         "--socket=wayland", | ||||
|         "--filesystem=host" | ||||
|     ], | ||||
|     "build-options": { | ||||
|         "append-path": "/usr/lib/sdk/rust-stable/bin", | ||||
|  |  | |||
							
								
								
									
										2
									
								
								diesel.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								diesel.toml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | |||
| [print_schema] | ||||
| file = "src/db/schema.rs" | ||||
							
								
								
									
										13
									
								
								migrations/2020-09-27-201047_initial_schema/down.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								migrations/2020-09-27-201047_initial_schema/down.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| PRAGMA defer_foreign_keys; | ||||
| 
 | ||||
| DROP TABLE "persons"; | ||||
| DROP TABLE "instruments"; | ||||
| DROP TABLE "works"; | ||||
| DROP TABLE "instrumentations"; | ||||
| DROP TABLE "work_parts"; | ||||
| DROP TABLE "ensembles"; | ||||
| DROP TABLE "recordings"; | ||||
| DROP TABLE "performances"; | ||||
| DROP TABLE "mediums"; | ||||
| DROP TABLE "tracks"; | ||||
| 
 | ||||
							
								
								
									
										65
									
								
								migrations/2020-09-27-201047_initial_schema/up.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								migrations/2020-09-27-201047_initial_schema/up.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,65 @@ | |||
| CREATE TABLE "persons" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "first_name" TEXT NOT NULL, | ||||
|     "last_name" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "instruments" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "name" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "works" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "composer" TEXT NOT NULL REFERENCES "persons"("id"), | ||||
|     "title" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "instrumentations" ( | ||||
|     "id" BIGINT NOT NULL PRIMARY KEY, | ||||
|     "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, | ||||
|     "instrument" TEXT NOT NULL REFERENCES "instruments"("id") ON DELETE CASCADE | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "work_parts" ( | ||||
|     "id" BIGINT NOT NULL PRIMARY KEY, | ||||
|     "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, | ||||
|     "part_index" BIGINT NOT NULL, | ||||
|     "title" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "ensembles" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "name" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "recordings" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "work" TEXT NOT NULL REFERENCES "works"("id"), | ||||
|     "comment" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "performances" ( | ||||
|     "id" BIGINT NOT NULL PRIMARY KEY, | ||||
|     "recording" TEXT NOT NULL REFERENCES "recordings"("id") ON DELETE CASCADE, | ||||
|     "person" TEXT REFERENCES "persons"("id"), | ||||
|     "ensemble" TEXT REFERENCES "ensembles"("id"), | ||||
|     "role" TEXT REFERENCES "instruments"("id") | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "mediums" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "name" TEXT NOT NULL, | ||||
|     "discid" TEXT | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "tracks" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "medium" TEXT NOT NULL REFERENCES "mediums"("id") ON DELETE CASCADE, | ||||
|     "index" INTEGER NOT NULL, | ||||
|     "recording" TEXT NOT NULL REFERENCES "recordings"("id"), | ||||
|     "work_parts" TEXT NOT NULL, | ||||
|     "source_index" INTEGER NOT NULL, | ||||
|     "path" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
							
								
								
									
										20
									
								
								migrations/2022-04-10-103835_access_history/down.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								migrations/2022-04-10-103835_access_history/down.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| ALTER TABLE "persons" DROP COLUMN "last_used"; | ||||
| ALTER TABLE "persons" DROP COLUMN "last_played"; | ||||
| 
 | ||||
| ALTER TABLE "instruments" DROP COLUMN "last_used"; | ||||
| ALTER TABLE "instruments" DROP COLUMN "last_played"; | ||||
| 
 | ||||
| ALTER TABLE "works" DROP COLUMN "last_used"; | ||||
| ALTER TABLE "works" DROP COLUMN "last_played"; | ||||
| 
 | ||||
| ALTER TABLE "ensembles" DROP COLUMN "last_used"; | ||||
| ALTER TABLE "ensembles" DROP COLUMN "last_played"; | ||||
| 
 | ||||
| ALTER TABLE "recordings" DROP COLUMN "last_used"; | ||||
| ALTER TABLE "recordings" DROP COLUMN "last_played"; | ||||
| 
 | ||||
| ALTER TABLE "mediums" DROP COLUMN "last_used"; | ||||
| ALTER TABLE "mediums" DROP COLUMN "last_played"; | ||||
| 
 | ||||
| ALTER TABLE "tracks" DROP COLUMN "last_used"; | ||||
| ALTER TABLE "tracks" DROP COLUMN "last_played"; | ||||
							
								
								
									
										21
									
								
								migrations/2022-04-10-103835_access_history/up.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								migrations/2022-04-10-103835_access_history/up.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| ALTER TABLE "persons" ADD COLUMN "last_used" BIGINT; | ||||
| ALTER TABLE "persons" ADD COLUMN "last_played" BIGINT; | ||||
| 
 | ||||
| ALTER TABLE "instruments" ADD COLUMN "last_used" BIGINT; | ||||
| ALTER TABLE "instruments" ADD COLUMN "last_played" BIGINT; | ||||
| 
 | ||||
| ALTER TABLE "works" ADD COLUMN "last_used" BIGINT; | ||||
| ALTER TABLE "works" ADD COLUMN "last_played" BIGINT; | ||||
| 
 | ||||
| ALTER TABLE "ensembles" ADD COLUMN "last_used" BIGINT; | ||||
| ALTER TABLE "ensembles" ADD COLUMN "last_played" BIGINT; | ||||
| 
 | ||||
| ALTER TABLE "recordings" ADD COLUMN "last_used" BIGINT; | ||||
| ALTER TABLE "recordings" ADD COLUMN "last_played" BIGINT; | ||||
| 
 | ||||
| ALTER TABLE "mediums" ADD COLUMN "last_used" BIGINT; | ||||
| ALTER TABLE "mediums" ADD COLUMN "last_played" BIGINT; | ||||
| 
 | ||||
| ALTER TABLE "tracks" ADD COLUMN "last_used" BIGINT; | ||||
| ALTER TABLE "tracks" ADD COLUMN "last_played" BIGINT; | ||||
| 
 | ||||
							
								
								
									
										15
									
								
								migrations/2023-02-11-094238_tracks_without_medium/down.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								migrations/2023-02-11-094238_tracks_without_medium/down.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| CREATE TABLE "old_tracks" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "medium" TEXT NOT NULL REFERENCES "mediums"("id") ON DELETE CASCADE, | ||||
|     "index" INTEGER NOT NULL, | ||||
|     "recording" TEXT NOT NULL REFERENCES "recordings"("id"), | ||||
|     "work_parts" TEXT NOT NULL, | ||||
|     "source_index" INTEGER NOT NULL, | ||||
|     "path" TEXT NOT NULL, | ||||
|     "last_used" BIGINT, | ||||
|     "last_played" BIGINT | ||||
| ); | ||||
| 
 | ||||
| INSERT INTO "old_tracks" SELECT * FROM "tracks" WHERE "medium" IS NOT NULL; | ||||
| DROP TABLE "tracks"; | ||||
| ALTER TABLE "old_tracks" RENAME TO "tracks"; | ||||
							
								
								
									
										15
									
								
								migrations/2023-02-11-094238_tracks_without_medium/up.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								migrations/2023-02-11-094238_tracks_without_medium/up.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| CREATE TABLE "new_tracks" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "medium" TEXT REFERENCES "mediums"("id") ON DELETE CASCADE, | ||||
|     "index" INTEGER NOT NULL, | ||||
|     "recording" TEXT NOT NULL REFERENCES "recordings"("id"), | ||||
|     "work_parts" TEXT NOT NULL, | ||||
|     "source_index" INTEGER NOT NULL, | ||||
|     "path" TEXT NOT NULL, | ||||
|     "last_used" BIGINT, | ||||
|     "last_played" BIGINT | ||||
| ); | ||||
| 
 | ||||
| INSERT INTO "new_tracks" SELECT * FROM "tracks"; | ||||
| DROP TABLE "tracks"; | ||||
| ALTER TABLE "new_tracks" RENAME TO "tracks"; | ||||
|  | @ -1,10 +1,8 @@ | |||
| use crate::{config::VERSION, MusicusWindow}; | ||||
| use adw::subclass::prelude::*; | ||||
| use gettextrs::gettext; | ||||
| use gtk::{gio, glib, prelude::*}; | ||||
| 
 | ||||
| use crate::config::VERSION; | ||||
| use crate::MusicusWindow; | ||||
| 
 | ||||
| mod imp { | ||||
|     use super::*; | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										74
									
								
								src/db/ensembles.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/db/ensembles.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,74 @@ | |||
| use chrono::Utc; | ||||
| use diesel::prelude::*; | ||||
| use log::info; | ||||
| 
 | ||||
| use crate::db::{defer_foreign_keys, schema::ensembles, Result}; | ||||
| 
 | ||||
| /// An ensemble that takes part in recordings.
 | ||||
| #[derive(Insertable, Queryable, PartialEq, Eq, Hash, Debug, Clone)] | ||||
| pub struct Ensemble { | ||||
|     pub id: String, | ||||
|     pub name: String, | ||||
|     pub last_used: Option<i64>, | ||||
|     pub last_played: Option<i64>, | ||||
| } | ||||
| 
 | ||||
| impl Ensemble { | ||||
|     pub fn new(id: String, name: String) -> Self { | ||||
|         Self { | ||||
|             id, | ||||
|             name, | ||||
|             last_used: Some(Utc::now().timestamp()), | ||||
|             last_played: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Update an existing ensemble or insert a new one.
 | ||||
| pub fn update_ensemble(connection: &mut SqliteConnection, mut ensemble: Ensemble) -> Result<()> { | ||||
|     info!("Updating ensemble {:?}", ensemble); | ||||
|     defer_foreign_keys(connection)?; | ||||
| 
 | ||||
|     ensemble.last_used = Some(Utc::now().timestamp()); | ||||
| 
 | ||||
|     connection.transaction(|connection| { | ||||
|         diesel::replace_into(ensembles::table) | ||||
|             .values(ensemble) | ||||
|             .execute(connection) | ||||
|     })?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /// Get an existing ensemble.
 | ||||
| pub fn get_ensemble(connection: &mut SqliteConnection, id: &str) -> Result<Option<Ensemble>> { | ||||
|     let ensemble = ensembles::table | ||||
|         .filter(ensembles::id.eq(id)) | ||||
|         .load::<Ensemble>(connection)? | ||||
|         .into_iter() | ||||
|         .next(); | ||||
| 
 | ||||
|     Ok(ensemble) | ||||
| } | ||||
| 
 | ||||
| /// Delete an existing ensemble.
 | ||||
| pub fn delete_ensemble(connection: &mut SqliteConnection, id: &str) -> Result<()> { | ||||
|     info!("Deleting ensemble {}", id); | ||||
|     diesel::delete(ensembles::table.filter(ensembles::id.eq(id))).execute(connection)?; | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /// Get all existing ensembles.
 | ||||
| pub fn get_ensembles(connection: &mut SqliteConnection) -> Result<Vec<Ensemble>> { | ||||
|     let ensembles = ensembles::table.load::<Ensemble>(connection)?; | ||||
|     Ok(ensembles) | ||||
| } | ||||
| 
 | ||||
| /// Get recently used ensembles.
 | ||||
| pub fn get_recent_ensembles(connection: &mut SqliteConnection) -> Result<Vec<Ensemble>> { | ||||
|     let ensembles = ensembles::table | ||||
|         .order(ensembles::last_used.desc()) | ||||
|         .load::<Ensemble>(connection)?; | ||||
| 
 | ||||
|     Ok(ensembles) | ||||
| } | ||||
							
								
								
									
										24
									
								
								src/db/error.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/db/error.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| /// Error that happens within the database module.
 | ||||
| #[derive(thiserror::Error, Debug)] | ||||
| pub enum Error { | ||||
|     #[error(transparent)] | ||||
|     Connection(#[from] diesel::result::ConnectionError), | ||||
| 
 | ||||
|     #[error(transparent)] | ||||
|     Migrations(#[from] Box<dyn std::error::Error + Send + Sync>), | ||||
| 
 | ||||
|     #[error(transparent)] | ||||
|     Query(#[from] diesel::result::Error), | ||||
| 
 | ||||
|     #[error("Missing item dependency ({0} {1})")] | ||||
|     MissingItem(&'static str, String), | ||||
| 
 | ||||
|     #[error("Failed to parse {0} from '{1}'")] | ||||
|     Parsing(&'static str, String), | ||||
| 
 | ||||
|     #[error("{0}")] | ||||
|     Other(&'static str), | ||||
| } | ||||
| 
 | ||||
| /// Return type for database methods.
 | ||||
| pub type Result<T> = std::result::Result<T, Error>; | ||||
							
								
								
									
										79
									
								
								src/db/instruments.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/db/instruments.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | |||
| use chrono::Utc; | ||||
| use diesel::prelude::*; | ||||
| use log::info; | ||||
| 
 | ||||
| use crate::db::{defer_foreign_keys, schema::instruments, Result}; | ||||
| 
 | ||||
| /// An instrument or any other possible role within a recording.
 | ||||
| #[derive(Insertable, Queryable, PartialEq, Eq, Hash, Debug, Clone)] | ||||
| pub struct Instrument { | ||||
|     pub id: String, | ||||
|     pub name: String, | ||||
|     pub last_used: Option<i64>, | ||||
|     pub last_played: Option<i64>, | ||||
| } | ||||
| 
 | ||||
| impl Instrument { | ||||
|     pub fn new(id: String, name: String) -> Self { | ||||
|         Self { | ||||
|             id, | ||||
|             name, | ||||
|             last_used: Some(Utc::now().timestamp()), | ||||
|             last_played: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Update an existing instrument or insert a new one.
 | ||||
| pub fn update_instrument( | ||||
|     connection: &mut SqliteConnection, | ||||
|     mut instrument: Instrument, | ||||
| ) -> Result<()> { | ||||
|     info!("Updating instrument {:?}", instrument); | ||||
|     defer_foreign_keys(connection)?; | ||||
| 
 | ||||
|     instrument.last_used = Some(Utc::now().timestamp()); | ||||
| 
 | ||||
|     connection.transaction(|connection| { | ||||
|         diesel::replace_into(instruments::table) | ||||
|             .values(instrument) | ||||
|             .execute(connection) | ||||
|     })?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /// Get an existing instrument.
 | ||||
| pub fn get_instrument(connection: &mut SqliteConnection, id: &str) -> Result<Option<Instrument>> { | ||||
|     let instrument = instruments::table | ||||
|         .filter(instruments::id.eq(id)) | ||||
|         .load::<Instrument>(connection)? | ||||
|         .into_iter() | ||||
|         .next(); | ||||
| 
 | ||||
|     Ok(instrument) | ||||
| } | ||||
| 
 | ||||
| /// Delete an existing instrument.
 | ||||
| pub fn delete_instrument(connection: &mut SqliteConnection, id: &str) -> Result<()> { | ||||
|     info!("Deleting instrument {}", id); | ||||
|     diesel::delete(instruments::table.filter(instruments::id.eq(id))).execute(connection)?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /// Get all existing instruments.
 | ||||
| pub fn get_instruments(connection: &mut SqliteConnection) -> Result<Vec<Instrument>> { | ||||
|     let instruments = instruments::table.load::<Instrument>(connection)?; | ||||
| 
 | ||||
|     Ok(instruments) | ||||
| } | ||||
| 
 | ||||
| /// Get recently used instruments.
 | ||||
| pub fn get_recent_instruments(connection: &mut SqliteConnection) -> Result<Vec<Instrument>> { | ||||
|     let instruments = instruments::table | ||||
|         .order(instruments::last_used.desc()) | ||||
|         .load::<Instrument>(connection)?; | ||||
| 
 | ||||
|     Ok(instruments) | ||||
| } | ||||
							
								
								
									
										351
									
								
								src/db/medium.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										351
									
								
								src/db/medium.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,351 @@ | |||
| use chrono::{DateTime, TimeZone, Utc}; | ||||
| use diesel::prelude::*; | ||||
| use log::info; | ||||
| 
 | ||||
| use crate::db::{ | ||||
|     defer_foreign_keys, generate_id, get_recording, | ||||
|     schema::{ensembles, mediums, performances, persons, recordings, tracks}, | ||||
|     update_recording, Error, Recording, Result, | ||||
| }; | ||||
| 
 | ||||
| /// Representation of someting like a physical audio disc or a folder with
 | ||||
| /// audio files (i.e. a collection of tracks for one or more recordings).
 | ||||
| #[derive(PartialEq, Eq, Hash, Debug, Clone)] | ||||
| pub struct Medium { | ||||
|     /// An unique ID for the medium.
 | ||||
|     pub id: String, | ||||
| 
 | ||||
|     /// The human identifier for the medium.
 | ||||
|     pub name: String, | ||||
| 
 | ||||
|     /// If applicable, the MusicBrainz DiscID.
 | ||||
|     pub discid: Option<String>, | ||||
| 
 | ||||
|     /// The tracks of the medium.
 | ||||
|     pub tracks: Vec<Track>, | ||||
| 
 | ||||
|     pub last_used: Option<DateTime<Utc>>, | ||||
|     pub last_played: Option<DateTime<Utc>>, | ||||
| } | ||||
| 
 | ||||
| impl Medium { | ||||
|     pub fn new(id: String, name: String, discid: Option<String>, tracks: Vec<Track>) -> Self { | ||||
|         Self { | ||||
|             id, | ||||
|             name, | ||||
|             discid, | ||||
|             tracks, | ||||
|             last_used: Some(Utc::now()), | ||||
|             last_played: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// A track on a medium.
 | ||||
| #[derive(PartialEq, Eq, Hash, Debug, Clone)] | ||||
| pub struct Track { | ||||
|     /// The recording on this track.
 | ||||
|     pub recording: Recording, | ||||
| 
 | ||||
|     /// The work parts that are played on this track. They are indices to the
 | ||||
|     /// work parts of the work that is associated with the recording.
 | ||||
|     pub work_parts: Vec<usize>, | ||||
| 
 | ||||
|     /// The index of the track within its source. This is used to associate
 | ||||
|     /// the metadata with the audio data from the source when importing.
 | ||||
|     pub source_index: usize, | ||||
| 
 | ||||
|     /// The path to the audio file containing this track.
 | ||||
|     pub path: String, | ||||
| 
 | ||||
|     pub last_used: Option<DateTime<Utc>>, | ||||
|     pub last_played: Option<DateTime<Utc>>, | ||||
| } | ||||
| 
 | ||||
| impl Track { | ||||
|     pub fn new( | ||||
|         recording: Recording, | ||||
|         work_parts: Vec<usize>, | ||||
|         source_index: usize, | ||||
|         path: String, | ||||
|     ) -> Self { | ||||
|         Self { | ||||
|             recording, | ||||
|             work_parts, | ||||
|             source_index, | ||||
|             path, | ||||
|             last_used: Some(Utc::now()), | ||||
|             last_played: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Table data for a [`Medium`].
 | ||||
| #[derive(Insertable, Queryable, Debug, Clone)] | ||||
| #[diesel(table_name = mediums)] | ||||
| struct MediumRow { | ||||
|     pub id: String, | ||||
|     pub name: String, | ||||
|     pub discid: Option<String>, | ||||
|     pub last_used: Option<i64>, | ||||
|     pub last_played: Option<i64>, | ||||
| } | ||||
| 
 | ||||
| /// Table data for a [`Track`].
 | ||||
| #[derive(Insertable, Queryable, QueryableByName, Debug, Clone)] | ||||
| #[diesel(table_name = tracks)] | ||||
| struct TrackRow { | ||||
|     pub id: String, | ||||
|     pub medium: Option<String>, | ||||
|     pub index: i32, | ||||
|     pub recording: String, | ||||
|     pub work_parts: String, | ||||
|     pub source_index: i32, | ||||
|     pub path: String, | ||||
|     pub last_used: Option<i64>, | ||||
|     pub last_played: Option<i64>, | ||||
| } | ||||
| 
 | ||||
| /// Update an existing medium or insert a new one.
 | ||||
| pub fn update_medium(connection: &mut SqliteConnection, medium: Medium) -> Result<()> { | ||||
|     info!("Updating medium {:?}", medium); | ||||
|     defer_foreign_keys(connection)?; | ||||
| 
 | ||||
|     connection.transaction::<(), Error, _>(|connection| { | ||||
|         let medium_id = &medium.id; | ||||
| 
 | ||||
|         // This will also delete the tracks.
 | ||||
|         delete_medium(connection, medium_id)?; | ||||
| 
 | ||||
|         // Add the new medium.
 | ||||
| 
 | ||||
|         let medium_row = MediumRow { | ||||
|             id: medium_id.to_owned(), | ||||
|             name: medium.name.clone(), | ||||
|             discid: medium.discid.clone(), | ||||
|             last_used: Some(Utc::now().timestamp()), | ||||
|             last_played: medium.last_played.map(|t| t.timestamp()), | ||||
|         }; | ||||
| 
 | ||||
|         diesel::insert_into(mediums::table) | ||||
|             .values(medium_row) | ||||
|             .execute(connection)?; | ||||
| 
 | ||||
|         for (index, track) in medium.tracks.iter().enumerate() { | ||||
|             // Add associated items from the server, if they don't already exist.
 | ||||
| 
 | ||||
|             if get_recording(connection, &track.recording.id)?.is_none() { | ||||
|                 update_recording(connection, track.recording.clone())?; | ||||
|             } | ||||
| 
 | ||||
|             // Add the actual track data.
 | ||||
| 
 | ||||
|             let work_parts = track | ||||
|                 .work_parts | ||||
|                 .iter() | ||||
|                 .map(|part_index| part_index.to_string()) | ||||
|                 .collect::<Vec<String>>() | ||||
|                 .join(","); | ||||
| 
 | ||||
|             let track_row = TrackRow { | ||||
|                 id: generate_id(), | ||||
|                 medium: Some(medium_id.to_owned()), | ||||
|                 index: index as i32, | ||||
|                 recording: track.recording.id.clone(), | ||||
|                 work_parts, | ||||
|                 source_index: track.source_index as i32, | ||||
|                 path: track.path.clone(), | ||||
|                 last_used: Some(Utc::now().timestamp()), | ||||
|                 last_played: track.last_played.map(|t| t.timestamp()), | ||||
|             }; | ||||
| 
 | ||||
|             diesel::insert_into(tracks::table) | ||||
|                 .values(track_row) | ||||
|                 .execute(connection)?; | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     })?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /// Get an existing medium.
 | ||||
| pub fn get_medium(connection: &mut SqliteConnection, id: &str) -> Result<Option<Medium>> { | ||||
|     let row = mediums::table | ||||
|         .filter(mediums::id.eq(id)) | ||||
|         .load::<MediumRow>(connection)? | ||||
|         .into_iter() | ||||
|         .next(); | ||||
| 
 | ||||
|     let medium = match row { | ||||
|         Some(row) => Some(get_medium_data(connection, row)?), | ||||
|         None => None, | ||||
|     }; | ||||
| 
 | ||||
|     Ok(medium) | ||||
| } | ||||
| 
 | ||||
| /// Get mediums that have a specific source ID.
 | ||||
| pub fn get_mediums_by_source_id( | ||||
|     connection: &mut SqliteConnection, | ||||
|     source_id: &str, | ||||
| ) -> Result<Vec<Medium>> { | ||||
|     let mut mediums: Vec<Medium> = Vec::new(); | ||||
| 
 | ||||
|     let rows = mediums::table | ||||
|         .filter(mediums::discid.nullable().eq(source_id)) | ||||
|         .load::<MediumRow>(connection)?; | ||||
| 
 | ||||
|     for row in rows { | ||||
|         let medium = get_medium_data(connection, row)?; | ||||
|         mediums.push(medium); | ||||
|     } | ||||
| 
 | ||||
|     Ok(mediums) | ||||
| } | ||||
| 
 | ||||
| /// Get mediums on which this person is performing.
 | ||||
| pub fn get_mediums_for_person( | ||||
|     connection: &mut SqliteConnection, | ||||
|     person_id: &str, | ||||
| ) -> Result<Vec<Medium>> { | ||||
|     let mut mediums: Vec<Medium> = Vec::new(); | ||||
| 
 | ||||
|     let rows = mediums::table | ||||
|         .inner_join(tracks::table.on(tracks::medium.eq(mediums::id.nullable()))) | ||||
|         .inner_join(recordings::table.on(recordings::id.eq(tracks::recording))) | ||||
|         .inner_join(performances::table.on(performances::recording.eq(recordings::id))) | ||||
|         .inner_join(persons::table.on(persons::id.nullable().eq(performances::person))) | ||||
|         .filter(persons::id.eq(person_id)) | ||||
|         .select(mediums::table::all_columns()) | ||||
|         .distinct() | ||||
|         .load::<MediumRow>(connection)?; | ||||
| 
 | ||||
|     for row in rows { | ||||
|         let medium = get_medium_data(connection, row)?; | ||||
|         mediums.push(medium); | ||||
|     } | ||||
| 
 | ||||
|     Ok(mediums) | ||||
| } | ||||
| 
 | ||||
| /// Get mediums on which this ensemble is performing.
 | ||||
| pub fn get_mediums_for_ensemble( | ||||
|     connection: &mut SqliteConnection, | ||||
|     ensemble_id: &str, | ||||
| ) -> Result<Vec<Medium>> { | ||||
|     let mut mediums: Vec<Medium> = Vec::new(); | ||||
| 
 | ||||
|     let rows = mediums::table | ||||
|         .inner_join(tracks::table.on(tracks::medium.eq(tracks::id.nullable()))) | ||||
|         .inner_join(recordings::table.on(recordings::id.eq(tracks::recording))) | ||||
|         .inner_join(performances::table.on(performances::recording.eq(recordings::id))) | ||||
|         .inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble))) | ||||
|         .filter(ensembles::id.eq(ensemble_id)) | ||||
|         .select(mediums::table::all_columns()) | ||||
|         .distinct() | ||||
|         .load::<MediumRow>(connection)?; | ||||
| 
 | ||||
|     for row in rows { | ||||
|         let medium = get_medium_data(connection, row)?; | ||||
|         mediums.push(medium); | ||||
|     } | ||||
| 
 | ||||
|     Ok(mediums) | ||||
| } | ||||
| 
 | ||||
| /// Delete a medium and all of its tracks. This will fail, if the music
 | ||||
| /// library contains audio files referencing any of those tracks.
 | ||||
| pub fn delete_medium(connection: &mut SqliteConnection, id: &str) -> Result<()> { | ||||
|     info!("Deleting medium {}", id); | ||||
|     diesel::delete(mediums::table.filter(mediums::id.eq(id))).execute(connection)?; | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /// Get all available tracks for a recording.
 | ||||
| pub fn get_tracks(connection: &mut SqliteConnection, recording_id: &str) -> Result<Vec<Track>> { | ||||
|     let mut tracks: Vec<Track> = Vec::new(); | ||||
| 
 | ||||
|     let rows = tracks::table | ||||
|         .inner_join(recordings::table.on(recordings::id.eq(tracks::recording))) | ||||
|         .filter(recordings::id.eq(recording_id)) | ||||
|         .select(tracks::table::all_columns()) | ||||
|         .load::<TrackRow>(connection)?; | ||||
| 
 | ||||
|     for row in rows { | ||||
|         let track = get_track_from_row(connection, row)?; | ||||
|         tracks.push(track); | ||||
|     } | ||||
| 
 | ||||
|     Ok(tracks) | ||||
| } | ||||
| 
 | ||||
| /// Get a random track from the database.
 | ||||
| pub fn random_track(connection: &mut SqliteConnection) -> Result<Track> { | ||||
|     let row = diesel::sql_query("SELECT * FROM tracks ORDER BY RANDOM() LIMIT 1") | ||||
|         .load::<TrackRow>(connection)? | ||||
|         .into_iter() | ||||
|         .next() | ||||
|         .ok_or(Error::Other("Failed to generate random track"))?; | ||||
| 
 | ||||
|     get_track_from_row(connection, row) | ||||
| } | ||||
| 
 | ||||
| /// Retrieve all available information on a medium from related tables.
 | ||||
| fn get_medium_data(connection: &mut SqliteConnection, row: MediumRow) -> Result<Medium> { | ||||
|     let track_rows = tracks::table | ||||
|         .filter(tracks::medium.eq(&row.id)) | ||||
|         .order_by(tracks::index) | ||||
|         .load::<TrackRow>(connection)?; | ||||
| 
 | ||||
|     let mut tracks = Vec::new(); | ||||
| 
 | ||||
|     for track_row in track_rows { | ||||
|         let track = get_track_from_row(connection, track_row)?; | ||||
|         tracks.push(track); | ||||
|     } | ||||
| 
 | ||||
|     let medium = Medium { | ||||
|         id: row.id, | ||||
|         name: row.name, | ||||
|         discid: row.discid, | ||||
|         tracks, | ||||
|         last_used: row.last_used.map(|t| Utc.timestamp_opt(t, 0).unwrap()), | ||||
|         last_played: row.last_played.map(|t| Utc.timestamp_opt(t, 0).unwrap()), | ||||
|     }; | ||||
| 
 | ||||
|     Ok(medium) | ||||
| } | ||||
| 
 | ||||
| /// Convert a track row from the database to an actual track.
 | ||||
| fn get_track_from_row(connection: &mut SqliteConnection, row: TrackRow) -> Result<Track> { | ||||
|     let recording_id = row.recording; | ||||
| 
 | ||||
|     let recording = get_recording(connection, &recording_id)? | ||||
|         .ok_or(Error::MissingItem("recording", recording_id))?; | ||||
| 
 | ||||
|     let mut part_indices = Vec::new(); | ||||
| 
 | ||||
|     let work_parts = row.work_parts.split(','); | ||||
| 
 | ||||
|     for part_index in work_parts { | ||||
|         if !part_index.is_empty() { | ||||
|             let index = str::parse(part_index) | ||||
|                 .map_err(|_| Error::Parsing("part index", String::from(part_index)))?; | ||||
| 
 | ||||
|             part_indices.push(index); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     let track = Track { | ||||
|         recording, | ||||
|         work_parts: part_indices, | ||||
|         source_index: row.source_index as usize, | ||||
|         path: row.path, | ||||
|         last_used: row.last_used.map(|t| Utc.timestamp_opt(t, 0).unwrap()), | ||||
|         last_played: row.last_played.map(|t| Utc.timestamp_opt(t, 0).unwrap()), | ||||
|     }; | ||||
| 
 | ||||
|     Ok(track) | ||||
| } | ||||
							
								
								
									
										54
									
								
								src/db/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/db/mod.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| use diesel::prelude::*; | ||||
| use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; | ||||
| use log::info; | ||||
| 
 | ||||
| pub use diesel::SqliteConnection; | ||||
| 
 | ||||
| pub mod ensembles; | ||||
| pub use ensembles::*; | ||||
| 
 | ||||
| pub mod error; | ||||
| pub use error::*; | ||||
| 
 | ||||
| pub mod instruments; | ||||
| pub use instruments::*; | ||||
| 
 | ||||
| pub mod medium; | ||||
| pub use medium::*; | ||||
| 
 | ||||
| pub mod persons; | ||||
| pub use persons::*; | ||||
| 
 | ||||
| pub mod recordings; | ||||
| pub use recordings::*; | ||||
| 
 | ||||
| pub mod works; | ||||
| pub use works::*; | ||||
| 
 | ||||
| mod schema; | ||||
| 
 | ||||
| // This makes the SQL migration scripts accessible from the code.
 | ||||
| const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); | ||||
| 
 | ||||
| /// Connect to a Musicus database running migrations if necessary.
 | ||||
| pub fn connect(file_name: &str) -> Result<SqliteConnection> { | ||||
|     info!("Opening database file '{}'", file_name); | ||||
|     let mut connection = SqliteConnection::establish(file_name)?; | ||||
|     diesel::sql_query("PRAGMA foreign_keys = ON").execute(&mut connection)?; | ||||
| 
 | ||||
|     info!("Running migrations if necessary"); | ||||
|     connection.run_pending_migrations(MIGRATIONS)?; | ||||
| 
 | ||||
|     Ok(connection) | ||||
| } | ||||
| 
 | ||||
| /// Generate a random string suitable as an item ID.
 | ||||
| pub fn generate_id() -> String { | ||||
|     uuid::Uuid::new_v4().simple().to_string() | ||||
| } | ||||
| 
 | ||||
| /// Defer all foreign keys for the next transaction.
 | ||||
| fn defer_foreign_keys(connection: &mut SqliteConnection) -> Result<()> { | ||||
|     diesel::sql_query("PRAGMA defer_foreign_keys = ON").execute(connection)?; | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										86
									
								
								src/db/persons.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/db/persons.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | |||
| use chrono::Utc; | ||||
| use diesel::prelude::*; | ||||
| use log::info; | ||||
| 
 | ||||
| use crate::db::{defer_foreign_keys, schema::persons, Result}; | ||||
| 
 | ||||
| /// A person that is a composer, an interpret or both.
 | ||||
| #[derive(Insertable, Queryable, PartialEq, Eq, Hash, Debug, Clone)] | ||||
| pub struct Person { | ||||
|     pub id: String, | ||||
|     pub first_name: String, | ||||
|     pub last_name: String, | ||||
|     pub last_used: Option<i64>, | ||||
|     pub last_played: Option<i64>, | ||||
| } | ||||
| 
 | ||||
| impl Person { | ||||
|     pub fn new(id: String, first_name: String, last_name: String) -> Self { | ||||
|         Self { | ||||
|             id, | ||||
|             first_name, | ||||
|             last_name, | ||||
|             last_used: Some(Utc::now().timestamp()), | ||||
|             last_played: None, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Get the full name in the form "First Last".
 | ||||
|     pub fn name_fl(&self) -> String { | ||||
|         format!("{} {}", self.first_name, self.last_name) | ||||
|     } | ||||
| 
 | ||||
|     /// Get the full name in the form "Last, First".
 | ||||
|     pub fn name_lf(&self) -> String { | ||||
|         format!("{}, {}", self.last_name, self.first_name) | ||||
|     } | ||||
| } | ||||
| /// Update an existing person or insert a new one.
 | ||||
| pub fn update_person(connection: &mut SqliteConnection, mut person: Person) -> Result<()> { | ||||
|     info!("Updating person {:?}", person); | ||||
|     defer_foreign_keys(connection)?; | ||||
| 
 | ||||
|     person.last_used = Some(Utc::now().timestamp()); | ||||
| 
 | ||||
|     connection.transaction(|connection| { | ||||
|         diesel::replace_into(persons::table) | ||||
|             .values(person) | ||||
|             .execute(connection) | ||||
|     })?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /// Get an existing person.
 | ||||
| pub fn get_person(connection: &mut SqliteConnection, id: &str) -> Result<Option<Person>> { | ||||
|     let person = persons::table | ||||
|         .filter(persons::id.eq(id)) | ||||
|         .load::<Person>(connection)? | ||||
|         .into_iter() | ||||
|         .next(); | ||||
| 
 | ||||
|     Ok(person) | ||||
| } | ||||
| 
 | ||||
| /// Delete an existing person.
 | ||||
| pub fn delete_person(connection: &mut SqliteConnection, id: &str) -> Result<()> { | ||||
|     info!("Deleting person {}", id); | ||||
|     diesel::delete(persons::table.filter(persons::id.eq(id))).execute(connection)?; | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /// Get all existing persons.
 | ||||
| pub fn get_persons(connection: &mut SqliteConnection) -> Result<Vec<Person>> { | ||||
|     let persons = persons::table.load::<Person>(connection)?; | ||||
| 
 | ||||
|     Ok(persons) | ||||
| } | ||||
| 
 | ||||
| /// Get recently used persons.
 | ||||
| pub fn get_recent_persons(connection: &mut SqliteConnection) -> Result<Vec<Person>> { | ||||
|     let persons = persons::table | ||||
|         .order(persons::last_used.desc()) | ||||
|         .load::<Person>(connection)?; | ||||
| 
 | ||||
|     Ok(persons) | ||||
| } | ||||
							
								
								
									
										350
									
								
								src/db/recordings.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										350
									
								
								src/db/recordings.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,350 @@ | |||
| use chrono::{DateTime, TimeZone, Utc}; | ||||
| use diesel::prelude::*; | ||||
| use log::info; | ||||
| 
 | ||||
| use crate::db::{ | ||||
|     defer_foreign_keys, generate_id, get_ensemble, get_instrument, get_person, get_work, | ||||
|     schema::{ensembles, performances, persons, recordings}, | ||||
|     update_ensemble, update_instrument, update_person, update_work, Ensemble, Error, Instrument, | ||||
|     Person, Result, Work, | ||||
| }; | ||||
| 
 | ||||
| /// A specific recording of a work.
 | ||||
| #[derive(PartialEq, Eq, Hash, Debug, Clone)] | ||||
| pub struct Recording { | ||||
|     pub id: String, | ||||
|     pub work: Work, | ||||
|     pub comment: String, | ||||
|     pub performances: Vec<Performance>, | ||||
|     pub last_used: Option<DateTime<Utc>>, | ||||
|     pub last_played: Option<DateTime<Utc>>, | ||||
| } | ||||
| 
 | ||||
| impl Recording { | ||||
|     pub fn new(id: String, work: Work, comment: String, performances: Vec<Performance>) -> Self { | ||||
|         Self { | ||||
|             id, | ||||
|             work, | ||||
|             comment, | ||||
|             performances, | ||||
|             last_used: Some(Utc::now()), | ||||
|             last_played: None, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Initialize a new recording with a work.
 | ||||
|     pub fn from_work(work: Work) -> Self { | ||||
|         Self { | ||||
|             id: generate_id(), | ||||
|             work, | ||||
|             comment: String::new(), | ||||
|             performances: Vec::new(), | ||||
|             last_used: Some(Utc::now()), | ||||
|             last_played: None, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Get a string representation of the performances in this recording.
 | ||||
|     // TODO: Maybe replace with impl Display?
 | ||||
|     pub fn get_performers(&self) -> String { | ||||
|         let texts: Vec<String> = self | ||||
|             .performances | ||||
|             .iter() | ||||
|             .map(|performance| performance.get_title()) | ||||
|             .collect(); | ||||
| 
 | ||||
|         texts.join(", ") | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// How a person or ensemble was involved in a recording.
 | ||||
| #[derive(PartialEq, Eq, Hash, Debug, Clone)] | ||||
| pub struct Performance { | ||||
|     pub performer: PersonOrEnsemble, | ||||
|     pub role: Option<Instrument>, | ||||
| } | ||||
| 
 | ||||
| impl Performance { | ||||
|     /// Get a string representation of the performance.
 | ||||
|     // TODO: Replace with impl Display.
 | ||||
|     pub fn get_title(&self) -> String { | ||||
|         let performer_title = self.performer.get_title(); | ||||
| 
 | ||||
|         if let Some(role) = &self.role { | ||||
|             format!("{} ({})", performer_title, role.name) | ||||
|         } else { | ||||
|             performer_title | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Either a person or an ensemble.
 | ||||
| #[derive(PartialEq, Eq, Hash, Clone, Debug)] | ||||
| pub enum PersonOrEnsemble { | ||||
|     Person(Person), | ||||
|     Ensemble(Ensemble), | ||||
| } | ||||
| 
 | ||||
| impl PersonOrEnsemble { | ||||
|     /// Get a short textual representation of the item.
 | ||||
|     pub fn get_title(&self) -> String { | ||||
|         match self { | ||||
|             PersonOrEnsemble::Person(person) => person.name_lf(), | ||||
|             PersonOrEnsemble::Ensemble(ensemble) => ensemble.name.clone(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Database table data for a recording.
 | ||||
| #[derive(Insertable, Queryable, QueryableByName, Debug, Clone)] | ||||
| #[diesel(table_name = recordings)] | ||||
| struct RecordingRow { | ||||
|     pub id: String, | ||||
|     pub work: String, | ||||
|     pub comment: String, | ||||
|     pub last_used: Option<i64>, | ||||
|     pub last_played: Option<i64>, | ||||
| } | ||||
| 
 | ||||
| impl From<Recording> for RecordingRow { | ||||
|     fn from(recording: Recording) -> Self { | ||||
|         RecordingRow { | ||||
|             id: recording.id, | ||||
|             work: recording.work.id, | ||||
|             comment: recording.comment, | ||||
|             last_used: Some(Utc::now().timestamp()), | ||||
|             last_played: recording.last_played.map(|t| t.timestamp()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Database table data for a performance.
 | ||||
| #[derive(Insertable, Queryable, Debug, Clone)] | ||||
| #[diesel(table_name = performances)] | ||||
| struct PerformanceRow { | ||||
|     pub id: i64, | ||||
|     pub recording: String, | ||||
|     pub person: Option<String>, | ||||
|     pub ensemble: Option<String>, | ||||
|     pub role: Option<String>, | ||||
| } | ||||
| 
 | ||||
| /// Update an existing recording or insert a new one.
 | ||||
| // TODO: Think about whether to also insert the other items.
 | ||||
| pub fn update_recording(connection: &mut SqliteConnection, recording: Recording) -> Result<()> { | ||||
|     info!("Updating recording {:?}", recording); | ||||
|     defer_foreign_keys(connection)?; | ||||
| 
 | ||||
|     connection.transaction::<(), Error, _>(|connection| { | ||||
|         let recording_id = &recording.id; | ||||
|         delete_recording(connection, recording_id)?; | ||||
| 
 | ||||
|         // Add associated items from the server, if they don't already exist.
 | ||||
| 
 | ||||
|         if get_work(connection, &recording.work.id)?.is_none() { | ||||
|             update_work(connection, recording.work.clone())?; | ||||
|         } | ||||
| 
 | ||||
|         for performance in &recording.performances { | ||||
|             match &performance.performer { | ||||
|                 PersonOrEnsemble::Person(person) => { | ||||
|                     if get_person(connection, &person.id)?.is_none() { | ||||
|                         update_person(connection, person.clone())?; | ||||
|                     } | ||||
|                 } | ||||
|                 PersonOrEnsemble::Ensemble(ensemble) => { | ||||
|                     if get_ensemble(connection, &ensemble.id)?.is_none() { | ||||
|                         update_ensemble(connection, ensemble.clone())?; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if let Some(role) = &performance.role { | ||||
|                 if get_instrument(connection, &role.id)?.is_none() { | ||||
|                     update_instrument(connection, role.clone())?; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Add the actual recording.
 | ||||
| 
 | ||||
|         let row: RecordingRow = recording.clone().into(); | ||||
|         diesel::insert_into(recordings::table) | ||||
|             .values(row) | ||||
|             .execute(connection)?; | ||||
| 
 | ||||
|         for performance in recording.performances { | ||||
|             let (person, ensemble) = match performance.performer { | ||||
|                 PersonOrEnsemble::Person(person) => (Some(person.id), None), | ||||
|                 PersonOrEnsemble::Ensemble(ensemble) => (None, Some(ensemble.id)), | ||||
|             }; | ||||
| 
 | ||||
|             let row = PerformanceRow { | ||||
|                 id: rand::random(), | ||||
|                 recording: recording_id.to_string(), | ||||
|                 person, | ||||
|                 ensemble, | ||||
|                 role: performance.role.map(|role| role.id), | ||||
|             }; | ||||
| 
 | ||||
|             diesel::insert_into(performances::table) | ||||
|                 .values(row) | ||||
|                 .execute(connection)?; | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     })?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /// Check whether the database contains a recording.
 | ||||
| pub fn recording_exists(connection: &mut SqliteConnection, id: &str) -> Result<bool> { | ||||
|     let exists = recordings::table | ||||
|         .filter(recordings::id.eq(id)) | ||||
|         .load::<RecordingRow>(connection)? | ||||
|         .first() | ||||
|         .is_some(); | ||||
| 
 | ||||
|     Ok(exists) | ||||
| } | ||||
| 
 | ||||
| /// Get an existing recording.
 | ||||
| pub fn get_recording(connection: &mut SqliteConnection, id: &str) -> Result<Option<Recording>> { | ||||
|     let row = recordings::table | ||||
|         .filter(recordings::id.eq(id)) | ||||
|         .load::<RecordingRow>(connection)? | ||||
|         .into_iter() | ||||
|         .next(); | ||||
| 
 | ||||
|     let recording = match row { | ||||
|         Some(row) => Some(get_recording_data(connection, row)?), | ||||
|         None => None, | ||||
|     }; | ||||
| 
 | ||||
|     Ok(recording) | ||||
| } | ||||
| 
 | ||||
| /// Get a random recording from the database.
 | ||||
| pub fn random_recording(connection: &mut SqliteConnection) -> Result<Recording> { | ||||
|     let row = diesel::sql_query("SELECT * FROM recordings ORDER BY RANDOM() LIMIT 1") | ||||
|         .load::<RecordingRow>(connection)? | ||||
|         .into_iter() | ||||
|         .next() | ||||
|         .ok_or(Error::Other("Failed to find random recording."))?; | ||||
| 
 | ||||
|     get_recording_data(connection, row) | ||||
| } | ||||
| 
 | ||||
| /// Retrieve all available information on a recording from related tables.
 | ||||
| fn get_recording_data(connection: &mut SqliteConnection, row: RecordingRow) -> Result<Recording> { | ||||
|     let mut performance_descriptions: Vec<Performance> = Vec::new(); | ||||
| 
 | ||||
|     let performance_rows = performances::table | ||||
|         .filter(performances::recording.eq(&row.id)) | ||||
|         .load::<PerformanceRow>(connection)?; | ||||
| 
 | ||||
|     for row in performance_rows { | ||||
|         performance_descriptions.push(Performance { | ||||
|             performer: if let Some(id) = row.person { | ||||
|                 PersonOrEnsemble::Person( | ||||
|                     get_person(connection, &id)?.ok_or(Error::MissingItem("person", id))?, | ||||
|                 ) | ||||
|             } else if let Some(id) = row.ensemble { | ||||
|                 PersonOrEnsemble::Ensemble( | ||||
|                     get_ensemble(connection, &id)?.ok_or(Error::MissingItem("ensemble", id))?, | ||||
|                 ) | ||||
|             } else { | ||||
|                 return Err(Error::Other("Performance without performer")); | ||||
|             }, | ||||
|             role: match row.role { | ||||
|                 Some(id) => Some( | ||||
|                     get_instrument(connection, &id)?.ok_or(Error::MissingItem("instrument", id))?, | ||||
|                 ), | ||||
|                 None => None, | ||||
|             }, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     let work_id = row.work; | ||||
|     let work = get_work(connection, &work_id)?.ok_or(Error::MissingItem("work", work_id))?; | ||||
| 
 | ||||
|     let recording_description = Recording { | ||||
|         id: row.id, | ||||
|         work, | ||||
|         comment: row.comment, | ||||
|         performances: performance_descriptions, | ||||
|         last_used: row.last_used.map(|t| Utc.timestamp_opt(t, 0).unwrap()), | ||||
|         last_played: row.last_played.map(|t| Utc.timestamp_opt(t, 0).unwrap()), | ||||
|     }; | ||||
| 
 | ||||
|     Ok(recording_description) | ||||
| } | ||||
| 
 | ||||
| /// Get all available information on all recordings where a person is performing.
 | ||||
| pub fn get_recordings_for_person( | ||||
|     connection: &mut SqliteConnection, | ||||
|     person_id: &str, | ||||
| ) -> Result<Vec<Recording>> { | ||||
|     let mut recordings: Vec<Recording> = Vec::new(); | ||||
| 
 | ||||
|     let rows = recordings::table | ||||
|         .inner_join(performances::table.on(performances::recording.eq(recordings::id))) | ||||
|         .inner_join(persons::table.on(persons::id.nullable().eq(performances::person))) | ||||
|         .filter(persons::id.eq(person_id)) | ||||
|         .select(recordings::table::all_columns()) | ||||
|         .load::<RecordingRow>(connection)?; | ||||
| 
 | ||||
|     for row in rows { | ||||
|         recordings.push(get_recording_data(connection, row)?); | ||||
|     } | ||||
| 
 | ||||
|     Ok(recordings) | ||||
| } | ||||
| 
 | ||||
| /// Get all available information on all recordings where an ensemble is performing.
 | ||||
| pub fn get_recordings_for_ensemble( | ||||
|     connection: &mut SqliteConnection, | ||||
|     ensemble_id: &str, | ||||
| ) -> Result<Vec<Recording>> { | ||||
|     let mut recordings: Vec<Recording> = Vec::new(); | ||||
| 
 | ||||
|     let rows = recordings::table | ||||
|         .inner_join(performances::table.on(performances::recording.eq(recordings::id))) | ||||
|         .inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble))) | ||||
|         .filter(ensembles::id.eq(ensemble_id)) | ||||
|         .select(recordings::table::all_columns()) | ||||
|         .load::<RecordingRow>(connection)?; | ||||
| 
 | ||||
|     for row in rows { | ||||
|         recordings.push(get_recording_data(connection, row)?); | ||||
|     } | ||||
| 
 | ||||
|     Ok(recordings) | ||||
| } | ||||
| 
 | ||||
| /// Get allavailable information on all recordings of a work.
 | ||||
| pub fn get_recordings_for_work( | ||||
|     connection: &mut SqliteConnection, | ||||
|     work_id: &str, | ||||
| ) -> Result<Vec<Recording>> { | ||||
|     let mut recordings: Vec<Recording> = Vec::new(); | ||||
| 
 | ||||
|     let rows = recordings::table | ||||
|         .filter(recordings::work.eq(work_id)) | ||||
|         .load::<RecordingRow>(connection)?; | ||||
| 
 | ||||
|     for row in rows { | ||||
|         recordings.push(get_recording_data(connection, row)?); | ||||
|     } | ||||
| 
 | ||||
|     Ok(recordings) | ||||
| } | ||||
| 
 | ||||
| /// Delete an existing recording. This will fail if there are still references to this
 | ||||
| /// recording from other tables that are not directly part of the recording data.
 | ||||
| pub fn delete_recording(connection: &mut SqliteConnection, id: &str) -> Result<()> { | ||||
|     info!("Deleting recording {}", id); | ||||
|     diesel::delete(recordings::table.filter(recordings::id.eq(id))).execute(connection)?; | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										125
									
								
								src/db/schema.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/db/schema.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,125 @@ | |||
| // @generated automatically by Diesel CLI.
 | ||||
| 
 | ||||
| diesel::table! { | ||||
|     ensembles (id) { | ||||
|         id -> Text, | ||||
|         name -> Text, | ||||
|         last_used -> Nullable<BigInt>, | ||||
|         last_played -> Nullable<BigInt>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     instrumentations (id) { | ||||
|         id -> BigInt, | ||||
|         work -> Text, | ||||
|         instrument -> Text, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     instruments (id) { | ||||
|         id -> Text, | ||||
|         name -> Text, | ||||
|         last_used -> Nullable<BigInt>, | ||||
|         last_played -> Nullable<BigInt>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     mediums (id) { | ||||
|         id -> Text, | ||||
|         name -> Text, | ||||
|         discid -> Nullable<Text>, | ||||
|         last_used -> Nullable<BigInt>, | ||||
|         last_played -> Nullable<BigInt>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     performances (id) { | ||||
|         id -> BigInt, | ||||
|         recording -> Text, | ||||
|         person -> Nullable<Text>, | ||||
|         ensemble -> Nullable<Text>, | ||||
|         role -> Nullable<Text>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     persons (id) { | ||||
|         id -> Text, | ||||
|         first_name -> Text, | ||||
|         last_name -> Text, | ||||
|         last_used -> Nullable<BigInt>, | ||||
|         last_played -> Nullable<BigInt>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     recordings (id) { | ||||
|         id -> Text, | ||||
|         work -> Text, | ||||
|         comment -> Text, | ||||
|         last_used -> Nullable<BigInt>, | ||||
|         last_played -> Nullable<BigInt>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     tracks (id) { | ||||
|         id -> Text, | ||||
|         medium -> Nullable<Text>, | ||||
|         index -> Integer, | ||||
|         recording -> Text, | ||||
|         work_parts -> Text, | ||||
|         source_index -> Integer, | ||||
|         path -> Text, | ||||
|         last_used -> Nullable<BigInt>, | ||||
|         last_played -> Nullable<BigInt>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     work_parts (id) { | ||||
|         id -> BigInt, | ||||
|         work -> Text, | ||||
|         part_index -> BigInt, | ||||
|         title -> Text, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     works (id) { | ||||
|         id -> Text, | ||||
|         composer -> Text, | ||||
|         title -> Text, | ||||
|         last_used -> Nullable<BigInt>, | ||||
|         last_played -> Nullable<BigInt>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::joinable!(instrumentations -> instruments (instrument)); | ||||
| diesel::joinable!(instrumentations -> works (work)); | ||||
| diesel::joinable!(performances -> ensembles (ensemble)); | ||||
| diesel::joinable!(performances -> instruments (role)); | ||||
| diesel::joinable!(performances -> persons (person)); | ||||
| diesel::joinable!(performances -> recordings (recording)); | ||||
| diesel::joinable!(recordings -> works (work)); | ||||
| diesel::joinable!(tracks -> mediums (medium)); | ||||
| diesel::joinable!(tracks -> recordings (recording)); | ||||
| diesel::joinable!(work_parts -> works (work)); | ||||
| diesel::joinable!(works -> persons (composer)); | ||||
| 
 | ||||
| diesel::allow_tables_to_appear_in_same_query!( | ||||
|     ensembles, | ||||
|     instrumentations, | ||||
|     instruments, | ||||
|     mediums, | ||||
|     performances, | ||||
|     persons, | ||||
|     recordings, | ||||
|     tracks, | ||||
|     work_parts, | ||||
|     works, | ||||
| ); | ||||
							
								
								
									
										252
									
								
								src/db/works.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								src/db/works.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,252 @@ | |||
| use chrono::{DateTime, TimeZone, Utc}; | ||||
| use diesel::{prelude::*, Insertable, Queryable}; | ||||
| use log::info; | ||||
| 
 | ||||
| use crate::db::{ | ||||
|     defer_foreign_keys, generate_id, get_instrument, get_person, | ||||
|     schema::{instrumentations, work_parts, works}, | ||||
|     update_instrument, update_person, Error, Instrument, Person, Result, | ||||
| }; | ||||
| 
 | ||||
| /// Table row data for a work.
 | ||||
| #[derive(Insertable, Queryable, Debug, Clone)] | ||||
| #[diesel(table_name = works)] | ||||
| struct WorkRow { | ||||
|     pub id: String, | ||||
|     pub composer: String, | ||||
|     pub title: String, | ||||
|     pub last_used: Option<i64>, | ||||
|     pub last_played: Option<i64>, | ||||
| } | ||||
| 
 | ||||
| impl From<Work> for WorkRow { | ||||
|     fn from(work: Work) -> Self { | ||||
|         WorkRow { | ||||
|             id: work.id, | ||||
|             composer: work.composer.id, | ||||
|             title: work.title, | ||||
|             last_used: Some(Utc::now().timestamp()), | ||||
|             last_played: work.last_played.map(|t| t.timestamp()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Definition that a work uses an instrument.
 | ||||
| #[derive(Insertable, Queryable, Debug, Clone)] | ||||
| #[diesel(table_name = instrumentations)] | ||||
| struct InstrumentationRow { | ||||
|     pub id: i64, | ||||
|     pub work: String, | ||||
|     pub instrument: String, | ||||
| } | ||||
| 
 | ||||
| /// Table row data for a work part.
 | ||||
| #[derive(Insertable, Queryable, Debug, Clone)] | ||||
| #[diesel(table_name = work_parts)] | ||||
| struct WorkPartRow { | ||||
|     pub id: i64, | ||||
|     pub work: String, | ||||
|     pub part_index: i64, | ||||
|     pub title: String, | ||||
| } | ||||
| 
 | ||||
| /// A concrete work part that can be recorded.
 | ||||
| #[derive(PartialEq, Eq, Hash, Debug, Clone)] | ||||
| pub struct WorkPart { | ||||
|     pub title: String, | ||||
| } | ||||
| 
 | ||||
| /// A specific work by a composer.
 | ||||
| #[derive(PartialEq, Eq, Hash, Debug, Clone)] | ||||
| pub struct Work { | ||||
|     pub id: String, | ||||
|     pub title: String, | ||||
|     pub composer: Person, | ||||
|     pub instruments: Vec<Instrument>, | ||||
|     pub parts: Vec<WorkPart>, | ||||
|     pub last_used: Option<DateTime<Utc>>, | ||||
|     pub last_played: Option<DateTime<Utc>>, | ||||
| } | ||||
| 
 | ||||
| impl Work { | ||||
|     pub fn new( | ||||
|         id: String, | ||||
|         title: String, | ||||
|         composer: Person, | ||||
|         instruments: Vec<Instrument>, | ||||
|         parts: Vec<WorkPart>, | ||||
|     ) -> Self { | ||||
|         Self { | ||||
|             id, | ||||
|             title, | ||||
|             composer, | ||||
|             instruments, | ||||
|             parts, | ||||
|             last_used: Some(Utc::now()), | ||||
|             last_played: None, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Initialize a new work with a composer.
 | ||||
|     pub fn from_composer(composer: Person) -> Self { | ||||
|         Self { | ||||
|             id: generate_id(), | ||||
|             title: String::new(), | ||||
|             composer, | ||||
|             instruments: Vec::new(), | ||||
|             parts: Vec::new(), | ||||
|             last_used: Some(Utc::now()), | ||||
|             last_played: None, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Get a string including the composer and title of the work.
 | ||||
|     // TODO: Replace with impl Display.
 | ||||
|     pub fn get_title(&self) -> String { | ||||
|         format!("{}: {}", self.composer.name_fl(), self.title) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Update an existing work or insert a new one.
 | ||||
| // TODO: Think about also inserting related items.
 | ||||
| pub fn update_work(connection: &mut SqliteConnection, work: Work) -> Result<()> { | ||||
|     info!("Updating work {:?}", work); | ||||
|     defer_foreign_keys(connection)?; | ||||
| 
 | ||||
|     connection.transaction::<(), Error, _>(|connection| { | ||||
|         let work_id = &work.id; | ||||
|         delete_work(connection, work_id)?; | ||||
| 
 | ||||
|         // Add associated items from the server, if they don't already exist.
 | ||||
| 
 | ||||
|         if get_person(connection, &work.composer.id)?.is_none() { | ||||
|             update_person(connection, work.composer.clone())?; | ||||
|         } | ||||
| 
 | ||||
|         for instrument in &work.instruments { | ||||
|             if get_instrument(connection, &instrument.id)?.is_none() { | ||||
|                 update_instrument(connection, instrument.clone())?; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Add the actual work.
 | ||||
| 
 | ||||
|         let row: WorkRow = work.clone().into(); | ||||
|         diesel::insert_into(works::table) | ||||
|             .values(row) | ||||
|             .execute(connection)?; | ||||
| 
 | ||||
|         let Work { | ||||
|             instruments, parts, .. | ||||
|         } = work; | ||||
| 
 | ||||
|         for instrument in instruments { | ||||
|             let row = InstrumentationRow { | ||||
|                 id: rand::random(), | ||||
|                 work: work_id.to_string(), | ||||
|                 instrument: instrument.id, | ||||
|             }; | ||||
| 
 | ||||
|             diesel::insert_into(instrumentations::table) | ||||
|                 .values(row) | ||||
|                 .execute(connection)?; | ||||
|         } | ||||
| 
 | ||||
|         for (index, part) in parts.into_iter().enumerate() { | ||||
|             let row = WorkPartRow { | ||||
|                 id: rand::random(), | ||||
|                 work: work_id.to_string(), | ||||
|                 part_index: index as i64, | ||||
|                 title: part.title, | ||||
|             }; | ||||
| 
 | ||||
|             diesel::insert_into(work_parts::table) | ||||
|                 .values(row) | ||||
|                 .execute(connection)?; | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     })?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /// Get an existing work.
 | ||||
| pub fn get_work(connection: &mut SqliteConnection, id: &str) -> Result<Option<Work>> { | ||||
|     let row = works::table | ||||
|         .filter(works::id.eq(id)) | ||||
|         .load::<WorkRow>(connection)? | ||||
|         .first() | ||||
|         .cloned(); | ||||
| 
 | ||||
|     let work = match row { | ||||
|         Some(row) => Some(get_work_data(connection, row)?), | ||||
|         None => None, | ||||
|     }; | ||||
| 
 | ||||
|     Ok(work) | ||||
| } | ||||
| 
 | ||||
| /// Retrieve all available information on a work from related tables.
 | ||||
| fn get_work_data(connection: &mut SqliteConnection, row: WorkRow) -> Result<Work> { | ||||
|     let mut instruments: Vec<Instrument> = Vec::new(); | ||||
| 
 | ||||
|     let instrumentations = instrumentations::table | ||||
|         .filter(instrumentations::work.eq(&row.id)) | ||||
|         .load::<InstrumentationRow>(connection)?; | ||||
| 
 | ||||
|     for instrumentation in instrumentations { | ||||
|         let id = instrumentation.instrument; | ||||
|         instruments | ||||
|             .push(get_instrument(connection, &id)?.ok_or(Error::MissingItem("instrument", id))?); | ||||
|     } | ||||
| 
 | ||||
|     let mut parts: Vec<WorkPart> = Vec::new(); | ||||
| 
 | ||||
|     let part_rows = work_parts::table | ||||
|         .filter(work_parts::work.eq(&row.id)) | ||||
|         .load::<WorkPartRow>(connection)?; | ||||
| 
 | ||||
|     for part_row in part_rows { | ||||
|         parts.push(WorkPart { | ||||
|             title: part_row.title, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     let person_id = row.composer; | ||||
|     let person = | ||||
|         get_person(connection, &person_id)?.ok_or(Error::MissingItem("person", person_id))?; | ||||
| 
 | ||||
|     Ok(Work { | ||||
|         id: row.id, | ||||
|         composer: person, | ||||
|         title: row.title, | ||||
|         instruments, | ||||
|         parts, | ||||
|         last_used: row.last_used.map(|t| Utc.timestamp_opt(t, 0).unwrap()), | ||||
|         last_played: row.last_played.map(|t| Utc.timestamp_opt(t, 0).unwrap()), | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| /// Delete an existing work. This will fail if there are still other tables that relate to
 | ||||
| /// this work except for the things that are part of the information on the work it
 | ||||
| pub fn delete_work(connection: &mut SqliteConnection, id: &str) -> Result<()> { | ||||
|     info!("Deleting work {}", id); | ||||
|     diesel::delete(works::table.filter(works::id.eq(id))).execute(connection)?; | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /// Get all existing works by a composer and related information from other tables.
 | ||||
| pub fn get_works(connection: &mut SqliteConnection, composer_id: &str) -> Result<Vec<Work>> { | ||||
|     let mut works: Vec<Work> = Vec::new(); | ||||
| 
 | ||||
|     let rows = works::table | ||||
|         .filter(works::composer.eq(composer_id)) | ||||
|         .load::<WorkRow>(connection)?; | ||||
| 
 | ||||
|     for row in rows { | ||||
|         works.push(get_work_data(connection, row)?); | ||||
|     } | ||||
| 
 | ||||
|     Ok(works) | ||||
| } | ||||
|  | @ -1,10 +1,12 @@ | |||
| use crate::{player::MusicusPlayer, tile::MusicusTile, search_entry::MusicusSearchEntry}; | ||||
| use crate::{ | ||||
|     library::MusicusLibrary, player::MusicusPlayer, search_entry::MusicusSearchEntry, | ||||
|     tile::MusicusTile, | ||||
| }; | ||||
| use adw::subclass::{navigation_page::NavigationPageImpl, prelude::*}; | ||||
| use gtk::{glib, glib::Properties, prelude::*}; | ||||
| use std::cell::RefCell; | ||||
| use std::cell::{OnceCell, RefCell}; | ||||
| 
 | ||||
| mod imp { | ||||
| 
 | ||||
|     use super::*; | ||||
| 
 | ||||
|     #[derive(Properties, Debug, Default, gtk::CompositeTemplate)] | ||||
|  | @ -14,6 +16,8 @@ mod imp { | |||
|         #[property(get, set)] | ||||
|         pub player: RefCell<MusicusPlayer>, | ||||
| 
 | ||||
|         pub library: OnceCell<MusicusLibrary>, | ||||
| 
 | ||||
|         #[template_child] | ||||
|         pub search_entry: TemplateChild<MusicusSearchEntry>, | ||||
|         #[template_child] | ||||
|  | @ -75,8 +79,10 @@ glib::wrapper! { | |||
| 
 | ||||
| #[gtk::template_callbacks] | ||||
| impl MusicusHomePage { | ||||
|     pub fn new(player: &MusicusPlayer) -> Self { | ||||
|         glib::Object::builder().property("player", player).build() | ||||
|     pub fn new(library: &MusicusLibrary, player: &MusicusPlayer) -> Self { | ||||
|         let obj: MusicusHomePage = glib::Object::builder().property("player", player).build(); | ||||
|         obj.imp().library.set(library.to_owned()).unwrap(); | ||||
|         obj | ||||
|     } | ||||
| 
 | ||||
|     #[template_callback] | ||||
|  |  | |||
							
								
								
									
										53
									
								
								src/library.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/library.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | |||
| use crate::db::{self, SqliteConnection}; | ||||
| use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*}; | ||||
| use std::{ | ||||
|     cell::{OnceCell, RefCell}, | ||||
|     path::Path, | ||||
| }; | ||||
| 
 | ||||
| mod imp { | ||||
|     use super::*; | ||||
| 
 | ||||
|     #[derive(Properties, Default)] | ||||
|     #[properties(wrapper_type = super::MusicusLibrary)] | ||||
|     pub struct MusicusLibrary { | ||||
|         #[property(get, set)] | ||||
|         pub folder: RefCell<String>, | ||||
|         pub connection: OnceCell<SqliteConnection>, | ||||
|     } | ||||
| 
 | ||||
|     #[glib::object_subclass] | ||||
|     impl ObjectSubclass for MusicusLibrary { | ||||
|         const NAME: &'static str = "MusicusLibrary"; | ||||
|         type Type = super::MusicusLibrary; | ||||
|     } | ||||
| 
 | ||||
|     #[glib::derived_properties] | ||||
|     impl ObjectImpl for MusicusLibrary {} | ||||
| } | ||||
| 
 | ||||
| glib::wrapper! { | ||||
|     pub struct MusicusLibrary(ObjectSubclass<imp::MusicusLibrary>); | ||||
| } | ||||
| 
 | ||||
| impl MusicusLibrary { | ||||
|     pub fn new(path: impl AsRef<Path>) -> Self { | ||||
|         let path = path.as_ref(); | ||||
|         let obj: MusicusLibrary = glib::Object::builder() | ||||
|             .property("folder", path.to_str().unwrap()) | ||||
|             .build(); | ||||
| 
 | ||||
|         let connection = db::connect(path.join("musicus.db").to_str().unwrap()).unwrap(); | ||||
| 
 | ||||
|         obj.imp() | ||||
|             .connection | ||||
|             .set(connection) | ||||
|             .unwrap_or_else(|_| panic!("Database connection already set")); | ||||
| 
 | ||||
|         obj | ||||
|     } | ||||
| 
 | ||||
|     pub fn db(&self) -> &SqliteConnection { | ||||
|         self.imp().connection.get().unwrap() | ||||
|     } | ||||
| } | ||||
|  | @ -1,6 +1,8 @@ | |||
| mod db; | ||||
| mod application; | ||||
| mod config; | ||||
| mod home_page; | ||||
| mod library; | ||||
| mod player; | ||||
| mod playlist_page; | ||||
| mod search_entry; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| use crate::{ | ||||
|     home_page::MusicusHomePage, player::MusicusPlayer, playlist_page::MusicusPlaylistPage, | ||||
|     welcome_page::MusicusWelcomePage, | ||||
|     home_page::MusicusHomePage, library::MusicusLibrary, player::MusicusPlayer, | ||||
|     playlist_page::MusicusPlaylistPage, welcome_page::MusicusWelcomePage, | ||||
| }; | ||||
| 
 | ||||
| use adw::subclass::prelude::*; | ||||
|  | @ -125,11 +125,11 @@ impl MusicusWindow { | |||
| 
 | ||||
|     #[template_callback] | ||||
|     fn set_library_folder(&self, folder: &gio::File) { | ||||
|         let path = folder.path(); | ||||
|         log::info!("{path:?}"); | ||||
|         let path = folder.path().unwrap(); | ||||
|         let library = MusicusLibrary::new(path); | ||||
|         self.imp() | ||||
|             .navigation_view | ||||
|             .replace(&[MusicusHomePage::new(&self.imp().player).into()]); | ||||
|             .replace(&[MusicusHomePage::new(&library, &self.imp().player).into()]); | ||||
|     } | ||||
| 
 | ||||
|     #[template_callback] | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue