diff --git a/Cargo.lock b/Cargo.lock index 9198034..294a0ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bitflags" version = "1.3.2" @@ -103,6 +109,12 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + [[package]] name = "bytes" version = "1.5.0" @@ -167,6 +179,22 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "crossterm" version = "0.27.0" @@ -204,6 +232,21 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.8" @@ -220,6 +263,27 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -235,12 +299,79 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "h2" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -275,6 +406,77 @@ dependencies = [ "libc", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.5.0" @@ -285,12 +487,28 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "2.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "indoc" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "itertools" version = "0.12.1" @@ -306,6 +524,15 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -355,6 +582,12 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -413,6 +646,7 @@ dependencies = [ "once_cell", "openssh", "ratatui", + "reqwest", "serde", "serde_json", "structopt", @@ -423,6 +657,24 @@ dependencies = [ "version_check", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "non-zero-byte-slice" version = "0.1.0" @@ -491,6 +743,50 @@ dependencies = [ "thiserror", ] +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -532,6 +828,18 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "predicates" version = "3.1.0" @@ -629,6 +937,46 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "reqwest" +version = "0.11.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -648,6 +996,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -660,12 +1017,44 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "sendfd" version = "0.4.3" @@ -707,6 +1096,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "shell-escape" version = "0.1.5" @@ -743,6 +1144,15 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.1" @@ -868,6 +1278,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" +dependencies = [ + "bitflags 2.4.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.10.0" @@ -967,6 +1404,16 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-pipe" version = "0.2.12" @@ -977,6 +1424,51 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typed-builder" version = "0.18.1" @@ -1047,6 +1539,12 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" @@ -1059,12 +1557,97 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[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.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1219,6 +1802,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index 099f65f..a0f8045 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ crossterm = { version = "0.27.0", optional = true} once_cell = { version = "1.19.0", optional = true} openssh = { version = "0.10.3", features = ["native-mux"], default-features = false, optional = true} ratatui = { version = "0.26.0", optional = true} +reqwest = { version = "0.11.25", features = ["blocking", "json"], optional = true } serde = { version = "1.0.196", features = ["derive"], optional = true } serde_json = { version = "1.0.113", optional = true} structopt = { version = "0.3.26", optional = true} @@ -32,6 +33,7 @@ bin = ["structopt"] database-json = ["serde", "serde_json"] library-beets = [] library-beets-ssh = ["openssh", "tokio"] +musicbrainz-api = ["reqwest", "serde", "serde_json"] tui = ["aho-corasick", "crossterm", "once_cell", "ratatui"] [[bin]] @@ -42,5 +44,9 @@ required-features = ["bin", "database-json", "library-beets", "library-beets-ssh name = "musichoard-edit" required-features = ["bin", "database-json"] +[[bin]] +name = "musichoard-reqwest" +required-features = ["musicbrainz-api"] + [package.metadata.docs.rs] all-features = true diff --git a/README.md b/README.md index f254767..77cd1da 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,18 @@ # Music Hoard +## Developing + +### Pre-requisites + +#### musicbrainz-api + +This feature requires the `openssl` system library. + +On Fedora: +``` sh +sudo dnf install openssl-devel +``` + ## Usage notes ### Text selection diff --git a/src/bin/musichoard-edit.rs b/src/bin/musichoard-edit.rs index 8d06e32..1881574 100644 --- a/src/bin/musichoard-edit.rs +++ b/src/bin/musichoard-edit.rs @@ -4,7 +4,7 @@ use structopt::{clap::AppSettings, StructOpt}; use musichoard::{ collection::{album::AlbumId, artist::ArtistId}, - database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, + external::database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, IMusicHoardDatabase, MusicHoard, MusicHoardBuilder, NoLibrary, }; diff --git a/src/core/collection/album.rs b/src/core/collection/album.rs index be1e8a6..e272894 100644 --- a/src/core/collection/album.rs +++ b/src/core/collection/album.rs @@ -5,7 +5,7 @@ use std::{ use crate::core::collection::{ merge::{Merge, MergeSorted, WithId}, - musicbrainz::MusicBrainzUrl, + musicbrainz::MbAlbumRef, track::{Track, TrackFormat}, }; @@ -15,7 +15,9 @@ pub struct Album { pub id: AlbumId, pub date: AlbumDate, pub seq: AlbumSeq, - pub musicbrainz: Option, + pub musicbrainz: Option, + pub primary_type: Option, + pub secondary_types: Vec, pub tracks: Vec, } @@ -35,83 +37,34 @@ pub struct AlbumId { // There are crates for handling dates, but we don't need much complexity beyond year-month-day. /// The album's release date. -#[derive(Clone, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct AlbumDate { - pub year: u32, - pub month: AlbumMonth, - pub day: u8, + pub year: Option, + pub month: Option, + pub day: Option, } impl AlbumDate { - pub fn new>(year: u32, month: M, day: u8) -> Self { - AlbumDate { - year, - month: month.into(), - day, - } + pub fn new(year: Option, month: Option, day: Option) -> Self { + AlbumDate { year, month, day } } } impl From for AlbumDate { fn from(value: u32) -> Self { - AlbumDate::new(value, AlbumMonth::default(), 0) + AlbumDate::new(Some(value), None, None) } } -impl> From<(u32, M)> for AlbumDate { - fn from(value: (u32, M)) -> Self { - AlbumDate::new(value.0, value.1, 0) +impl From<(u32, u8)> for AlbumDate { + fn from(value: (u32, u8)) -> Self { + AlbumDate::new(Some(value.0), Some(value.1), None) } } -impl> From<(u32, M, u8)> for AlbumDate { - fn from(value: (u32, M, u8)) -> Self { - AlbumDate::new(value.0, value.1, value.2) - } -} - -#[repr(u8)] -#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)] -pub enum AlbumMonth { - #[default] - None = 0, - January = 1, - February = 2, - March = 3, - April = 4, - May = 5, - June = 6, - July = 7, - August = 8, - September = 9, - October = 10, - November = 11, - December = 12, -} - -impl From for AlbumMonth { - fn from(value: u8) -> Self { - match value { - 1 => AlbumMonth::January, - 2 => AlbumMonth::February, - 3 => AlbumMonth::March, - 4 => AlbumMonth::April, - 5 => AlbumMonth::May, - 6 => AlbumMonth::June, - 7 => AlbumMonth::July, - 8 => AlbumMonth::August, - 9 => AlbumMonth::September, - 10 => AlbumMonth::October, - 11 => AlbumMonth::November, - 12 => AlbumMonth::December, - _ => AlbumMonth::None, - } - } -} - -impl AlbumMonth { - pub fn is_none(&self) -> bool { - matches!(self, AlbumMonth::None) +impl From<(u32, u8, u8)> for AlbumDate { + fn from(value: (u32, u8, u8)) -> Self { + AlbumDate::new(Some(value.0), Some(value.1), Some(value.2)) } } @@ -119,6 +72,50 @@ impl AlbumMonth { #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)] pub struct AlbumSeq(pub u8); +/// Based on [MusicBrainz types](https://musicbrainz.org/doc/Release_Group/Type). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AlbumPrimaryType { + /// Album + Album, + /// Single + Single, + /// EP + Ep, + /// Broadcast + Broadcast, + /// Other + Other, +} + +/// Based on [MusicBrainz types](https://musicbrainz.org/doc/Release_Group/Type). +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum AlbumSecondaryType { + /// Compilation + Compilation, + /// Soundtrack + Soundtrack, + /// Spokenword + Spokenword, + /// Interview + Interview, + /// Audiobook + Audiobook, + /// Audio drama + AudioDrama, + /// Live + Live, + /// Remix + Remix, + /// DJ-mix + DjMix, + /// Mixtape/Street + MixtapeStreet, + /// Demo + Demo, + /// Field recording + FieldRecording, +} + /// The album's ownership status. pub enum AlbumStatus { None, @@ -135,12 +132,19 @@ impl AlbumStatus { } impl Album { - pub fn new, Date: Into>(id: Id, date: Date) -> Self { + pub fn new, Date: Into>( + id: Id, + date: Date, + primary_type: Option, + secondary_types: Vec, + ) -> Self { Album { id: id.into(), date: date.into(), seq: AlbumSeq::default(), musicbrainz: None, + primary_type, + secondary_types, tracks: vec![], } } @@ -160,6 +164,14 @@ impl Album { pub fn clear_seq(&mut self) { self.seq = AlbumSeq::default(); } + + pub fn set_musicbrainz_ref(&mut self, mbref: MbAlbumRef) { + _ = self.musicbrainz.insert(mbref); + } + + pub fn clear_musicbrainz_ref(&mut self) { + _ = self.musicbrainz.take(); + } } impl PartialOrd for Album { @@ -178,6 +190,11 @@ impl Merge for Album { fn merge_in_place(&mut self, other: Self) { assert_eq!(self.id, other.id); self.seq = std::cmp::max(self.seq, other.seq); + + self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz); + self.primary_type = self.primary_type.take().or(other.primary_type); + self.secondary_types.merge_in_place(other.secondary_types); + let tracks = mem::take(&mut self.tracks); self.tracks = MergeSorted::new(tracks.into_iter(), other.tracks.into_iter()).collect(); } @@ -213,54 +230,32 @@ mod tests { use super::*; - #[test] - fn album_month() { - assert_eq!(>::into(0), AlbumMonth::None); - assert_eq!(>::into(1), AlbumMonth::January); - assert_eq!(>::into(2), AlbumMonth::February); - assert_eq!(>::into(3), AlbumMonth::March); - assert_eq!(>::into(4), AlbumMonth::April); - assert_eq!(>::into(5), AlbumMonth::May); - assert_eq!(>::into(6), AlbumMonth::June); - assert_eq!(>::into(7), AlbumMonth::July); - assert_eq!(>::into(8), AlbumMonth::August); - assert_eq!(>::into(9), AlbumMonth::September); - assert_eq!(>::into(10), AlbumMonth::October); - assert_eq!(>::into(11), AlbumMonth::November); - assert_eq!(>::into(12), AlbumMonth::December); - assert_eq!(>::into(13), AlbumMonth::None); - assert_eq!(>::into(255), AlbumMonth::None); - } - #[test] fn album_date_from() { let date: AlbumDate = 1986.into(); - assert_eq!(date, AlbumDate::new(1986, AlbumMonth::default(), 0)); + assert_eq!(date, AlbumDate::new(Some(1986), None, None)); let date: AlbumDate = (1986, 5).into(); - assert_eq!(date, AlbumDate::new(1986, AlbumMonth::May, 0)); - - let date: AlbumDate = (1986, AlbumMonth::June).into(); - assert_eq!(date, AlbumDate::new(1986, AlbumMonth::June, 0)); + assert_eq!(date, AlbumDate::new(Some(1986), Some(5), None)); let date: AlbumDate = (1986, 6, 8).into(); - assert_eq!(date, AlbumDate::new(1986, AlbumMonth::June, 8)); + assert_eq!(date, AlbumDate::new(Some(1986), Some(6), Some(8))); } #[test] fn same_date_seq_cmp() { - let date = AlbumDate::new(2024, 3, 2); + let date: AlbumDate = (2024, 3, 2).into(); let album_id_1 = AlbumId { title: String::from("album z"), }; - let mut album_1 = Album::new(album_id_1, date.clone()); + let mut album_1 = Album::new(album_id_1, date.clone(), None, vec![]); album_1.set_seq(AlbumSeq(1)); let album_id_2 = AlbumId { title: String::from("album a"), }; - let mut album_2 = Album::new(album_id_2, date.clone()); + let mut album_2 = Album::new(album_id_2, date.clone(), None, vec![]); album_2.set_seq(AlbumSeq(2)); assert_ne!(album_1, album_2); @@ -269,7 +264,7 @@ mod tests { #[test] fn set_clear_seq() { - let mut album = Album::new("An album", AlbumDate::default()); + let mut album = Album::new("An album", AlbumDate::default(), None, vec![]); assert_eq!(album.seq, AlbumSeq(0)); @@ -322,4 +317,34 @@ mod tests { let merged = left.clone().merge(right); assert_eq!(expected, merged); } + + #[test] + fn set_clear_musicbrainz_url() { + const MUSICBRAINZ: &str = + "https://musicbrainz.org/release-group/c12897a3-af7a-3466-8892-58af84765813"; + const MUSICBRAINZ_2: &str = + "https://musicbrainz.org/release-group/0eaa9306-e6df-47be-94ce-04bfe3df782c"; + + let mut album = Album::new(AlbumId::new("an album"), AlbumDate::default(), None, vec![]); + + let mut expected: Option = None; + assert_eq!(album.musicbrainz, expected); + + // Setting a URL on an album. + album.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap()); + _ = expected.insert(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap()); + assert_eq!(album.musicbrainz, expected); + + album.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap()); + assert_eq!(album.musicbrainz, expected); + + album.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap()); + _ = expected.insert(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap()); + assert_eq!(album.musicbrainz, expected); + + // Clearing URLs. + album.clear_musicbrainz_ref(); + _ = expected.take(); + assert_eq!(album.musicbrainz, expected); + } } diff --git a/src/core/collection/artist.rs b/src/core/collection/artist.rs index 0cd1d1c..dd0c851 100644 --- a/src/core/collection/artist.rs +++ b/src/core/collection/artist.rs @@ -7,7 +7,7 @@ use std::{ use crate::core::collection::{ album::Album, merge::{Merge, MergeCollections, WithId}, - musicbrainz::MusicBrainzUrl, + musicbrainz::MbArtistRef, }; /// An artist. @@ -15,7 +15,7 @@ use crate::core::collection::{ pub struct Artist { pub id: ArtistId, pub sort: Option, - pub musicbrainz: Option, + pub musicbrainz: Option, pub properties: HashMap>, pub albums: Vec, } @@ -58,11 +58,11 @@ impl Artist { _ = self.sort.take(); } - pub fn set_musicbrainz_url(&mut self, url: MusicBrainzUrl) { - _ = self.musicbrainz.insert(url); + pub fn set_musicbrainz_ref(&mut self, mbref: MbArtistRef) { + _ = self.musicbrainz.insert(mbref); } - pub fn clear_musicbrainz_url(&mut self) { + pub fn clear_musicbrainz_ref(&mut self) { _ = self.musicbrainz.take(); } @@ -216,23 +216,23 @@ mod tests { fn set_clear_musicbrainz_url() { let mut artist = Artist::new(ArtistId::new("an artist")); - let mut expected: Option = None; + let mut expected: Option = None; assert_eq!(artist.musicbrainz, expected); // Setting a URL on an artist. - artist.set_musicbrainz_url(MusicBrainzUrl::artist_from_str(MUSICBRAINZ).unwrap()); - _ = expected.insert(MusicBrainzUrl::artist_from_str(MUSICBRAINZ).unwrap()); + artist.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); + _ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); assert_eq!(artist.musicbrainz, expected); - artist.set_musicbrainz_url(MusicBrainzUrl::artist_from_str(MUSICBRAINZ).unwrap()); + artist.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); assert_eq!(artist.musicbrainz, expected); - artist.set_musicbrainz_url(MusicBrainzUrl::artist_from_str(MUSICBRAINZ_2).unwrap()); - _ = expected.insert(MusicBrainzUrl::artist_from_str(MUSICBRAINZ_2).unwrap()); + artist.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap()); + _ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap()); assert_eq!(artist.musicbrainz, expected); // Clearing URLs. - artist.clear_musicbrainz_url(); + artist.clear_musicbrainz_ref(); _ = expected.take(); assert_eq!(artist.musicbrainz, expected); } diff --git a/src/core/collection/musicbrainz.rs b/src/core/collection/musicbrainz.rs index 6710b43..938f705 100644 --- a/src/core/collection/musicbrainz.rs +++ b/src/core/collection/musicbrainz.rs @@ -3,65 +3,110 @@ use std::fmt::{Debug, Display}; use url::Url; use uuid::Uuid; -use crate::core::collection::Error; +use crate::{core::collection::Error, interface::musicbrainz::Mbid}; -/// MusicBrainz reference. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct MusicBrainzUrl(Url); +const MB_DOMAIN: &str = "musicbrainz.org"; -impl MusicBrainzUrl { - pub fn mbid(&self) -> &str { - // The URL is assumed to have been validated. - self.0.path_segments().and_then(|mut ps| ps.nth(1)).unwrap() - } +#[derive(Clone, Debug, PartialEq, Eq)] +struct MusicBrainzRef { + mbid: Mbid, + url: Url, +} - pub fn artist_from_str>(url: S) -> Result { - Self::artist_from_url(url.as_ref().try_into()?) - } +pub trait IMusicBrainzRef { + fn mbid(&self) -> &Mbid; + fn url(&self) -> &Url; + fn entity() -> &'static str; +} - pub fn album_from_str>(url: S) -> Result { - Self::album_from_url(url.as_ref().try_into()?) - } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MbArtistRef(MusicBrainzRef); - pub fn artist_from_url(url: Url) -> Result { - Self::new(url, "artist") - } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MbAlbumRef(MusicBrainzRef); - pub fn album_from_url(url: Url) -> Result { - Self::new(url, "release-group") - } +macro_rules! impl_imusicbrainzref { + ($mbref:ident, $entity:literal) => { + impl IMusicBrainzRef for $mbref { + fn mbid(&self) -> &Mbid { + &self.0.mbid + } - fn new(url: Url, mb_type: &str) -> Result { + fn url(&self) -> &Url { + &self.0.url + } + + fn entity() -> &'static str { + $entity + } + } + + impl TryFrom for $mbref { + type Error = Error; + + fn try_from(url: Url) -> Result { + Ok($mbref(MusicBrainzRef::from_url(url, $mbref::entity())?)) + } + } + + impl From for $mbref { + fn from(uuid: Uuid) -> Self { + $mbref(MusicBrainzRef::from_uuid(uuid, $mbref::entity())) + } + } + + impl $mbref { + pub fn from_url_str>(url: S) -> Result { + let url: Url = url.as_ref().try_into()?; + url.try_into() + } + } + + impl $mbref { + pub fn from_uuid_str>(uuid: S) -> Result { + let uuid: Uuid = uuid.as_ref().try_into()?; + Ok(uuid.into()) + } + } + }; +} + +impl_imusicbrainzref!(MbArtistRef, "artist"); +impl_imusicbrainzref!(MbAlbumRef, "release-group"); + +impl MusicBrainzRef { + fn from_url(url: Url, entity: &'static str) -> Result { if !url .domain() - .map(|u| u.ends_with("musicbrainz.org")) + .map(|u| u.ends_with(MB_DOMAIN)) .unwrap_or(false) { - return Err(Self::invalid_url_error(url, mb_type)); + return Err(Self::invalid_url_error(url, entity)); } // path_segments only returns an empty iterator if the URL cannot-be-a-base. However, if the // URL cannot-be-a-base then it will fail the check above already as it won't have a domain. - if url.path_segments().and_then(|mut ps| ps.nth(0)).unwrap() != mb_type { - return Err(Self::invalid_url_error(url, mb_type)); + if url.path_segments().and_then(|mut ps| ps.nth(0)).unwrap() != entity { + return Err(Self::invalid_url_error(url, entity)); } - match url.path_segments().and_then(|mut ps| ps.nth(1)) { - Some(segment) => Uuid::try_parse(segment)?, - None => return Err(Self::invalid_url_error(url, mb_type)), + let mbid = match url.path_segments().and_then(|mut ps| ps.nth(1)) { + Some(segment) => Uuid::try_parse(segment)?.into(), + None => return Err(Self::invalid_url_error(url, entity)), }; - Ok(MusicBrainzUrl(url)) + Ok(MusicBrainzRef { mbid, url }) } - fn invalid_url_error(url: U, mb_type: &str) -> Error { - Error::UrlError(format!("invalid {mb_type} MusicBrainz URL: {url}")) + fn from_uuid(uuid: Uuid, entity: &'static str) -> Self { + let uuid_str = uuid.to_string(); + let mbid = uuid.into(); + let url = Url::parse(&format!("https://{MB_DOMAIN}/{entity}/{uuid_str}")).unwrap(); + MusicBrainzRef { mbid, url } } -} -impl AsRef for MusicBrainzUrl { - fn as_ref(&self) -> &str { - self.0.as_ref() + fn invalid_url_error(url: U, entity: &'static str) -> Error { + Error::UrlError(format!("invalid {entity} MusicBrainz URL: {url}")) } } @@ -74,14 +119,18 @@ mod tests { let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8"; let url_str = format!("https://musicbrainz.org/artist/{uuid}"); - let mb = MusicBrainzUrl::artist_from_str(&url_str).unwrap(); - assert_eq!(url_str, mb.as_ref()); - assert_eq!(uuid, mb.mbid()); + let mb = MbArtistRef::from_url_str(&url_str).unwrap(); + assert_eq!(url_str, mb.url().as_ref()); + assert_eq!(uuid, mb.mbid().uuid().to_string()); + + let mb = MbArtistRef::from_uuid_str(uuid).unwrap(); + assert_eq!(url_str, mb.url().as_ref()); + assert_eq!(uuid, mb.mbid().uuid().to_string()); let url: Url = url_str.as_str().try_into().unwrap(); - let mb = MusicBrainzUrl::artist_from_url(url).unwrap(); - assert_eq!(url_str, mb.as_ref()); - assert_eq!(uuid, mb.mbid()); + let mb: MbArtistRef = url.try_into().unwrap(); + assert_eq!(url_str, mb.url().as_ref()); + assert_eq!(uuid, mb.mbid().uuid().to_string()); } #[test] @@ -89,21 +138,25 @@ mod tests { let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8"; let url_str = format!("https://musicbrainz.org/release-group/{uuid}"); - let mb = MusicBrainzUrl::album_from_str(&url_str).unwrap(); - assert_eq!(url_str, mb.as_ref()); - assert_eq!(uuid, mb.mbid()); + let mb = MbAlbumRef::from_url_str(&url_str).unwrap(); + assert_eq!(url_str, mb.url().as_ref()); + assert_eq!(uuid, mb.mbid().uuid().to_string()); + + let mb = MbAlbumRef::from_uuid_str(uuid).unwrap(); + assert_eq!(url_str, mb.url().as_ref()); + assert_eq!(uuid, mb.mbid().uuid().to_string()); let url: Url = url_str.as_str().try_into().unwrap(); - let mb = MusicBrainzUrl::album_from_url(url).unwrap(); - assert_eq!(url_str, mb.as_ref()); - assert_eq!(uuid, mb.mbid()); + let mb: MbAlbumRef = url.try_into().unwrap(); + assert_eq!(url_str, mb.url().as_ref()); + assert_eq!(uuid, mb.mbid().uuid().to_string()); } #[test] fn not_a_url() { let url = "not a url at all"; let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into(); - let actual_error = MusicBrainzUrl::artist_from_str(url).unwrap_err(); + let actual_error = MbArtistRef::from_url_str(url).unwrap_err(); assert_eq!(actual_error, expected_error); assert_eq!(actual_error.to_string(), expected_error.to_string()); } @@ -112,7 +165,7 @@ mod tests { fn invalid_url() { let url = "https://www.musicbutler.io/artist-page/483340948"; let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}")); - let actual_error = MusicBrainzUrl::artist_from_str(url).unwrap_err(); + let actual_error = MbArtistRef::from_url_str(url).unwrap_err(); assert_eq!(actual_error, expected_error); assert_eq!(actual_error.to_string(), expected_error.to_string()); } @@ -121,7 +174,7 @@ mod tests { fn artist_invalid_type() { let url = "https://musicbrainz.org/release-group/i-am-not-a-uuid"; let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}")); - let actual_error = MusicBrainzUrl::artist_from_str(url).unwrap_err(); + let actual_error = MbArtistRef::from_url_str(url).unwrap_err(); assert_eq!(actual_error, expected_error); assert_eq!(actual_error.to_string(), expected_error.to_string()); } @@ -131,7 +184,7 @@ mod tests { let url = "https://musicbrainz.org/artist/i-am-not-a-uuid"; let expected_error = Error::UrlError(format!("invalid release-group MusicBrainz URL: {url}")); - let actual_error = MusicBrainzUrl::album_from_str(url).unwrap_err(); + let actual_error = MbAlbumRef::from_url_str(url).unwrap_err(); assert_eq!(actual_error, expected_error); assert_eq!(actual_error.to_string(), expected_error.to_string()); } @@ -140,7 +193,7 @@ mod tests { fn invalid_uuid() { let url = "https://musicbrainz.org/artist/i-am-not-a-uuid"; let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into(); - let actual_error = MusicBrainzUrl::artist_from_str(url).unwrap_err(); + let actual_error = MbArtistRef::from_url_str(url).unwrap_err(); assert_eq!(actual_error, expected_error); assert_eq!(actual_error.to_string(), expected_error.to_string()); } @@ -149,7 +202,7 @@ mod tests { fn missing_type() { let url = "https://musicbrainz.org"; let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}/")); - let actual_error = MusicBrainzUrl::artist_from_str(url).unwrap_err(); + let actual_error = MbArtistRef::from_url_str(url).unwrap_err(); assert_eq!(actual_error, expected_error); assert_eq!(actual_error.to_string(), expected_error.to_string()); } @@ -158,7 +211,7 @@ mod tests { fn missing_uuid() { let url = "https://musicbrainz.org/artist"; let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}")); - let actual_error = MusicBrainzUrl::artist_from_str(url).unwrap_err(); + let actual_error = MbArtistRef::from_url_str(url).unwrap_err(); assert_eq!(actual_error, expected_error); assert_eq!(actual_error.to_string(), expected_error.to_string()); } diff --git a/src/core/interface/database/mod.rs b/src/core/interface/database/mod.rs index 9ae7435..5e05561 100644 --- a/src/core/interface/database/mod.rs +++ b/src/core/interface/database/mod.rs @@ -97,13 +97,13 @@ mod tests { use super::*; #[test] - fn no_database_load() { + fn null_database_load() { let database = NullDatabase; assert!(database.load().unwrap().is_empty()); } #[test] - fn no_database_save() { + fn null_database_save() { let mut database = NullDatabase; assert!(database.save(&vec![]).is_ok()); } diff --git a/src/core/interface/library/mod.rs b/src/core/interface/library/mod.rs index 448e063..76609bc 100644 --- a/src/core/interface/library/mod.rs +++ b/src/core/interface/library/mod.rs @@ -5,7 +5,7 @@ use std::{collections::HashSet, fmt, num::ParseIntError, str::Utf8Error}; #[cfg(test)] use mockall::automock; -use crate::core::collection::{album::AlbumMonth, track::TrackFormat}; +use crate::core::collection::track::TrackFormat; /// Trait for interacting with the music library. #[cfg_attr(test, automock)] @@ -29,7 +29,7 @@ pub struct Item { pub album_artist: String, pub album_artist_sort: Option, pub album_year: u32, - pub album_month: AlbumMonth, + pub album_month: u8, pub album_day: u8, pub album_title: String, pub track_number: u32, @@ -45,7 +45,7 @@ pub enum Field { AlbumArtist(String), AlbumArtistSort(String), AlbumYear(u32), - AlbumMonth(AlbumMonth), + AlbumMonth(u8), AlbumDay(u8), AlbumTitle(String), TrackNumber(u32), @@ -136,7 +136,7 @@ mod tests { use super::*; #[test] - fn no_library_list() { + fn null_library_list() { let mut library = NullLibrary; assert!(library.list(&Query::default()).unwrap().is_empty()); } diff --git a/src/core/interface/library/testmod.rs b/src/core/interface/library/testmod.rs index 2649422..6afc5b9 100644 --- a/src/core/interface/library/testmod.rs +++ b/src/core/interface/library/testmod.rs @@ -1,9 +1,6 @@ use once_cell::sync::Lazy; -use crate::core::{ - collection::{album::AlbumMonth, track::TrackFormat}, - interface::library::Item, -}; +use crate::core::{collection::track::TrackFormat, interface::library::Item}; pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { vec![ @@ -11,7 +8,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 1998, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title a.a"), track_number: 1, @@ -24,7 +21,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 1998, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title a.a"), track_number: 2, @@ -40,7 +37,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 1998, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title a.a"), track_number: 3, @@ -53,7 +50,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 1998, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title a.a"), track_number: 4, @@ -66,7 +63,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 2015, - album_month: AlbumMonth::April, + album_month: 4, album_day: 0, album_title: String::from("album_title a.b"), track_number: 1, @@ -79,7 +76,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 2015, - album_month: AlbumMonth::April, + album_month: 4, album_day: 0, album_title: String::from("album_title a.b"), track_number: 2, @@ -92,7 +89,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2003, - album_month: AlbumMonth::June, + album_month: 6, album_day: 6, album_title: String::from("album_title b.a"), track_number: 1, @@ -105,7 +102,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2003, - album_month: AlbumMonth::June, + album_month: 6, album_day: 6, album_title: String::from("album_title b.a"), track_number: 2, @@ -121,7 +118,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title b.b"), track_number: 1, @@ -134,7 +131,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title b.b"), track_number: 2, @@ -150,7 +147,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2009, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title b.c"), track_number: 1, @@ -163,7 +160,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2009, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title b.c"), track_number: 2, @@ -179,7 +176,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2015, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title b.d"), track_number: 1, @@ -192,7 +189,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2015, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title b.d"), track_number: 2, @@ -208,7 +205,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("The Album_Artist ‘C’"), album_artist_sort: Some(String::from("Album_Artist ‘C’, The")), album_year: 1985, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title c.a"), track_number: 1, @@ -221,7 +218,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("The Album_Artist ‘C’"), album_artist_sort: Some(String::from("Album_Artist ‘C’, The")), album_year: 1985, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title c.a"), track_number: 2, @@ -237,7 +234,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("The Album_Artist ‘C’"), album_artist_sort: Some(String::from("Album_Artist ‘C’, The")), album_year: 2018, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title c.b"), track_number: 1, @@ -250,7 +247,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("The Album_Artist ‘C’"), album_artist_sort: Some(String::from("Album_Artist ‘C’, The")), album_year: 2018, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title c.b"), track_number: 2, @@ -266,7 +263,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘D’"), album_artist_sort: None, album_year: 1995, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title d.a"), track_number: 1, @@ -279,7 +276,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘D’"), album_artist_sort: None, album_year: 1995, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title d.a"), track_number: 2, @@ -295,7 +292,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘D’"), album_artist_sort: None, album_year: 2028, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title d.b"), track_number: 1, @@ -308,7 +305,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘D’"), album_artist_sort: None, album_year: 2028, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("album_title d.b"), track_number: 2, diff --git a/src/core/interface/mod.rs b/src/core/interface/mod.rs index 2b9c4cf..6becfd7 100644 --- a/src/core/interface/mod.rs +++ b/src/core/interface/mod.rs @@ -1,2 +1,3 @@ pub mod database; pub mod library; +pub mod musicbrainz; diff --git a/src/core/interface/musicbrainz/mod.rs b/src/core/interface/musicbrainz/mod.rs new file mode 100644 index 0000000..3adc8c0 --- /dev/null +++ b/src/core/interface/musicbrainz/mod.rs @@ -0,0 +1,180 @@ +//! Module for accessing MusicBrainz metadata. + +use std::{fmt, num}; + +// TODO: #[cfg(test)] +// TODO: use mockall::automock; + +use uuid::{self, Uuid}; + +use crate::collection::album::Album; + +/// Trait for interacting with the MusicBrainz API. +// TODO: #[cfg_attr(test, automock)] +pub trait IMusicBrainz { + fn lookup_artist_release_groups(&mut self, mbid: &Mbid) -> Result, Error>; + fn search_release_group( + &mut self, + arid: &Mbid, + album: Album, + ) -> Result>, Error>; +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Match { + pub score: u8, + pub item: T, +} + +impl Match { + pub fn new(score: u8, item: T) -> Self { + Match { score, item } + } +} + +/// Null implementation of [`IMusicBrainz`] for when the trait is required, but no communication +/// with the MusicBrainz is desired. +pub struct NullMusicBrainz; + +impl IMusicBrainz for NullMusicBrainz { + fn lookup_artist_release_groups(&mut self, _mbid: &Mbid) -> Result, Error> { + Ok(vec![]) + } + + fn search_release_group( + &mut self, + _arid: &Mbid, + _album: Album, + ) -> Result>, Error> { + Ok(vec![]) + } +} + +/// The MusicBrainz ID. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Mbid(Uuid); + +impl Mbid { + pub fn uuid(&self) -> &Uuid { + &self.0 + } +} + +impl From for Mbid { + fn from(value: Uuid) -> Self { + Mbid(value) + } +} + +macro_rules! try_from_impl_for_mbid { + ($from:ty) => { + impl TryFrom<$from> for Mbid { + type Error = Error; + + fn try_from(value: $from) -> Result { + Ok(Uuid::parse_str(value.as_ref())?.into()) + } + } + }; +} + +try_from_impl_for_mbid!(&str); +try_from_impl_for_mbid!(&String); +try_from_impl_for_mbid!(String); + +/// Error type for musicbrainz calls. +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + /// Failed to parse input into an MBID. + MbidParse(String), + /// The API client failed. + Client(String), + /// The client reached the API rate limit. + RateLimit, + /// The API response could not be understood. + Unknown(u16), + /// Part of the response could not be parsed. + Parse(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::MbidParse(s) => write!(f, "failed to parse input into an MBID: {s}"), + Error::Client(s) => write!(f, "the API client failed: {s}"), + Error::RateLimit => write!(f, "the API client reached the rate limit"), + Error::Unknown(u) => write!(f, "the API response could not be understood: status {u}"), + Error::Parse(s) => write!(f, "part of the response could not be parsed: {s}"), + } + } +} + +impl From for Error { + fn from(value: uuid::Error) -> Self { + Error::MbidParse(value.to_string()) + } +} + +impl From for Error { + fn from(err: num::ParseIntError) -> Error { + Error::Parse(err.to_string()) + } +} + +#[cfg(test)] +mod tests { + use crate::core::collection::album::{AlbumDate, AlbumId}; + + use super::*; + + #[test] + fn null_lookup_artist_release_groups() { + let mut musicbrainz = NullMusicBrainz; + let mbid: Mbid = "d368baa8-21ca-4759-9731-0b2753071ad8".try_into().unwrap(); + assert!(musicbrainz + .lookup_artist_release_groups(&mbid) + .unwrap() + .is_empty()); + } + + #[test] + fn null_search_release_group() { + let mut musicbrainz = NullMusicBrainz; + let mbid: Mbid = "d368baa8-21ca-4759-9731-0b2753071ad8".try_into().unwrap(); + let album = Album::new(AlbumId::new("an album"), AlbumDate::default(), None, vec![]); + assert!(musicbrainz + .search_release_group(&mbid, album) + .unwrap() + .is_empty()); + } + + #[test] + fn match_type() { + let album = Album::new(AlbumId::new("an album"), AlbumDate::default(), None, vec![]); + let hit = Match::new(56, album); + assert!(!format!("{hit:?}").is_empty()); + } + + #[test] + fn errors() { + let mbid_err: Error = TryInto::::try_into("i-am-not-a-uuid").unwrap_err(); + assert!(!mbid_err.to_string().is_empty()); + assert!(!format!("{mbid_err:?}").is_empty()); + + let client_err: Error = Error::Client(String::from("a client error")); + assert!(!client_err.to_string().is_empty()); + assert!(!format!("{client_err:?}").is_empty()); + + let rate_err: Error = Error::RateLimit; + assert!(!rate_err.to_string().is_empty()); + assert!(!format!("{rate_err:?}").is_empty()); + + let unk_err: Error = Error::Unknown(404); + assert!(!unk_err.to_string().is_empty()); + assert!(!format!("{unk_err:?}").is_empty()); + + let parse_err: Error = "not-a-number".parse::().unwrap_err().into(); + assert!(!parse_err.to_string().is_empty()); + assert!(!format!("{parse_err:?}").is_empty()); + } +} diff --git a/src/core/musichoard/builder.rs b/src/core/musichoard/builder.rs index 90bdb0a..41eba10 100644 --- a/src/core/musichoard/builder.rs +++ b/src/core/musichoard/builder.rs @@ -1,11 +1,8 @@ use std::collections::HashMap; -use crate::{ - core::{ - interface::{database::IDatabase, library::ILibrary}, - musichoard::{database::IMusicHoardDatabase, MusicHoard, NoDatabase, NoLibrary}, - }, - Error, +use crate::core::{ + interface::{database::IDatabase, library::ILibrary}, + musichoard::{database::IMusicHoardDatabase, Error, MusicHoard, NoDatabase, NoLibrary}, }; /// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of diff --git a/src/core/musichoard/database.rs b/src/core/musichoard/database.rs index d9aa6c0..8303144 100644 --- a/src/core/musichoard/database.rs +++ b/src/core/musichoard/database.rs @@ -2,7 +2,7 @@ use crate::core::{ collection::{ album::{Album, AlbumId, AlbumSeq}, artist::{Artist, ArtistId}, - musicbrainz::MusicBrainzUrl, + musicbrainz::MbArtistRef, Collection, }, interface::database::IDatabase, @@ -123,15 +123,15 @@ impl IMusicHoardDatabase for MusicHoard Result<(), Error> { - let mb = MusicBrainzUrl::artist_from_str(url)?; - self.update_artist(artist_id.as_ref(), |artist| artist.set_musicbrainz_url(mb)) + let mb = MbArtistRef::from_url_str(url)?; + self.update_artist(artist_id.as_ref(), |artist| artist.set_musicbrainz_ref(mb)) } fn clear_artist_musicbrainz>( &mut self, artist_id: Id, ) -> Result<(), Error> { - self.update_artist(artist_id.as_ref(), |artist| artist.clear_musicbrainz_url()) + self.update_artist(artist_id.as_ref(), |artist| artist.clear_musicbrainz_ref()) } fn add_to_artist_property, S: AsRef + Into>( @@ -290,7 +290,7 @@ mod tests { use mockall::{predicate, Sequence}; use crate::core::{ - collection::{album::AlbumDate, artist::ArtistId, musicbrainz::MusicBrainzUrl}, + collection::{album::AlbumDate, artist::ArtistId}, interface::database::{self, MockIDatabase}, musichoard::{base::IMusicHoardBase, NoLibrary}, testmod::FULL_COLLECTION, @@ -433,7 +433,7 @@ mod tests { assert!(music_hoard.add_artist(artist_id.clone()).is_ok()); - let mut expected: Option = None; + let mut expected: Option = None; assert_eq!(music_hoard.collection[0].musicbrainz, expected); // Setting a URL on an artist not in the collection is an error. @@ -446,7 +446,7 @@ mod tests { assert!(music_hoard .set_artist_musicbrainz(&artist_id, MUSICBRAINZ) .is_ok()); - _ = expected.insert(MusicBrainzUrl::artist_from_str(MUSICBRAINZ).unwrap()); + _ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); assert_eq!(music_hoard.collection[0].musicbrainz, expected); // Clearing URLs on an artist that does not exist is an error. @@ -567,9 +567,12 @@ mod tests { let album_id_2 = AlbumId::new("another album"); let mut database_result = vec![Artist::new(artist_id.clone())]; - database_result[0] - .albums - .push(Album::new(album_id.clone(), AlbumDate::default())); + database_result[0].albums.push(Album::new( + album_id.clone(), + AlbumDate::default(), + None, + vec![], + )); database .expect_load() diff --git a/src/core/musichoard/library.rs b/src/core/musichoard/library.rs index 7194f07..d2e9888 100644 --- a/src/core/musichoard/library.rs +++ b/src/core/musichoard/library.rs @@ -59,9 +59,9 @@ impl MusicHoard { }; let album_date = AlbumDate { - year: item.album_year, - month: item.album_month, - day: item.album_day, + year: Some(item.album_year).filter(|y| y > &0), + month: Some(item.album_month).filter(|m| m > &0), + day: Some(item.album_day).filter(|d| d > &0), }; let track = Track { @@ -109,7 +109,7 @@ impl MusicHoard { { Some(album) => album.tracks.push(track), None => { - let mut album = Album::new(album_id, album_date); + let mut album = Album::new(album_id, album_date, None, vec![]); album.tracks.push(track); artist.albums.push(album); } diff --git a/src/core/testmod.rs b/src/core/testmod.rs index 131bd17..7503316 100644 --- a/src/core/testmod.rs +++ b/src/core/testmod.rs @@ -2,12 +2,12 @@ use once_cell::sync::Lazy; use std::collections::HashMap; use crate::core::collection::{ - album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq}, + album::{Album, AlbumId, AlbumPrimaryType, AlbumSeq}, artist::{Artist, ArtistId}, - musicbrainz::MusicBrainzUrl, + musicbrainz::{MbAlbumRef, MbArtistRef}, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, }; -use crate::tests::*; +use crate::testmod::*; -pub static LIBRARY_COLLECTION: Lazy> = Lazy::new(|| library_collection!()); -pub static FULL_COLLECTION: Lazy> = Lazy::new(|| full_collection!()); +pub static LIBRARY_COLLECTION: Lazy> = Lazy::new(|| library::library_collection!()); +pub static FULL_COLLECTION: Lazy> = Lazy::new(|| full::full_collection!()); diff --git a/src/database/json/backend.rs b/src/external/database/json/backend.rs similarity index 93% rename from src/database/json/backend.rs rename to src/external/database/json/backend.rs index 07a0a1c..76a190c 100644 --- a/src/database/json/backend.rs +++ b/src/external/database/json/backend.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::PathBuf; -use crate::database::json::IJsonDatabaseBackend; +use crate::external::database::json::IJsonDatabaseBackend; /// JSON database backend that uses a local file for persistent storage. pub struct JsonDatabaseFileBackend { diff --git a/src/database/json/mod.rs b/src/external/database/json/mod.rs similarity index 99% rename from src/database/json/mod.rs rename to src/external/database/json/mod.rs index 80cc6c7..b791203 100644 --- a/src/database/json/mod.rs +++ b/src/external/database/json/mod.rs @@ -109,6 +109,7 @@ mod tests { fn load() { let expected = expected(); let result = Ok(DATABASE_JSON.to_owned()); + eprintln!("{DATABASE_JSON}"); let mut backend = MockIJsonDatabaseBackend::new(); backend.expect_read().times(1).return_once(|| result); diff --git a/src/database/json/testmod.rs b/src/external/database/json/testmod.rs similarity index 58% rename from src/database/json/testmod.rs rename to src/external/database/json/testmod.rs index 7bba672..ec0121f 100644 --- a/src/database/json/testmod.rs +++ b/src/external/database/json/testmod.rs @@ -1,5 +1,5 @@ pub static DATABASE_JSON: &str = "{\ - \"V20240308\":\ + \"V20240313\":\ [\ {\ \"name\":\"Album_Artist ‘A’\",\ @@ -12,9 +12,13 @@ pub static DATABASE_JSON: &str = "{\ \"albums\":[\ {\ \"title\":\"album_title a.a\",\"seq\":1,\ - \"musicbrainz\":\"https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000\"\ + \"musicbrainz\":\"https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000\",\ + \"primary_type\":\"Album\",\"secondary_types\":[]\ },\ - {\"title\":\"album_title a.b\",\"seq\":1,\"musicbrainz\":null}\ + {\ + \"title\":\"album_title a.b\",\"seq\":1,\"musicbrainz\":null,\ + \"primary_type\":\"Album\",\"secondary_types\":[]\ + }\ ]\ },\ {\ @@ -30,16 +34,24 @@ pub static DATABASE_JSON: &str = "{\ \"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums\"]\ },\ \"albums\":[\ - {\"title\":\"album_title b.a\",\"seq\":1,\"musicbrainz\":null},\ + {\ + \"title\":\"album_title b.a\",\"seq\":1,\"musicbrainz\":null,\ + \"primary_type\":\"Album\",\"secondary_types\":[]\ + },\ {\ \"title\":\"album_title b.b\",\"seq\":3,\ - \"musicbrainz\":\"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111\"\ + \"musicbrainz\":\"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111\",\ + \"primary_type\":\"Album\",\"secondary_types\":[]\ },\ {\ \"title\":\"album_title b.c\",\"seq\":2,\ - \"musicbrainz\":\"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112\"\ + \"musicbrainz\":\"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112\",\ + \"primary_type\":\"Album\",\"secondary_types\":[]\ },\ - {\"title\":\"album_title b.d\",\"seq\":4,\"musicbrainz\":null}\ + {\ + \"title\":\"album_title b.d\",\"seq\":4,\"musicbrainz\":null,\ + \"primary_type\":\"Album\",\"secondary_types\":[]\ + }\ ]\ },\ {\ @@ -48,8 +60,14 @@ pub static DATABASE_JSON: &str = "{\ \"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\ \"properties\":{},\ \"albums\":[\ - {\"title\":\"album_title c.a\",\"seq\":0,\"musicbrainz\":null},\ - {\"title\":\"album_title c.b\",\"seq\":0,\"musicbrainz\":null}\ + {\ + \"title\":\"album_title c.a\",\"seq\":0,\"musicbrainz\":null,\ + \"primary_type\":\"Album\",\"secondary_types\":[]\ + },\ + {\ + \"title\":\"album_title c.b\",\"seq\":0,\"musicbrainz\":null,\ + \"primary_type\":\"Album\",\"secondary_types\":[]\ + }\ ]\ },\ {\ @@ -58,8 +76,14 @@ pub static DATABASE_JSON: &str = "{\ \"musicbrainz\":null,\ \"properties\":{},\ \"albums\":[\ - {\"title\":\"album_title d.a\",\"seq\":0,\"musicbrainz\":null},\ - {\"title\":\"album_title d.b\",\"seq\":0,\"musicbrainz\":null}\ + {\ + \"title\":\"album_title d.a\",\"seq\":0,\"musicbrainz\":null,\ + \"primary_type\":\"Album\",\"secondary_types\":[]\ + },\ + {\ + \"title\":\"album_title d.b\",\"seq\":0,\"musicbrainz\":null,\ + \"primary_type\":\"Album\",\"secondary_types\":[]\ + }\ ]\ }\ ]\ diff --git a/src/database/mod.rs b/src/external/database/mod.rs similarity index 100% rename from src/database/mod.rs rename to src/external/database/mod.rs diff --git a/src/external/database/serde/common.rs b/src/external/database/serde/common.rs new file mode 100644 index 0000000..28f9c65 --- /dev/null +++ b/src/external/database/serde/common.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; + +use crate::core::collection::album::{AlbumPrimaryType, AlbumSecondaryType}; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(remote = "AlbumPrimaryType")] +pub enum SerdeAlbumPrimaryTypeDef { + Album, + Single, + Ep, + Broadcast, + Other, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SerdeAlbumPrimaryType(#[serde(with = "SerdeAlbumPrimaryTypeDef")] AlbumPrimaryType); + +impl From for AlbumPrimaryType { + fn from(value: SerdeAlbumPrimaryType) -> Self { + value.0 + } +} + +impl From for SerdeAlbumPrimaryType { + fn from(value: AlbumPrimaryType) -> Self { + SerdeAlbumPrimaryType(value) + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(remote = "AlbumSecondaryType")] +pub enum SerdeAlbumSecondaryTypeDef { + Compilation, + Soundtrack, + Spokenword, + Interview, + Audiobook, + AudioDrama, + Live, + Remix, + DjMix, + MixtapeStreet, + Demo, + FieldRecording, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SerdeAlbumSecondaryType( + #[serde(with = "SerdeAlbumSecondaryTypeDef")] AlbumSecondaryType, +); + +impl From for AlbumSecondaryType { + fn from(value: SerdeAlbumSecondaryType) -> Self { + value.0 + } +} + +impl From for SerdeAlbumSecondaryType { + fn from(value: AlbumSecondaryType) -> Self { + SerdeAlbumSecondaryType(value) + } +} diff --git a/src/database/serde/deserialize.rs b/src/external/database/serde/deserialize.rs similarity index 65% rename from src/database/serde/deserialize.rs rename to src/external/database/serde/deserialize.rs index c1a058d..84e742c 100644 --- a/src/database/serde/deserialize.rs +++ b/src/external/database/serde/deserialize.rs @@ -2,19 +2,22 @@ use std::collections::HashMap; use serde::Deserialize; -use crate::core::{ - collection::{ - album::{Album, AlbumDate, AlbumId, AlbumSeq}, - artist::{Artist, ArtistId}, - musicbrainz::MusicBrainzUrl, - Collection, +use crate::{ + core::{ + collection::{ + album::{Album, AlbumDate, AlbumId, AlbumSeq}, + artist::{Artist, ArtistId}, + musicbrainz::{MbAlbumRef, MbArtistRef}, + Collection, + }, + interface::database::LoadError, }, - interface::database::LoadError, + external::database::serde::common::{SerdeAlbumPrimaryType, SerdeAlbumSecondaryType}, }; #[derive(Debug, Deserialize)] pub enum DeserializeDatabase { - V20240308(Vec), + V20240313(Vec), } impl TryFrom for Collection { @@ -22,10 +25,9 @@ impl TryFrom for Collection { fn try_from(database: DeserializeDatabase) -> Result { match database { - DeserializeDatabase::V20240308(collection) => collection - .into_iter() - .map(|artist| artist.try_into()) - .collect(), + DeserializeDatabase::V20240313(collection) => { + collection.into_iter().map(TryInto::try_into).collect() + } } } } @@ -44,6 +46,8 @@ pub struct DeserializeAlbum { title: String, seq: u8, musicbrainz: Option, + primary_type: Option, + secondary_types: Vec, } impl TryFrom for Artist { @@ -55,7 +59,7 @@ impl TryFrom for Artist { sort: artist.sort.map(ArtistId::new), musicbrainz: artist .musicbrainz - .map(MusicBrainzUrl::artist_from_str) + .map(MbArtistRef::from_url_str) .transpose()?, properties: artist.properties, albums: artist @@ -77,8 +81,10 @@ impl TryFrom for Album { seq: AlbumSeq(album.seq), musicbrainz: album .musicbrainz - .map(MusicBrainzUrl::album_from_str) + .map(MbAlbumRef::from_url_str) .transpose()?, + primary_type: album.primary_type.map(Into::into), + secondary_types: album.secondary_types.into_iter().map(Into::into).collect(), tracks: vec![], }) } diff --git a/src/database/serde/mod.rs b/src/external/database/serde/mod.rs similarity index 90% rename from src/database/serde/mod.rs rename to src/external/database/serde/mod.rs index d0a878f..6ccd8c4 100644 --- a/src/database/serde/mod.rs +++ b/src/external/database/serde/mod.rs @@ -1,4 +1,5 @@ //! Helper module for backends that can use serde for (de)serialisation. +mod common; pub mod deserialize; pub mod serialize; diff --git a/src/database/serde/serialize.rs b/src/external/database/serde/serialize.rs similarity index 61% rename from src/database/serde/serialize.rs rename to src/external/database/serde/serialize.rs index eeb219f..acfe155 100644 --- a/src/database/serde/serialize.rs +++ b/src/external/database/serde/serialize.rs @@ -2,16 +2,19 @@ use std::collections::BTreeMap; use serde::Serialize; -use crate::core::collection::{album::Album, artist::Artist, Collection}; +use crate::{ + core::collection::{album::Album, artist::Artist, musicbrainz::IMusicBrainzRef, Collection}, + external::database::serde::common::{SerdeAlbumPrimaryType, SerdeAlbumSecondaryType}, +}; #[derive(Debug, Serialize)] pub enum SerializeDatabase<'a> { - V20240308(Vec>), + V20240313(Vec>), } impl<'a> From<&'a Collection> for SerializeDatabase<'a> { fn from(collection: &'a Collection) -> Self { - SerializeDatabase::V20240308(collection.iter().map(Into::into).collect()) + SerializeDatabase::V20240313(collection.iter().map(Into::into).collect()) } } @@ -29,6 +32,8 @@ pub struct SerializeAlbum<'a> { title: &'a str, seq: u8, musicbrainz: Option<&'a str>, + primary_type: Option, + secondary_types: Vec, } impl<'a> From<&'a Artist> for SerializeArtist<'a> { @@ -36,7 +41,7 @@ impl<'a> From<&'a Artist> for SerializeArtist<'a> { SerializeArtist { name: &artist.id.name, sort: artist.sort.as_ref().map(|id| id.name.as_ref()), - musicbrainz: artist.musicbrainz.as_ref().map(AsRef::as_ref), + musicbrainz: artist.musicbrainz.as_ref().map(|mb| mb.url().as_str()), properties: artist .properties .iter() @@ -52,7 +57,14 @@ impl<'a> From<&'a Album> for SerializeAlbum<'a> { SerializeAlbum { title: &album.id.title, seq: album.seq.0, - musicbrainz: album.musicbrainz.as_ref().map(AsRef::as_ref), + musicbrainz: album.musicbrainz.as_ref().map(|mb| mb.url().as_str()), + primary_type: album.primary_type.map(Into::into), + secondary_types: album + .secondary_types + .iter() + .copied() + .map(Into::into) + .collect(), } } } diff --git a/src/library/beets/executor.rs b/src/external/library/beets/executor.rs similarity index 98% rename from src/library/beets/executor.rs rename to src/external/library/beets/executor.rs index 68e799e..80882c0 100644 --- a/src/library/beets/executor.rs +++ b/src/external/library/beets/executor.rs @@ -8,8 +8,7 @@ use std::{ str, }; -use crate::core::interface::library::Error; -use crate::library::beets::IBeetsLibraryExecutor; +use crate::{core::interface::library::Error, external::library::beets::IBeetsLibraryExecutor}; const BEET_DEFAULT: &str = "beet"; diff --git a/src/library/beets/mod.rs b/src/external/library/beets/mod.rs similarity index 96% rename from src/library/beets/mod.rs rename to src/external/library/beets/mod.rs index 90c5307..107df2f 100644 --- a/src/library/beets/mod.rs +++ b/src/external/library/beets/mod.rs @@ -76,7 +76,7 @@ impl ToBeetsArg for Field { Field::AlbumArtist(ref s) => format!("{negate}albumartist:{s}"), Field::AlbumArtistSort(ref s) => format!("{negate}albumartist_sort:{s}"), Field::AlbumYear(ref u) => format!("{negate}year:{u}"), - Field::AlbumMonth(ref e) => format!("{negate}month:{}", *e as u8), + Field::AlbumMonth(ref e) => format!("{negate}month:{}", { *e }), Field::AlbumDay(ref u) => format!("{negate}day:{u}"), Field::AlbumTitle(ref s) => format!("{negate}album:{s}"), Field::TrackNumber(ref u) => format!("{negate}track:{u}"), @@ -111,11 +111,6 @@ pub struct BeetsLibrary { executor: BLE, } -trait ILibraryPrivate { - fn list_cmd_and_args(query: &Query) -> Vec; - fn list_to_items>(list_output: &[S]) -> Result, Error>; -} - impl BeetsLibrary { /// Create a new beets library with the provided executor, e.g. /// [`executor::BeetsLibraryProcessExecutor`]. @@ -132,7 +127,7 @@ impl ILibrary for BeetsLibrary { } } -impl ILibraryPrivate for BeetsLibrary { +impl BeetsLibrary { fn list_cmd_and_args(query: &Query) -> Vec { let mut cmd: Vec = vec![String::from(CMD_LIST)]; cmd.push(LIST_FORMAT_ARG.to_string()); @@ -159,7 +154,7 @@ impl ILibraryPrivate for BeetsLibrary { false => None, }; let album_year = split[2].parse::()?; - let album_month = split[3].parse::()?.into(); + let album_month = split[3].parse::()?; let album_day = split[4].parse::()?; let album_title = split[5].to_string(); let track_number = split[6].parse::()?; @@ -201,7 +196,7 @@ mod testmod; mod tests { use mockall::predicate; - use crate::{collection::album::AlbumMonth, core::interface::library::testmod::LIBRARY_ITEMS}; + use crate::core::interface::library::testmod::LIBRARY_ITEMS; use super::*; use testmod::LIBRARY_BEETS; @@ -235,7 +230,7 @@ mod tests { .exclude(Field::AlbumArtist(String::from("some.albumartist"))) .exclude(Field::AlbumArtistSort(String::from("some.albumartist"))) .include(Field::AlbumYear(3030)) - .include(Field::AlbumMonth(AlbumMonth::April)) + .include(Field::AlbumMonth(4)) .include(Field::AlbumDay(6)) .include(Field::TrackTitle(String::from("some.track"))) .include(Field::TrackFormat(TrackFormat::Flac)) diff --git a/src/library/beets/testmod.rs b/src/external/library/beets/testmod.rs similarity index 100% rename from src/library/beets/testmod.rs rename to src/external/library/beets/testmod.rs diff --git a/src/library/mod.rs b/src/external/library/mod.rs similarity index 100% rename from src/library/mod.rs rename to src/external/library/mod.rs diff --git a/src/external/mod.rs b/src/external/mod.rs new file mode 100644 index 0000000..6becfd7 --- /dev/null +++ b/src/external/mod.rs @@ -0,0 +1,3 @@ +pub mod database; +pub mod library; +pub mod musicbrainz; diff --git a/src/external/musicbrainz/api/client.rs b/src/external/musicbrainz/api/client.rs new file mode 100644 index 0000000..deb405b --- /dev/null +++ b/src/external/musicbrainz/api/client.rs @@ -0,0 +1,46 @@ +//! Module for interacting with the MusicBrainz API via an HTTP client. + +use reqwest::{self, blocking::Client, header}; +use serde::de::DeserializeOwned; + +use crate::external::musicbrainz::api::{ClientError, IMusicBrainzApiClient}; + +// GRCOV_EXCL_START +pub struct MusicBrainzApiClient(Client); + +impl MusicBrainzApiClient { + pub fn new(user_agent: &'static str) -> Result { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::USER_AGENT, + header::HeaderValue::from_static(user_agent), + ); + headers.insert( + header::ACCEPT, + header::HeaderValue::from_static("application/json"), + ); + + Ok(MusicBrainzApiClient( + Client::builder().default_headers(headers).build()?, + )) + } +} + +impl IMusicBrainzApiClient for MusicBrainzApiClient { + fn get(&mut self, url: &str) -> Result { + let response = self.0.get(url).send()?; + + if response.status().is_success() { + Ok(response.json()?) + } else { + Err(ClientError::Status(response.status().as_u16())) + } + } +} + +impl From for ClientError { + fn from(err: reqwest::Error) -> Self { + ClientError::Client(err.to_string()) + } +} +// GRCOV_EXCL_STOP diff --git a/src/external/musicbrainz/api/mod.rs b/src/external/musicbrainz/api/mod.rs new file mode 100644 index 0000000..488ff48 --- /dev/null +++ b/src/external/musicbrainz/api/mod.rs @@ -0,0 +1,404 @@ +//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API). + +pub mod client; + +use serde::{de::DeserializeOwned, Deserialize}; +use url::form_urlencoded; + +#[cfg(test)] +use mockall::automock; + +use crate::core::{ + collection::{ + album::{Album, AlbumDate, AlbumPrimaryType, AlbumSecondaryType}, + musicbrainz::MbAlbumRef, + }, + interface::musicbrainz::{Error, IMusicBrainz, Match, Mbid}, +}; + +const MB_BASE_URL: &str = "https://musicbrainz.org/ws/2"; +const MB_RATE_LIMIT_CODE: u16 = 503; + +#[cfg_attr(test, automock)] +pub trait IMusicBrainzApiClient { + fn get(&mut self, url: &str) -> Result; +} + +#[derive(Debug)] +pub enum ClientError { + Client(String), + Status(u16), +} + +impl From for Error { + fn from(err: ClientError) -> Self { + match err { + ClientError::Client(s) => Error::Client(s), + ClientError::Status(status) => match status { + MB_RATE_LIMIT_CODE => Error::RateLimit, + _ => Error::Unknown(status), + }, + } + } +} + +pub struct MusicBrainzApi { + client: Mbc, +} + +impl MusicBrainzApi { + pub fn new(client: Mbc) -> Self { + MusicBrainzApi { client } + } +} + +impl IMusicBrainz for MusicBrainzApi { + fn lookup_artist_release_groups(&mut self, mbid: &Mbid) -> Result, Error> { + let mbid = mbid.uuid().as_hyphenated().to_string(); + + let artist: ResponseLookupArtist = self + .client + .get(&format!("{MB_BASE_URL}/artist/{mbid}?inc=release-groups"))?; + + artist + .release_groups + .into_iter() + .map(TryInto::try_into) + .collect() + } + + fn search_release_group( + &mut self, + arid: &Mbid, + album: Album, + ) -> Result>, Error> { + let title = &album.id.title; + let arid = arid.uuid().as_hyphenated().to_string(); + let mut query = format!("\"{title}\" AND arid:{arid}"); + + if let Some(year) = album.date.year { + query.push_str(&format!(" AND firstreleasedate:{year}")); + } + + let query: String = form_urlencoded::byte_serialize(query.as_bytes()).collect(); + + let results: ResponseSearchReleaseGroup = self + .client + .get(&format!("{MB_BASE_URL}/release-group?query={query}"))?; + + results + .release_groups + .into_iter() + .map(TryInto::try_into) + .collect() + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all(deserialize = "kebab-case"))] +struct ResponseLookupArtist { + release_groups: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all(deserialize = "kebab-case"))] +struct LookupReleaseGroup { + id: String, + title: String, + first_release_date: String, + primary_type: SerdeAlbumPrimaryType, + secondary_types: Vec, +} + +impl TryFrom for Album { + type Error = Error; + + fn try_from(entity: LookupReleaseGroup) -> Result { + let mut album = Album::new( + entity.title, + AlbumDate::from_mb_date(&entity.first_release_date)?, + Some(entity.primary_type.into()), + entity.secondary_types.into_iter().map(Into::into).collect(), + ); + let mbref = MbAlbumRef::from_uuid_str(entity.id) + .map_err(|err| Error::MbidParse(err.to_string()))?; + album.set_musicbrainz_ref(mbref); + Ok(album) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all(deserialize = "kebab-case"))] +struct ResponseSearchReleaseGroup { + release_groups: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all(deserialize = "kebab-case"))] +struct SearchReleaseGroup { + score: u8, + id: String, + title: String, + first_release_date: String, + primary_type: SerdeAlbumPrimaryType, +} + +impl TryFrom for Match { + type Error = Error; + + fn try_from(entity: SearchReleaseGroup) -> Result { + let mut album = Album::new( + entity.title, + AlbumDate::from_mb_date(&entity.first_release_date)?, + Some(entity.primary_type.into()), + vec![], + ); + let mbref = MbAlbumRef::from_uuid_str(entity.id) + .map_err(|err| Error::MbidParse(err.to_string()))?; + album.set_musicbrainz_ref(mbref); + Ok(Match::new(entity.score, album)) + } +} + +impl AlbumDate { + fn from_mb_date(mb_date: &str) -> Result { + let mut elems = mb_date.split('-'); + + let elem = elems.next(); + let year = elem + .and_then(|s| if s.is_empty() { None } else { Some(s.parse()) }) + .transpose()?; + + let elem = elems.next(); + let month = elem.map(|s| s.parse()).transpose()?; + + let elem = elems.next(); + let day = elem.map(|s| s.parse()).transpose()?; + + Ok(AlbumDate::new(year, month, day)) + } +} + +#[derive(Debug, Deserialize)] +#[serde(remote = "AlbumPrimaryType")] +pub enum SerdeAlbumPrimaryTypeDef { + Album, + Single, + #[serde(rename = "EP")] + Ep, + Broadcast, + Other, +} + +#[derive(Debug, Deserialize)] +pub struct SerdeAlbumPrimaryType(#[serde(with = "SerdeAlbumPrimaryTypeDef")] AlbumPrimaryType); + +impl From for AlbumPrimaryType { + fn from(value: SerdeAlbumPrimaryType) -> Self { + value.0 + } +} + +#[derive(Debug, Deserialize)] +#[serde(remote = "AlbumSecondaryType")] +pub enum SerdeAlbumSecondaryTypeDef { + Compilation, + Soundtrack, + Spokenword, + Interview, + Audiobook, + #[serde(rename = "Audio drama")] + AudioDrama, + Live, + Remix, + #[serde(rename = "DJ-mix")] + DjMix, + #[serde(rename = "Mixtape/Street")] + MixtapeStreet, + Demo, + #[serde(rename = "Field recording")] + FieldRecording, +} + +#[derive(Debug, Deserialize)] +pub struct SerdeAlbumSecondaryType( + #[serde(with = "SerdeAlbumSecondaryTypeDef")] AlbumSecondaryType, +); + +impl From for AlbumSecondaryType { + fn from(value: SerdeAlbumSecondaryType) -> Self { + value.0 + } +} + +#[cfg(test)] +mod tests { + use mockall::predicate; + + use crate::collection::album::AlbumId; + + use super::*; + + #[test] + fn lookup_artist_release_group() { + let mut client = MockIMusicBrainzApiClient::new(); + let url = format!( + "https://musicbrainz.org/ws/2/artist/{mbid}?inc=release-groups", + mbid = "00000000-0000-0000-0000-000000000000", + ); + + let release_group = LookupReleaseGroup { + id: String::from("11111111-1111-1111-1111-111111111111"), + title: String::from("an album"), + first_release_date: String::from("1986-04"), + primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), + secondary_types: vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Compilation)], + }; + let response = ResponseLookupArtist { + release_groups: vec![release_group], + }; + + // For code coverage of derive(Debug). + assert!(!format!("{response:?}").is_empty()); + + client + .expect_get() + .times(1) + .with(predicate::eq(url)) + .return_once(|_| Ok(response)); + + let mut api = MusicBrainzApi::new(client); + + let mbid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap(); + let results = api.lookup_artist_release_groups(&mbid).unwrap(); + + let mut album = Album::new( + AlbumId::new("an album"), + (1986, 4), + Some(AlbumPrimaryType::Album), + vec![AlbumSecondaryType::Compilation], + ); + album.set_musicbrainz_ref( + MbAlbumRef::from_uuid_str("11111111-1111-1111-1111-111111111111").unwrap(), + ); + let expected = vec![album]; + + assert_eq!(results, expected); + } + + #[test] + fn search_release_group() { + let mut client = MockIMusicBrainzApiClient::new(); + let url = format!( + "https://musicbrainz.org/ws/2\ + /release-group\ + ?query=%22{title}%22+AND+arid%3A{arid}+AND+firstreleasedate%3A{year}", + title = "an+album", + arid = "00000000-0000-0000-0000-000000000000", + year = "1986" + ); + + let release_group = SearchReleaseGroup { + score: 67, + id: String::from("11111111-1111-1111-1111-111111111111"), + title: String::from("an album"), + first_release_date: String::from("1986-04"), + primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), + }; + let response = ResponseSearchReleaseGroup { + release_groups: vec![release_group], + }; + + // For code coverage of derive(Debug). + assert!(!format!("{response:?}").is_empty()); + + client + .expect_get() + .times(1) + .with(predicate::eq(url)) + .return_once(|_| Ok(response)); + + let mut api = MusicBrainzApi::new(client); + + let arid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap(); + let album = Album::new(AlbumId::new("an album"), (1986, 4), None, vec![]); + let matches = api.search_release_group(&arid, album).unwrap(); + + let mut album = Album::new( + AlbumId::new("an album"), + (1986, 4), + Some(AlbumPrimaryType::Album), + vec![], + ); + album.set_musicbrainz_ref( + MbAlbumRef::from_uuid_str("11111111-1111-1111-1111-111111111111").unwrap(), + ); + let expected = vec![Match::new(67, album)]; + + assert_eq!(matches, expected); + } + + #[test] + fn client_errors() { + let mut client = MockIMusicBrainzApiClient::new(); + + let error = ClientError::Client(String::from("get rekt")); + assert!(!format!("{error:?}").is_empty()); + + client + .expect_get::() + .times(1) + .return_once(|_| Err(ClientError::Client(String::from("get rekt scrub")))); + + client + .expect_get::() + .times(1) + .return_once(|_| Err(ClientError::Status(503))); + + client + .expect_get::() + .times(1) + .return_once(|_| Err(ClientError::Status(504))); + + let mut api = MusicBrainzApi::new(client); + + let mbid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap(); + + let error = api.lookup_artist_release_groups(&mbid).unwrap_err(); + assert_eq!(error, Error::Client(String::from("get rekt scrub"))); + + let error = api.lookup_artist_release_groups(&mbid).unwrap_err(); + assert_eq!(error, Error::RateLimit); + + let error = api.lookup_artist_release_groups(&mbid).unwrap_err(); + assert_eq!(error, Error::Unknown(504)); + } + + #[test] + fn from_mb_date() { + assert_eq!(AlbumDate::from_mb_date("").unwrap(), AlbumDate::default()); + assert_eq!(AlbumDate::from_mb_date("1984").unwrap(), 1984.into()); + assert_eq!( + AlbumDate::from_mb_date("1984-05").unwrap(), + (1984, 5).into() + ); + assert_eq!( + AlbumDate::from_mb_date("1984-05-18").unwrap(), + (1984, 5, 18).into() + ); + assert!(AlbumDate::from_mb_date("1984-get-rekt").is_err()); + } + + #[test] + fn serde() { + let primary_type = "\"EP\""; + let primary_type: SerdeAlbumPrimaryType = serde_json::from_str(primary_type).unwrap(); + let primary_type: AlbumPrimaryType = primary_type.into(); + assert_eq!(primary_type, AlbumPrimaryType::Ep); + + let secondary_type = "\"Field recording\""; + let secondary_type: SerdeAlbumSecondaryType = serde_json::from_str(secondary_type).unwrap(); + let secondary_type: AlbumSecondaryType = secondary_type.into(); + assert_eq!(secondary_type, AlbumSecondaryType::FieldRecording); + } +} diff --git a/src/external/musicbrainz/mod.rs b/src/external/musicbrainz/mod.rs new file mode 100644 index 0000000..98e2d12 --- /dev/null +++ b/src/external/musicbrainz/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "musicbrainz-api")] +pub mod api; diff --git a/src/lib.rs b/src/lib.rs index f864d95..0213dc6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,7 @@ //! MusicHoard - a music collection manager. mod core; -pub mod database; -pub mod library; +pub mod external; pub use core::collection; pub use core::interface; @@ -14,4 +13,4 @@ pub use core::musichoard::{ #[cfg(test)] #[macro_use] -mod tests; +mod testmod; diff --git a/src/main.rs b/src/main.rs index 96d57a6..d9edcae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,15 +10,17 @@ use ratatui::{backend::CrosstermBackend, Terminal}; use structopt::StructOpt; use musichoard::{ - database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, + external::{ + database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, + library::beets::{ + executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor}, + BeetsLibrary, + }, + }, interface::{ database::{IDatabase, NullDatabase}, library::{ILibrary, NullLibrary}, }, - library::beets::{ - executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor}, - BeetsLibrary, - }, MusicHoardBuilder, NoDatabase, NoLibrary, }; @@ -135,4 +137,4 @@ fn main() { #[cfg(test)] #[macro_use] -mod tests; +mod testmod; diff --git a/src/testmod/full.rs b/src/testmod/full.rs new file mode 100644 index 0000000..b91f846 --- /dev/null +++ b/src/testmod/full.rs @@ -0,0 +1,473 @@ +macro_rules! full_collection { + () => { + vec![ + Artist { + id: ArtistId { + name: "Album_Artist ‘A’".to_string(), + }, + sort: None, + musicbrainz: Some(MbArtistRef::from_url_str( + "https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000" + ).unwrap()), + properties: HashMap::from([ + (String::from("MusicButler"), vec![ + String::from("https://www.musicbutler.io/artist-page/000000000"), + ]), + (String::from("Qobuz"), vec![ + String::from( + "https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums", + ) + ]), + ]), + albums: vec![ + Album { + id: AlbumId { + title: "album_title a.a".to_string(), + }, + date: 1998.into(), + seq: AlbumSeq(1), + musicbrainz: Some(MbAlbumRef::from_url_str( + "https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000" + ).unwrap()), + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + tracks: vec![ + Track { + id: TrackId { + title: "track a.a.1".to_string(), + }, + number: TrackNum(1), + artist: vec!["artist a.a.1".to_string()], + quality: TrackQuality { + format: TrackFormat::Flac, + bitrate: 992, + }, + }, + Track { + id: TrackId { + title: "track a.a.2".to_string(), + }, + number: TrackNum(2), + artist: vec![ + "artist a.a.2.1".to_string(), + "artist a.a.2.2".to_string(), + ], + quality: TrackQuality { + format: TrackFormat::Mp3, + bitrate: 320, + }, + }, + Track { + id: TrackId { + title: "track a.a.3".to_string(), + }, + number: TrackNum(3), + artist: vec!["artist a.a.3".to_string()], + quality: TrackQuality { + format: TrackFormat::Flac, + bitrate: 1061, + }, + }, + Track { + id: TrackId { + title: "track a.a.4".to_string(), + }, + number: TrackNum(4), + artist: vec!["artist a.a.4".to_string()], + quality: TrackQuality { + format: TrackFormat::Flac, + bitrate: 1042, + }, + }, + ], + }, + Album { + id: AlbumId { + title: "album_title a.b".to_string(), + }, + date: (2015, 4).into(), + seq: AlbumSeq(1), + musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + tracks: vec![ + Track { + id: TrackId { + title: "track a.b.1".to_string(), + }, + number: TrackNum(1), + artist: vec!["artist a.b.1".to_string()], + quality: TrackQuality { + format: TrackFormat::Flac, + bitrate: 1004, + }, + }, + Track { + id: TrackId { + title: "track a.b.2".to_string(), + }, + number: TrackNum(2), + artist: vec!["artist a.b.2".to_string()], + quality: TrackQuality { + format: TrackFormat::Flac, + bitrate: 1077, + }, + }, + ], + }, + ], + }, + Artist { + id: ArtistId { + name: "Album_Artist ‘B’".to_string(), + }, + sort: None, + musicbrainz: Some(MbArtistRef::from_url_str( + "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111" + ).unwrap()), + properties: HashMap::from([ + (String::from("MusicButler"), vec![ + String::from("https://www.musicbutler.io/artist-page/111111111"), + String::from("https://www.musicbutler.io/artist-page/111111112"), + ]), + (String::from("Bandcamp"), vec![ + String::from("https://artist-b.bandcamp.com/") + ]), + (String::from("Qobuz"), vec![ + String::from( + "https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums", + ) + ]), + ]), + albums: vec![ + Album { + id: AlbumId { + title: "album_title b.a".to_string(), + }, + date: (2003, 6, 6).into(), + seq: AlbumSeq(1), + musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + tracks: vec![ + Track { + id: TrackId { + title: "track b.a.1".to_string(), + }, + number: TrackNum(1), + artist: vec!["artist b.a.1".to_string()], + quality: TrackQuality { + format: TrackFormat::Mp3, + bitrate: 190, + }, + }, + Track { + id: TrackId { + title: "track b.a.2".to_string(), + }, + number: TrackNum(2), + artist: vec![ + "artist b.a.2.1".to_string(), + "artist b.a.2.2".to_string(), + ], + quality: TrackQuality { + format: TrackFormat::Mp3, + bitrate: 120, + }, + }, + ], + }, + Album { + id: AlbumId { + title: "album_title b.b".to_string(), + }, + date: 2008.into(), + seq: AlbumSeq(3), + musicbrainz: Some(MbAlbumRef::from_url_str( + "https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111" + ).unwrap()), + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + tracks: vec![ + Track { + id: TrackId { + title: "track b.b.1".to_string(), + }, + number: TrackNum(1), + artist: vec!["artist b.b.1".to_string()], + quality: TrackQuality { + format: TrackFormat::Flac, + bitrate: 1077, + }, + }, + Track { + id: TrackId { + title: "track b.b.2".to_string(), + }, + number: TrackNum(2), + artist: vec![ + "artist b.b.2.1".to_string(), + "artist b.b.2.2".to_string(), + ], + quality: TrackQuality { + format: TrackFormat::Mp3, + bitrate: 320, + }, + }, + ], + }, + Album { + id: AlbumId { + title: "album_title b.c".to_string(), + }, + date: 2009.into(), + seq: AlbumSeq(2), + musicbrainz: Some(MbAlbumRef::from_url_str( + "https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112" + ).unwrap()), + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + tracks: vec![ + Track { + id: TrackId { + title: "track b.c.1".to_string(), + }, + number: TrackNum(1), + artist: vec!["artist b.c.1".to_string()], + quality: TrackQuality { + format: TrackFormat::Mp3, + bitrate: 190, + }, + }, + Track { + id: TrackId { + title: "track b.c.2".to_string(), + }, + number: TrackNum(2), + artist: vec![ + "artist b.c.2.1".to_string(), + "artist b.c.2.2".to_string(), + ], + quality: TrackQuality { + format: TrackFormat::Mp3, + bitrate: 120, + }, + }, + ], + }, + Album { + id: AlbumId { + title: "album_title b.d".to_string(), + }, + date: 2015.into(), + seq: AlbumSeq(4), + musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + tracks: vec![ + Track { + id: TrackId { + title: "track b.d.1".to_string(), + }, + number: TrackNum(1), + artist: vec!["artist b.d.1".to_string()], + quality: TrackQuality { + format: TrackFormat::Mp3, + bitrate: 190, + }, + }, + Track { + id: TrackId { + title: "track b.d.2".to_string(), + }, + number: TrackNum(2), + artist: vec![ + "artist b.d.2.1".to_string(), + "artist b.d.2.2".to_string(), + ], + quality: TrackQuality { + format: TrackFormat::Mp3, + bitrate: 120, + }, + }, + ], + }, + ], + }, + Artist { + id: ArtistId { + name: "The Album_Artist ‘C’".to_string(), + }, + sort: Some(ArtistId { + name: "Album_Artist ‘C’, The".to_string(), + }), + musicbrainz: Some(MbArtistRef::from_url_str( + "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111" + ).unwrap()), + properties: HashMap::new(), + albums: vec![ + Album { + id: AlbumId { + title: "album_title c.a".to_string(), + }, + date: 1985.into(), + seq: AlbumSeq(0), + musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + tracks: vec![ + Track { + id: TrackId { + title: "track c.a.1".to_string(), + }, + number: TrackNum(1), + artist: vec!["artist c.a.1".to_string()], + quality: TrackQuality { + format: TrackFormat::Mp3, + bitrate: 320, + }, + }, + Track { + id: TrackId { + title: "track c.a.2".to_string(), + }, + number: TrackNum(2), + artist: vec![ + "artist c.a.2.1".to_string(), + "artist c.a.2.2".to_string(), + ], + quality: TrackQuality { + format: TrackFormat::Mp3, + bitrate: 120, + }, + }, + ], + }, + Album { + id: AlbumId { + title: "album_title c.b".to_string(), + }, + date: 2018.into(), + seq: AlbumSeq(0), + musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + tracks: vec![ + Track { + id: TrackId { + title: "track c.b.1".to_string(), + }, + number: TrackNum(1), + artist: vec!["artist c.b.1".to_string()], + quality: TrackQuality { + format: TrackFormat::Flac, + bitrate: 1041, + }, + }, + Track { + id: TrackId { + title: "track c.b.2".to_string(), + }, + number: TrackNum(2), + artist: vec![ + "artist c.b.2.1".to_string(), + "artist c.b.2.2".to_string(), + ], + quality: TrackQuality { + format: TrackFormat::Flac, + bitrate: 756, + }, + }, + ], + }, + ], + }, + Artist { + id: ArtistId { + name: "Album_Artist ‘D’".to_string(), + }, + sort: None, + musicbrainz: None, + properties: HashMap::new(), + albums: vec![ + Album { + id: AlbumId { + title: "album_title d.a".to_string(), + }, + date: 1995.into(), + seq: AlbumSeq(0), + musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + tracks: vec![ + Track { + id: TrackId { + title: "track d.a.1".to_string(), + }, + number: TrackNum(1), + artist: vec!["artist d.a.1".to_string()], + quality: TrackQuality { + format: TrackFormat::Mp3, + bitrate: 120, + }, + }, + Track { + id: TrackId { + title: "track d.a.2".to_string(), + }, + number: TrackNum(2), + artist: vec![ + "artist d.a.2.1".to_string(), + "artist d.a.2.2".to_string(), + ], + quality: TrackQuality { + format: TrackFormat::Mp3, + bitrate: 120, + }, + }, + ], + }, + Album { + id: AlbumId { + title: "album_title d.b".to_string(), + }, + date: 2028.into(), + seq: AlbumSeq(0), + musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + tracks: vec![ + Track { + id: TrackId { + title: "track d.b.1".to_string(), + }, + number: TrackNum(1), + artist: vec!["artist d.b.1".to_string()], + quality: TrackQuality { + format: TrackFormat::Flac, + bitrate: 841, + }, + }, + Track { + id: TrackId { + title: "track d.b.2".to_string(), + }, + number: TrackNum(2), + artist: vec![ + "artist d.b.2.1".to_string(), + "artist d.b.2.2".to_string(), + ], + quality: TrackQuality { + format: TrackFormat::Flac, + bitrate: 756, + }, + }, + ], + }, + ], + }, + ] + }; +} + +pub(crate) use full_collection; diff --git a/src/tests.rs b/src/testmod/library.rs similarity index 78% rename from src/tests.rs rename to src/testmod/library.rs index ee4f0c6..7011083 100644 --- a/src/tests.rs +++ b/src/testmod/library.rs @@ -1,3 +1,4 @@ +#[allow(unused_macros)] macro_rules! library_collection { () => { vec![ @@ -13,13 +14,11 @@ macro_rules! library_collection { id: AlbumId { title: "album_title a.a".to_string(), }, - date: AlbumDate { - year: 1998, - month: AlbumMonth::None, - day: 0, - }, + date: 1998.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: None, + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -74,13 +73,11 @@ macro_rules! library_collection { id: AlbumId { title: "album_title a.b".to_string(), }, - date: AlbumDate { - year: 2015, - month: AlbumMonth::April, - day: 0, - }, + date: (2015, 4).into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: None, + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -120,13 +117,11 @@ macro_rules! library_collection { id: AlbumId { title: "album_title b.a".to_string(), }, - date: AlbumDate { - year: 2003, - month: AlbumMonth::June, - day: 6, - }, + date: (2003, 6, 6).into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: None, + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -159,13 +154,11 @@ macro_rules! library_collection { id: AlbumId { title: "album_title b.b".to_string(), }, - date: AlbumDate { - year: 2008, - month: AlbumMonth::None, - day: 0, - }, + date: 2008.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: None, + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -198,13 +191,11 @@ macro_rules! library_collection { id: AlbumId { title: "album_title b.c".to_string(), }, - date: AlbumDate { - year: 2009, - month: AlbumMonth::None, - day: 0, - }, + date: 2009.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: None, + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -237,13 +228,11 @@ macro_rules! library_collection { id: AlbumId { title: "album_title b.d".to_string(), }, - date: AlbumDate { - year: 2015, - month: AlbumMonth::None, - day: 0, - }, + date: 2015.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: None, + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -288,13 +277,11 @@ macro_rules! library_collection { id: AlbumId { title: "album_title c.a".to_string(), }, - date: AlbumDate { - year: 1985, - month: AlbumMonth::None, - day: 0, - }, + date: 1985.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: None, + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -327,13 +314,11 @@ macro_rules! library_collection { id: AlbumId { title: "album_title c.b".to_string(), }, - date: AlbumDate { - year: 2018, - month: AlbumMonth::None, - day: 0, - }, + date: 2018.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: None, + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -376,13 +361,11 @@ macro_rules! library_collection { id: AlbumId { title: "album_title d.a".to_string(), }, - date: AlbumDate { - year: 1995, - month: AlbumMonth::None, - day: 0, - }, + date: 1995.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: None, + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -415,13 +398,11 @@ macro_rules! library_collection { id: AlbumId { title: "album_title d.b".to_string(), }, - date: AlbumDate { - year: 2028, - month: AlbumMonth::None, - day: 0, - }, + date: 2028.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: None, + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -456,81 +437,5 @@ macro_rules! library_collection { }; } -macro_rules! full_collection { - () => {{ - let mut collection = library_collection!(); - let mut iter = collection.iter_mut(); - - let artist_a = iter.next().unwrap(); - assert_eq!(artist_a.id.name, "Album_Artist ‘A’"); - - artist_a.musicbrainz = Some(MusicBrainzUrl::artist_from_str( - "https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000", - ).unwrap()); - - artist_a.properties = HashMap::from([ - (String::from("MusicButler"), vec![ - String::from("https://www.musicbutler.io/artist-page/000000000"), - ]), - (String::from("Qobuz"), vec![ - String::from( - "https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums", - ) - ]), - ]); - - artist_a.albums[0].seq = AlbumSeq(1); - artist_a.albums[1].seq = AlbumSeq(1); - - artist_a.albums[0].musicbrainz = Some(MusicBrainzUrl::album_from_str( - "https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000" - ).unwrap()); - - let artist_b = iter.next().unwrap(); - assert_eq!(artist_b.id.name, "Album_Artist ‘B’"); - - artist_b.musicbrainz = Some(MusicBrainzUrl::artist_from_str( - "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", - ).unwrap()); - - artist_b.properties = HashMap::from([ - (String::from("MusicButler"), vec![ - String::from("https://www.musicbutler.io/artist-page/111111111"), - String::from("https://www.musicbutler.io/artist-page/111111112"), - ]), - (String::from("Bandcamp"), vec![String::from("https://artist-b.bandcamp.com/")]), - (String::from("Qobuz"), vec![ - String::from( - "https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums", - ) - ]), - ]); - - artist_b.albums[0].seq = AlbumSeq(1); - artist_b.albums[1].seq = AlbumSeq(3); - artist_b.albums[2].seq = AlbumSeq(2); - artist_b.albums[3].seq = AlbumSeq(4); - - artist_b.albums[1].musicbrainz = Some(MusicBrainzUrl::album_from_str( - "https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111" - ).unwrap()); - - artist_b.albums[2].musicbrainz = Some(MusicBrainzUrl::album_from_str( - "https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112" - ).unwrap()); - - let artist_c = iter.next().unwrap(); - assert_eq!(artist_c.id.name, "The Album_Artist ‘C’"); - - artist_c.musicbrainz = Some(MusicBrainzUrl::artist_from_str( - "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", - ).unwrap()); - - // Nothing for artist_d - - collection - }}; -} - -pub(crate) use full_collection; +#[allow(unused_imports)] pub(crate) use library_collection; diff --git a/src/testmod/mod.rs b/src/testmod/mod.rs new file mode 100644 index 0000000..7239439 --- /dev/null +++ b/src/testmod/mod.rs @@ -0,0 +1,2 @@ +pub mod full; +pub mod library; diff --git a/src/tui/testmod.rs b/src/tui/testmod.rs index bc2ede8..5340087 100644 --- a/src/tui/testmod.rs +++ b/src/tui/testmod.rs @@ -1,13 +1,13 @@ use std::collections::HashMap; use musichoard::collection::{ - album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq}, + album::{Album, AlbumId, AlbumPrimaryType, AlbumSeq}, artist::{Artist, ArtistId}, - musicbrainz::MusicBrainzUrl, + musicbrainz::{MbAlbumRef, MbArtistRef}, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, }; use once_cell::sync::Lazy; -use crate::tests::*; +use crate::testmod::*; -pub static COLLECTION: Lazy> = Lazy::new(|| full_collection!()); +pub static COLLECTION: Lazy> = Lazy::new(|| full::full_collection!()); diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 119c15c..8949e13 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use musichoard::collection::{ album::{Album, AlbumDate, AlbumSeq, AlbumStatus}, artist::Artist, + musicbrainz::IMusicBrainzRef, track::{Track, TrackFormat, TrackQuality}, Collection, }; @@ -202,7 +203,7 @@ struct ArtistOverlay<'a> { } impl<'a> ArtistOverlay<'a> { - fn opt_opt_to_str>(opt: Option>) -> &str { + fn opt_opt_to_str + ?Sized>(opt: Option>) -> &str { opt.flatten().map(|item| item.as_ref()).unwrap_or("") } @@ -264,7 +265,7 @@ impl<'a> ArtistOverlay<'a> { MusicBrainz: {}\n{item_indent}\ Properties: {}", artist.map(|a| a.id.name.as_str()).unwrap_or(""), - Self::opt_opt_to_str(artist.map(|a| a.musicbrainz.as_ref())), + Self::opt_opt_to_str(artist.map(|a| a.musicbrainz.as_ref().map(|mb| mb.url()))), Self::opt_hashmap_to_string( artist.map(|a| &a.properties), &double_item_indent, @@ -329,12 +330,15 @@ impl<'a, 'b> AlbumState<'a, 'b> { } fn display_album_date(date: &AlbumDate) -> String { - if date.month.is_none() { - format!("{}", date.year) - } else if date.day == 0 { - format!("{}‐{:02}", date.year, date.month as u8) - } else { - format!("{}‐{:02}‐{:02}", date.year, date.month as u8, date.day) + match date.year { + Some(year) => match date.month { + Some(month) => match date.day { + Some(day) => format!("{year}‐{month:02}‐{day:02}"), + None => format!("{year}‐{month:02}"), + }, + None => format!("{year}"), + }, + None => String::from(""), } } @@ -804,17 +808,11 @@ mod tests { #[test] fn display_album_date() { - assert_eq!(AlbumState::display_album_date(&AlbumDate::default()), "0"); + assert_eq!(AlbumState::display_album_date(&AlbumDate::default()), ""); + assert_eq!(AlbumState::display_album_date(&1990.into()), "1990"); + assert_eq!(AlbumState::display_album_date(&(1990, 5).into()), "1990‐05"); assert_eq!( - AlbumState::display_album_date(&AlbumDate::new(1990, 0, 0)), - "1990" - ); - assert_eq!( - AlbumState::display_album_date(&AlbumDate::new(1990, 5, 0)), - "1990‐05" - ); - assert_eq!( - AlbumState::display_album_date(&AlbumDate::new(1990, 5, 6)), + AlbumState::display_album_date(&(1990, 5, 6).into()), "1990‐05‐06" ); } @@ -870,7 +868,7 @@ mod tests { let mut artists: Vec = vec![Artist::new(ArtistId::new("An artist"))]; artists[0] .albums - .push(Album::new("An album", AlbumDate::default())); + .push(Album::new("An album", AlbumDate::default(), None, vec![])); let mut selection = Selection::new(&artists); draw_test_suite(&artists, &mut selection); diff --git a/tests/database/json.rs b/tests/database/json.rs index a72657a..e803557 100644 --- a/tests/database/json.rs +++ b/tests/database/json.rs @@ -5,7 +5,7 @@ use tempfile::NamedTempFile; use musichoard::{ collection::{album::AlbumDate, artist::Artist, Collection}, - database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, + external::database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, interface::database::IDatabase, }; diff --git a/tests/files/database/database.json b/tests/files/database/database.json index ae7043e..9a288e0 100644 --- a/tests/files/database/database.json +++ b/tests/files/database/database.json @@ -1 +1 @@ -{"V20240308":[{"name":"Аркона","sort":"Arkona","musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","properties":{"Bandcamp":["https://arkonamoscow.bandcamp.com/"],"MusicButler":["https://www.musicbutler.io/artist-page/283448581"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"]},"albums":[{"title":"Slovo","seq":0,"musicbrainz":null}]},{"name":"Eluveitie","sort":null,"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/269358403"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"]},"albums":[{"title":"Vên [re‐recorded]","seq":0,"musicbrainz":null},{"title":"Slania","seq":0,"musicbrainz":null}]},{"name":"Frontside","sort":null,"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/826588800"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"]},"albums":[{"title":"…nasze jest królestwo, potęga i chwała na wieki…","seq":0,"musicbrainz":null}]},{"name":"Heaven’s Basement","sort":"Heaven’s Basement","musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/291158685"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"]},"albums":[{"title":"Paper Plague","seq":0,"musicbrainz":null},{"title":"Unbreakable","seq":0,"musicbrainz":null}]},{"name":"Metallica","sort":null,"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/3996865"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"]},"albums":[{"title":"Ride the Lightning","seq":0,"musicbrainz":null},{"title":"S&M","seq":0,"musicbrainz":null}]}]} \ No newline at end of file +{"V20240313":[{"name":"Аркона","sort":"Arkona","musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","properties":{"Bandcamp":["https://arkonamoscow.bandcamp.com/"],"MusicButler":["https://www.musicbutler.io/artist-page/283448581"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"]},"albums":[{"title":"Slovo","seq":0,"musicbrainz":null,"primary_type":"Album","secondary_types":[]}]},{"name":"Eluveitie","sort":null,"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/269358403"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"]},"albums":[{"title":"Vên [re‐recorded]","seq":0,"musicbrainz":null,"primary_type":"Ep","secondary_types":[]},{"title":"Slania","seq":0,"musicbrainz":null,"primary_type":"Album","secondary_types":[]}]},{"name":"Frontside","sort":null,"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/826588800"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"]},"albums":[{"title":"…nasze jest królestwo, potęga i chwała na wieki…","seq":0,"musicbrainz":null,"primary_type":"Album","secondary_types":[]}]},{"name":"Heaven’s Basement","sort":"Heaven’s Basement","musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/291158685"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"]},"albums":[{"title":"Paper Plague","seq":0,"musicbrainz":null,"primary_type":null,"secondary_types":[]},{"title":"Unbreakable","seq":0,"musicbrainz":null,"primary_type":"Album","secondary_types":[]}]},{"name":"Metallica","sort":null,"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/3996865"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"]},"albums":[{"title":"Ride the Lightning","seq":0,"musicbrainz":null,"primary_type":"Album","secondary_types":[]},{"title":"S&M","seq":0,"musicbrainz":null,"primary_type":"Album","secondary_types":["Live"]}]}]} \ No newline at end of file diff --git a/tests/lib.rs b/tests/lib.rs index ce2e5aa..72f6417 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -7,8 +7,10 @@ mod library; mod testlib; use musichoard::{ - database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, - library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary}, + external::{ + database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, + library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary}, + }, IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard, }; diff --git a/tests/library/beets.rs b/tests/library/beets.rs index 0a7a066..fdfd76d 100644 --- a/tests/library/beets.rs +++ b/tests/library/beets.rs @@ -8,8 +8,8 @@ use std::{ use once_cell::sync::Lazy; use musichoard::{ + external::library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary}, interface::library::{Field, ILibrary, Item, Query}, - library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary}, }; use crate::library::testmod::LIBRARY_ITEMS; diff --git a/tests/library/testmod.rs b/tests/library/testmod.rs index aa6e671..ba62508 100644 --- a/tests/library/testmod.rs +++ b/tests/library/testmod.rs @@ -1,9 +1,6 @@ use once_cell::sync::Lazy; -use musichoard::{ - collection::{album::AlbumMonth, track::TrackFormat}, - interface::library::Item, -}; +use musichoard::{collection::track::TrackFormat, interface::library::Item}; pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { vec![ @@ -11,7 +8,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 1, @@ -24,7 +21,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 2, @@ -37,7 +34,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 3, @@ -50,7 +47,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 4, @@ -63,7 +60,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 5, @@ -76,7 +73,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 6, @@ -89,7 +86,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 7, @@ -102,7 +99,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 8, @@ -115,7 +112,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 9, @@ -128,7 +125,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 10, @@ -141,7 +138,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 11, @@ -154,7 +151,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 12, @@ -167,7 +164,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 13, @@ -180,7 +177,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slovo"), track_number: 14, @@ -193,7 +190,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2004, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Vên [re‐recorded]"), track_number: 1, @@ -206,7 +203,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2004, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Vên [re‐recorded]"), track_number: 2, @@ -219,7 +216,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2004, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Vên [re‐recorded]"), track_number: 3, @@ -232,7 +229,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2004, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Vên [re‐recorded]"), track_number: 4, @@ -245,7 +242,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2004, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Vên [re‐recorded]"), track_number: 5, @@ -258,7 +255,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2004, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Vên [re‐recorded]"), track_number: 6, @@ -271,7 +268,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slania"), track_number: 1, @@ -284,7 +281,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slania"), track_number: 2, @@ -297,7 +294,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slania"), track_number: 3, @@ -310,7 +307,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slania"), track_number: 4, @@ -323,7 +320,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slania"), track_number: 5, @@ -336,7 +333,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slania"), track_number: 6, @@ -349,7 +346,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slania"), track_number: 7, @@ -362,7 +359,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slania"), track_number: 8, @@ -375,7 +372,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slania"), track_number: 9, @@ -388,7 +385,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slania"), track_number: 10, @@ -401,7 +398,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slania"), track_number: 11, @@ -414,7 +411,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Slania"), track_number: 12, @@ -427,7 +424,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 1, @@ -440,7 +437,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 2, @@ -453,7 +450,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 3, @@ -466,7 +463,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 4, @@ -479,7 +476,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 5, @@ -492,7 +489,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 6, @@ -505,7 +502,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 7, @@ -518,7 +515,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 8, @@ -531,7 +528,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 9, @@ -544,7 +541,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 10, @@ -557,7 +554,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 11, @@ -570,7 +567,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Heaven’s Basement"), album_artist_sort: None, album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Paper Plague"), track_number: 0, @@ -583,7 +580,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Unbreakable"), track_number: 1, @@ -596,7 +593,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Unbreakable"), track_number: 2, @@ -609,7 +606,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Unbreakable"), track_number: 3, @@ -622,7 +619,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Unbreakable"), track_number: 4, @@ -635,7 +632,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Unbreakable"), track_number: 5, @@ -648,7 +645,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Unbreakable"), track_number: 6, @@ -661,7 +658,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Unbreakable"), track_number: 7, @@ -674,7 +671,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 1, @@ -687,7 +684,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 2, @@ -700,7 +697,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 3, @@ -713,7 +710,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 4, @@ -726,7 +723,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 5, @@ -739,7 +736,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 6, @@ -752,7 +749,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 7, @@ -765,7 +762,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 8, @@ -778,7 +775,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 1, @@ -791,7 +788,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 2, @@ -804,7 +801,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 3, @@ -817,7 +814,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 4, @@ -830,7 +827,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 5, @@ -843,7 +840,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 6, @@ -856,7 +853,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 7, @@ -869,7 +866,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 8, @@ -882,7 +879,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 9, @@ -895,7 +892,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 10, @@ -908,7 +905,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 11, @@ -921,7 +918,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 12, @@ -934,7 +931,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 13, @@ -947,7 +944,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 14, @@ -960,7 +957,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 15, @@ -973,7 +970,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 16, @@ -986,7 +983,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 17, @@ -999,7 +996,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 18, @@ -1012,7 +1009,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 19, @@ -1025,7 +1022,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 20, @@ -1038,7 +1035,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, - album_month: AlbumMonth::None, + album_month: 0, album_day: 0, album_title: String::from("S&M"), track_number: 21, diff --git a/tests/testlib.rs b/tests/testlib.rs index a545586..3261e0a 100644 --- a/tests/testlib.rs +++ b/tests/testlib.rs @@ -2,9 +2,9 @@ use once_cell::sync::Lazy; use std::collections::HashMap; use musichoard::collection::{ - album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq}, + album::{Album, AlbumId, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq}, artist::{Artist, ArtistId}, - musicbrainz::MusicBrainzUrl, + musicbrainz::MbArtistRef, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, Collection, }; @@ -18,7 +18,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { sort: Some(ArtistId{ name: String::from("Arkona") }), - musicbrainz: Some(MusicBrainzUrl::artist_from_str( + musicbrainz: Some(MbArtistRef::from_url_str( "https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212" ).unwrap()), properties: HashMap::from([ @@ -36,13 +36,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: AlbumId { title: String::from("Slovo"), }, - date: AlbumDate { - year: 2011, - month: AlbumMonth::None, - day: 0, - }, + date: 2011.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -206,8 +204,8 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Eluveitie"), }, sort: None, - musicbrainz: Some(MusicBrainzUrl::artist_from_str( - "https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38", + musicbrainz: Some(MbArtistRef::from_url_str( + "https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38" ).unwrap()), properties: HashMap::from([ (String::from("MusicButler"), vec![ @@ -222,13 +220,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: AlbumId { title: String::from("Vên [re‐recorded]"), }, - date: AlbumDate { - year: 2004, - month: AlbumMonth::None, - day: 0, - }, + date: 2004.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Ep), + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -302,13 +298,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: AlbumId { title: String::from("Slania"), }, - date: AlbumDate { - year: 2008, - month: AlbumMonth::None, - day: 0, - }, + date: 2008.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -451,8 +445,8 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Frontside"), }, sort: None, - musicbrainz: Some(MusicBrainzUrl::artist_from_str( - "https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490", + musicbrainz: Some(MbArtistRef::from_url_str( + "https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490" ).unwrap()), properties: HashMap::from([ (String::from("MusicButler"), vec![ @@ -466,13 +460,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: AlbumId { title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), }, - date: AlbumDate { - year: 2001, - month: AlbumMonth::None, - day: 0, - }, + date: 2001.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -605,8 +597,8 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { sort: Some(ArtistId { name: String::from("Heaven’s Basement"), }), - musicbrainz: Some(MusicBrainzUrl::artist_from_str( - "https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc", + musicbrainz: Some(MbArtistRef::from_url_str( + "https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc" ).unwrap()), properties: HashMap::from([ (String::from("MusicButler"), vec![ @@ -620,13 +612,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: AlbumId { title: String::from("Paper Plague"), }, - date: AlbumDate { - year: 2011, - month: AlbumMonth::None, - day: 0, - }, + date: 2011.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: None, + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -644,13 +634,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: AlbumId { title: String::from("Unbreakable"), }, - date: AlbumDate { - year: 2011, - month: AlbumMonth::None, - day: 0, - }, + date: 2011.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -737,8 +725,8 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Metallica"), }, sort: None, - musicbrainz: Some(MusicBrainzUrl::artist_from_str( - "https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab", + musicbrainz: Some(MbArtistRef::from_url_str( + "https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab" ).unwrap()), properties: HashMap::from([ (String::from("MusicButler"), vec![ @@ -753,13 +741,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: AlbumId { title: String::from("Ride the Lightning"), }, - date: AlbumDate { - year: 1984, - month: AlbumMonth::None, - day: 0, - }, + date: 1984.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], tracks: vec![ Track { id: TrackId { @@ -855,13 +841,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: AlbumId { title: String::from("S&M"), }, - date: AlbumDate { - year: 1999, - month: AlbumMonth::None, - day: 0, - }, + date: 1999.into(), seq: AlbumSeq(0), musicbrainz: None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![AlbumSecondaryType::Live], tracks: vec![ Track { id: TrackId {