feat: add metadata-agregator gRPC client integration

- Add tonic/prost for gRPC client generation
- Add proto definitions from metadata-agregator service
- Add MetadataClient and MetadataService for gRPC communication
- Add REST controller exposing metadata endpoints (search, get, sync)
- Update config with metadata.endpoint setting
- Update flake.nix with protobuf for proto compilation
This commit is contained in:
Alexander
2026-04-28 18:58:31 +02:00
parent 1aaaab4640
commit 5afcbd68ad
15 changed files with 1250 additions and 21 deletions
Generated
+462 -20
View File
@@ -17,6 +17,28 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "async-stream"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -34,13 +56,46 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "axum"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core 0.4.5",
"bytes",
"futures-util",
"http",
"http-body",
"http-body-util",
"itoa",
"matchit 0.7.3",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"sync_wrapper",
"tower 0.5.3",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [
"axum-core",
"axum-core 0.5.6",
"bytes",
"form_urlencoded",
"futures-util",
@@ -50,7 +105,7 @@ dependencies = [
"hyper",
"hyper-util",
"itoa",
"matchit",
"matchit 0.8.4",
"memchr",
"mime",
"percent-encoding",
@@ -61,12 +116,32 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower 0.5.3",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.5.6"
@@ -190,18 +265,52 @@ dependencies = [
"litrs",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
@@ -232,6 +341,12 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-sink"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
version = "0.3.32"
@@ -290,6 +405,31 @@ dependencies = [
"wasip3",
]
[[package]]
name = "h2"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap 2.14.0",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -366,6 +506,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
"h2",
"http",
"http-body",
"httparse",
@@ -393,6 +534,19 @@ dependencies = [
"webpki-roots",
]
[[package]]
name = "hyper-timeout"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
dependencies = [
"hyper",
"hyper-util",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
@@ -410,7 +564,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"socket2 0.6.3",
"tokio",
"tower-service",
"tracing",
@@ -525,6 +679,16 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.14.0"
@@ -553,6 +717,15 @@ dependencies = [
"serde",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -589,6 +762,12 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.2"
@@ -622,6 +801,12 @@ dependencies = [
"regex-automata",
]
[[package]]
name = "matchit"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "matchit"
version = "0.8.4"
@@ -661,13 +846,20 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "multimap"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
[[package]]
name = "music-agregator"
version = "0.1.0"
dependencies = [
"async-trait",
"axum",
"axum 0.8.9",
"base64",
"prost",
"reqwest",
"roxmltree",
"serde",
@@ -675,6 +867,8 @@ dependencies = [
"serde_yaml",
"thiserror",
"tokio",
"tonic",
"tonic-build",
"tower-http",
"tracing",
"tracing-subscriber",
@@ -709,6 +903,36 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "petgraph"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
dependencies = [
"fixedbitset",
"indexmap 2.14.0",
]
[[package]]
name = "pin-project"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
@@ -758,6 +982,58 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "prost"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-build"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
dependencies = [
"heck",
"itertools",
"log",
"multimap",
"once_cell",
"petgraph",
"prettyplease",
"prost",
"prost-types",
"regex",
"syn",
"tempfile",
]
[[package]]
name = "prost-derive"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "prost-types"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
dependencies = [
"prost",
]
[[package]]
name = "psl-types"
version = "2.0.11"
@@ -787,7 +1063,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"socket2 0.6.3",
"thiserror",
"tokio",
"tracing",
@@ -803,7 +1079,7 @@ dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand",
"rand 0.9.4",
"ring",
"rustc-hash",
"rustls",
@@ -824,7 +1100,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"socket2 0.6.3",
"tracing",
"windows-sys 0.52.0",
]
@@ -850,14 +1126,35 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha",
"rand_core",
"rand_chacha 0.9.0",
"rand_core 0.9.5",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
@@ -867,7 +1164,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.9.5",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]]
@@ -879,6 +1185,18 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
@@ -928,7 +1246,7 @@ dependencies = [
"sync_wrapper",
"tokio",
"tokio-rustls",
"tower",
"tower 0.5.3",
"tower-http",
"tower-service",
"url",
@@ -964,6 +1282,19 @@ version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.40"
@@ -1089,7 +1420,7 @@ version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"indexmap 2.14.0",
"itoa",
"ryu",
"serde",
@@ -1123,6 +1454,16 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.3"
@@ -1176,6 +1517,19 @@ dependencies = [
"syn",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "2.0.18"
@@ -1271,7 +1625,7 @@ dependencies = [
"libc",
"mio",
"pin-project-lite",
"socket2",
"socket2 0.6.3",
"tokio-macros",
"windows-sys 0.61.2",
]
@@ -1297,6 +1651,94 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tonic"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
dependencies = [
"async-stream",
"async-trait",
"axum 0.7.9",
"base64",
"bytes",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-timeout",
"hyper-util",
"percent-encoding",
"pin-project",
"prost",
"socket2 0.5.10",
"tokio",
"tokio-stream",
"tower 0.4.13",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tonic-build"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11"
dependencies = [
"prettyplease",
"proc-macro2",
"prost-build",
"prost-types",
"quote",
"syn",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"indexmap 1.9.3",
"pin-project",
"pin-project-lite",
"rand 0.8.6",
"slab",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower"
version = "0.5.3"
@@ -1326,7 +1768,7 @@ dependencies = [
"http-body",
"iri-string",
"pin-project-lite",
"tower",
"tower 0.5.3",
"tower-layer",
"tower-service",
"tracing",
@@ -1589,7 +2031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"indexmap 2.14.0",
"wasm-encoder",
"wasmparser",
]
@@ -1602,7 +2044,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"indexmap 2.14.0",
"semver",
]
@@ -1757,7 +2199,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"indexmap 2.14.0",
"prettyplease",
"syn",
"wasm-metadata",
@@ -1788,7 +2230,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"indexmap 2.14.0",
"log",
"serde",
"serde_derive",
@@ -1807,7 +2249,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"indexmap 2.14.0",
"log",
"semver",
"serde",
+5
View File
@@ -23,6 +23,11 @@ thiserror = "2"
url = "2"
roxmltree = "0.20"
base64 = "0.22"
tonic = "0.12"
prost = "0.13"
[build-dependencies]
tonic-build = "0.12"
[profile.release]
opt-level = 3
+7
View File
@@ -0,0 +1,7 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
.build_server(false)
.build_client(true)
.compile_protos(&["proto/metadata/v1/metadata.proto"], &["proto"])?;
Ok(())
}
+3
View File
@@ -1,6 +1,9 @@
database:
url: "postgresql://music:music@localhost:5433/music_aggregator"
metadata:
endpoint: "http://localhost:50051"
indexers:
- name: "Jackett"
url: "http://localhost:9117"
+2
View File
@@ -42,6 +42,7 @@
version = "0.1.0";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = [ pkgs.protobuf ];
};
in
{
@@ -63,6 +64,7 @@
pre-commit
gitleaks
plantuml
protobuf
rustc
cargo
+186
View File
@@ -0,0 +1,186 @@
syntax = "proto3";
package metadata.v1;
option go_package = "github.com/metadata-agregator/pkg/gen/metadata/v1;metadatav1";
enum Provider {
PROVIDER_UNSPECIFIED = 0;
PROVIDER_MUSICBRAINZ = 1;
}
// MetadataService provides music metadata aggregation.
service MetadataService {
// GetArtist retrieves an artist by ID or external source ID.
rpc GetArtist(GetArtistRequest) returns (Artist);
// SearchArtists searches for artists by name.
rpc SearchArtists(SearchArtistsRequest) returns (SearchArtistsResponse);
// GetAlbum retrieves an album by ID.
rpc GetAlbum(GetAlbumRequest) returns (Album);
// GetArtistAlbums retrieves all albums by an artist.
rpc GetArtistAlbums(GetArtistAlbumsRequest) returns (GetArtistAlbumsResponse);
// GetTrack retrieves a track by ID.
rpc GetTrack(GetTrackRequest) returns (Track);
// GetAlbumTracks retrieves all tracks on an album.
rpc GetAlbumTracks(GetAlbumTracksRequest) returns (GetAlbumTracksResponse);
// SyncArtist triggers ingestion of an artist from external sources.
rpc SyncArtist(SyncArtistRequest) returns (SyncArtistResponse);
}
// Requests
message GetArtistRequest {
oneof identifier {
string id = 1; // Internal UUID
ExternalID external = 2; // External source ID (e.g., musicbrainz MBID)
}
Provider provider = 3; // UNSPECIFIED = query all providers
}
message SearchArtistsRequest {
string query = 1;
int32 limit = 2;
int32 offset = 3;
Provider provider = 4;
}
message GetAlbumRequest {
oneof identifier {
string id = 1;
ExternalID external = 2;
}
Provider provider = 3;
}
message GetArtistAlbumsRequest {
string artist_id = 1;
int32 limit = 2;
int32 offset = 3;
Provider provider = 4;
}
message GetTrackRequest {
oneof identifier {
string id = 1;
ExternalID external = 2;
string isrc = 3;
}
Provider provider = 4;
}
message GetAlbumTracksRequest {
string album_id = 1;
Provider provider = 2;
}
message SyncArtistRequest {
oneof target {
string name = 1;
ExternalID external = 2;
}
Provider provider = 3;
}
// Responses
message SearchArtistsResponse {
repeated Artist artists = 1;
int32 total = 2;
}
message GetArtistAlbumsResponse {
repeated Album albums = 1;
int32 total = 2;
}
message GetAlbumTracksResponse {
repeated Track tracks = 1;
}
message SyncArtistResponse {
Artist artist = 1;
int32 albums_synced = 2;
int32 tracks_synced = 3;
}
// Core Entities
message Artist {
string id = 1;
string name = 2;
string sort_name = 3;
string artist_type = 4; // person, group, orchestra, etc.
string country = 5;
string formed_date = 6;
string disbanded_date = 7;
string description = 8;
string image_url = 9;
repeated Genre genres = 10;
repeated ExternalID external_ids = 11;
}
message Album {
string id = 1;
string title = 2;
string album_type = 3; // album, ep, single, compilation
string release_date = 4;
string upc = 5;
int32 total_tracks = 6;
int32 total_discs = 7;
string cover_url = 8;
repeated ArtistCredit artists = 9;
Label label = 10;
repeated Genre genres = 11;
repeated ExternalID external_ids = 12;
}
message Track {
string id = 1;
string title = 2;
int32 duration_ms = 3;
string isrc = 4;
bool explicit = 5;
int32 disc_number = 6;
int32 track_number = 7;
repeated ArtistCredit artists = 8;
Work work = 9;
repeated ExternalID external_ids = 10;
}
message Work {
string id = 1;
string title = 2;
string work_type = 3;
string language = 4;
repeated ArtistCredit composers = 5;
}
message Label {
string id = 1;
string name = 2;
string country = 3;
}
message Genre {
string id = 1;
string name = 2;
}
message ArtistCredit {
Artist artist = 1;
string role = 2; // primary, featured, remixer, producer
int32 position = 3;
string join_phrase = 4; // " & ", " feat. ", etc.
}
message ExternalID {
string source = 1; // musicbrainz, spotify, discogs, etc.
string source_id = 2;
string url = 3;
}
+342
View File
@@ -0,0 +1,342 @@
use axum::{
extract::{Path, Query, State},
http::StatusCode,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use crate::metadata::proto::{Album, Artist, ArtistCredit, ExternalId, Genre, Label, Track, Work};
use crate::AppState;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/artists/search", get(search_artists))
.route("/artists/:id", get(get_artist))
.route("/artists/:id/albums", get(get_artist_albums))
.route("/artists/sync", post(sync_artist))
.route("/albums/:id", get(get_album))
.route("/albums/:id/tracks", get(get_album_tracks))
.route("/status", get(connection_status))
}
#[derive(Debug, Deserialize)]
pub struct SearchQuery {
pub q: String,
pub limit: Option<i32>,
pub offset: Option<i32>,
}
#[derive(Debug, Serialize)]
pub struct ArtistResponse {
pub id: String,
pub name: String,
pub sort_name: String,
pub artist_type: String,
pub country: String,
pub formed_date: String,
pub disbanded_date: String,
pub description: String,
pub image_url: String,
pub genres: Vec<GenreResponse>,
pub external_ids: Vec<ExternalIdResponse>,
}
#[derive(Debug, Serialize)]
pub struct GenreResponse {
pub id: String,
pub name: String,
}
#[derive(Debug, Serialize)]
pub struct ExternalIdResponse {
pub source: String,
pub source_id: String,
pub url: String,
}
#[derive(Debug, Serialize)]
pub struct AlbumResponse {
pub id: String,
pub title: String,
pub album_type: String,
pub release_date: String,
pub upc: String,
pub total_tracks: i32,
pub total_discs: i32,
pub cover_url: String,
pub artists: Vec<ArtistCreditResponse>,
pub label: Option<LabelResponse>,
pub genres: Vec<GenreResponse>,
pub external_ids: Vec<ExternalIdResponse>,
}
#[derive(Debug, Serialize)]
pub struct ArtistCreditResponse {
pub artist: Option<ArtistResponse>,
pub role: String,
pub position: i32,
pub join_phrase: String,
}
#[derive(Debug, Serialize)]
pub struct LabelResponse {
pub id: String,
pub name: String,
pub country: String,
}
#[derive(Debug, Serialize)]
pub struct TrackResponse {
pub id: String,
pub title: String,
pub duration_ms: i32,
pub isrc: String,
pub explicit: bool,
pub disc_number: i32,
pub track_number: i32,
pub artists: Vec<ArtistCreditResponse>,
pub work: Option<WorkResponse>,
pub external_ids: Vec<ExternalIdResponse>,
}
#[derive(Debug, Serialize)]
pub struct WorkResponse {
pub id: String,
pub title: String,
pub work_type: String,
pub language: String,
}
#[derive(Debug, Serialize)]
pub struct SearchArtistsResponse {
pub artists: Vec<ArtistResponse>,
pub total: i32,
}
#[derive(Debug, Serialize)]
pub struct ArtistAlbumsResponse {
pub albums: Vec<AlbumResponse>,
pub total: i32,
}
#[derive(Debug, Serialize)]
pub struct AlbumTracksResponse {
pub tracks: Vec<TrackResponse>,
}
#[derive(Debug, Deserialize)]
pub struct SyncRequest {
pub name: String,
}
#[derive(Debug, Serialize)]
pub struct SyncResponse {
pub artist: Option<ArtistResponse>,
pub albums_synced: i32,
pub tracks_synced: i32,
}
fn map_genre(g: &Genre) -> GenreResponse {
GenreResponse {
id: g.id.clone(),
name: g.name.clone(),
}
}
fn map_external_id(e: &ExternalId) -> ExternalIdResponse {
ExternalIdResponse {
source: e.source.clone(),
source_id: e.source_id.clone(),
url: e.url.clone(),
}
}
fn map_artist(a: &Artist) -> ArtistResponse {
ArtistResponse {
id: a.id.clone(),
name: a.name.clone(),
sort_name: a.sort_name.clone(),
artist_type: a.artist_type.clone(),
country: a.country.clone(),
formed_date: a.formed_date.clone(),
disbanded_date: a.disbanded_date.clone(),
description: a.description.clone(),
image_url: a.image_url.clone(),
genres: a.genres.iter().map(map_genre).collect(),
external_ids: a.external_ids.iter().map(map_external_id).collect(),
}
}
fn map_label(l: &Label) -> LabelResponse {
LabelResponse {
id: l.id.clone(),
name: l.name.clone(),
country: l.country.clone(),
}
}
fn map_artist_credit(c: &ArtistCredit) -> ArtistCreditResponse {
ArtistCreditResponse {
artist: c.artist.as_ref().map(map_artist),
role: c.role.clone(),
position: c.position,
join_phrase: c.join_phrase.clone(),
}
}
fn map_album(a: &Album) -> AlbumResponse {
AlbumResponse {
id: a.id.clone(),
title: a.title.clone(),
album_type: a.album_type.clone(),
release_date: a.release_date.clone(),
upc: a.upc.clone(),
total_tracks: a.total_tracks,
total_discs: a.total_discs,
cover_url: a.cover_url.clone(),
artists: a.artists.iter().map(map_artist_credit).collect(),
label: a.label.as_ref().map(map_label),
genres: a.genres.iter().map(map_genre).collect(),
external_ids: a.external_ids.iter().map(map_external_id).collect(),
}
}
fn map_work(w: &Work) -> WorkResponse {
WorkResponse {
id: w.id.clone(),
title: w.title.clone(),
work_type: w.work_type.clone(),
language: w.language.clone(),
}
}
fn map_track(t: &Track) -> TrackResponse {
TrackResponse {
id: t.id.clone(),
title: t.title.clone(),
duration_ms: t.duration_ms,
isrc: t.isrc.clone(),
explicit: t.explicit,
disc_number: t.disc_number,
track_number: t.track_number,
artists: t.artists.iter().map(map_artist_credit).collect(),
work: t.work.as_ref().map(map_work),
external_ids: t.external_ids.iter().map(map_external_id).collect(),
}
}
async fn search_artists(
State(state): State<AppState>,
Query(query): Query<SearchQuery>,
) -> Result<Json<SearchArtistsResponse>, (StatusCode, String)> {
let state = state.read().await;
let response = state
.metadata_service
.search_artists(&query.q, query.limit, query.offset)
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
Ok(Json(SearchArtistsResponse {
artists: response.artists.iter().map(map_artist).collect(),
total: response.total,
}))
}
async fn get_artist(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<ArtistResponse>, (StatusCode, String)> {
let state = state.read().await;
let artist = state
.metadata_service
.get_artist(&id)
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
Ok(Json(map_artist(&artist)))
}
async fn get_artist_albums(
State(state): State<AppState>,
Path(id): Path<String>,
Query(query): Query<PaginationQuery>,
) -> Result<Json<ArtistAlbumsResponse>, (StatusCode, String)> {
let state = state.read().await;
let response = state
.metadata_service
.get_artist_albums(&id, query.limit, query.offset)
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
Ok(Json(ArtistAlbumsResponse {
albums: response.albums.iter().map(map_album).collect(),
total: response.total,
}))
}
#[derive(Debug, Deserialize)]
pub struct PaginationQuery {
pub limit: Option<i32>,
pub offset: Option<i32>,
}
async fn get_album(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<AlbumResponse>, (StatusCode, String)> {
let state = state.read().await;
let album = state
.metadata_service
.get_album(&id)
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
Ok(Json(map_album(&album)))
}
async fn get_album_tracks(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<AlbumTracksResponse>, (StatusCode, String)> {
let state = state.read().await;
let response = state
.metadata_service
.get_album_tracks(&id)
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
Ok(Json(AlbumTracksResponse {
tracks: response.tracks.iter().map(map_track).collect(),
}))
}
async fn sync_artist(
State(state): State<AppState>,
Json(req): Json<SyncRequest>,
) -> Result<Json<SyncResponse>, (StatusCode, String)> {
let state = state.read().await;
let response = state
.metadata_service
.sync_artist(&req.name)
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
Ok(Json(SyncResponse {
artist: response.artist.as_ref().map(map_artist),
albums_synced: response.albums_synced,
tracks_synced: response.tracks_synced,
}))
}
#[derive(Debug, Serialize)]
pub struct StatusResponse {
pub connected: bool,
}
async fn connection_status(State(state): State<AppState>) -> Json<StatusResponse> {
let state = state.read().await;
Json(StatusResponse {
connected: state.metadata_service.is_connected(),
})
}
+2
View File
@@ -1,4 +1,5 @@
mod indexer_controller;
mod metadata_controller;
mod torrent_controller;
use axum::{
@@ -23,6 +24,7 @@ pub fn routes(state: AppState) -> Router {
.route("/stats", get(get_stats))
.nest("/indexers", indexer_controller::routes())
.nest("/torrents", torrent_controller::routes())
.nest("/metadata", metadata_controller::routes())
.with_state(state)
}
+6
View File
@@ -15,10 +15,16 @@ pub enum ConfigError {
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
pub database: DatabaseConfig,
pub metadata: MetadataConfig,
pub indexers: Vec<IndexerConfig>,
pub torrent: TorrentConfig,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MetadataConfig {
pub endpoint: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DatabaseConfig {
pub url: String,
+4
View File
@@ -1,6 +1,7 @@
pub mod api;
pub mod config;
pub mod indexer;
pub mod metadata;
pub mod models;
pub mod services;
pub mod torrent;
@@ -12,17 +13,20 @@ pub struct AppServices {
pub aggregator: services::Aggregator,
pub indexer_service: services::IndexerService,
pub torrent_service: services::TorrentService,
pub metadata_service: services::MetadataService,
}
impl AppServices {
pub fn new(
indexer_service: services::IndexerService,
torrent_service: services::TorrentService,
metadata_service: services::MetadataService,
) -> Self {
Self {
aggregator: services::Aggregator::new(),
indexer_service,
torrent_service,
metadata_service,
}
}
}
+18 -1
View File
@@ -4,7 +4,7 @@ use tokio::sync::RwLock;
use axum::Router;
use music_agregator::{
api, config,
services::{IndexerService, TorrentService},
services::{IndexerService, MetadataService, TorrentService},
AppServices, AppState,
};
use tower_http::cors::{Any, CorsLayer};
@@ -60,9 +60,26 @@ async fn main() {
TorrentService::new()
};
let mut metadata_service = MetadataService::new(&config.metadata.endpoint);
match metadata_service.connect().await {
Ok(()) => {
tracing::info!(
"connected to metadata service at {}",
config.metadata.endpoint
);
}
Err(e) => {
tracing::warn!(
"failed to connect to metadata service: {} (continuing without metadata)",
e
);
}
}
let state: AppState = Arc::new(RwLock::new(AppServices::new(
indexer_service,
torrent_service,
metadata_service,
)));
let cors = CorsLayer::new()
+112
View File
@@ -0,0 +1,112 @@
use thiserror::Error;
use tonic::transport::Channel;
use super::proto::{
get_album_request, get_artist_request, metadata_service_client::MetadataServiceClient,
sync_artist_request, Album, Artist, GetAlbumRequest, GetAlbumTracksRequest,
GetAlbumTracksResponse, GetArtistAlbumsRequest, GetArtistAlbumsResponse, GetArtistRequest,
Provider, SearchArtistsRequest, SearchArtistsResponse, SyncArtistRequest, SyncArtistResponse,
};
#[derive(Debug, Error)]
pub enum MetadataClientError {
#[error("connection failed: {0}")]
ConnectionFailed(String),
#[error("request failed: {0}")]
RequestFailed(#[from] tonic::Status),
#[error("transport error: {0}")]
Transport(#[from] tonic::transport::Error),
}
pub struct MetadataClient {
client: MetadataServiceClient<Channel>,
}
impl MetadataClient {
pub async fn connect(endpoint: &str) -> Result<Self, MetadataClientError> {
let client = MetadataServiceClient::connect(endpoint.to_string()).await?;
Ok(Self { client })
}
pub async fn search_artists(
&mut self,
query: &str,
limit: Option<i32>,
offset: Option<i32>,
) -> Result<SearchArtistsResponse, MetadataClientError> {
let request = SearchArtistsRequest {
query: query.to_string(),
limit: limit.unwrap_or(20),
offset: offset.unwrap_or(0),
provider: Provider::Unspecified as i32,
};
let response = self.client.search_artists(request).await?;
Ok(response.into_inner())
}
pub async fn get_artist(&mut self, id: &str) -> Result<Artist, MetadataClientError> {
let request = GetArtistRequest {
identifier: Some(get_artist_request::Identifier::Id(id.to_string())),
provider: Provider::Unspecified as i32,
};
let response = self.client.get_artist(request).await?;
Ok(response.into_inner())
}
pub async fn get_artist_albums(
&mut self,
artist_id: &str,
limit: Option<i32>,
offset: Option<i32>,
) -> Result<GetArtistAlbumsResponse, MetadataClientError> {
let request = GetArtistAlbumsRequest {
artist_id: artist_id.to_string(),
limit: limit.unwrap_or(50),
offset: offset.unwrap_or(0),
provider: Provider::Unspecified as i32,
};
let response = self.client.get_artist_albums(request).await?;
Ok(response.into_inner())
}
pub async fn get_album(&mut self, id: &str) -> Result<Album, MetadataClientError> {
let request = GetAlbumRequest {
identifier: Some(get_album_request::Identifier::Id(id.to_string())),
provider: Provider::Unspecified as i32,
};
let response = self.client.get_album(request).await?;
Ok(response.into_inner())
}
pub async fn get_album_tracks(
&mut self,
album_id: &str,
) -> Result<GetAlbumTracksResponse, MetadataClientError> {
let request = GetAlbumTracksRequest {
album_id: album_id.to_string(),
provider: Provider::Unspecified as i32,
};
let response = self.client.get_album_tracks(request).await?;
Ok(response.into_inner())
}
pub async fn sync_artist(
&mut self,
name: &str,
) -> Result<SyncArtistResponse, MetadataClientError> {
let request = SyncArtistRequest {
target: Some(sync_artist_request::Target::Name(name.to_string())),
provider: Provider::Musicbrainz as i32,
};
let response = self.client.sync_artist(request).await?;
Ok(response.into_inner())
}
}
+7
View File
@@ -0,0 +1,7 @@
mod client;
pub use client::{MetadataClient, MetadataClientError};
pub mod proto {
tonic::include_proto!("metadata.v1");
}
+92
View File
@@ -0,0 +1,92 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::metadata::{MetadataClient, MetadataClientError};
pub struct MetadataService {
client: Option<Arc<Mutex<MetadataClient>>>,
endpoint: String,
}
impl MetadataService {
pub fn new(endpoint: &str) -> Self {
Self {
client: None,
endpoint: endpoint.to_string(),
}
}
pub async fn connect(&mut self) -> Result<(), MetadataClientError> {
let client = MetadataClient::connect(&self.endpoint).await?;
self.client = Some(Arc::new(Mutex::new(client)));
Ok(())
}
pub fn is_connected(&self) -> bool {
self.client.is_some()
}
fn client(&self) -> Result<Arc<Mutex<MetadataClient>>, MetadataClientError> {
self.client
.clone()
.ok_or_else(|| MetadataClientError::ConnectionFailed("not connected".into()))
}
pub async fn search_artists(
&self,
query: &str,
limit: Option<i32>,
offset: Option<i32>,
) -> Result<crate::metadata::proto::SearchArtistsResponse, MetadataClientError> {
let client = self.client()?;
let mut guard = client.lock().await;
guard.search_artists(query, limit, offset).await
}
pub async fn get_artist(
&self,
id: &str,
) -> Result<crate::metadata::proto::Artist, MetadataClientError> {
let client = self.client()?;
let mut guard = client.lock().await;
guard.get_artist(id).await
}
pub async fn get_artist_albums(
&self,
artist_id: &str,
limit: Option<i32>,
offset: Option<i32>,
) -> Result<crate::metadata::proto::GetArtistAlbumsResponse, MetadataClientError> {
let client = self.client()?;
let mut guard = client.lock().await;
guard.get_artist_albums(artist_id, limit, offset).await
}
pub async fn get_album(
&self,
id: &str,
) -> Result<crate::metadata::proto::Album, MetadataClientError> {
let client = self.client()?;
let mut guard = client.lock().await;
guard.get_album(id).await
}
pub async fn get_album_tracks(
&self,
album_id: &str,
) -> Result<crate::metadata::proto::GetAlbumTracksResponse, MetadataClientError> {
let client = self.client()?;
let mut guard = client.lock().await;
guard.get_album_tracks(album_id).await
}
pub async fn sync_artist(
&self,
name: &str,
) -> Result<crate::metadata::proto::SyncArtistResponse, MetadataClientError> {
let client = self.client()?;
let mut guard = client.lock().await;
guard.sync_artist(name).await
}
}
+2
View File
@@ -1,7 +1,9 @@
mod indexer_service;
mod metadata_service;
mod torrent_service;
pub use indexer_service::{IndexerInfo, IndexerService};
pub use metadata_service::MetadataService;
pub use torrent_service::TorrentService;
use uuid::Uuid;