Add a MusicBrainz API #163

Merged
wojtek merged 2 commits from 158---add-a-musicbrainz-api into main 2024-03-16 16:57:50 +01:00
45 changed files with 2342 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]]

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);
pub fn artist_from_url(url: Url) -> Result<Self, Error> {
Self::new(url, "artist")
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MbAlbumRef(MusicBrainzRef);
pub fn album_from_url(url: Url) -> Result<Self, Error> {
Self::new(url, "release-group")
}
macro_rules! impl_imusicbrainzref {
($mbref:ident, $entity:literal) => {
impl IMusicBrainzRef for $mbref {
fn mbid(&self) -> &Mbid {
&self.0.mbid
}
fn new(url: Url, mb_type: &str) -> Result<Self, Error> {
fn url(&self) -> &Url {
&self.0.url
}
fn entity() -> &'static str {
$entity
}
}
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::{
interface::{database::IDatabase, library::ILibrary},
musichoard::{database::IMusicHoardDatabase, MusicHoard, NoDatabase, NoLibrary},
},
Error,
use crate::core::{
interface::{database::IDatabase, library::ILibrary},
musichoard::{database::IMusicHoardDatabase, Error, MusicHoard, NoDatabase, NoLibrary},
};
/// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of

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::{
collection::{
album::{Album, AlbumDate, AlbumId, AlbumSeq},
artist::{Artist, ArtistId},
musicbrainz::MusicBrainzUrl,
Collection,
use crate::{
core::{
collection::{
album::{Album, AlbumDate, AlbumId, AlbumSeq},
artist::{Artist, ArtistId},
musicbrainz::{MbAlbumRef, MbArtistRef},
Collection,
},
interface::database::LoadError,
},
interface::database::LoadError,
external::database::serde::common::{SerdeAlbumPrimaryType, SerdeAlbumSecondaryType},
};
#[derive(Debug, Deserialize)]
pub enum DeserializeDatabase {
V20240308(Vec<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::{
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
external::{
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
library::beets::{
executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor},
BeetsLibrary,
},
},
interface::{
database::{IDatabase, NullDatabase},
library::{ILibrary, NullLibrary},
},
library::beets::{
executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor},
BeetsLibrary,
},
MusicHoardBuilder, NoDatabase, NoLibrary,
};
@ -135,4 +137,4 @@ fn main() {
#[cfg(test)]
#[macro_use]
mod tests;
mod testmod;

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::{
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary},
external::{
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary},
},
IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard,
};

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 {