From 473825b396fb6cba60ecec701359f05fbf091298 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 10 Mar 2024 09:43:53 +0100 Subject: [PATCH] First draft of musicbrainz interface Some reorganisation Remove unnecessary trait Basic example working Handle errors Handle dates Expand scope of MusicBrainz reference Type the musicbrainz refs Explicit constructors for str Handle MBIDs for albums Add search to the API Handle primary and secondary types Simplify AlbumDate Passing unit tests Tests pass Prevent compiler/clippy warnings Finish unit tests Clippy Remove old deserialize version --- Cargo.lock | 593 ++++++++++++++++++ Cargo.toml | 6 + README.md | 13 + src/bin/musichoard-edit.rs | 2 +- src/core/collection/album.rs | 213 ++++--- src/core/collection/artist.rs | 24 +- src/core/collection/musicbrainz.rs | 163 +++-- src/core/interface/database/mod.rs | 4 +- src/core/interface/library/mod.rs | 8 +- src/core/interface/library/testmod.rs | 49 +- src/core/interface/mod.rs | 1 + src/core/interface/musicbrainz/mod.rs | 180 ++++++ src/core/musichoard/builder.rs | 9 +- src/core/musichoard/database.rs | 23 +- src/core/musichoard/library.rs | 8 +- src/core/testmod.rs | 10 +- src/{ => external}/database/json/backend.rs | 2 +- src/{ => external}/database/json/mod.rs | 1 + src/{ => external}/database/json/testmod.rs | 46 +- src/{ => external}/database/mod.rs | 0 src/external/database/serde/common.rs | 62 ++ .../database/serde/deserialize.rs | 34 +- src/{ => external}/database/serde/mod.rs | 1 + .../database/serde/serialize.rs | 22 +- src/{ => external}/library/beets/executor.rs | 3 +- src/{ => external}/library/beets/mod.rs | 15 +- src/{ => external}/library/beets/testmod.rs | 0 src/{ => external}/library/mod.rs | 0 src/external/mod.rs | 3 + src/external/musicbrainz/api/client.rs | 46 ++ src/external/musicbrainz/api/mod.rs | 404 ++++++++++++ src/external/musicbrainz/mod.rs | 2 + src/lib.rs | 5 +- src/main.rs | 14 +- src/testmod/full.rs | 473 ++++++++++++++ src/{tests.rs => testmod/library.rs} | 159 +---- src/testmod/mod.rs | 2 + src/tui/testmod.rs | 8 +- src/tui/ui.rs | 36 +- tests/database/json.rs | 2 +- tests/files/database/database.json | 2 +- tests/lib.rs | 6 +- tests/library/beets.rs | 2 +- tests/library/testmod.rs | 165 +++-- tests/testlib.rs | 86 ++- 45 files changed, 2346 insertions(+), 561 deletions(-) create mode 100644 src/core/interface/musicbrainz/mod.rs rename src/{ => external}/database/json/backend.rs (93%) rename src/{ => external}/database/json/mod.rs (99%) rename src/{ => external}/database/json/testmod.rs (58%) rename src/{ => external}/database/mod.rs (100%) create mode 100644 src/external/database/serde/common.rs rename src/{ => external}/database/serde/deserialize.rs (65%) rename src/{ => external}/database/serde/mod.rs (90%) rename src/{ => external}/database/serde/serialize.rs (61%) rename src/{ => external}/library/beets/executor.rs (98%) rename src/{ => external}/library/beets/mod.rs (96%) rename src/{ => external}/library/beets/testmod.rs (100%) rename src/{ => external}/library/mod.rs (100%) create mode 100644 src/external/mod.rs create mode 100644 src/external/musicbrainz/api/client.rs create mode 100644 src/external/musicbrainz/api/mod.rs create mode 100644 src/external/musicbrainz/mod.rs create mode 100644 src/testmod/full.rs rename src/{tests.rs => testmod/library.rs} (78%) create mode 100644 src/testmod/mod.rs 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 {