First draft of musicbrainz interface
Some checks failed
Cargo CI / Build and Test (pull_request) Failing after 1m37s
Cargo CI / Lint (pull_request) Failing after 16s

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
This commit is contained in:
Wojciech Kozlowski 2024-03-10 09:43:53 +01:00
parent c53ba8f35f
commit 473825b396
45 changed files with 2346 additions and 561 deletions

593
Cargo.lock generated
View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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,
};

View File

@ -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<MusicBrainzUrl>,
pub musicbrainz: Option<MbAlbumRef>,
pub primary_type: Option<AlbumPrimaryType>,
pub secondary_types: Vec<AlbumSecondaryType>,
pub tracks: Vec<Track>,
}
@ -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<u32>,
pub month: Option<u8>,
pub day: Option<u8>,
}
impl AlbumDate {
pub fn new<M: Into<AlbumMonth>>(year: u32, month: M, day: u8) -> Self {
AlbumDate {
year,
month: month.into(),
day,
}
pub fn new(year: Option<u32>, month: Option<u8>, day: Option<u8>) -> Self {
AlbumDate { year, month, day }
}
}
impl From<u32> for AlbumDate {
fn from(value: u32) -> Self {
AlbumDate::new(value, AlbumMonth::default(), 0)
AlbumDate::new(Some(value), None, None)
}
}
impl<M: Into<AlbumMonth>> 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<M: Into<AlbumMonth>> 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<u8> 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<Id: Into<AlbumId>, Date: Into<AlbumDate>>(id: Id, date: Date) -> Self {
pub fn new<Id: Into<AlbumId>, Date: Into<AlbumDate>>(
id: Id,
date: Date,
primary_type: Option<AlbumPrimaryType>,
secondary_types: Vec<AlbumSecondaryType>,
) -> 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!(<u8 as Into<AlbumMonth>>::into(0), AlbumMonth::None);
assert_eq!(<u8 as Into<AlbumMonth>>::into(1), AlbumMonth::January);
assert_eq!(<u8 as Into<AlbumMonth>>::into(2), AlbumMonth::February);
assert_eq!(<u8 as Into<AlbumMonth>>::into(3), AlbumMonth::March);
assert_eq!(<u8 as Into<AlbumMonth>>::into(4), AlbumMonth::April);
assert_eq!(<u8 as Into<AlbumMonth>>::into(5), AlbumMonth::May);
assert_eq!(<u8 as Into<AlbumMonth>>::into(6), AlbumMonth::June);
assert_eq!(<u8 as Into<AlbumMonth>>::into(7), AlbumMonth::July);
assert_eq!(<u8 as Into<AlbumMonth>>::into(8), AlbumMonth::August);
assert_eq!(<u8 as Into<AlbumMonth>>::into(9), AlbumMonth::September);
assert_eq!(<u8 as Into<AlbumMonth>>::into(10), AlbumMonth::October);
assert_eq!(<u8 as Into<AlbumMonth>>::into(11), AlbumMonth::November);
assert_eq!(<u8 as Into<AlbumMonth>>::into(12), AlbumMonth::December);
assert_eq!(<u8 as Into<AlbumMonth>>::into(13), AlbumMonth::None);
assert_eq!(<u8 as Into<AlbumMonth>>::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<MbAlbumRef> = 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);
}
}

View File

@ -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<ArtistId>,
pub musicbrainz: Option<MusicBrainzUrl>,
pub musicbrainz: Option<MbArtistRef>,
pub properties: HashMap<String, Vec<String>>,
pub albums: Vec<Album>,
}
@ -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<MusicBrainzUrl> = None;
let mut expected: Option<MbArtistRef> = 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);
}

View File

@ -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<S: AsRef<str>>(url: S) -> Result<Self, Error> {
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<S: AsRef<str>>(url: S) -> Result<Self, Error> {
Self::album_from_url(url.as_ref().try_into()?)
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MbArtistRef(MusicBrainzRef);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MbAlbumRef(MusicBrainzRef);
macro_rules! impl_imusicbrainzref {
($mbref:ident, $entity:literal) => {
impl IMusicBrainzRef for $mbref {
fn mbid(&self) -> &Mbid {
&self.0.mbid
}
pub fn artist_from_url(url: Url) -> Result<Self, Error> {
Self::new(url, "artist")
fn url(&self) -> &Url {
&self.0.url
}
pub fn album_from_url(url: Url) -> Result<Self, Error> {
Self::new(url, "release-group")
fn entity() -> &'static str {
$entity
}
}
fn new(url: Url, mb_type: &str) -> Result<Self, Error> {
impl TryFrom<Url> for $mbref {
type Error = Error;
fn try_from(url: Url) -> Result<Self, Self::Error> {
Ok($mbref(MusicBrainzRef::from_url(url, $mbref::entity())?))
}
}
impl From<Uuid> for $mbref {
fn from(uuid: Uuid) -> Self {
$mbref(MusicBrainzRef::from_uuid(uuid, $mbref::entity()))
}
}
impl $mbref {
pub fn from_url_str<S: AsRef<str>>(url: S) -> Result<Self, Error> {
let url: Url = url.as_ref().try_into()?;
url.try_into()
}
}
impl $mbref {
pub fn from_uuid_str<S: AsRef<str>>(uuid: S) -> Result<Self, Error> {
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<Self, Error> {
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<U: Display>(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<str> for MusicBrainzUrl {
fn as_ref(&self) -> &str {
self.0.as_ref()
fn invalid_url_error<U: Display>(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());
}

View File

@ -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());
}

View File

@ -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<String>,
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());
}

View File

@ -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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
vec![
@ -11,7 +8,7 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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,

View File

@ -1,2 +1,3 @@
pub mod database;
pub mod library;
pub mod musicbrainz;

View File

@ -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<Vec<Album>, Error>;
fn search_release_group(
&mut self,
arid: &Mbid,
album: Album,
) -> Result<Vec<Match<Album>>, Error>;
}
#[derive(Debug, PartialEq, Eq)]
pub struct Match<T> {
pub score: u8,
pub item: T,
}
impl<T> Match<T> {
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<Vec<Album>, Error> {
Ok(vec![])
}
fn search_release_group(
&mut self,
_arid: &Mbid,
_album: Album,
) -> Result<Vec<Match<Album>>, 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<Uuid> 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<Self, Self::Error> {
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<uuid::Error> for Error {
fn from(value: uuid::Error) -> Self {
Error::MbidParse(value.to_string())
}
}
impl From<num::ParseIntError> 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::<Mbid>::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::<u32>().unwrap_err().into();
assert!(!parse_err.to_string().is_empty());
assert!(!format!("{parse_err:?}").is_empty());
}
}

View File

@ -1,11 +1,8 @@
use std::collections::HashMap;
use crate::{
core::{
use crate::core::{
interface::{database::IDatabase, library::ILibrary},
musichoard::{database::IMusicHoardDatabase, MusicHoard, NoDatabase, NoLibrary},
},
Error,
musichoard::{database::IMusicHoardDatabase, Error, MusicHoard, NoDatabase, NoLibrary},
};
/// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of

View File

@ -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<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
artist_id: Id,
url: S,
) -> 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<Id: AsRef<ArtistId>>(
&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<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
@ -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<MusicBrainzUrl> = None;
let mut expected: Option<MbArtistRef> = 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()

View File

@ -59,9 +59,9 @@ impl<Database, Library: ILibrary> MusicHoard<Database, Library> {
};
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<Database, Library: ILibrary> MusicHoard<Database, Library> {
{
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);
}

View File

@ -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<Vec<Artist>> = Lazy::new(|| library_collection!());
pub static FULL_COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| full_collection!());
pub static LIBRARY_COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| library::library_collection!());
pub static FULL_COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| full::full_collection!());

View File

@ -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 {

View File

@ -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);

View File

@ -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\":[]\
}\
]\
}\
]\

62
src/external/database/serde/common.rs vendored Normal file
View File

@ -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<SerdeAlbumPrimaryType> for AlbumPrimaryType {
fn from(value: SerdeAlbumPrimaryType) -> Self {
value.0
}
}
impl From<AlbumPrimaryType> 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<SerdeAlbumSecondaryType> for AlbumSecondaryType {
fn from(value: SerdeAlbumSecondaryType) -> Self {
value.0
}
}
impl From<AlbumSecondaryType> for SerdeAlbumSecondaryType {
fn from(value: AlbumSecondaryType) -> Self {
SerdeAlbumSecondaryType(value)
}
}

View File

@ -2,19 +2,22 @@ use std::collections::HashMap;
use serde::Deserialize;
use crate::core::{
use crate::{
core::{
collection::{
album::{Album, AlbumDate, AlbumId, AlbumSeq},
artist::{Artist, ArtistId},
musicbrainz::MusicBrainzUrl,
musicbrainz::{MbAlbumRef, MbArtistRef},
Collection,
},
interface::database::LoadError,
},
external::database::serde::common::{SerdeAlbumPrimaryType, SerdeAlbumSecondaryType},
};
#[derive(Debug, Deserialize)]
pub enum DeserializeDatabase {
V20240308(Vec<DeserializeArtist>),
V20240313(Vec<DeserializeArtist>),
}
impl TryFrom<DeserializeDatabase> for Collection {
@ -22,10 +25,9 @@ impl TryFrom<DeserializeDatabase> for Collection {
fn try_from(database: DeserializeDatabase) -> Result<Self, Self::Error> {
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<String>,
primary_type: Option<SerdeAlbumPrimaryType>,
secondary_types: Vec<SerdeAlbumSecondaryType>,
}
impl TryFrom<DeserializeArtist> for Artist {
@ -55,7 +59,7 @@ impl TryFrom<DeserializeArtist> 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<DeserializeAlbum> 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![],
})
}

View File

@ -1,4 +1,5 @@
//! Helper module for backends that can use serde for (de)serialisation.
mod common;
pub mod deserialize;
pub mod serialize;

View File

@ -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<SerializeArtist<'a>>),
V20240313(Vec<SerializeArtist<'a>>),
}
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<SerdeAlbumPrimaryType>,
secondary_types: Vec<SerdeAlbumSecondaryType>,
}
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(),
}
}
}

View File

@ -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";

View File

@ -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<BLE> {
executor: BLE,
}
trait ILibraryPrivate {
fn list_cmd_and_args(query: &Query) -> Vec<String>;
fn list_to_items<S: AsRef<str>>(list_output: &[S]) -> Result<Vec<Item>, Error>;
}
impl<BLE: IBeetsLibraryExecutor> BeetsLibrary<BLE> {
/// Create a new beets library with the provided executor, e.g.
/// [`executor::BeetsLibraryProcessExecutor`].
@ -132,7 +127,7 @@ impl<BLE: IBeetsLibraryExecutor> ILibrary for BeetsLibrary<BLE> {
}
}
impl<BLE: IBeetsLibraryExecutor> ILibraryPrivate for BeetsLibrary<BLE> {
impl<BLE: IBeetsLibraryExecutor> BeetsLibrary<BLE> {
fn list_cmd_and_args(query: &Query) -> Vec<String> {
let mut cmd: Vec<String> = vec![String::from(CMD_LIST)];
cmd.push(LIST_FORMAT_ARG.to_string());
@ -159,7 +154,7 @@ impl<BLE: IBeetsLibraryExecutor> ILibraryPrivate for BeetsLibrary<BLE> {
false => None,
};
let album_year = split[2].parse::<u32>()?;
let album_month = split[3].parse::<u8>()?.into();
let album_month = split[3].parse::<u8>()?;
let album_day = split[4].parse::<u8>()?;
let album_title = split[5].to_string();
let track_number = split[6].parse::<u32>()?;
@ -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))

3
src/external/mod.rs vendored Normal file
View File

@ -0,0 +1,3 @@
pub mod database;
pub mod library;
pub mod musicbrainz;

46
src/external/musicbrainz/api/client.rs vendored Normal file
View File

@ -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<Self, ClientError> {
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<D: DeserializeOwned + 'static>(&mut self, url: &str) -> Result<D, ClientError> {
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<reqwest::Error> for ClientError {
fn from(err: reqwest::Error) -> Self {
ClientError::Client(err.to_string())
}
}
// GRCOV_EXCL_STOP

404
src/external/musicbrainz/api/mod.rs vendored Normal file
View File

@ -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<D: DeserializeOwned + 'static>(&mut self, url: &str) -> Result<D, ClientError>;
}
#[derive(Debug)]
pub enum ClientError {
Client(String),
Status(u16),
}
impl From<ClientError> 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<Mbc> {
client: Mbc,
}
impl<Mbc> MusicBrainzApi<Mbc> {
pub fn new(client: Mbc) -> Self {
MusicBrainzApi { client }
}
}
impl<Mbc: IMusicBrainzApiClient> IMusicBrainz for MusicBrainzApi<Mbc> {
fn lookup_artist_release_groups(&mut self, mbid: &Mbid) -> Result<Vec<Album>, 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<Vec<Match<Album>>, 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<LookupReleaseGroup>,
}
#[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<SerdeAlbumSecondaryType>,
}
impl TryFrom<LookupReleaseGroup> for Album {
type Error = Error;
fn try_from(entity: LookupReleaseGroup) -> Result<Self, Self::Error> {
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<SearchReleaseGroup>,
}
#[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<SearchReleaseGroup> for Match<Album> {
type Error = Error;
fn try_from(entity: SearchReleaseGroup) -> Result<Self, Self::Error> {
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<AlbumDate, Error> {
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<SerdeAlbumPrimaryType> 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<SerdeAlbumSecondaryType> 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::<ResponseLookupArtist>()
.times(1)
.return_once(|_| Err(ClientError::Client(String::from("get rekt scrub"))));
client
.expect_get::<ResponseLookupArtist>()
.times(1)
.return_once(|_| Err(ClientError::Status(503)));
client
.expect_get::<ResponseLookupArtist>()
.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);
}
}

2
src/external/musicbrainz/mod.rs vendored Normal file
View File

@ -0,0 +1,2 @@
#[cfg(feature = "musicbrainz-api")]
pub mod api;

View File

@ -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;

View File

@ -10,15 +10,17 @@ use ratatui::{backend::CrosstermBackend, Terminal};
use structopt::StructOpt;
use musichoard::{
external::{
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
interface::{
database::{IDatabase, NullDatabase},
library::{ILibrary, NullLibrary},
},
library::beets::{
executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor},
BeetsLibrary,
},
},
interface::{
database::{IDatabase, NullDatabase},
library::{ILibrary, NullLibrary},
},
MusicHoardBuilder, NoDatabase, NoLibrary,
};
@ -135,4 +137,4 @@ fn main() {
#[cfg(test)]
#[macro_use]
mod tests;
mod testmod;

473
src/testmod/full.rs Normal file
View File

@ -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;

View File

@ -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;

2
src/testmod/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod full;
pub mod library;

View File

@ -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<Vec<Artist>> = Lazy::new(|| full_collection!());
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| full::full_collection!());

View File

@ -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<S: AsRef<str>>(opt: Option<Option<&S>>) -> &str {
fn opt_opt_to_str<S: AsRef<str> + ?Sized>(opt: Option<Option<&S>>) -> &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()), "199005");
assert_eq!(
AlbumState::display_album_date(&AlbumDate::new(1990, 0, 0)),
"1990"
);
assert_eq!(
AlbumState::display_album_date(&AlbumDate::new(1990, 5, 0)),
"199005"
);
assert_eq!(
AlbumState::display_album_date(&AlbumDate::new(1990, 5, 6)),
AlbumState::display_album_date(&(1990, 5, 6).into()),
"19900506"
);
}
@ -870,7 +868,7 @@ mod tests {
let mut artists: Vec<Artist> = 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);

View File

@ -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,
};

View File

@ -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 [rerecorded]","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":"Heavens Basement","sort":"Heavens 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}]}]}
{"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 [rerecorded]","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":"Heavens Basement","sort":"Heavens 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"]}]}]}

View File

@ -7,8 +7,10 @@ mod library;
mod testlib;
use musichoard::{
external::{
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary},
},
IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard,
};

View File

@ -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;

View File

@ -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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
vec![
@ -11,7 +8,7 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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 [rerecorded]"),
track_number: 1,
@ -206,7 +203,7 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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 [rerecorded]"),
track_number: 2,
@ -219,7 +216,7 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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 [rerecorded]"),
track_number: 3,
@ -232,7 +229,7 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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 [rerecorded]"),
track_number: 4,
@ -245,7 +242,7 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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 [rerecorded]"),
track_number: 5,
@ -258,7 +255,7 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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 [rerecorded]"),
track_number: 6,
@ -271,7 +268,7 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
album_artist: String::from("Heavens 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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
album_artist: String::from("Heavens Basement"),
album_artist_sort: Some(String::from("Heavens 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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
album_artist: String::from("Heavens Basement"),
album_artist_sort: Some(String::from("Heavens 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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
album_artist: String::from("Heavens Basement"),
album_artist_sort: Some(String::from("Heavens 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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
album_artist: String::from("Heavens Basement"),
album_artist_sort: Some(String::from("Heavens 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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
album_artist: String::from("Heavens Basement"),
album_artist_sort: Some(String::from("Heavens 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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
album_artist: String::from("Heavens Basement"),
album_artist_sort: Some(String::from("Heavens 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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
album_artist: String::from("Heavens Basement"),
album_artist_sort: Some(String::from("Heavens 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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
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,

View File

@ -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<Vec<Artist>> = 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<Vec<Artist>> = 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<Vec<Artist>> = 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<Vec<Artist>> = Lazy::new(|| -> Collection {
id: AlbumId {
title: String::from("Vên [rerecorded]"),
},
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<Vec<Artist>> = 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<Vec<Artist>> = 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<Vec<Artist>> = 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<Vec<Artist>> = Lazy::new(|| -> Collection {
sort: Some(ArtistId {
name: String::from("Heavens 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<Vec<Artist>> = 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<Vec<Artist>> = 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<Vec<Artist>> = 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<Vec<Artist>> = 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<Vec<Artist>> = 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 {