Start from scratch

This commit is contained in:
Alexander
2026-07-01 21:16:40 +02:00
parent d52ce9508e
commit af7d69980f
157 changed files with 11876 additions and 37022 deletions
+8 -1
View File
@@ -1 +1,8 @@
use flake
#!/usr/bin/env bash
export GIT_CONFIG_GLOBAL=/dev/null
eval "$(devenv direnvrc)"
# You can pass flags to the devenv command
# For example: use devenv --impure --option services.postgres.enable:bool true
use devenv
+16
View File
@@ -31,3 +31,19 @@ config.yaml
# Protobuf generated Go code — committed for reproducible builds
# Regenerate with: buf generate
# gen/
# Devenv
.devenv*
devenv.local.nix
devenv.local.yaml
# direnv
.direnv
# pre-commit
.pre-commit-config.yaml
# Added by cargo
/target
-3
View File
@@ -1,3 +0,0 @@
[submodule "beetfs"]
path = beetfs
url = git@github.com:LichHunter/beetfs.git
+1 -1
View File
@@ -1 +1 @@
/nix/store/mchzk3cbvp456fd3nbajm120nrry3pls-pre-commit-config.json
/nix/store/gd69vhx5mbadafs4bwhh66b5mnp3wvyg-pre-commit-config.json
Generated
+982
View File
@@ -0,0 +1,982 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "axum"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [
"axum-core",
"bytes",
"futures-util",
"http",
"http-body",
"http-body-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"serde_core",
"sync_wrapper",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"sync_wrapper",
"tower-layer",
"tower-service",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "bytes"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"num-traits",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "either"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[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",
]
[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "futures-channel"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "h2"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "http"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"libc",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
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 = "log"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[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.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "mio"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
dependencies = [
"libc",
"wasi",
"windows-sys",
]
[[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 = [
"bytes",
"pbjson",
"pbjson-types",
"prost",
"serde",
"tokio",
"tonic",
"tonic-prost",
"tonic-reflection",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pbjson"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "898bac3fa00d0ba57a4e8289837e965baa2dee8c3749f3b11d45a64b4223d9c3"
dependencies = [
"base64",
"serde",
]
[[package]]
name = "pbjson-build"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af22d08a625a2213a78dbb0ffa253318c5c79ce3133d32d296655a7bdfb02095"
dependencies = [
"heck",
"itertools",
"prost",
"prost-types",
]
[[package]]
name = "pbjson-types"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e748e28374f10a330ee3bb9f29b828c0ac79831a32bab65015ad9b661ead526"
dependencies = [
"bytes",
"chrono",
"pbjson",
"pbjson-build",
"prost",
"prost-build",
"serde",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "petgraph"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset",
"hashbrown 0.15.5",
"indexmap",
]
[[package]]
name = "pin-project"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "prost"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-build"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
dependencies = [
"heck",
"itertools",
"log",
"multimap",
"petgraph",
"prettyplease",
"prost",
"prost-types",
"regex",
"syn",
"tempfile",
]
[[package]]
name = "prost-derive"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "prost-types"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a"
dependencies = [
"prost",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[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",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "simd-adler32"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
name = "socket2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "tokio"
version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [
"bytes",
"libc",
"mio",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[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.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
dependencies = [
"async-trait",
"axum",
"base64",
"bytes",
"flate2",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-timeout",
"hyper-util",
"percent-encoding",
"pin-project",
"socket2",
"sync_wrapper",
"tokio",
"tokio-stream",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tonic-prost"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0"
dependencies = [
"bytes",
"prost",
"tonic",
]
[[package]]
name = "tonic-reflection"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acccd136a4bf19810a1fde9c74edc6129b42a66b44d0c1c8aaa67aeb49a146a7"
dependencies = [
"prost",
"prost-types",
"tokio",
"tokio-stream",
"tonic",
"tonic-prost",
]
[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"indexmap",
"pin-project-lite",
"slab",
"sync_wrapper",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
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 = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[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.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "music-agregator"
version = "0.1.0"
edition = "2024"
[features]
default = ["proto_full", "serde"]
serde = ["dep:serde"]
# @@protoc_deletion_point(features)
# This section is automatically generated by protoc-gen-prost-crate.
# Changes in this area may be lost on regeneration.
proto_full = ["musicfs"]
"musicfs" = []
# @@protoc_insertion_point(features)
[dependencies]
bytes = "1.11.1"
prost = "0.14.1"
pbjson = "0.8.0"
pbjson-types = "0.8.0"
serde = { version = "1.0.228", optional = true }
tonic = { version = "0.14.2", features = ["gzip"] }
tonic-prost = "0.14.2"
tokio = { version = "1.52.3", features = [ "macros", "rt-multi-thread" ] }
tonic-reflection = "0.14.6"
[workspace]
-22
View File
@@ -1,22 +0,0 @@
meta {
name: Analyze Album
type: grpc
seq: 10
}
grpc {
url: localhost:3000
method: /music_agregator.v1.MusicAgregatorService/AnalyzeAlbumRelease
body: grpc
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"album_id": "1d51e4a7-1a8b-4160-bd08-1aee658a991a"
}
'''
}
-22
View File
@@ -1,22 +0,0 @@
meta {
name: Get Album
type: grpc
seq: 9
}
grpc {
url: localhost:3000
method: /music_agregator.v1.MusicAgregatorService/GetAlbum
body: grpc
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"album_id": "1d51e4a7-1a8b-4160-bd08-1aee658a991a"
}
'''
}
-22
View File
@@ -1,22 +0,0 @@
meta {
name: Get Artist Albums
type: grpc
seq: 11
}
grpc {
url: localhost:3000
method: /music_agregator.v1.MusicAgregatorService/GetArtistAlbums
body: grpc
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"artist_id": "3b497c6b-e676-4242-bcad-0a03125faaaa"
}
'''
}
-20
View File
@@ -1,20 +0,0 @@
meta {
name: Get Artists
type: grpc
seq: 7
}
grpc {
url: localhost:3000
method: /music_agregator.v1.MusicAgregatorService/GetArtists
body: grpc
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{}
'''
}
-23
View File
@@ -1,23 +0,0 @@
meta {
name: Hello Echo
type: grpc
seq: 3
}
grpc {
url: localhost:3000
method: /music_agregator.hello.v1.HelloService/Echo
body: grpc
protoPath: ../proto/music_agregator/hello/v1/service.proto
auth: none
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"msg": "hello"
}
'''
}
-21
View File
@@ -1,21 +0,0 @@
meta {
name: Hello Ping
type: grpc
seq: 1
}
grpc {
url: localhost:8081
method: /music_agregator.hello.v1.HelloService/Ping
body: grpc
protoPath: ../proto/music_agregator/hello/v1/service.proto
auth: none
methodType: unary
}
body:grpc {
name: message 1
content: '''
{}
'''
}
-24
View File
@@ -1,24 +0,0 @@
meta {
name: Capabilities
type: grpc
seq: 1
}
grpc {
url: localhost:3000
method: /music_agregator.indexer.v1.IndexerService/Capabilities
body: grpc
protoPath: ../proto/music_agregator/indexer/v1/indexer.proto
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"indexer": "rutracker"
}
'''
}
-25
View File
@@ -1,25 +0,0 @@
meta {
name: Search
type: grpc
seq: 2
}
grpc {
url: localhost:3000
method: /music_agregator.indexer.v1.IndexerService/Search
body: grpc
protoPath: ../proto/music_agregator/indexer/v1/indexer.proto
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"tracker": "",
"query": "Metallica",
"limit": 1
}
'''
}
-8
View File
@@ -1,8 +0,0 @@
meta {
name: Indexer
seq: 7
}
auth {
mode: inherit
}
-20
View File
@@ -1,20 +0,0 @@
meta {
name: Capabilities
type: http
seq: 2
}
get {
url: http://localhost:9117/api/v2.0/indexers/rutracker/results/torznab/api?apikey=3jfvdvt1etzz36drkw5id5sb95sc47fi&t=caps
body: none
auth: inherit
}
params:query {
apikey: 3jfvdvt1etzz36drkw5id5sb95sc47fi
t: caps
}
settings {
encodeUrl: true
}
-23
View File
@@ -1,23 +0,0 @@
meta {
name: Search
type: http
seq: 1
}
get {
url: http://localhost:9117/api/v2.0/indexers/all/results/torznab?apikey=3jfvdvt1etzz36drkw5id5sb95sc47fi&limit=2&q=Metallica&t=search
body: none
auth: inherit
}
params:query {
apikey: 3jfvdvt1etzz36drkw5id5sb95sc47fi
limit: 2
q: Metallica
t: search
}
settings {
encodeUrl: true
timeout: 0
}
-8
View File
@@ -1,8 +0,0 @@
meta {
name: Jackett
seq: 5
}
auth {
mode: inherit
}
-22
View File
@@ -1,22 +0,0 @@
meta {
name: Get Album
type: grpc
seq: 4
}
grpc {
url: localhost:3000
method: /metadata.v1.MetadataService/GetAlbum
body: grpc
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"id": "a0b7b436-94db-4df6-8c5f-bc0e5932a90e"
}
'''
}
@@ -1,22 +0,0 @@
meta {
name: Get Artist Albums
type: grpc
seq: 3
}
grpc {
url: localhost:3000
method: /metadata.v1.MetadataService/GetArtistAlbums
body: grpc
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"artist_id": "65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab"
}
'''
}
-22
View File
@@ -1,22 +0,0 @@
meta {
name: Get Artist
type: grpc
seq: 2
}
grpc {
url: localhost:3000
method: /metadata.v1.MetadataService/GetArtist
body: grpc
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"id": "65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab"
}
'''
}
-30
View File
@@ -1,30 +0,0 @@
meta {
name: Search Albums
type: grpc
seq: 5
}
grpc {
url: localhost:3000
method: /metadata.v1.MetadataService/SearchAlbums
body: grpc
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"query": "desidero",
"artist": "corrigo",
"limit": 770,
"offset": 396,
"provider": 0,
"album_types": [
"spiculum",
"spiculum"
]
}
'''
}
-22
View File
@@ -1,22 +0,0 @@
meta {
name: Search Artists
type: grpc
seq: 1
}
grpc {
url: localhost:3000
method: /metadata.v1.MetadataService/SearchArtists
body: grpc
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"query": "Metallica"
}
'''
}
-8
View File
@@ -1,8 +0,0 @@
meta {
name: Metadata Agregator
seq: 6
}
auth {
mode: inherit
}
-26
View File
@@ -1,26 +0,0 @@
meta {
name: Monitor Album
type: grpc
seq: 5
}
grpc {
url: localhost:3000
method: /music_agregator.v1.MusicAgregatorService/MonitorAlbum
body: grpc
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"album_id": "a0b7b436-94db-4df6-8c5f-bc0e5932a90e",
"indexer_options": {
"tracker": ""
},
"quality_type": 1
}
'''
}
-22
View File
@@ -1,22 +0,0 @@
meta {
name: SearchArtists
type: grpc
seq: 10
}
grpc {
url: localhost:3000
method: /music_agregator.v1.MusicAgregatorService/SearchArtists
body: grpc
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"query": "metal"
}
'''
}
-27
View File
@@ -1,27 +0,0 @@
{
"version": "1",
"name": "Music Agregator",
"type": "collection",
"protobuf": {
"importPaths": [
{
"path": "../proto",
"enabled": true
}
],
"protoFiles": [
{
"path": "../proto/music_agregator/hello/v1/service.proto",
"type": "file"
},
{
"path": "../proto/music_agregator/indexer/v1/indexer.proto",
"type": "file"
}
]
},
"presets": {
"requestType": "grpc",
"requestUrl": ""
}
}
View File
+26 -7
View File
@@ -2,11 +2,30 @@ version: v2
inputs:
- directory: proto
- directory: ../metadata-agregator/proto
- directory: ../musicfs/crates/musicfs-grpc/proto
- directory: ../musicfs/proto
plugins:
- local: protoc-gen-go
out: gen
opt: paths=source_relative
- local: protoc-gen-go-grpc
out: gen
opt: paths=source_relative
- local: protoc-gen-prost
#path: ../../target/debug/protoc-gen-prost
out: src/generated
opt:
# - bytes=.
- compile_well_known_types
- extern_path=.google.protobuf=::pbjson_types
- file_descriptor_set
- enable_type_names
- local: protoc-gen-prost-serde
#path: ../../target/debug/protoc-gen-prost-serde
out: src/generated
- local: protoc-gen-tonic
#path: ../../target/debug/protoc-gen-tonic
out: src/generated
opt:
- compile_well_known_types
- extern_path=.google.protobuf=::pbjson_types
- local: protoc-gen-prost-crate
#path: ../../target/debug/protoc-gen-prost-crate
out: .
strategy: all
opt:
- include_file=src/generated/mod.rs
- gen_crate
-268
View File
@@ -1,268 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"net"
"net/http"
"os"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
"github.com/jackc/pgx/v5"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/riverqueue/river"
"github.com/riverqueue/river/riverdriver/riverpgxv5"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
"gopkg.in/yaml.v2"
grpcprom "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
"homelab.lan/music-agregator/internal"
"homelab.lan/music-agregator/internal/analysis"
"homelab.lan/music-agregator/internal/config"
"homelab.lan/music-agregator/internal/database"
"homelab.lan/music-agregator/internal/eventbus"
"homelab.lan/music-agregator/internal/hello"
"homelab.lan/music-agregator/internal/indexer"
"homelab.lan/music-agregator/internal/metadata"
"homelab.lan/music-agregator/internal/musicfs"
"homelab.lan/music-agregator/internal/torrent"
"homelab.lan/music-agregator/internal/workers"
)
func main() {
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).
With().Timestamp().Logger()
configPath := flag.String("config", "", "Path to the config file")
flag.Parse()
cfg, err := parseConfig(configPath)
if err != nil {
log.Fatal().Err(err).Msg("Failed to load config")
}
log.Info().Interface("config", cfg).Msg("Loaded config")
serveGrpc(*cfg)
}
func interceptorLogger(l zerolog.Logger) logging.Logger {
return logging.LoggerFunc(func(ctx context.Context, lvl logging.Level, msg string, fields ...any) {
l := l.With().Fields(fields).Logger()
switch lvl {
case logging.LevelDebug:
l.Debug().Msg(msg)
case logging.LevelInfo:
l.Info().Msg(msg)
case logging.LevelWarn:
l.Warn().Msg(msg)
case logging.LevelError:
l.Error().Msg(msg)
default:
l.Info().Msg(msg)
}
})
}
func setupDatabase(ctx context.Context, cfg config.Config) *database.DB {
db, err := database.New(ctx, cfg.Database.URL)
if err != nil {
log.Fatal().Err(err).Msg("failed to connect to database")
}
return db
}
type riverSetup struct {
client *river.Client[pgx.Tx]
cacheRefreshWorker *indexer.CacheRefreshWorker
}
func setupRiver(ctx context.Context, cfg config.Config, db *database.DB, torrentClient torrent.TorrentClient, pathMapper *torrent.PathMapper, musicfsClient *musicfs.Client, metadataService *metadata.MetadataService, bus *eventbus.EventBus) *riverSetup {
cacheWorker := &indexer.CacheRefreshWorker{}
pollWorker := &workers.PollDownloadWorker{
Downloads: database.NewDownloadRepository(db.Pool),
DownloadFiles: database.NewDownloadFileRepository(db.Pool),
AlbumReleases: database.NewAlbumReleaseRepository(db.Pool),
TrackReleases: database.NewTrackReleaseRepository(db.Pool),
TorrentClient: torrentClient,
PathMapper: pathMapper,
Analyzer: analysis.NewReleaseAnalyzer(db),
MusicFSClient: musicfsClient,
MusicFSOriginID: cfg.MusicFS.OriginID,
MusicFSOriginRoot: cfg.MusicFS.OriginRoot,
MetadataService: metadataService,
EventBus: bus,
AlbumEvents: database.NewAlbumEventRepository(db.Pool),
}
riverWorkers := river.NewWorkers()
river.AddWorker(riverWorkers, cacheWorker)
river.AddWorker(riverWorkers, pollWorker)
riverClient, err := river.NewClient(riverpgxv5.New(db.Pool), &river.Config{
Queues: map[string]river.QueueConfig{
river.QueueDefault: {MaxWorkers: 4},
},
Workers: riverWorkers,
})
if err != nil {
log.Fatal().Err(err).Msg("failed to create River client")
}
cacheWorker.RiverClient = riverClient
pollWorker.RiverClient = riverClient
if err := riverClient.Start(ctx); err != nil {
log.Fatal().Err(err).Msg("failed to start River client")
}
log.Info().Msg("River queue started")
pollWorker.RecoverOrphanedDownloads(ctx)
return &riverSetup{
client: riverClient,
cacheRefreshWorker: cacheWorker,
}
}
func serveGrpc(config config.Config) {
srvMetrics := grpcprom.NewServerMetrics(
grpcprom.WithServerHandlingTimeHistogram(
grpcprom.WithHistogramBuckets([]float64{0.001, 0.01, 0.1, 0.3, 0.6, 1, 3, 6, 10, 30}),
),
)
logOpts := []logging.Option{
logging.WithLogOnEvents(logging.StartCall, logging.FinishCall),
}
recoveryOpts := []recovery.Option{
recovery.WithRecoveryHandler(func(p any) (err error) {
log.Error().Interface("panic", p).Msg("recovered from panic")
return status.Errorf(codes.Internal, "internal error")
}),
}
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(
srvMetrics.UnaryServerInterceptor(),
logging.UnaryServerInterceptor(interceptorLogger(log.Logger), logOpts...),
recovery.UnaryServerInterceptor(recoveryOpts...),
),
grpc.ChainStreamInterceptor(
srvMetrics.StreamServerInterceptor(),
logging.StreamServerInterceptor(interceptorLogger(log.Logger), logOpts...),
recovery.StreamServerInterceptor(recoveryOpts...),
),
)
ctx := context.Background()
db := setupDatabase(ctx, config)
defer db.Close()
torrentClient, err := torrent.NewTorrentClient(config)
if err != nil {
log.Fatal().Err(err).Msg("failed to create torrent client")
}
pathMapper, err := torrent.NewPathMapper(config.Torrent.ContainerName, torrentClient)
if err != nil {
log.Fatal().Err(err).Msg("failed to create path mapper")
}
var musicfsClient *musicfs.Client
if config.MusicFS.Enabled {
client, conn, err := musicfs.NewClient(config.MusicFS.Endpoint)
if err != nil {
log.Warn().Err(err).Msg("failed to connect to musicfs, continuing without it")
} else {
musicfsClient = client
defer conn.Close()
}
}
metadataClient, metadataConn, err := metadata.NewMetadataClient(config.Metadata.Endpoint)
if err != nil {
log.Fatal().Err(err).Msg("failed to connect to metadata service")
}
defer metadataConn.Close()
metadataService := metadata.NewMetadataService(metadataClient, db)
bus := eventbus.New()
rs := setupRiver(ctx, config, db, torrentClient, pathMapper, musicfsClient, metadataService, bus)
musiscAgregatorSeerver, err := internal.NewMusicAgregatorServer(config, rs.client, torrentClient, pathMapper, db, bus, musicfsClient)
if err != nil {
log.Fatal().Err(err).Msg("failed to create MusicAgregatorServer")
}
indexerServer, err := indexer.NewIndexerServer(config, rs.client, rs.cacheRefreshWorker)
if err != nil {
log.Fatal().Err(err).Msg("failed to create IndexerServer")
}
torrentServer := torrent.NewTorrentServer(torrentClient)
metadataServer, err := metadata.NewMetadataServer(config)
if err != nil {
log.Fatal().Err(err).Msg("failed to create MetadataServer")
}
services := []internal.Registrable{
hello.NewHelloServer(),
indexerServer,
torrentServer,
metadataServer,
musiscAgregatorSeerver,
}
for _, service := range services {
service.Register(server)
}
srvMetrics.InitializeMetrics(server)
prometheus.MustRegister(srvMetrics)
reflection.Register(server)
go func() {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
log.Info().Msg("Prometheus metrics available at :9090/metrics")
if err := http.ListenAndServe(":9090", mux); err != nil {
log.Fatal().Err(err).Msg("Failed to start metrics server")
}
}()
listener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", config.App.Host, config.App.Port))
if err != nil {
log.Fatal().Err(err).Msg("Failed to listen")
}
log.Info().Str("addr", listener.Addr().String()).Msg("gRPC server listening")
server.Serve(listener)
}
func parseConfig(path *string) (*config.Config, error) {
f, err := os.Open(*path)
if err != nil {
log.Error().Str("path", *path).Msg("Unable to opent config by path")
return nil, err
}
defer f.Close()
cfg := config.NewConfig()
decoder := yaml.NewDecoder(f)
err = decoder.Decode(&cfg)
if err != nil {
// processError(err)
}
return cfg, nil
}
-31
View File
@@ -1,31 +0,0 @@
app:
port: 3000
database:
url: "postgresql://music:music@localhost:5433/music_aggregator"
metadata:
endpoint: "http://localhost:50051"
indexer:
name: "Jackett"
indexer_type: jackett # jackett, prowlarr, or torznab
url: "http://localhost:9117"
api_key: "your-jackett-api-key"
# Torrent client - choose one of: qbittorrent, stub, none
torrent:
client_type: qbittorrent
url: "http://localhost:8080"
username: "admin"
password: "changeme"
# Alternative: stub client for testing
# torrent:
# client_type: stub
# log_path: "/tmp/torrent-stub.log"
# save_path: "/tmp/downloads"
# Alternative: no torrent client
# torrent:
# client_type: none
-22
View File
@@ -1,22 +0,0 @@
app:
port: 3000
database:
url: "postgresql://music:music@localhost:5433/music_aggregator"
metadata:
endpoint: "http://localhost:50051"
indexers:
- name: "Jackett"
indexer_type: jackett
url: "http://localhost:9117"
api_key: "apmtthy50kiu47wth8i1we22k5sn8ut6"
torrent:
client_type: stub
save_path: "/tmp/music-downloads"
log_path: "/tmp/torrent-stub.log"
storage:
base_path: "/tmp/music-library-test"
@@ -1,112 +0,0 @@
CREATE TYPE monitor_state AS ENUM ('unmonitored', 'monitored', 'excluded');
CREATE TYPE download_state AS ENUM ('pending', 'downloading', 'completed', 'failed', 'seeding');
CREATE TABLE artists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
external_id VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
artist_type VARCHAR(50) NOT NULL,
country VARCHAR(10),
genres TEXT[] NOT NULL DEFAULT '{}',
image_url TEXT,
monitor_state monitor_state NOT NULL DEFAULT 'unmonitored',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_artists_monitor_state ON artists(monitor_state);
CREATE TABLE albums (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
external_id VARCHAR(255) NOT NULL UNIQUE,
artist_id UUID NOT NULL REFERENCES artists(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
album_type VARCHAR(50) NOT NULL,
release_date DATE,
total_tracks INTEGER NOT NULL DEFAULT 0,
total_discs INTEGER NOT NULL DEFAULT 1,
label VARCHAR(255),
genres TEXT[] NOT NULL DEFAULT '{}',
cover_url TEXT,
monitor_state monitor_state NOT NULL DEFAULT 'unmonitored',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_albums_artist_id ON albums(artist_id);
CREATE INDEX idx_albums_monitor_state ON albums(monitor_state);
CREATE TABLE tracks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
external_id VARCHAR(255) NOT NULL UNIQUE,
album_id UUID NOT NULL REFERENCES albums(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
duration_ms INTEGER NOT NULL DEFAULT 0,
isrc VARCHAR(20),
disc_number INTEGER NOT NULL DEFAULT 1,
track_number INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_tracks_album_id ON tracks(album_id);
CREATE TABLE torrents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
album_id UUID NOT NULL REFERENCES albums(id) ON DELETE CASCADE,
info_hash VARCHAR(64) NOT NULL UNIQUE,
tracker VARCHAR(100) NOT NULL,
title TEXT NOT NULL,
format VARCHAR(20) NOT NULL,
quality VARCHAR(20) NOT NULL,
source VARCHAR(50),
bit_depth INTEGER,
sample_rate INTEGER,
seeders INTEGER NOT NULL DEFAULT 0,
peers INTEGER NOT NULL DEFAULT 0,
size BIGINT NOT NULL DEFAULT 0,
track_count INTEGER NOT NULL DEFAULT 0,
has_cover_art BOOLEAN NOT NULL DEFAULT false,
has_cue_sheet BOOLEAN NOT NULL DEFAULT false,
has_rip_log BOOLEAN NOT NULL DEFAULT false,
download_link TEXT,
torrent_file BYTEA,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_torrents_album_id ON torrents(album_id);
CREATE TABLE downloads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
torrent_id UUID NOT NULL REFERENCES torrents(id) ON DELETE CASCADE,
album_id UUID NOT NULL REFERENCES albums(id) ON DELETE CASCADE,
format VARCHAR(20) NOT NULL,
quality VARCHAR(20) NOT NULL,
state download_state NOT NULL DEFAULT 'pending',
qbit_hash VARCHAR(64),
save_path TEXT,
error_message TEXT,
queued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_downloads_album_id ON downloads(album_id);
CREATE INDEX idx_downloads_torrent_id ON downloads(torrent_id);
CREATE INDEX idx_downloads_state ON downloads(state);
CREATE TABLE download_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
download_id UUID NOT NULL REFERENCES downloads(id) ON DELETE CASCADE,
track_id UUID REFERENCES tracks(id) ON DELETE SET NULL,
file_path TEXT NOT NULL,
file_size BIGINT NOT NULL DEFAULT 0,
file_type VARCHAR(50) NOT NULL,
sha256_hash VARCHAR(64),
verified_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_download_files_download_id ON download_files(download_id);
@@ -1,33 +0,0 @@
CREATE TABLE workflow_runs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
album_id UUID NOT NULL REFERENCES albums(id) ON DELETE CASCADE,
quality VARCHAR(20) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'running',
error_message TEXT,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
running_lock BOOLEAN GENERATED ALWAYS AS (CASE WHEN status = 'running' THEN TRUE ELSE NULL END) STORED,
CONSTRAINT idx_workflow_runs_active UNIQUE (album_id, quality, running_lock)
);
CREATE INDEX idx_workflow_runs_status ON workflow_runs(status);
CREATE TABLE album_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
seq BIGSERIAL NOT NULL,
workflow_run_id UUID NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
album_id UUID NOT NULL,
event_type VARCHAR(20) NOT NULL,
step VARCHAR(50) NOT NULL,
message TEXT NOT NULL,
data_json JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_album_events_workflow ON album_events(workflow_run_id);
CREATE INDEX idx_album_events_album ON album_events(album_id);
CREATE INDEX idx_album_events_seq ON album_events(seq);
ALTER TYPE download_state ADD VALUE IF NOT EXISTS 'cancelled';
-63
View File
@@ -1,63 +0,0 @@
services:
jackett:
image: lscr.io/linuxserver/jackett:latest
container_name: music-aggregator-jackett
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Warsaw
- AUTO_UPDATE=true
volumes:
- jackett_config:/config
- jackett_downloads:/downloads
ports:
- "9117:9117"
gluetun:
image: qmcgaw/gluetun:latest
container_name: music-aggregator-vpn
restart: unless-stopped
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=custom
- VPN_TYPE=wireguard
- TZ=Europe/Warsaw
volumes:
- ./wg.conf:/gluetun/wireguard/wg0.conf:ro
ports:
- "8080:8080"
- "6881:6881"
- "6881:6881/udp"
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:9999"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: music-aggregator-qbittorrent
restart: unless-stopped
network_mode: "service:gluetun"
depends_on:
gluetun:
condition: service_healthy
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Warsaw
- WEBUI_PORT=8080
volumes:
- qbittorrent_config:/config
- downloads:/downloads
volumes:
jackett_config:
jackett_downloads:
qbittorrent_config:
downloads:
+124
View File
@@ -0,0 +1,124 @@
{
"nodes": {
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1782920231,
"narHash": "sha256-hE3aMd68LTOfoG+wxZFLi2LXp6xaDsxLLeluuNpQPLE=",
"owner": "cachix",
"repo": "devenv",
"rev": "9c262553e2d71f3b6c60b24405bd8567f28f384c",
"type": "github"
},
"original": {
"dir": "src/modules",
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1782908218,
"narHash": "sha256-wLMOrPgVyeF3XmP+qfYcLqnVdTxikdcSvbIY7rA9jTA=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "9f7e99119ece7705299595299f3b031f39356de1",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"nixpkgs": {
"inputs": {
"nixpkgs-src": "nixpkgs-src"
},
"locked": {
"lastModified": 1782924808,
"narHash": "sha256-tn2ahNv3ZNXyVHdPx/uw+N0xa7YSMtXUr6OEvu+s8ng=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "e2c881cb8d5f1cac6cef9d71f0d450a55691b999",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs-src": {
"flake": false,
"locked": {
"lastModified": 1782636521,
"narHash": "sha256-OG8laCOGtkxlB1JH3XqOnXxnkGJme4mQdXo5hkhCQIY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e1c1b84752fb0897897380a3cae9dc7fcab91ca3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs",
"treefmt-nix": "treefmt-nix"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1780220602,
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+43
View File
@@ -0,0 +1,43 @@
{
pkgs,
lib,
config,
inputs,
...
}:
{
languages.rust.enable = true;
git-hooks.hooks = {
clippy = {
enable = true;
settings.allFeatures = true;
};
treefmt.enable = true;
};
treefmt = {
enable = true;
config.programs = {
nixfmt.enable = true;
rustfmt.enable = true;
};
};
packages = with pkgs; [
git
just
protobuf
buf
grpcurl
# Protobuf generators
protoc-gen-prost-crate
protoc-gen-prost-serde
protoc-gen-tonic
protoc-gen-prost
opencode
];
}
+13
View File
@@ -0,0 +1,13 @@
inputs:
git-hooks:
url: github:cachix/git-hooks.nix
inputs:
nixpkgs:
follows: nixpkgs
nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling
treefmt-nix:
url: github:numtide/treefmt-nix
inputs:
nixpkgs:
follows: nixpkgs
-362
View File
@@ -1,362 +0,0 @@
@startuml Lidarr-Style Music Aggregator ERD
skinparam linetype ortho
skinparam ranksep 60
skinparam nodesep 40
skinparam entity {
BackgroundColor White
BorderColor #333333
}
skinparam package {
BackgroundColor #FAFAFA
BorderColor #DDDDDD
}
title Music Aggregator - Lidarr-Style Database Structure
' ══════════════════════════════════════════════════════════════
' CORE MUSIC ENTITIES
' ══════════════════════════════════════════════════════════════
package "Core Music Entities" #E3F2FD {
entity "artist_metadata" {
* id : UUID <<PK>>
--
foreign_artist_id : TEXT <<UNIQUE>>
name : TEXT
sort_name : TEXT
disambiguation : TEXT
artist_type : TEXT
status : TEXT
overview : TEXT
images : JSONB
links : JSONB
genres : JSONB
ratings : JSONB
members : JSONB
--
created_at : TIMESTAMPTZ
updated_at : TIMESTAMPTZ
}
entity "artists" {
* id : UUID <<PK>>
--
metadata_id : UUID <<FK>>
quality_profile_id : UUID <<FK>>
metadata_profile_id : UUID <<FK>>
root_folder_id : UUID <<FK>>
--
path : TEXT
monitored : BOOLEAN
monitor_new_items : TEXT
--
last_info_sync : TIMESTAMPTZ
added_at : TIMESTAMPTZ
tags : INT[]
}
entity "albums" {
* id : UUID <<PK>>
--
artist_metadata_id : UUID <<FK>>
--
foreign_album_id : TEXT <<UNIQUE>>
title : TEXT
clean_title : TEXT
disambiguation : TEXT
overview : TEXT
album_type : TEXT
secondary_types : JSONB
release_date : DATE
images : JSONB
links : JSONB
genres : JSONB
ratings : JSONB
--
monitored : BOOLEAN
any_release_ok : BOOLEAN
last_search_time : TIMESTAMPTZ
added_at : TIMESTAMPTZ
}
entity "album_releases" {
* id : UUID <<PK>>
--
album_id : UUID <<FK>>
--
foreign_release_id : TEXT <<UNIQUE>>
title : TEXT
disambiguation : TEXT
status : TEXT
duration_ms : INT
release_date : DATE
country : TEXT[]
label : TEXT[]
media : JSONB
track_count : INT
--
monitored : BOOLEAN
}
entity "tracks" {
* id : UUID <<PK>>
--
album_release_id : UUID <<FK>>
artist_metadata_id : UUID <<FK>>
track_file_id : UUID <<FK NULL>>
--
foreign_track_id : TEXT <<UNIQUE>>
foreign_recording_id : TEXT
title : TEXT
track_number : INT
disc_number : INT
duration_ms : INT
explicit : BOOLEAN
ratings : JSONB
}
entity "track_files" {
* id : UUID <<PK>>
--
album_id : UUID <<FK>>
--
path : TEXT
relative_path : TEXT
size : BIGINT
quality : JSONB
media_info : JSONB
audio_tags : JSONB
--
scene_name : TEXT
release_group : TEXT
--
date_added : TIMESTAMPTZ
modified_at : TIMESTAMPTZ
}
}
' ══════════════════════════════════════════════════════════════
' CONFIGURATION
' ══════════════════════════════════════════════════════════════
package "Configuration" #FFF3E0 {
entity "quality_profiles" {
* id : UUID <<PK>>
--
name : TEXT <<UNIQUE>>
cutoff : INT
items : JSONB
upgrade_allowed : BOOLEAN
min_format_score : INT
cutoff_format_score : INT
}
entity "metadata_profiles" {
* id : UUID <<PK>>
--
name : TEXT <<UNIQUE>>
primary_album_types : JSONB
secondary_album_types : JSONB
release_statuses : JSONB
}
entity "root_folders" {
* id : UUID <<PK>>
--
name : TEXT
path : TEXT <<UNIQUE>>
default_quality_profile_id : UUID <<FK>>
default_metadata_profile_id : UUID <<FK>>
default_monitor_option : TEXT
default_tags : INT[]
}
entity "tags" {
* id : SERIAL <<PK>>
--
label : TEXT <<UNIQUE>>
}
entity "indexers" {
* id : UUID <<PK>>
--
name : TEXT
implementation : TEXT
settings : JSONB
enable_rss : BOOLEAN
enable_search : BOOLEAN
priority : INT
tags : INT[]
}
entity "download_clients" {
* id : UUID <<PK>>
--
name : TEXT
implementation : TEXT
settings : JSONB
protocol : TEXT
priority : INT
remove_completed : BOOLEAN
remove_failed : BOOLEAN
tags : INT[]
enabled : BOOLEAN
}
}
' ══════════════════════════════════════════════════════════════
' DOWNLOAD TRACKING
' ══════════════════════════════════════════════════════════════
package "Download Tracking" #E8F5E9 {
entity "download_queue" {
* id : UUID <<PK>>
--
artist_id : UUID <<FK>>
album_id : UUID <<FK>>
--
download_id : TEXT
title : TEXT
size : BIGINT
size_left : BIGINT
time_left : INTERVAL
estimated_completion : TIMESTAMPTZ
--
status : TEXT
state : TEXT
status_messages : JSONB
--
protocol : TEXT
indexer : TEXT
download_client : TEXT
output_path : TEXT
download_forced : BOOLEAN
--
added_at : TIMESTAMPTZ
}
entity "pending_releases" {
* id : UUID <<PK>>
--
artist_id : UUID <<FK>>
album_id : UUID <<FK>>
--
title : TEXT
release : JSONB
parsed_album_info : JSONB
reason : TEXT
additional_info : JSONB
--
added_at : TIMESTAMPTZ
}
entity "download_history" {
* id : UUID <<PK>>
--
artist_id : UUID <<FK>>
album_id : UUID <<FK>>
--
event_type : TEXT
download_id : TEXT
source_title : TEXT
protocol : TEXT
indexer_id : UUID
download_client_id : UUID
release : JSONB
data : JSONB
--
date : TIMESTAMPTZ
}
entity "blocklist" {
* id : UUID <<PK>>
--
artist_id : UUID <<FK>>
album_ids : UUID[]
--
source_title : TEXT
quality : JSONB
size : BIGINT
protocol : TEXT
indexer : TEXT
message : TEXT
torrent_hash : TEXT
--
published_date : TIMESTAMPTZ
date : TIMESTAMPTZ
}
}
' ══════════════════════════════════════════════════════════════
' HISTORY & TRACKING
' ══════════════════════════════════════════════════════════════
package "History & Tracking" #FCE4EC {
entity "history" {
* id : UUID <<PK>>
--
artist_id : UUID <<FK>>
album_id : UUID <<FK>>
track_id : UUID <<FK NULL>>
--
event_type : TEXT
source_title : TEXT
quality : JSONB
download_id : TEXT
data : JSONB
--
date : TIMESTAMPTZ
}
entity "wanted_albums" {
' View/materialized view for missing albums
* album_id : UUID <<FK>>
--
artist_id : UUID
title : TEXT
release_date : DATE
monitored : BOOLEAN
track_count : INT
track_file_count : INT
}
}
' ══════════════════════════════════════════════════════════════
' RELATIONSHIPS
' ══════════════════════════════════════════════════════════════
' Core music relationships
artist_metadata ||--|| artists : "has config"
artist_metadata ||--o{ albums : "released"
albums ||--o{ album_releases : "has releases"
album_releases ||--o{ tracks : "contains"
tracks }o--o| track_files : "stored in"
track_files }o--|| albums : "belongs to"
' Artist relationships
artists }o--|| quality_profiles : "uses"
artists }o--o| metadata_profiles : "uses"
artists }o--o| root_folders : "stored in"
' Configuration relationships
root_folders }o--o| quality_profiles : "default"
root_folders }o--o| metadata_profiles : "default"
' Download tracking relationships
download_queue }o--o| artists : "for"
download_queue }o--o| albums : "for"
pending_releases }o--|| artists : "for"
pending_releases }o--o| albums : "for"
download_history }o--|| artists : "for"
download_history }o--o| albums : "for"
blocklist }o--|| artists : "for"
' History relationships
history }o--|| artists : "for"
history }o--o| albums : "for"
history }o--o| tracks : "for"
@enduml
-93
View File
@@ -1,93 +0,0 @@
# Lidarr Database Research Summary
## Overview
Lidarr is a music collection manager that uses a **Release-based** system. Key design principles:
1. **Metadata Separation** - Artist metadata separated from configuration
2. **Release-Centric** - Works with releases, not loose tracks
3. **Monitoring Hierarchy** - Artist → Album → Release (only one release monitored per album)
4. **Quality Profiles** - Separate profiles for quality and metadata preferences
5. **Download Lifecycle** - Pending → Queue → Imported → History/Blocklist
## Core Entity Hierarchy
```
ArtistMetadata (1) ←→ (1) Artist (1) ←→ (N) Albums
(N) AlbumReleases (only 1 monitored)
(N) Tracks (N) ←→ (1) TrackFile
```
## Key Entities
### Music Entities
- **Artists** - Configuration (path, monitoring, profiles)
- **ArtistMetadata** - Metadata (name, images, genres, members)
- **Albums** - Album info with monitoring and search tracking
- **AlbumReleases** - Physical releases (CD, Vinyl, Digital) - only ONE monitored per album
- **Tracks** - Individual tracks linked to releases
- **TrackFiles** - Actual files on disk with quality info
### Configuration
- **QualityProfiles** - Acceptable formats and upgrade cutoff
- **MetadataProfiles** - Which album types to include (Studio, EP, Live, etc.)
- **RootFolders** - Storage locations with defaults
### Download Tracking
- **PendingReleases** - Delayed downloads (waiting for better quality)
- **DownloadHistory** - Download lifecycle events
- **Blocklist** - Failed/rejected releases (prevent re-download)
- **History** - Complete audit trail of all events
### System
- **Indexers** - Search sources (Newznab/Torznab)
- **DownloadClients** - Torrent/Usenet clients
- **ImportLists** - Auto-import from Spotify, Last.fm, etc.
- **Tags** - Categorization
## Monitoring States
### Artist Level
- `monitored` - Is artist being tracked
- `monitor_new_items` - Policy for new releases (All/Future/Missing/Existing/None)
### Album Level
- `monitored` - Should we look for this album
- `any_release_ok` - Auto-switch releases during import
### Release Level
- `monitored` - Is this the release we want (exactly ONE per album)
## Download States
```
TrackedDownloadState:
Downloading → ImportPending → Importing → Imported
→ Ignored
→ DownloadFailed → DownloadFailedPending
```
## History Event Types
- Grabbed - Download started
- TrackFileImported - File imported to library
- DownloadFailed - Download failed
- TrackFileDeleted - File removed
- TrackFileRenamed - File renamed
- TrackFileRetagged - Metadata updated
- AlbumImportIncomplete - Partial import
- DownloadIgnored - Download skipped
## Sources
- GitHub: Lidarr/Lidarr
- Key files:
- `src/NzbDrone.Core/Music/Model/Artist.cs`
- `src/NzbDrone.Core/Music/Model/Album.cs`
- `src/NzbDrone.Core/Music/Model/Release.cs` (AlbumRelease)
- `src/NzbDrone.Core/Music/Model/Track.cs`
- `src/NzbDrone.Core/MediaFiles/TrackFile.cs`
- `src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs`
- `src/NzbDrone.Core/Datastore/Migration/023_add_release_groups_etc.cs`
Generated
+9 -31
View File
@@ -21,11 +21,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1777988971,
"narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=",
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
@@ -37,15 +37,14 @@
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1776796298,
"narHash": "sha256-PcRvlWayisPSjd0UcRQbhG8Oqw78AcPE6x872cPRHN8=",
"lastModified": 1782908218,
"narHash": "sha256-wLMOrPgVyeF3XmP+qfYcLqnVdTxikdcSvbIY7rA9jTA=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "3cfd774b0a530725a077e17354fbdb87ea1c4aad",
"rev": "9f7e99119ece7705299595299f3b031f39356de1",
"type": "github"
},
"original": {
@@ -54,27 +53,6 @@
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1770073757,
@@ -108,11 +86,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1778095059,
"narHash": "sha256-LW2nru9+O0oyR3lfzgzFLwTibhINoIL/dx2/1dBMKWU=",
"lastModified": 1782929431,
"narHash": "sha256-Ys6O7G+rrgcvhyuEER7KkFjhAXXHpFCmsBzVaRFYe1s=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "94a37dc9da62b41f0c70a91da739bc318d049c11",
"rev": "1b56c1483c5fd996da3180b7f2023ec8d2c8cc0f",
"type": "github"
},
"original": {
File diff suppressed because it is too large Load Diff
-387
View File
@@ -1,387 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: metadata/v1/metadata.proto
package metadatav1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
MetadataService_GetArtist_FullMethodName = "/metadata.v1.MetadataService/GetArtist"
MetadataService_SearchArtists_FullMethodName = "/metadata.v1.MetadataService/SearchArtists"
MetadataService_GetAlbum_FullMethodName = "/metadata.v1.MetadataService/GetAlbum"
MetadataService_GetArtistAlbums_FullMethodName = "/metadata.v1.MetadataService/GetArtistAlbums"
MetadataService_GetTrack_FullMethodName = "/metadata.v1.MetadataService/GetTrack"
MetadataService_GetAlbumTracks_FullMethodName = "/metadata.v1.MetadataService/GetAlbumTracks"
MetadataService_SearchAlbums_FullMethodName = "/metadata.v1.MetadataService/SearchAlbums"
MetadataService_SyncArtist_FullMethodName = "/metadata.v1.MetadataService/SyncArtist"
)
// MetadataServiceClient is the client API for MetadataService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type MetadataServiceClient interface {
GetArtist(ctx context.Context, in *GetArtistRequest, opts ...grpc.CallOption) (*GetArtistResponse, error)
SearchArtists(ctx context.Context, in *SearchArtistsRequest, opts ...grpc.CallOption) (*SearchArtistsResponse, error)
GetAlbum(ctx context.Context, in *GetAlbumRequest, opts ...grpc.CallOption) (*GetAlbumResponse, error)
GetArtistAlbums(ctx context.Context, in *GetArtistAlbumsRequest, opts ...grpc.CallOption) (*GetArtistAlbumsResponse, error)
GetTrack(ctx context.Context, in *GetTrackRequest, opts ...grpc.CallOption) (*GetTrackResponse, error)
GetAlbumTracks(ctx context.Context, in *GetAlbumTracksRequest, opts ...grpc.CallOption) (*GetAlbumTracksResponse, error)
SearchAlbums(ctx context.Context, in *SearchAlbumsRequest, opts ...grpc.CallOption) (*SearchAlbumsResponse, error)
SyncArtist(ctx context.Context, in *SyncArtistRequest, opts ...grpc.CallOption) (*SyncArtistResponse, error)
}
type metadataServiceClient struct {
cc grpc.ClientConnInterface
}
func NewMetadataServiceClient(cc grpc.ClientConnInterface) MetadataServiceClient {
return &metadataServiceClient{cc}
}
func (c *metadataServiceClient) GetArtist(ctx context.Context, in *GetArtistRequest, opts ...grpc.CallOption) (*GetArtistResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetArtistResponse)
err := c.cc.Invoke(ctx, MetadataService_GetArtist_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) SearchArtists(ctx context.Context, in *SearchArtistsRequest, opts ...grpc.CallOption) (*SearchArtistsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SearchArtistsResponse)
err := c.cc.Invoke(ctx, MetadataService_SearchArtists_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) GetAlbum(ctx context.Context, in *GetAlbumRequest, opts ...grpc.CallOption) (*GetAlbumResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetAlbumResponse)
err := c.cc.Invoke(ctx, MetadataService_GetAlbum_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) GetArtistAlbums(ctx context.Context, in *GetArtistAlbumsRequest, opts ...grpc.CallOption) (*GetArtistAlbumsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetArtistAlbumsResponse)
err := c.cc.Invoke(ctx, MetadataService_GetArtistAlbums_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) GetTrack(ctx context.Context, in *GetTrackRequest, opts ...grpc.CallOption) (*GetTrackResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetTrackResponse)
err := c.cc.Invoke(ctx, MetadataService_GetTrack_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) GetAlbumTracks(ctx context.Context, in *GetAlbumTracksRequest, opts ...grpc.CallOption) (*GetAlbumTracksResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetAlbumTracksResponse)
err := c.cc.Invoke(ctx, MetadataService_GetAlbumTracks_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) SearchAlbums(ctx context.Context, in *SearchAlbumsRequest, opts ...grpc.CallOption) (*SearchAlbumsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SearchAlbumsResponse)
err := c.cc.Invoke(ctx, MetadataService_SearchAlbums_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) SyncArtist(ctx context.Context, in *SyncArtistRequest, opts ...grpc.CallOption) (*SyncArtistResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SyncArtistResponse)
err := c.cc.Invoke(ctx, MetadataService_SyncArtist_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// MetadataServiceServer is the server API for MetadataService service.
// All implementations must embed UnimplementedMetadataServiceServer
// for forward compatibility.
type MetadataServiceServer interface {
GetArtist(context.Context, *GetArtistRequest) (*GetArtistResponse, error)
SearchArtists(context.Context, *SearchArtistsRequest) (*SearchArtistsResponse, error)
GetAlbum(context.Context, *GetAlbumRequest) (*GetAlbumResponse, error)
GetArtistAlbums(context.Context, *GetArtistAlbumsRequest) (*GetArtistAlbumsResponse, error)
GetTrack(context.Context, *GetTrackRequest) (*GetTrackResponse, error)
GetAlbumTracks(context.Context, *GetAlbumTracksRequest) (*GetAlbumTracksResponse, error)
SearchAlbums(context.Context, *SearchAlbumsRequest) (*SearchAlbumsResponse, error)
SyncArtist(context.Context, *SyncArtistRequest) (*SyncArtistResponse, error)
mustEmbedUnimplementedMetadataServiceServer()
}
// UnimplementedMetadataServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedMetadataServiceServer struct{}
func (UnimplementedMetadataServiceServer) GetArtist(context.Context, *GetArtistRequest) (*GetArtistResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetArtist not implemented")
}
func (UnimplementedMetadataServiceServer) SearchArtists(context.Context, *SearchArtistsRequest) (*SearchArtistsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SearchArtists not implemented")
}
func (UnimplementedMetadataServiceServer) GetAlbum(context.Context, *GetAlbumRequest) (*GetAlbumResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetAlbum not implemented")
}
func (UnimplementedMetadataServiceServer) GetArtistAlbums(context.Context, *GetArtistAlbumsRequest) (*GetArtistAlbumsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetArtistAlbums not implemented")
}
func (UnimplementedMetadataServiceServer) GetTrack(context.Context, *GetTrackRequest) (*GetTrackResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetTrack not implemented")
}
func (UnimplementedMetadataServiceServer) GetAlbumTracks(context.Context, *GetAlbumTracksRequest) (*GetAlbumTracksResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetAlbumTracks not implemented")
}
func (UnimplementedMetadataServiceServer) SearchAlbums(context.Context, *SearchAlbumsRequest) (*SearchAlbumsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SearchAlbums not implemented")
}
func (UnimplementedMetadataServiceServer) SyncArtist(context.Context, *SyncArtistRequest) (*SyncArtistResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SyncArtist not implemented")
}
func (UnimplementedMetadataServiceServer) mustEmbedUnimplementedMetadataServiceServer() {}
func (UnimplementedMetadataServiceServer) testEmbeddedByValue() {}
// UnsafeMetadataServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to MetadataServiceServer will
// result in compilation errors.
type UnsafeMetadataServiceServer interface {
mustEmbedUnimplementedMetadataServiceServer()
}
func RegisterMetadataServiceServer(s grpc.ServiceRegistrar, srv MetadataServiceServer) {
// If the following call panics, it indicates UnimplementedMetadataServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&MetadataService_ServiceDesc, srv)
}
func _MetadataService_GetArtist_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetArtistRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).GetArtist(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_GetArtist_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).GetArtist(ctx, req.(*GetArtistRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_SearchArtists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SearchArtistsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).SearchArtists(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_SearchArtists_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).SearchArtists(ctx, req.(*SearchArtistsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_GetAlbum_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetAlbumRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).GetAlbum(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_GetAlbum_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).GetAlbum(ctx, req.(*GetAlbumRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_GetArtistAlbums_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetArtistAlbumsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).GetArtistAlbums(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_GetArtistAlbums_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).GetArtistAlbums(ctx, req.(*GetArtistAlbumsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_GetTrack_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetTrackRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).GetTrack(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_GetTrack_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).GetTrack(ctx, req.(*GetTrackRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_GetAlbumTracks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetAlbumTracksRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).GetAlbumTracks(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_GetAlbumTracks_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).GetAlbumTracks(ctx, req.(*GetAlbumTracksRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_SearchAlbums_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SearchAlbumsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).SearchAlbums(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_SearchAlbums_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).SearchAlbums(ctx, req.(*SearchAlbumsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_SyncArtist_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SyncArtistRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).SyncArtist(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_SyncArtist_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).SyncArtist(ctx, req.(*SyncArtistRequest))
}
return interceptor(ctx, in, info, handler)
}
// MetadataService_ServiceDesc is the grpc.ServiceDesc for MetadataService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var MetadataService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "metadata.v1.MetadataService",
HandlerType: (*MetadataServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetArtist",
Handler: _MetadataService_GetArtist_Handler,
},
{
MethodName: "SearchArtists",
Handler: _MetadataService_SearchArtists_Handler,
},
{
MethodName: "GetAlbum",
Handler: _MetadataService_GetAlbum_Handler,
},
{
MethodName: "GetArtistAlbums",
Handler: _MetadataService_GetArtistAlbums_Handler,
},
{
MethodName: "GetTrack",
Handler: _MetadataService_GetTrack_Handler,
},
{
MethodName: "GetAlbumTracks",
Handler: _MetadataService_GetAlbumTracks_Handler,
},
{
MethodName: "SearchAlbums",
Handler: _MetadataService_SearchAlbums_Handler,
},
{
MethodName: "SyncArtist",
Handler: _MetadataService_SyncArtist_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "metadata/v1/metadata.proto",
}
-252
View File
@@ -1,252 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: music_agregator/hello/v1/service.proto
package hello
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type PingRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PingRequest) Reset() {
*x = PingRequest{}
mi := &file_music_agregator_hello_v1_service_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PingRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PingRequest) ProtoMessage() {}
func (x *PingRequest) ProtoReflect() protoreflect.Message {
mi := &file_music_agregator_hello_v1_service_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead.
func (*PingRequest) Descriptor() ([]byte, []int) {
return file_music_agregator_hello_v1_service_proto_rawDescGZIP(), []int{0}
}
type PongResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PongResponse) Reset() {
*x = PongResponse{}
mi := &file_music_agregator_hello_v1_service_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PongResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PongResponse) ProtoMessage() {}
func (x *PongResponse) ProtoReflect() protoreflect.Message {
mi := &file_music_agregator_hello_v1_service_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PongResponse.ProtoReflect.Descriptor instead.
func (*PongResponse) Descriptor() ([]byte, []int) {
return file_music_agregator_hello_v1_service_proto_rawDescGZIP(), []int{1}
}
type EchoRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Msg string `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EchoRequest) Reset() {
*x = EchoRequest{}
mi := &file_music_agregator_hello_v1_service_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EchoRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EchoRequest) ProtoMessage() {}
func (x *EchoRequest) ProtoReflect() protoreflect.Message {
mi := &file_music_agregator_hello_v1_service_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EchoRequest.ProtoReflect.Descriptor instead.
func (*EchoRequest) Descriptor() ([]byte, []int) {
return file_music_agregator_hello_v1_service_proto_rawDescGZIP(), []int{2}
}
func (x *EchoRequest) GetMsg() string {
if x != nil {
return x.Msg
}
return ""
}
type EchoResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Response string `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EchoResponse) Reset() {
*x = EchoResponse{}
mi := &file_music_agregator_hello_v1_service_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EchoResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EchoResponse) ProtoMessage() {}
func (x *EchoResponse) ProtoReflect() protoreflect.Message {
mi := &file_music_agregator_hello_v1_service_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EchoResponse.ProtoReflect.Descriptor instead.
func (*EchoResponse) Descriptor() ([]byte, []int) {
return file_music_agregator_hello_v1_service_proto_rawDescGZIP(), []int{3}
}
func (x *EchoResponse) GetResponse() string {
if x != nil {
return x.Response
}
return ""
}
var File_music_agregator_hello_v1_service_proto protoreflect.FileDescriptor
const file_music_agregator_hello_v1_service_proto_rawDesc = "" +
"\n" +
"&music_agregator/hello/v1/service.proto\x12\x18music_agregator.hello.v1\"\r\n" +
"\vPingRequest\"\x0e\n" +
"\fPongResponse\"\x1f\n" +
"\vEchoRequest\x12\x10\n" +
"\x03msg\x18\x01 \x01(\tR\x03msg\"*\n" +
"\fEchoResponse\x12\x1a\n" +
"\bresponse\x18\x01 \x01(\tR\bresponse2\xc0\x01\n" +
"\fHelloService\x12W\n" +
"\x04Ping\x12%.music_agregator.hello.v1.PingRequest\x1a&.music_agregator.hello.v1.PongResponse\"\x00\x12W\n" +
"\x04Echo\x12%.music_agregator.hello.v1.EchoRequest\x1a&.music_agregator.hello.v1.EchoResponse\"\x00B:Z8homelab.lan/music-agregator/gen/music_agregator/v1/hellob\x06proto3"
var (
file_music_agregator_hello_v1_service_proto_rawDescOnce sync.Once
file_music_agregator_hello_v1_service_proto_rawDescData []byte
)
func file_music_agregator_hello_v1_service_proto_rawDescGZIP() []byte {
file_music_agregator_hello_v1_service_proto_rawDescOnce.Do(func() {
file_music_agregator_hello_v1_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_music_agregator_hello_v1_service_proto_rawDesc), len(file_music_agregator_hello_v1_service_proto_rawDesc)))
})
return file_music_agregator_hello_v1_service_proto_rawDescData
}
var file_music_agregator_hello_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_music_agregator_hello_v1_service_proto_goTypes = []any{
(*PingRequest)(nil), // 0: music_agregator.hello.v1.PingRequest
(*PongResponse)(nil), // 1: music_agregator.hello.v1.PongResponse
(*EchoRequest)(nil), // 2: music_agregator.hello.v1.EchoRequest
(*EchoResponse)(nil), // 3: music_agregator.hello.v1.EchoResponse
}
var file_music_agregator_hello_v1_service_proto_depIdxs = []int32{
0, // 0: music_agregator.hello.v1.HelloService.Ping:input_type -> music_agregator.hello.v1.PingRequest
2, // 1: music_agregator.hello.v1.HelloService.Echo:input_type -> music_agregator.hello.v1.EchoRequest
1, // 2: music_agregator.hello.v1.HelloService.Ping:output_type -> music_agregator.hello.v1.PongResponse
3, // 3: music_agregator.hello.v1.HelloService.Echo:output_type -> music_agregator.hello.v1.EchoResponse
2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_music_agregator_hello_v1_service_proto_init() }
func file_music_agregator_hello_v1_service_proto_init() {
if File_music_agregator_hello_v1_service_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_music_agregator_hello_v1_service_proto_rawDesc), len(file_music_agregator_hello_v1_service_proto_rawDesc)),
NumEnums: 0,
NumMessages: 4,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_music_agregator_hello_v1_service_proto_goTypes,
DependencyIndexes: file_music_agregator_hello_v1_service_proto_depIdxs,
MessageInfos: file_music_agregator_hello_v1_service_proto_msgTypes,
}.Build()
File_music_agregator_hello_v1_service_proto = out.File
file_music_agregator_hello_v1_service_proto_goTypes = nil
file_music_agregator_hello_v1_service_proto_depIdxs = nil
}
@@ -1,159 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: music_agregator/hello/v1/service.proto
package hello
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
HelloService_Ping_FullMethodName = "/music_agregator.hello.v1.HelloService/Ping"
HelloService_Echo_FullMethodName = "/music_agregator.hello.v1.HelloService/Echo"
)
// HelloServiceClient is the client API for HelloService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HelloServiceClient interface {
Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PongResponse, error)
Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error)
}
type helloServiceClient struct {
cc grpc.ClientConnInterface
}
func NewHelloServiceClient(cc grpc.ClientConnInterface) HelloServiceClient {
return &helloServiceClient{cc}
}
func (c *helloServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PongResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(PongResponse)
err := c.cc.Invoke(ctx, HelloService_Ping_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *helloServiceClient) Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(EchoResponse)
err := c.cc.Invoke(ctx, HelloService_Echo_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// HelloServiceServer is the server API for HelloService service.
// All implementations must embed UnimplementedHelloServiceServer
// for forward compatibility.
type HelloServiceServer interface {
Ping(context.Context, *PingRequest) (*PongResponse, error)
Echo(context.Context, *EchoRequest) (*EchoResponse, error)
mustEmbedUnimplementedHelloServiceServer()
}
// UnimplementedHelloServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedHelloServiceServer struct{}
func (UnimplementedHelloServiceServer) Ping(context.Context, *PingRequest) (*PongResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Ping not implemented")
}
func (UnimplementedHelloServiceServer) Echo(context.Context, *EchoRequest) (*EchoResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Echo not implemented")
}
func (UnimplementedHelloServiceServer) mustEmbedUnimplementedHelloServiceServer() {}
func (UnimplementedHelloServiceServer) testEmbeddedByValue() {}
// UnsafeHelloServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to HelloServiceServer will
// result in compilation errors.
type UnsafeHelloServiceServer interface {
mustEmbedUnimplementedHelloServiceServer()
}
func RegisterHelloServiceServer(s grpc.ServiceRegistrar, srv HelloServiceServer) {
// If the following call panics, it indicates UnimplementedHelloServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&HelloService_ServiceDesc, srv)
}
func _HelloService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PingRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HelloServiceServer).Ping(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: HelloService_Ping_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HelloServiceServer).Ping(ctx, req.(*PingRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HelloService_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(EchoRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HelloServiceServer).Echo(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: HelloService_Echo_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HelloServiceServer).Echo(ctx, req.(*EchoRequest))
}
return interceptor(ctx, in, info, handler)
}
// HelloService_ServiceDesc is the grpc.ServiceDesc for HelloService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var HelloService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "music_agregator.hello.v1.HelloService",
HandlerType: (*HelloServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Ping",
Handler: _HelloService_Ping_Handler,
},
{
MethodName: "Echo",
Handler: _HelloService_Echo_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "music_agregator/hello/v1/service.proto",
}
File diff suppressed because it is too large Load Diff
@@ -1,159 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: music_agregator/indexer/v1/indexer.proto
package indexer
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
IndexerService_Search_FullMethodName = "/music_agregator.indexer.v1.IndexerService/Search"
IndexerService_Capabilities_FullMethodName = "/music_agregator.indexer.v1.IndexerService/Capabilities"
)
// IndexerServiceClient is the client API for IndexerService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type IndexerServiceClient interface {
Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error)
Capabilities(ctx context.Context, in *CapabilitiesRequest, opts ...grpc.CallOption) (*CapabilitiesResponse, error)
}
type indexerServiceClient struct {
cc grpc.ClientConnInterface
}
func NewIndexerServiceClient(cc grpc.ClientConnInterface) IndexerServiceClient {
return &indexerServiceClient{cc}
}
func (c *indexerServiceClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SearchResponse)
err := c.cc.Invoke(ctx, IndexerService_Search_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *indexerServiceClient) Capabilities(ctx context.Context, in *CapabilitiesRequest, opts ...grpc.CallOption) (*CapabilitiesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CapabilitiesResponse)
err := c.cc.Invoke(ctx, IndexerService_Capabilities_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// IndexerServiceServer is the server API for IndexerService service.
// All implementations must embed UnimplementedIndexerServiceServer
// for forward compatibility.
type IndexerServiceServer interface {
Search(context.Context, *SearchRequest) (*SearchResponse, error)
Capabilities(context.Context, *CapabilitiesRequest) (*CapabilitiesResponse, error)
mustEmbedUnimplementedIndexerServiceServer()
}
// UnimplementedIndexerServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedIndexerServiceServer struct{}
func (UnimplementedIndexerServiceServer) Search(context.Context, *SearchRequest) (*SearchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Search not implemented")
}
func (UnimplementedIndexerServiceServer) Capabilities(context.Context, *CapabilitiesRequest) (*CapabilitiesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Capabilities not implemented")
}
func (UnimplementedIndexerServiceServer) mustEmbedUnimplementedIndexerServiceServer() {}
func (UnimplementedIndexerServiceServer) testEmbeddedByValue() {}
// UnsafeIndexerServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to IndexerServiceServer will
// result in compilation errors.
type UnsafeIndexerServiceServer interface {
mustEmbedUnimplementedIndexerServiceServer()
}
func RegisterIndexerServiceServer(s grpc.ServiceRegistrar, srv IndexerServiceServer) {
// If the following call panics, it indicates UnimplementedIndexerServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&IndexerService_ServiceDesc, srv)
}
func _IndexerService_Search_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SearchRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IndexerServiceServer).Search(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: IndexerService_Search_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IndexerServiceServer).Search(ctx, req.(*SearchRequest))
}
return interceptor(ctx, in, info, handler)
}
func _IndexerService_Capabilities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CapabilitiesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IndexerServiceServer).Capabilities(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: IndexerService_Capabilities_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IndexerServiceServer).Capabilities(ctx, req.(*CapabilitiesRequest))
}
return interceptor(ctx, in, info, handler)
}
// IndexerService_ServiceDesc is the grpc.ServiceDesc for IndexerService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var IndexerService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "music_agregator.indexer.v1.IndexerService",
HandlerType: (*IndexerServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Search",
Handler: _IndexerService_Search_Handler,
},
{
MethodName: "Capabilities",
Handler: _IndexerService_Capabilities_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "music_agregator/indexer/v1/indexer.proto",
}
@@ -1,60 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: music_agregator/metadata/v1/metadata.proto
package metadata
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
var File_music_agregator_metadata_v1_metadata_proto protoreflect.FileDescriptor
const file_music_agregator_metadata_v1_metadata_proto_rawDesc = "" +
"\n" +
"*music_agregator/metadata/v1/metadata.proto\x12\x1bmusic_agregator.metadata.v12\x11\n" +
"\x0fMetadataServiceB=Z;homelab.lan/music-agregator/gen/music_agregator/v1/metadatab\x06proto3"
var file_music_agregator_metadata_v1_metadata_proto_goTypes = []any{}
var file_music_agregator_metadata_v1_metadata_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_music_agregator_metadata_v1_metadata_proto_init() }
func file_music_agregator_metadata_v1_metadata_proto_init() {
if File_music_agregator_metadata_v1_metadata_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_music_agregator_metadata_v1_metadata_proto_rawDesc), len(file_music_agregator_metadata_v1_metadata_proto_rawDesc)),
NumEnums: 0,
NumMessages: 0,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_music_agregator_metadata_v1_metadata_proto_goTypes,
DependencyIndexes: file_music_agregator_metadata_v1_metadata_proto_depIdxs,
}.Build()
File_music_agregator_metadata_v1_metadata_proto = out.File
file_music_agregator_metadata_v1_metadata_proto_goTypes = nil
file_music_agregator_metadata_v1_metadata_proto_depIdxs = nil
}
@@ -1,76 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: music_agregator/metadata/v1/metadata.proto
package metadata
import (
grpc "google.golang.org/grpc"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
// MetadataServiceClient is the client API for MetadataService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type MetadataServiceClient interface {
}
type metadataServiceClient struct {
cc grpc.ClientConnInterface
}
func NewMetadataServiceClient(cc grpc.ClientConnInterface) MetadataServiceClient {
return &metadataServiceClient{cc}
}
// MetadataServiceServer is the server API for MetadataService service.
// All implementations must embed UnimplementedMetadataServiceServer
// for forward compatibility.
type MetadataServiceServer interface {
mustEmbedUnimplementedMetadataServiceServer()
}
// UnimplementedMetadataServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedMetadataServiceServer struct{}
func (UnimplementedMetadataServiceServer) mustEmbedUnimplementedMetadataServiceServer() {}
func (UnimplementedMetadataServiceServer) testEmbeddedByValue() {}
// UnsafeMetadataServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to MetadataServiceServer will
// result in compilation errors.
type UnsafeMetadataServiceServer interface {
mustEmbedUnimplementedMetadataServiceServer()
}
func RegisterMetadataServiceServer(s grpc.ServiceRegistrar, srv MetadataServiceServer) {
// If the following call panics, it indicates UnimplementedMetadataServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&MetadataService_ServiceDesc, srv)
}
// MetadataService_ServiceDesc is the grpc.ServiceDesc for MetadataService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var MetadataService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "music_agregator.metadata.v1.MetadataService",
HandlerType: (*MetadataServiceServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{},
Metadata: "music_agregator/metadata/v1/metadata.proto",
}
@@ -1,713 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: music_agregator/torrent/v1/torrent.proto
package torrent
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ListRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ClientName string `protobuf:"bytes,1,opt,name=client_name,json=clientName,proto3" json:"client_name,omitempty"`
Filter string `protobuf:"bytes,2,opt,name=filter,proto3" json:"filter,omitempty"`
Category string `protobuf:"bytes,3,opt,name=category,proto3" json:"category,omitempty"`
Tag string `protobuf:"bytes,4,opt,name=tag,proto3" json:"tag,omitempty"`
Sort string `protobuf:"bytes,5,opt,name=sort,proto3" json:"sort,omitempty"`
Reverse bool `protobuf:"varint,6,opt,name=reverse,proto3" json:"reverse,omitempty"`
Limit int32 `protobuf:"varint,7,opt,name=limit,proto3" json:"limit,omitempty"`
Offset int32 `protobuf:"varint,8,opt,name=offset,proto3" json:"offset,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListRequest) Reset() {
*x = ListRequest{}
mi := &file_music_agregator_torrent_v1_torrent_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListRequest) ProtoMessage() {}
func (x *ListRequest) ProtoReflect() protoreflect.Message {
mi := &file_music_agregator_torrent_v1_torrent_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListRequest.ProtoReflect.Descriptor instead.
func (*ListRequest) Descriptor() ([]byte, []int) {
return file_music_agregator_torrent_v1_torrent_proto_rawDescGZIP(), []int{0}
}
func (x *ListRequest) GetClientName() string {
if x != nil {
return x.ClientName
}
return ""
}
func (x *ListRequest) GetFilter() string {
if x != nil {
return x.Filter
}
return ""
}
func (x *ListRequest) GetCategory() string {
if x != nil {
return x.Category
}
return ""
}
func (x *ListRequest) GetTag() string {
if x != nil {
return x.Tag
}
return ""
}
func (x *ListRequest) GetSort() string {
if x != nil {
return x.Sort
}
return ""
}
func (x *ListRequest) GetReverse() bool {
if x != nil {
return x.Reverse
}
return false
}
func (x *ListRequest) GetLimit() int32 {
if x != nil {
return x.Limit
}
return 0
}
func (x *ListRequest) GetOffset() int32 {
if x != nil {
return x.Offset
}
return 0
}
type ListResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Items []*ListItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListResponse) Reset() {
*x = ListResponse{}
mi := &file_music_agregator_torrent_v1_torrent_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListResponse) ProtoMessage() {}
func (x *ListResponse) ProtoReflect() protoreflect.Message {
mi := &file_music_agregator_torrent_v1_torrent_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListResponse.ProtoReflect.Descriptor instead.
func (*ListResponse) Descriptor() ([]byte, []int) {
return file_music_agregator_torrent_v1_torrent_proto_rawDescGZIP(), []int{1}
}
func (x *ListResponse) GetItems() []*ListItem {
if x != nil {
return x.Items
}
return nil
}
type ListItem struct {
state protoimpl.MessageState `protogen:"open.v1"`
Hash string `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Size int64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"`
Progress float64 `protobuf:"fixed64,4,opt,name=progress,proto3" json:"progress,omitempty"`
Dlspeed int64 `protobuf:"varint,5,opt,name=dlspeed,proto3" json:"dlspeed,omitempty"`
Upspeed int64 `protobuf:"varint,6,opt,name=upspeed,proto3" json:"upspeed,omitempty"`
NumSeeds int32 `protobuf:"varint,7,opt,name=num_seeds,json=numSeeds,proto3" json:"num_seeds,omitempty"`
NumLeechs int32 `protobuf:"varint,8,opt,name=num_leechs,json=numLeechs,proto3" json:"num_leechs,omitempty"`
State string `protobuf:"bytes,9,opt,name=state,proto3" json:"state,omitempty"`
Eta int64 `protobuf:"varint,10,opt,name=eta,proto3" json:"eta,omitempty"`
Ratio float64 `protobuf:"fixed64,11,opt,name=ratio,proto3" json:"ratio,omitempty"`
Category string `protobuf:"bytes,12,opt,name=category,proto3" json:"category,omitempty"`
Tags string `protobuf:"bytes,13,opt,name=tags,proto3" json:"tags,omitempty"`
AddedOn int64 `protobuf:"varint,14,opt,name=added_on,json=addedOn,proto3" json:"added_on,omitempty"`
CompletionOn int64 `protobuf:"varint,15,opt,name=completion_on,json=completionOn,proto3" json:"completion_on,omitempty"`
SavePath string `protobuf:"bytes,16,opt,name=save_path,json=savePath,proto3" json:"save_path,omitempty"`
ContentPath string `protobuf:"bytes,17,opt,name=content_path,json=contentPath,proto3" json:"content_path,omitempty"`
Downloaded int64 `protobuf:"varint,18,opt,name=downloaded,proto3" json:"downloaded,omitempty"`
Uploaded int64 `protobuf:"varint,19,opt,name=uploaded,proto3" json:"uploaded,omitempty"`
Tracker string `protobuf:"bytes,20,opt,name=tracker,proto3" json:"tracker,omitempty"`
SeedingTime int64 `protobuf:"varint,21,opt,name=seeding_time,json=seedingTime,proto3" json:"seeding_time,omitempty"`
AmountLeft int64 `protobuf:"varint,22,opt,name=amount_left,json=amountLeft,proto3" json:"amount_left,omitempty"`
Availability float64 `protobuf:"fixed64,23,opt,name=availability,proto3" json:"availability,omitempty"`
SizeFormatted string `protobuf:"bytes,100,opt,name=size_formatted,json=sizeFormatted,proto3" json:"size_formatted,omitempty"`
ProgressFormatted string `protobuf:"bytes,101,opt,name=progress_formatted,json=progressFormatted,proto3" json:"progress_formatted,omitempty"`
DlspeedFormatted string `protobuf:"bytes,102,opt,name=dlspeed_formatted,json=dlspeedFormatted,proto3" json:"dlspeed_formatted,omitempty"`
UpspeedFormatted string `protobuf:"bytes,103,opt,name=upspeed_formatted,json=upspeedFormatted,proto3" json:"upspeed_formatted,omitempty"`
AddedOnFormatted string `protobuf:"bytes,104,opt,name=added_on_formatted,json=addedOnFormatted,proto3" json:"added_on_formatted,omitempty"`
CompletionOnFormatted string `protobuf:"bytes,105,opt,name=completion_on_formatted,json=completionOnFormatted,proto3" json:"completion_on_formatted,omitempty"`
DownloadedFormatted string `protobuf:"bytes,106,opt,name=downloaded_formatted,json=downloadedFormatted,proto3" json:"downloaded_formatted,omitempty"`
UploadedFormatted string `protobuf:"bytes,107,opt,name=uploaded_formatted,json=uploadedFormatted,proto3" json:"uploaded_formatted,omitempty"`
AmountLeftFormatted string `protobuf:"bytes,108,opt,name=amount_left_formatted,json=amountLeftFormatted,proto3" json:"amount_left_formatted,omitempty"`
AvailabilityFormatted string `protobuf:"bytes,109,opt,name=availability_formatted,json=availabilityFormatted,proto3" json:"availability_formatted,omitempty"`
EtaFormatted string `protobuf:"bytes,110,opt,name=eta_formatted,json=etaFormatted,proto3" json:"eta_formatted,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListItem) Reset() {
*x = ListItem{}
mi := &file_music_agregator_torrent_v1_torrent_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListItem) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListItem) ProtoMessage() {}
func (x *ListItem) ProtoReflect() protoreflect.Message {
mi := &file_music_agregator_torrent_v1_torrent_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListItem.ProtoReflect.Descriptor instead.
func (*ListItem) Descriptor() ([]byte, []int) {
return file_music_agregator_torrent_v1_torrent_proto_rawDescGZIP(), []int{2}
}
func (x *ListItem) GetHash() string {
if x != nil {
return x.Hash
}
return ""
}
func (x *ListItem) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ListItem) GetSize() int64 {
if x != nil {
return x.Size
}
return 0
}
func (x *ListItem) GetProgress() float64 {
if x != nil {
return x.Progress
}
return 0
}
func (x *ListItem) GetDlspeed() int64 {
if x != nil {
return x.Dlspeed
}
return 0
}
func (x *ListItem) GetUpspeed() int64 {
if x != nil {
return x.Upspeed
}
return 0
}
func (x *ListItem) GetNumSeeds() int32 {
if x != nil {
return x.NumSeeds
}
return 0
}
func (x *ListItem) GetNumLeechs() int32 {
if x != nil {
return x.NumLeechs
}
return 0
}
func (x *ListItem) GetState() string {
if x != nil {
return x.State
}
return ""
}
func (x *ListItem) GetEta() int64 {
if x != nil {
return x.Eta
}
return 0
}
func (x *ListItem) GetRatio() float64 {
if x != nil {
return x.Ratio
}
return 0
}
func (x *ListItem) GetCategory() string {
if x != nil {
return x.Category
}
return ""
}
func (x *ListItem) GetTags() string {
if x != nil {
return x.Tags
}
return ""
}
func (x *ListItem) GetAddedOn() int64 {
if x != nil {
return x.AddedOn
}
return 0
}
func (x *ListItem) GetCompletionOn() int64 {
if x != nil {
return x.CompletionOn
}
return 0
}
func (x *ListItem) GetSavePath() string {
if x != nil {
return x.SavePath
}
return ""
}
func (x *ListItem) GetContentPath() string {
if x != nil {
return x.ContentPath
}
return ""
}
func (x *ListItem) GetDownloaded() int64 {
if x != nil {
return x.Downloaded
}
return 0
}
func (x *ListItem) GetUploaded() int64 {
if x != nil {
return x.Uploaded
}
return 0
}
func (x *ListItem) GetTracker() string {
if x != nil {
return x.Tracker
}
return ""
}
func (x *ListItem) GetSeedingTime() int64 {
if x != nil {
return x.SeedingTime
}
return 0
}
func (x *ListItem) GetAmountLeft() int64 {
if x != nil {
return x.AmountLeft
}
return 0
}
func (x *ListItem) GetAvailability() float64 {
if x != nil {
return x.Availability
}
return 0
}
func (x *ListItem) GetSizeFormatted() string {
if x != nil {
return x.SizeFormatted
}
return ""
}
func (x *ListItem) GetProgressFormatted() string {
if x != nil {
return x.ProgressFormatted
}
return ""
}
func (x *ListItem) GetDlspeedFormatted() string {
if x != nil {
return x.DlspeedFormatted
}
return ""
}
func (x *ListItem) GetUpspeedFormatted() string {
if x != nil {
return x.UpspeedFormatted
}
return ""
}
func (x *ListItem) GetAddedOnFormatted() string {
if x != nil {
return x.AddedOnFormatted
}
return ""
}
func (x *ListItem) GetCompletionOnFormatted() string {
if x != nil {
return x.CompletionOnFormatted
}
return ""
}
func (x *ListItem) GetDownloadedFormatted() string {
if x != nil {
return x.DownloadedFormatted
}
return ""
}
func (x *ListItem) GetUploadedFormatted() string {
if x != nil {
return x.UploadedFormatted
}
return ""
}
func (x *ListItem) GetAmountLeftFormatted() string {
if x != nil {
return x.AmountLeftFormatted
}
return ""
}
func (x *ListItem) GetAvailabilityFormatted() string {
if x != nil {
return x.AvailabilityFormatted
}
return ""
}
func (x *ListItem) GetEtaFormatted() string {
if x != nil {
return x.EtaFormatted
}
return ""
}
type AddRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
DownloadUrl string `protobuf:"bytes,1,opt,name=download_url,json=downloadUrl,proto3" json:"download_url,omitempty"`
Filename string `protobuf:"bytes,2,opt,name=filename,proto3" json:"filename,omitempty"`
TorrentData []byte `protobuf:"bytes,3,opt,name=torrent_data,json=torrentData,proto3" json:"torrent_data,omitempty"`
SavePath string `protobuf:"bytes,4,opt,name=save_path,json=savePath,proto3" json:"save_path,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AddRequest) Reset() {
*x = AddRequest{}
mi := &file_music_agregator_torrent_v1_torrent_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AddRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AddRequest) ProtoMessage() {}
func (x *AddRequest) ProtoReflect() protoreflect.Message {
mi := &file_music_agregator_torrent_v1_torrent_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AddRequest.ProtoReflect.Descriptor instead.
func (*AddRequest) Descriptor() ([]byte, []int) {
return file_music_agregator_torrent_v1_torrent_proto_rawDescGZIP(), []int{3}
}
func (x *AddRequest) GetDownloadUrl() string {
if x != nil {
return x.DownloadUrl
}
return ""
}
func (x *AddRequest) GetFilename() string {
if x != nil {
return x.Filename
}
return ""
}
func (x *AddRequest) GetTorrentData() []byte {
if x != nil {
return x.TorrentData
}
return nil
}
func (x *AddRequest) GetSavePath() string {
if x != nil {
return x.SavePath
}
return ""
}
type AddResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Item *ListItem `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AddResponse) Reset() {
*x = AddResponse{}
mi := &file_music_agregator_torrent_v1_torrent_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AddResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AddResponse) ProtoMessage() {}
func (x *AddResponse) ProtoReflect() protoreflect.Message {
mi := &file_music_agregator_torrent_v1_torrent_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AddResponse.ProtoReflect.Descriptor instead.
func (*AddResponse) Descriptor() ([]byte, []int) {
return file_music_agregator_torrent_v1_torrent_proto_rawDescGZIP(), []int{4}
}
func (x *AddResponse) GetItem() *ListItem {
if x != nil {
return x.Item
}
return nil
}
var File_music_agregator_torrent_v1_torrent_proto protoreflect.FileDescriptor
const file_music_agregator_torrent_v1_torrent_proto_rawDesc = "" +
"\n" +
"(music_agregator/torrent/v1/torrent.proto\x12\x1amusic_agregator.torrent.v1\"\xd0\x01\n" +
"\vListRequest\x12\x1f\n" +
"\vclient_name\x18\x01 \x01(\tR\n" +
"clientName\x12\x16\n" +
"\x06filter\x18\x02 \x01(\tR\x06filter\x12\x1a\n" +
"\bcategory\x18\x03 \x01(\tR\bcategory\x12\x10\n" +
"\x03tag\x18\x04 \x01(\tR\x03tag\x12\x12\n" +
"\x04sort\x18\x05 \x01(\tR\x04sort\x12\x18\n" +
"\areverse\x18\x06 \x01(\bR\areverse\x12\x14\n" +
"\x05limit\x18\a \x01(\x05R\x05limit\x12\x16\n" +
"\x06offset\x18\b \x01(\x05R\x06offset\"J\n" +
"\fListResponse\x12:\n" +
"\x05items\x18\x01 \x03(\v2$.music_agregator.torrent.v1.ListItemR\x05items\"\x86\t\n" +
"\bListItem\x12\x12\n" +
"\x04hash\x18\x01 \x01(\tR\x04hash\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
"\x04size\x18\x03 \x01(\x03R\x04size\x12\x1a\n" +
"\bprogress\x18\x04 \x01(\x01R\bprogress\x12\x18\n" +
"\adlspeed\x18\x05 \x01(\x03R\adlspeed\x12\x18\n" +
"\aupspeed\x18\x06 \x01(\x03R\aupspeed\x12\x1b\n" +
"\tnum_seeds\x18\a \x01(\x05R\bnumSeeds\x12\x1d\n" +
"\n" +
"num_leechs\x18\b \x01(\x05R\tnumLeechs\x12\x14\n" +
"\x05state\x18\t \x01(\tR\x05state\x12\x10\n" +
"\x03eta\x18\n" +
" \x01(\x03R\x03eta\x12\x14\n" +
"\x05ratio\x18\v \x01(\x01R\x05ratio\x12\x1a\n" +
"\bcategory\x18\f \x01(\tR\bcategory\x12\x12\n" +
"\x04tags\x18\r \x01(\tR\x04tags\x12\x19\n" +
"\badded_on\x18\x0e \x01(\x03R\aaddedOn\x12#\n" +
"\rcompletion_on\x18\x0f \x01(\x03R\fcompletionOn\x12\x1b\n" +
"\tsave_path\x18\x10 \x01(\tR\bsavePath\x12!\n" +
"\fcontent_path\x18\x11 \x01(\tR\vcontentPath\x12\x1e\n" +
"\n" +
"downloaded\x18\x12 \x01(\x03R\n" +
"downloaded\x12\x1a\n" +
"\buploaded\x18\x13 \x01(\x03R\buploaded\x12\x18\n" +
"\atracker\x18\x14 \x01(\tR\atracker\x12!\n" +
"\fseeding_time\x18\x15 \x01(\x03R\vseedingTime\x12\x1f\n" +
"\vamount_left\x18\x16 \x01(\x03R\n" +
"amountLeft\x12\"\n" +
"\favailability\x18\x17 \x01(\x01R\favailability\x12%\n" +
"\x0esize_formatted\x18d \x01(\tR\rsizeFormatted\x12-\n" +
"\x12progress_formatted\x18e \x01(\tR\x11progressFormatted\x12+\n" +
"\x11dlspeed_formatted\x18f \x01(\tR\x10dlspeedFormatted\x12+\n" +
"\x11upspeed_formatted\x18g \x01(\tR\x10upspeedFormatted\x12,\n" +
"\x12added_on_formatted\x18h \x01(\tR\x10addedOnFormatted\x126\n" +
"\x17completion_on_formatted\x18i \x01(\tR\x15completionOnFormatted\x121\n" +
"\x14downloaded_formatted\x18j \x01(\tR\x13downloadedFormatted\x12-\n" +
"\x12uploaded_formatted\x18k \x01(\tR\x11uploadedFormatted\x122\n" +
"\x15amount_left_formatted\x18l \x01(\tR\x13amountLeftFormatted\x125\n" +
"\x16availability_formatted\x18m \x01(\tR\x15availabilityFormatted\x12#\n" +
"\reta_formatted\x18n \x01(\tR\fetaFormatted\"\x8b\x01\n" +
"\n" +
"AddRequest\x12!\n" +
"\fdownload_url\x18\x01 \x01(\tR\vdownloadUrl\x12\x1a\n" +
"\bfilename\x18\x02 \x01(\tR\bfilename\x12!\n" +
"\ftorrent_data\x18\x03 \x01(\fR\vtorrentData\x12\x1b\n" +
"\tsave_path\x18\x04 \x01(\tR\bsavePath\"G\n" +
"\vAddResponse\x128\n" +
"\x04item\x18\x01 \x01(\v2$.music_agregator.torrent.v1.ListItemR\x04item2\xc7\x01\n" +
"\x0eTorrentService\x12[\n" +
"\x04List\x12'.music_agregator.torrent.v1.ListRequest\x1a(.music_agregator.torrent.v1.ListResponse\"\x00\x12X\n" +
"\x03Add\x12&.music_agregator.torrent.v1.AddRequest\x1a'.music_agregator.torrent.v1.AddResponse\"\x00B<Z:homelab.lan/music-agregator/gen/music_agregator/v1/torrentb\x06proto3"
var (
file_music_agregator_torrent_v1_torrent_proto_rawDescOnce sync.Once
file_music_agregator_torrent_v1_torrent_proto_rawDescData []byte
)
func file_music_agregator_torrent_v1_torrent_proto_rawDescGZIP() []byte {
file_music_agregator_torrent_v1_torrent_proto_rawDescOnce.Do(func() {
file_music_agregator_torrent_v1_torrent_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_music_agregator_torrent_v1_torrent_proto_rawDesc), len(file_music_agregator_torrent_v1_torrent_proto_rawDesc)))
})
return file_music_agregator_torrent_v1_torrent_proto_rawDescData
}
var file_music_agregator_torrent_v1_torrent_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_music_agregator_torrent_v1_torrent_proto_goTypes = []any{
(*ListRequest)(nil), // 0: music_agregator.torrent.v1.ListRequest
(*ListResponse)(nil), // 1: music_agregator.torrent.v1.ListResponse
(*ListItem)(nil), // 2: music_agregator.torrent.v1.ListItem
(*AddRequest)(nil), // 3: music_agregator.torrent.v1.AddRequest
(*AddResponse)(nil), // 4: music_agregator.torrent.v1.AddResponse
}
var file_music_agregator_torrent_v1_torrent_proto_depIdxs = []int32{
2, // 0: music_agregator.torrent.v1.ListResponse.items:type_name -> music_agregator.torrent.v1.ListItem
2, // 1: music_agregator.torrent.v1.AddResponse.item:type_name -> music_agregator.torrent.v1.ListItem
0, // 2: music_agregator.torrent.v1.TorrentService.List:input_type -> music_agregator.torrent.v1.ListRequest
3, // 3: music_agregator.torrent.v1.TorrentService.Add:input_type -> music_agregator.torrent.v1.AddRequest
1, // 4: music_agregator.torrent.v1.TorrentService.List:output_type -> music_agregator.torrent.v1.ListResponse
4, // 5: music_agregator.torrent.v1.TorrentService.Add:output_type -> music_agregator.torrent.v1.AddResponse
4, // [4:6] is the sub-list for method output_type
2, // [2:4] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_music_agregator_torrent_v1_torrent_proto_init() }
func file_music_agregator_torrent_v1_torrent_proto_init() {
if File_music_agregator_torrent_v1_torrent_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_music_agregator_torrent_v1_torrent_proto_rawDesc), len(file_music_agregator_torrent_v1_torrent_proto_rawDesc)),
NumEnums: 0,
NumMessages: 5,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_music_agregator_torrent_v1_torrent_proto_goTypes,
DependencyIndexes: file_music_agregator_torrent_v1_torrent_proto_depIdxs,
MessageInfos: file_music_agregator_torrent_v1_torrent_proto_msgTypes,
}.Build()
File_music_agregator_torrent_v1_torrent_proto = out.File
file_music_agregator_torrent_v1_torrent_proto_goTypes = nil
file_music_agregator_torrent_v1_torrent_proto_depIdxs = nil
}
@@ -1,159 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: music_agregator/torrent/v1/torrent.proto
package torrent
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
TorrentService_List_FullMethodName = "/music_agregator.torrent.v1.TorrentService/List"
TorrentService_Add_FullMethodName = "/music_agregator.torrent.v1.TorrentService/Add"
)
// TorrentServiceClient is the client API for TorrentService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type TorrentServiceClient interface {
List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error)
Add(ctx context.Context, in *AddRequest, opts ...grpc.CallOption) (*AddResponse, error)
}
type torrentServiceClient struct {
cc grpc.ClientConnInterface
}
func NewTorrentServiceClient(cc grpc.ClientConnInterface) TorrentServiceClient {
return &torrentServiceClient{cc}
}
func (c *torrentServiceClient) List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListResponse)
err := c.cc.Invoke(ctx, TorrentService_List_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *torrentServiceClient) Add(ctx context.Context, in *AddRequest, opts ...grpc.CallOption) (*AddResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AddResponse)
err := c.cc.Invoke(ctx, TorrentService_Add_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// TorrentServiceServer is the server API for TorrentService service.
// All implementations must embed UnimplementedTorrentServiceServer
// for forward compatibility.
type TorrentServiceServer interface {
List(context.Context, *ListRequest) (*ListResponse, error)
Add(context.Context, *AddRequest) (*AddResponse, error)
mustEmbedUnimplementedTorrentServiceServer()
}
// UnimplementedTorrentServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedTorrentServiceServer struct{}
func (UnimplementedTorrentServiceServer) List(context.Context, *ListRequest) (*ListResponse, error) {
return nil, status.Error(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedTorrentServiceServer) Add(context.Context, *AddRequest) (*AddResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Add not implemented")
}
func (UnimplementedTorrentServiceServer) mustEmbedUnimplementedTorrentServiceServer() {}
func (UnimplementedTorrentServiceServer) testEmbeddedByValue() {}
// UnsafeTorrentServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to TorrentServiceServer will
// result in compilation errors.
type UnsafeTorrentServiceServer interface {
mustEmbedUnimplementedTorrentServiceServer()
}
func RegisterTorrentServiceServer(s grpc.ServiceRegistrar, srv TorrentServiceServer) {
// If the following call panics, it indicates UnimplementedTorrentServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&TorrentService_ServiceDesc, srv)
}
func _TorrentService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TorrentServiceServer).List(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: TorrentService_List_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TorrentServiceServer).List(ctx, req.(*ListRequest))
}
return interceptor(ctx, in, info, handler)
}
func _TorrentService_Add_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AddRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TorrentServiceServer).Add(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: TorrentService_Add_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TorrentServiceServer).Add(ctx, req.(*AddRequest))
}
return interceptor(ctx, in, info, handler)
}
// TorrentService_ServiceDesc is the grpc.ServiceDesc for TorrentService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var TorrentService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "music_agregator.torrent.v1.TorrentService",
HandlerType: (*TorrentServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "List",
Handler: _TorrentService_List_Handler,
},
{
MethodName: "Add",
Handler: _TorrentService_Add_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "music_agregator/torrent/v1/torrent.proto",
}
File diff suppressed because it is too large Load Diff
@@ -1,426 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: music_agregator/v1/music_agregator.proto
package v1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
MusicAgregatorService_MonitorAlbum_FullMethodName = "/music_agregator.v1.MusicAgregatorService/MonitorAlbum"
MusicAgregatorService_MonitorAlbumStream_FullMethodName = "/music_agregator.v1.MusicAgregatorService/MonitorAlbumStream"
MusicAgregatorService_GetArtists_FullMethodName = "/music_agregator.v1.MusicAgregatorService/GetArtists"
MusicAgregatorService_GetAlbum_FullMethodName = "/music_agregator.v1.MusicAgregatorService/GetAlbum"
MusicAgregatorService_AnalyzeAlbumRelease_FullMethodName = "/music_agregator.v1.MusicAgregatorService/AnalyzeAlbumRelease"
MusicAgregatorService_SearchArtists_FullMethodName = "/music_agregator.v1.MusicAgregatorService/SearchArtists"
MusicAgregatorService_GetArtistAlbums_FullMethodName = "/music_agregator.v1.MusicAgregatorService/GetArtistAlbums"
MusicAgregatorService_SubscribeEvents_FullMethodName = "/music_agregator.v1.MusicAgregatorService/SubscribeEvents"
MusicAgregatorService_ResyncAlbum_FullMethodName = "/music_agregator.v1.MusicAgregatorService/ResyncAlbum"
)
// MusicAgregatorServiceClient is the client API for MusicAgregatorService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type MusicAgregatorServiceClient interface {
// Deprecated: Do not use.
MonitorAlbum(ctx context.Context, in *MonitorAlbumRequest, opts ...grpc.CallOption) (*MonitorAlbumResponse, error)
MonitorAlbumStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[MonitorAlbumStreamRequest, MonitorAlbumStreamResponse], error)
GetArtists(ctx context.Context, in *GetArtistsRequest, opts ...grpc.CallOption) (*GetArtistsResponse, error)
GetAlbum(ctx context.Context, in *GetAlbumRequest, opts ...grpc.CallOption) (*GetAlbumResponse, error)
AnalyzeAlbumRelease(ctx context.Context, in *AnalyzeAlbumReleaseRequest, opts ...grpc.CallOption) (*AnalyzeAlbumReleaseResponse, error)
SearchArtists(ctx context.Context, in *SearchArtistsRequest, opts ...grpc.CallOption) (*SearchArtistsResponse, error)
GetArtistAlbums(ctx context.Context, in *GetArtistAlbumsRequest, opts ...grpc.CallOption) (*GetArtistAlbumsResponse, error)
SubscribeEvents(ctx context.Context, in *SubscribeEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[AlbumEvent], error)
ResyncAlbum(ctx context.Context, in *ResyncAlbumRequest, opts ...grpc.CallOption) (*ResyncAlbumResponse, error)
}
type musicAgregatorServiceClient struct {
cc grpc.ClientConnInterface
}
func NewMusicAgregatorServiceClient(cc grpc.ClientConnInterface) MusicAgregatorServiceClient {
return &musicAgregatorServiceClient{cc}
}
// Deprecated: Do not use.
func (c *musicAgregatorServiceClient) MonitorAlbum(ctx context.Context, in *MonitorAlbumRequest, opts ...grpc.CallOption) (*MonitorAlbumResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MonitorAlbumResponse)
err := c.cc.Invoke(ctx, MusicAgregatorService_MonitorAlbum_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicAgregatorServiceClient) MonitorAlbumStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[MonitorAlbumStreamRequest, MonitorAlbumStreamResponse], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MusicAgregatorService_ServiceDesc.Streams[0], MusicAgregatorService_MonitorAlbumStream_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[MonitorAlbumStreamRequest, MonitorAlbumStreamResponse]{ClientStream: stream}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicAgregatorService_MonitorAlbumStreamClient = grpc.BidiStreamingClient[MonitorAlbumStreamRequest, MonitorAlbumStreamResponse]
func (c *musicAgregatorServiceClient) GetArtists(ctx context.Context, in *GetArtistsRequest, opts ...grpc.CallOption) (*GetArtistsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetArtistsResponse)
err := c.cc.Invoke(ctx, MusicAgregatorService_GetArtists_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicAgregatorServiceClient) GetAlbum(ctx context.Context, in *GetAlbumRequest, opts ...grpc.CallOption) (*GetAlbumResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetAlbumResponse)
err := c.cc.Invoke(ctx, MusicAgregatorService_GetAlbum_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicAgregatorServiceClient) AnalyzeAlbumRelease(ctx context.Context, in *AnalyzeAlbumReleaseRequest, opts ...grpc.CallOption) (*AnalyzeAlbumReleaseResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AnalyzeAlbumReleaseResponse)
err := c.cc.Invoke(ctx, MusicAgregatorService_AnalyzeAlbumRelease_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicAgregatorServiceClient) SearchArtists(ctx context.Context, in *SearchArtistsRequest, opts ...grpc.CallOption) (*SearchArtistsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SearchArtistsResponse)
err := c.cc.Invoke(ctx, MusicAgregatorService_SearchArtists_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicAgregatorServiceClient) GetArtistAlbums(ctx context.Context, in *GetArtistAlbumsRequest, opts ...grpc.CallOption) (*GetArtistAlbumsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetArtistAlbumsResponse)
err := c.cc.Invoke(ctx, MusicAgregatorService_GetArtistAlbums_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicAgregatorServiceClient) SubscribeEvents(ctx context.Context, in *SubscribeEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[AlbumEvent], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MusicAgregatorService_ServiceDesc.Streams[1], MusicAgregatorService_SubscribeEvents_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[SubscribeEventsRequest, AlbumEvent]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicAgregatorService_SubscribeEventsClient = grpc.ServerStreamingClient[AlbumEvent]
func (c *musicAgregatorServiceClient) ResyncAlbum(ctx context.Context, in *ResyncAlbumRequest, opts ...grpc.CallOption) (*ResyncAlbumResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResyncAlbumResponse)
err := c.cc.Invoke(ctx, MusicAgregatorService_ResyncAlbum_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// MusicAgregatorServiceServer is the server API for MusicAgregatorService service.
// All implementations must embed UnimplementedMusicAgregatorServiceServer
// for forward compatibility.
type MusicAgregatorServiceServer interface {
// Deprecated: Do not use.
MonitorAlbum(context.Context, *MonitorAlbumRequest) (*MonitorAlbumResponse, error)
MonitorAlbumStream(grpc.BidiStreamingServer[MonitorAlbumStreamRequest, MonitorAlbumStreamResponse]) error
GetArtists(context.Context, *GetArtistsRequest) (*GetArtistsResponse, error)
GetAlbum(context.Context, *GetAlbumRequest) (*GetAlbumResponse, error)
AnalyzeAlbumRelease(context.Context, *AnalyzeAlbumReleaseRequest) (*AnalyzeAlbumReleaseResponse, error)
SearchArtists(context.Context, *SearchArtistsRequest) (*SearchArtistsResponse, error)
GetArtistAlbums(context.Context, *GetArtistAlbumsRequest) (*GetArtistAlbumsResponse, error)
SubscribeEvents(*SubscribeEventsRequest, grpc.ServerStreamingServer[AlbumEvent]) error
ResyncAlbum(context.Context, *ResyncAlbumRequest) (*ResyncAlbumResponse, error)
mustEmbedUnimplementedMusicAgregatorServiceServer()
}
// UnimplementedMusicAgregatorServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedMusicAgregatorServiceServer struct{}
func (UnimplementedMusicAgregatorServiceServer) MonitorAlbum(context.Context, *MonitorAlbumRequest) (*MonitorAlbumResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MonitorAlbum not implemented")
}
func (UnimplementedMusicAgregatorServiceServer) MonitorAlbumStream(grpc.BidiStreamingServer[MonitorAlbumStreamRequest, MonitorAlbumStreamResponse]) error {
return status.Error(codes.Unimplemented, "method MonitorAlbumStream not implemented")
}
func (UnimplementedMusicAgregatorServiceServer) GetArtists(context.Context, *GetArtistsRequest) (*GetArtistsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetArtists not implemented")
}
func (UnimplementedMusicAgregatorServiceServer) GetAlbum(context.Context, *GetAlbumRequest) (*GetAlbumResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetAlbum not implemented")
}
func (UnimplementedMusicAgregatorServiceServer) AnalyzeAlbumRelease(context.Context, *AnalyzeAlbumReleaseRequest) (*AnalyzeAlbumReleaseResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AnalyzeAlbumRelease not implemented")
}
func (UnimplementedMusicAgregatorServiceServer) SearchArtists(context.Context, *SearchArtistsRequest) (*SearchArtistsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SearchArtists not implemented")
}
func (UnimplementedMusicAgregatorServiceServer) GetArtistAlbums(context.Context, *GetArtistAlbumsRequest) (*GetArtistAlbumsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetArtistAlbums not implemented")
}
func (UnimplementedMusicAgregatorServiceServer) SubscribeEvents(*SubscribeEventsRequest, grpc.ServerStreamingServer[AlbumEvent]) error {
return status.Error(codes.Unimplemented, "method SubscribeEvents not implemented")
}
func (UnimplementedMusicAgregatorServiceServer) ResyncAlbum(context.Context, *ResyncAlbumRequest) (*ResyncAlbumResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ResyncAlbum not implemented")
}
func (UnimplementedMusicAgregatorServiceServer) mustEmbedUnimplementedMusicAgregatorServiceServer() {}
func (UnimplementedMusicAgregatorServiceServer) testEmbeddedByValue() {}
// UnsafeMusicAgregatorServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to MusicAgregatorServiceServer will
// result in compilation errors.
type UnsafeMusicAgregatorServiceServer interface {
mustEmbedUnimplementedMusicAgregatorServiceServer()
}
func RegisterMusicAgregatorServiceServer(s grpc.ServiceRegistrar, srv MusicAgregatorServiceServer) {
// If the following call panics, it indicates UnimplementedMusicAgregatorServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&MusicAgregatorService_ServiceDesc, srv)
}
func _MusicAgregatorService_MonitorAlbum_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MonitorAlbumRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicAgregatorServiceServer).MonitorAlbum(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicAgregatorService_MonitorAlbum_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicAgregatorServiceServer).MonitorAlbum(ctx, req.(*MonitorAlbumRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicAgregatorService_MonitorAlbumStream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(MusicAgregatorServiceServer).MonitorAlbumStream(&grpc.GenericServerStream[MonitorAlbumStreamRequest, MonitorAlbumStreamResponse]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicAgregatorService_MonitorAlbumStreamServer = grpc.BidiStreamingServer[MonitorAlbumStreamRequest, MonitorAlbumStreamResponse]
func _MusicAgregatorService_GetArtists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetArtistsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicAgregatorServiceServer).GetArtists(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicAgregatorService_GetArtists_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicAgregatorServiceServer).GetArtists(ctx, req.(*GetArtistsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicAgregatorService_GetAlbum_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetAlbumRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicAgregatorServiceServer).GetAlbum(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicAgregatorService_GetAlbum_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicAgregatorServiceServer).GetAlbum(ctx, req.(*GetAlbumRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicAgregatorService_AnalyzeAlbumRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AnalyzeAlbumReleaseRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicAgregatorServiceServer).AnalyzeAlbumRelease(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicAgregatorService_AnalyzeAlbumRelease_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicAgregatorServiceServer).AnalyzeAlbumRelease(ctx, req.(*AnalyzeAlbumReleaseRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicAgregatorService_SearchArtists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SearchArtistsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicAgregatorServiceServer).SearchArtists(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicAgregatorService_SearchArtists_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicAgregatorServiceServer).SearchArtists(ctx, req.(*SearchArtistsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicAgregatorService_GetArtistAlbums_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetArtistAlbumsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicAgregatorServiceServer).GetArtistAlbums(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicAgregatorService_GetArtistAlbums_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicAgregatorServiceServer).GetArtistAlbums(ctx, req.(*GetArtistAlbumsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicAgregatorService_SubscribeEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SubscribeEventsRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MusicAgregatorServiceServer).SubscribeEvents(m, &grpc.GenericServerStream[SubscribeEventsRequest, AlbumEvent]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicAgregatorService_SubscribeEventsServer = grpc.ServerStreamingServer[AlbumEvent]
func _MusicAgregatorService_ResyncAlbum_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ResyncAlbumRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicAgregatorServiceServer).ResyncAlbum(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicAgregatorService_ResyncAlbum_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicAgregatorServiceServer).ResyncAlbum(ctx, req.(*ResyncAlbumRequest))
}
return interceptor(ctx, in, info, handler)
}
// MusicAgregatorService_ServiceDesc is the grpc.ServiceDesc for MusicAgregatorService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var MusicAgregatorService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "music_agregator.v1.MusicAgregatorService",
HandlerType: (*MusicAgregatorServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "MonitorAlbum",
Handler: _MusicAgregatorService_MonitorAlbum_Handler,
},
{
MethodName: "GetArtists",
Handler: _MusicAgregatorService_GetArtists_Handler,
},
{
MethodName: "GetAlbum",
Handler: _MusicAgregatorService_GetAlbum_Handler,
},
{
MethodName: "AnalyzeAlbumRelease",
Handler: _MusicAgregatorService_AnalyzeAlbumRelease_Handler,
},
{
MethodName: "SearchArtists",
Handler: _MusicAgregatorService_SearchArtists_Handler,
},
{
MethodName: "GetArtistAlbums",
Handler: _MusicAgregatorService_GetArtistAlbums_Handler,
},
{
MethodName: "ResyncAlbum",
Handler: _MusicAgregatorService_ResyncAlbum_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "MonitorAlbumStream",
Handler: _MusicAgregatorService_MonitorAlbumStream_Handler,
ServerStreams: true,
ClientStreams: true,
},
{
StreamName: "SubscribeEvents",
Handler: _MusicAgregatorService_SubscribeEvents_Handler,
ServerStreams: true,
},
},
Metadata: "music_agregator/v1/music_agregator.proto",
}
-3274
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-775
View File
@@ -1,775 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: musicfs.proto
package musicfsv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
MusicFS_Search_FullMethodName = "/musicfs.v1.MusicFS/Search"
MusicFS_SearchStream_FullMethodName = "/musicfs.v1.MusicFS/SearchStream"
MusicFS_GetStatus_FullMethodName = "/musicfs.v1.MusicFS/GetStatus"
MusicFS_Shutdown_FullMethodName = "/musicfs.v1.MusicFS/Shutdown"
MusicFS_GetCacheStats_FullMethodName = "/musicfs.v1.MusicFS/GetCacheStats"
MusicFS_ClearCache_FullMethodName = "/musicfs.v1.MusicFS/ClearCache"
MusicFS_Prefetch_FullMethodName = "/musicfs.v1.MusicFS/Prefetch"
MusicFS_ListOrigins_FullMethodName = "/musicfs.v1.MusicFS/ListOrigins"
MusicFS_GetOriginHealth_FullMethodName = "/musicfs.v1.MusicFS/GetOriginHealth"
MusicFS_RescanOrigin_FullMethodName = "/musicfs.v1.MusicFS/RescanOrigin"
MusicFS_SubscribeEvents_FullMethodName = "/musicfs.v1.MusicFS/SubscribeEvents"
)
// MusicFSClient is the client API for MusicFS service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type MusicFSClient interface {
Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error)
SearchStream(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SearchResult], error)
GetStatus(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*StatusResponse, error)
Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*Empty, error)
GetCacheStats(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*CacheStats, error)
ClearCache(ctx context.Context, in *ClearCacheRequest, opts ...grpc.CallOption) (*ClearCacheResponse, error)
Prefetch(ctx context.Context, in *PrefetchRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[PrefetchProgress], error)
ListOrigins(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*OriginsResponse, error)
GetOriginHealth(ctx context.Context, in *OriginRequest, opts ...grpc.CallOption) (*OriginHealthResponse, error)
RescanOrigin(ctx context.Context, in *OriginRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SyncProgress], error)
SubscribeEvents(ctx context.Context, in *EventFilter, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Event], error)
}
type musicFSClient struct {
cc grpc.ClientConnInterface
}
func NewMusicFSClient(cc grpc.ClientConnInterface) MusicFSClient {
return &musicFSClient{cc}
}
func (c *musicFSClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SearchResponse)
err := c.cc.Invoke(ctx, MusicFS_Search_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) SearchStream(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SearchResult], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MusicFS_ServiceDesc.Streams[0], MusicFS_SearchStream_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[SearchRequest, SearchResult]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_SearchStreamClient = grpc.ServerStreamingClient[SearchResult]
func (c *musicFSClient) GetStatus(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*StatusResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StatusResponse)
err := c.cc.Invoke(ctx, MusicFS_GetStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Empty)
err := c.cc.Invoke(ctx, MusicFS_Shutdown_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) GetCacheStats(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*CacheStats, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CacheStats)
err := c.cc.Invoke(ctx, MusicFS_GetCacheStats_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) ClearCache(ctx context.Context, in *ClearCacheRequest, opts ...grpc.CallOption) (*ClearCacheResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ClearCacheResponse)
err := c.cc.Invoke(ctx, MusicFS_ClearCache_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) Prefetch(ctx context.Context, in *PrefetchRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[PrefetchProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MusicFS_ServiceDesc.Streams[1], MusicFS_Prefetch_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[PrefetchRequest, PrefetchProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_PrefetchClient = grpc.ServerStreamingClient[PrefetchProgress]
func (c *musicFSClient) ListOrigins(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*OriginsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(OriginsResponse)
err := c.cc.Invoke(ctx, MusicFS_ListOrigins_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) GetOriginHealth(ctx context.Context, in *OriginRequest, opts ...grpc.CallOption) (*OriginHealthResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(OriginHealthResponse)
err := c.cc.Invoke(ctx, MusicFS_GetOriginHealth_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) RescanOrigin(ctx context.Context, in *OriginRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SyncProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MusicFS_ServiceDesc.Streams[2], MusicFS_RescanOrigin_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[OriginRequest, SyncProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_RescanOriginClient = grpc.ServerStreamingClient[SyncProgress]
func (c *musicFSClient) SubscribeEvents(ctx context.Context, in *EventFilter, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Event], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MusicFS_ServiceDesc.Streams[3], MusicFS_SubscribeEvents_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[EventFilter, Event]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_SubscribeEventsClient = grpc.ServerStreamingClient[Event]
// MusicFSServer is the server API for MusicFS service.
// All implementations must embed UnimplementedMusicFSServer
// for forward compatibility.
type MusicFSServer interface {
Search(context.Context, *SearchRequest) (*SearchResponse, error)
SearchStream(*SearchRequest, grpc.ServerStreamingServer[SearchResult]) error
GetStatus(context.Context, *Empty) (*StatusResponse, error)
Shutdown(context.Context, *ShutdownRequest) (*Empty, error)
GetCacheStats(context.Context, *Empty) (*CacheStats, error)
ClearCache(context.Context, *ClearCacheRequest) (*ClearCacheResponse, error)
Prefetch(*PrefetchRequest, grpc.ServerStreamingServer[PrefetchProgress]) error
ListOrigins(context.Context, *Empty) (*OriginsResponse, error)
GetOriginHealth(context.Context, *OriginRequest) (*OriginHealthResponse, error)
RescanOrigin(*OriginRequest, grpc.ServerStreamingServer[SyncProgress]) error
SubscribeEvents(*EventFilter, grpc.ServerStreamingServer[Event]) error
mustEmbedUnimplementedMusicFSServer()
}
// UnimplementedMusicFSServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedMusicFSServer struct{}
func (UnimplementedMusicFSServer) Search(context.Context, *SearchRequest) (*SearchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Search not implemented")
}
func (UnimplementedMusicFSServer) SearchStream(*SearchRequest, grpc.ServerStreamingServer[SearchResult]) error {
return status.Error(codes.Unimplemented, "method SearchStream not implemented")
}
func (UnimplementedMusicFSServer) GetStatus(context.Context, *Empty) (*StatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetStatus not implemented")
}
func (UnimplementedMusicFSServer) Shutdown(context.Context, *ShutdownRequest) (*Empty, error) {
return nil, status.Error(codes.Unimplemented, "method Shutdown not implemented")
}
func (UnimplementedMusicFSServer) GetCacheStats(context.Context, *Empty) (*CacheStats, error) {
return nil, status.Error(codes.Unimplemented, "method GetCacheStats not implemented")
}
func (UnimplementedMusicFSServer) ClearCache(context.Context, *ClearCacheRequest) (*ClearCacheResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ClearCache not implemented")
}
func (UnimplementedMusicFSServer) Prefetch(*PrefetchRequest, grpc.ServerStreamingServer[PrefetchProgress]) error {
return status.Error(codes.Unimplemented, "method Prefetch not implemented")
}
func (UnimplementedMusicFSServer) ListOrigins(context.Context, *Empty) (*OriginsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListOrigins not implemented")
}
func (UnimplementedMusicFSServer) GetOriginHealth(context.Context, *OriginRequest) (*OriginHealthResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetOriginHealth not implemented")
}
func (UnimplementedMusicFSServer) RescanOrigin(*OriginRequest, grpc.ServerStreamingServer[SyncProgress]) error {
return status.Error(codes.Unimplemented, "method RescanOrigin not implemented")
}
func (UnimplementedMusicFSServer) SubscribeEvents(*EventFilter, grpc.ServerStreamingServer[Event]) error {
return status.Error(codes.Unimplemented, "method SubscribeEvents not implemented")
}
func (UnimplementedMusicFSServer) mustEmbedUnimplementedMusicFSServer() {}
func (UnimplementedMusicFSServer) testEmbeddedByValue() {}
// UnsafeMusicFSServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to MusicFSServer will
// result in compilation errors.
type UnsafeMusicFSServer interface {
mustEmbedUnimplementedMusicFSServer()
}
func RegisterMusicFSServer(s grpc.ServiceRegistrar, srv MusicFSServer) {
// If the following call panics, it indicates UnimplementedMusicFSServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&MusicFS_ServiceDesc, srv)
}
func _MusicFS_Search_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SearchRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).Search(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_Search_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).Search(ctx, req.(*SearchRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_SearchStream_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SearchRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MusicFSServer).SearchStream(m, &grpc.GenericServerStream[SearchRequest, SearchResult]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_SearchStreamServer = grpc.ServerStreamingServer[SearchResult]
func _MusicFS_GetStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).GetStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_GetStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).GetStatus(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_Shutdown_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ShutdownRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).Shutdown(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_Shutdown_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).Shutdown(ctx, req.(*ShutdownRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_GetCacheStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).GetCacheStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_GetCacheStats_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).GetCacheStats(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_ClearCache_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClearCacheRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).ClearCache(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_ClearCache_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).ClearCache(ctx, req.(*ClearCacheRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_Prefetch_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(PrefetchRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MusicFSServer).Prefetch(m, &grpc.GenericServerStream[PrefetchRequest, PrefetchProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_PrefetchServer = grpc.ServerStreamingServer[PrefetchProgress]
func _MusicFS_ListOrigins_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).ListOrigins(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_ListOrigins_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).ListOrigins(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_GetOriginHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(OriginRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).GetOriginHealth(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_GetOriginHealth_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).GetOriginHealth(ctx, req.(*OriginRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_RescanOrigin_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(OriginRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MusicFSServer).RescanOrigin(m, &grpc.GenericServerStream[OriginRequest, SyncProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_RescanOriginServer = grpc.ServerStreamingServer[SyncProgress]
func _MusicFS_SubscribeEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(EventFilter)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MusicFSServer).SubscribeEvents(m, &grpc.GenericServerStream[EventFilter, Event]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_SubscribeEventsServer = grpc.ServerStreamingServer[Event]
// MusicFS_ServiceDesc is the grpc.ServiceDesc for MusicFS service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var MusicFS_ServiceDesc = grpc.ServiceDesc{
ServiceName: "musicfs.v1.MusicFS",
HandlerType: (*MusicFSServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Search",
Handler: _MusicFS_Search_Handler,
},
{
MethodName: "GetStatus",
Handler: _MusicFS_GetStatus_Handler,
},
{
MethodName: "Shutdown",
Handler: _MusicFS_Shutdown_Handler,
},
{
MethodName: "GetCacheStats",
Handler: _MusicFS_GetCacheStats_Handler,
},
{
MethodName: "ClearCache",
Handler: _MusicFS_ClearCache_Handler,
},
{
MethodName: "ListOrigins",
Handler: _MusicFS_ListOrigins_Handler,
},
{
MethodName: "GetOriginHealth",
Handler: _MusicFS_GetOriginHealth_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "SearchStream",
Handler: _MusicFS_SearchStream_Handler,
ServerStreams: true,
},
{
StreamName: "Prefetch",
Handler: _MusicFS_Prefetch_Handler,
ServerStreams: true,
},
{
StreamName: "RescanOrigin",
Handler: _MusicFS_RescanOrigin_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeEvents",
Handler: _MusicFS_SubscribeEvents_Handler,
ServerStreams: true,
},
},
Metadata: "musicfs.proto",
}
const (
MetadataService_GetMetadata_FullMethodName = "/musicfs.v1.MetadataService/GetMetadata"
MetadataService_UpdateMetadata_FullMethodName = "/musicfs.v1.MetadataService/UpdateMetadata"
MetadataService_ClearOverlay_FullMethodName = "/musicfs.v1.MetadataService/ClearOverlay"
MetadataService_BatchUpdateMetadata_FullMethodName = "/musicfs.v1.MetadataService/BatchUpdateMetadata"
MetadataService_ImportMetadata_FullMethodName = "/musicfs.v1.MetadataService/ImportMetadata"
)
// MetadataServiceClient is the client API for MetadataService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type MetadataServiceClient interface {
GetMetadata(ctx context.Context, in *GetMetadataRequest, opts ...grpc.CallOption) (*MetadataResponse, error)
UpdateMetadata(ctx context.Context, in *UpdateMetadataRequest, opts ...grpc.CallOption) (*UpdateMetadataResponse, error)
ClearOverlay(ctx context.Context, in *ClearOverlayRequest, opts ...grpc.CallOption) (*ClearOverlayResponse, error)
BatchUpdateMetadata(ctx context.Context, in *BatchUpdateRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[BatchUpdateProgress], error)
ImportMetadata(ctx context.Context, in *ImportMetadataRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ImportProgress], error)
}
type metadataServiceClient struct {
cc grpc.ClientConnInterface
}
func NewMetadataServiceClient(cc grpc.ClientConnInterface) MetadataServiceClient {
return &metadataServiceClient{cc}
}
func (c *metadataServiceClient) GetMetadata(ctx context.Context, in *GetMetadataRequest, opts ...grpc.CallOption) (*MetadataResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MetadataResponse)
err := c.cc.Invoke(ctx, MetadataService_GetMetadata_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) UpdateMetadata(ctx context.Context, in *UpdateMetadataRequest, opts ...grpc.CallOption) (*UpdateMetadataResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateMetadataResponse)
err := c.cc.Invoke(ctx, MetadataService_UpdateMetadata_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) ClearOverlay(ctx context.Context, in *ClearOverlayRequest, opts ...grpc.CallOption) (*ClearOverlayResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ClearOverlayResponse)
err := c.cc.Invoke(ctx, MetadataService_ClearOverlay_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) BatchUpdateMetadata(ctx context.Context, in *BatchUpdateRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[BatchUpdateProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MetadataService_ServiceDesc.Streams[0], MetadataService_BatchUpdateMetadata_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[BatchUpdateRequest, BatchUpdateProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MetadataService_BatchUpdateMetadataClient = grpc.ServerStreamingClient[BatchUpdateProgress]
func (c *metadataServiceClient) ImportMetadata(ctx context.Context, in *ImportMetadataRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ImportProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MetadataService_ServiceDesc.Streams[1], MetadataService_ImportMetadata_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[ImportMetadataRequest, ImportProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MetadataService_ImportMetadataClient = grpc.ServerStreamingClient[ImportProgress]
// MetadataServiceServer is the server API for MetadataService service.
// All implementations must embed UnimplementedMetadataServiceServer
// for forward compatibility.
type MetadataServiceServer interface {
GetMetadata(context.Context, *GetMetadataRequest) (*MetadataResponse, error)
UpdateMetadata(context.Context, *UpdateMetadataRequest) (*UpdateMetadataResponse, error)
ClearOverlay(context.Context, *ClearOverlayRequest) (*ClearOverlayResponse, error)
BatchUpdateMetadata(*BatchUpdateRequest, grpc.ServerStreamingServer[BatchUpdateProgress]) error
ImportMetadata(*ImportMetadataRequest, grpc.ServerStreamingServer[ImportProgress]) error
mustEmbedUnimplementedMetadataServiceServer()
}
// UnimplementedMetadataServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedMetadataServiceServer struct{}
func (UnimplementedMetadataServiceServer) GetMetadata(context.Context, *GetMetadataRequest) (*MetadataResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetMetadata not implemented")
}
func (UnimplementedMetadataServiceServer) UpdateMetadata(context.Context, *UpdateMetadataRequest) (*UpdateMetadataResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateMetadata not implemented")
}
func (UnimplementedMetadataServiceServer) ClearOverlay(context.Context, *ClearOverlayRequest) (*ClearOverlayResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ClearOverlay not implemented")
}
func (UnimplementedMetadataServiceServer) BatchUpdateMetadata(*BatchUpdateRequest, grpc.ServerStreamingServer[BatchUpdateProgress]) error {
return status.Error(codes.Unimplemented, "method BatchUpdateMetadata not implemented")
}
func (UnimplementedMetadataServiceServer) ImportMetadata(*ImportMetadataRequest, grpc.ServerStreamingServer[ImportProgress]) error {
return status.Error(codes.Unimplemented, "method ImportMetadata not implemented")
}
func (UnimplementedMetadataServiceServer) mustEmbedUnimplementedMetadataServiceServer() {}
func (UnimplementedMetadataServiceServer) testEmbeddedByValue() {}
// UnsafeMetadataServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to MetadataServiceServer will
// result in compilation errors.
type UnsafeMetadataServiceServer interface {
mustEmbedUnimplementedMetadataServiceServer()
}
func RegisterMetadataServiceServer(s grpc.ServiceRegistrar, srv MetadataServiceServer) {
// If the following call panics, it indicates UnimplementedMetadataServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&MetadataService_ServiceDesc, srv)
}
func _MetadataService_GetMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMetadataRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).GetMetadata(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_GetMetadata_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).GetMetadata(ctx, req.(*GetMetadataRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_UpdateMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateMetadataRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).UpdateMetadata(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_UpdateMetadata_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).UpdateMetadata(ctx, req.(*UpdateMetadataRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_ClearOverlay_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClearOverlayRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).ClearOverlay(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_ClearOverlay_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).ClearOverlay(ctx, req.(*ClearOverlayRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_BatchUpdateMetadata_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(BatchUpdateRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MetadataServiceServer).BatchUpdateMetadata(m, &grpc.GenericServerStream[BatchUpdateRequest, BatchUpdateProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MetadataService_BatchUpdateMetadataServer = grpc.ServerStreamingServer[BatchUpdateProgress]
func _MetadataService_ImportMetadata_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(ImportMetadataRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MetadataServiceServer).ImportMetadata(m, &grpc.GenericServerStream[ImportMetadataRequest, ImportProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MetadataService_ImportMetadataServer = grpc.ServerStreamingServer[ImportProgress]
// MetadataService_ServiceDesc is the grpc.ServiceDesc for MetadataService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var MetadataService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "musicfs.v1.MetadataService",
HandlerType: (*MetadataServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetMetadata",
Handler: _MetadataService_GetMetadata_Handler,
},
{
MethodName: "UpdateMetadata",
Handler: _MetadataService_UpdateMetadata_Handler,
},
{
MethodName: "ClearOverlay",
Handler: _MetadataService_ClearOverlay_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "BatchUpdateMetadata",
Handler: _MetadataService_BatchUpdateMetadata_Handler,
ServerStreams: true,
},
{
StreamName: "ImportMetadata",
Handler: _MetadataService_ImportMetadata_Handler,
ServerStreams: true,
},
},
Metadata: "musicfs.proto",
}
-775
View File
@@ -1,775 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: musicfs.proto
package musicfsv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
MusicFS_Search_FullMethodName = "/musicfs.v1.MusicFS/Search"
MusicFS_SearchStream_FullMethodName = "/musicfs.v1.MusicFS/SearchStream"
MusicFS_GetStatus_FullMethodName = "/musicfs.v1.MusicFS/GetStatus"
MusicFS_Shutdown_FullMethodName = "/musicfs.v1.MusicFS/Shutdown"
MusicFS_GetCacheStats_FullMethodName = "/musicfs.v1.MusicFS/GetCacheStats"
MusicFS_ClearCache_FullMethodName = "/musicfs.v1.MusicFS/ClearCache"
MusicFS_Prefetch_FullMethodName = "/musicfs.v1.MusicFS/Prefetch"
MusicFS_ListOrigins_FullMethodName = "/musicfs.v1.MusicFS/ListOrigins"
MusicFS_GetOriginHealth_FullMethodName = "/musicfs.v1.MusicFS/GetOriginHealth"
MusicFS_RescanOrigin_FullMethodName = "/musicfs.v1.MusicFS/RescanOrigin"
MusicFS_SubscribeEvents_FullMethodName = "/musicfs.v1.MusicFS/SubscribeEvents"
)
// MusicFSClient is the client API for MusicFS service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type MusicFSClient interface {
Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error)
SearchStream(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SearchResult], error)
GetStatus(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*StatusResponse, error)
Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*Empty, error)
GetCacheStats(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*CacheStats, error)
ClearCache(ctx context.Context, in *ClearCacheRequest, opts ...grpc.CallOption) (*ClearCacheResponse, error)
Prefetch(ctx context.Context, in *PrefetchRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[PrefetchProgress], error)
ListOrigins(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*OriginsResponse, error)
GetOriginHealth(ctx context.Context, in *OriginRequest, opts ...grpc.CallOption) (*OriginHealthResponse, error)
RescanOrigin(ctx context.Context, in *OriginRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SyncProgress], error)
SubscribeEvents(ctx context.Context, in *EventFilter, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Event], error)
}
type musicFSClient struct {
cc grpc.ClientConnInterface
}
func NewMusicFSClient(cc grpc.ClientConnInterface) MusicFSClient {
return &musicFSClient{cc}
}
func (c *musicFSClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SearchResponse)
err := c.cc.Invoke(ctx, MusicFS_Search_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) SearchStream(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SearchResult], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MusicFS_ServiceDesc.Streams[0], MusicFS_SearchStream_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[SearchRequest, SearchResult]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_SearchStreamClient = grpc.ServerStreamingClient[SearchResult]
func (c *musicFSClient) GetStatus(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*StatusResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StatusResponse)
err := c.cc.Invoke(ctx, MusicFS_GetStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Empty)
err := c.cc.Invoke(ctx, MusicFS_Shutdown_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) GetCacheStats(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*CacheStats, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CacheStats)
err := c.cc.Invoke(ctx, MusicFS_GetCacheStats_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) ClearCache(ctx context.Context, in *ClearCacheRequest, opts ...grpc.CallOption) (*ClearCacheResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ClearCacheResponse)
err := c.cc.Invoke(ctx, MusicFS_ClearCache_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) Prefetch(ctx context.Context, in *PrefetchRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[PrefetchProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MusicFS_ServiceDesc.Streams[1], MusicFS_Prefetch_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[PrefetchRequest, PrefetchProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_PrefetchClient = grpc.ServerStreamingClient[PrefetchProgress]
func (c *musicFSClient) ListOrigins(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*OriginsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(OriginsResponse)
err := c.cc.Invoke(ctx, MusicFS_ListOrigins_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) GetOriginHealth(ctx context.Context, in *OriginRequest, opts ...grpc.CallOption) (*OriginHealthResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(OriginHealthResponse)
err := c.cc.Invoke(ctx, MusicFS_GetOriginHealth_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *musicFSClient) RescanOrigin(ctx context.Context, in *OriginRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SyncProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MusicFS_ServiceDesc.Streams[2], MusicFS_RescanOrigin_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[OriginRequest, SyncProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_RescanOriginClient = grpc.ServerStreamingClient[SyncProgress]
func (c *musicFSClient) SubscribeEvents(ctx context.Context, in *EventFilter, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Event], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MusicFS_ServiceDesc.Streams[3], MusicFS_SubscribeEvents_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[EventFilter, Event]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_SubscribeEventsClient = grpc.ServerStreamingClient[Event]
// MusicFSServer is the server API for MusicFS service.
// All implementations must embed UnimplementedMusicFSServer
// for forward compatibility.
type MusicFSServer interface {
Search(context.Context, *SearchRequest) (*SearchResponse, error)
SearchStream(*SearchRequest, grpc.ServerStreamingServer[SearchResult]) error
GetStatus(context.Context, *Empty) (*StatusResponse, error)
Shutdown(context.Context, *ShutdownRequest) (*Empty, error)
GetCacheStats(context.Context, *Empty) (*CacheStats, error)
ClearCache(context.Context, *ClearCacheRequest) (*ClearCacheResponse, error)
Prefetch(*PrefetchRequest, grpc.ServerStreamingServer[PrefetchProgress]) error
ListOrigins(context.Context, *Empty) (*OriginsResponse, error)
GetOriginHealth(context.Context, *OriginRequest) (*OriginHealthResponse, error)
RescanOrigin(*OriginRequest, grpc.ServerStreamingServer[SyncProgress]) error
SubscribeEvents(*EventFilter, grpc.ServerStreamingServer[Event]) error
mustEmbedUnimplementedMusicFSServer()
}
// UnimplementedMusicFSServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedMusicFSServer struct{}
func (UnimplementedMusicFSServer) Search(context.Context, *SearchRequest) (*SearchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Search not implemented")
}
func (UnimplementedMusicFSServer) SearchStream(*SearchRequest, grpc.ServerStreamingServer[SearchResult]) error {
return status.Error(codes.Unimplemented, "method SearchStream not implemented")
}
func (UnimplementedMusicFSServer) GetStatus(context.Context, *Empty) (*StatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetStatus not implemented")
}
func (UnimplementedMusicFSServer) Shutdown(context.Context, *ShutdownRequest) (*Empty, error) {
return nil, status.Error(codes.Unimplemented, "method Shutdown not implemented")
}
func (UnimplementedMusicFSServer) GetCacheStats(context.Context, *Empty) (*CacheStats, error) {
return nil, status.Error(codes.Unimplemented, "method GetCacheStats not implemented")
}
func (UnimplementedMusicFSServer) ClearCache(context.Context, *ClearCacheRequest) (*ClearCacheResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ClearCache not implemented")
}
func (UnimplementedMusicFSServer) Prefetch(*PrefetchRequest, grpc.ServerStreamingServer[PrefetchProgress]) error {
return status.Error(codes.Unimplemented, "method Prefetch not implemented")
}
func (UnimplementedMusicFSServer) ListOrigins(context.Context, *Empty) (*OriginsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListOrigins not implemented")
}
func (UnimplementedMusicFSServer) GetOriginHealth(context.Context, *OriginRequest) (*OriginHealthResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetOriginHealth not implemented")
}
func (UnimplementedMusicFSServer) RescanOrigin(*OriginRequest, grpc.ServerStreamingServer[SyncProgress]) error {
return status.Error(codes.Unimplemented, "method RescanOrigin not implemented")
}
func (UnimplementedMusicFSServer) SubscribeEvents(*EventFilter, grpc.ServerStreamingServer[Event]) error {
return status.Error(codes.Unimplemented, "method SubscribeEvents not implemented")
}
func (UnimplementedMusicFSServer) mustEmbedUnimplementedMusicFSServer() {}
func (UnimplementedMusicFSServer) testEmbeddedByValue() {}
// UnsafeMusicFSServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to MusicFSServer will
// result in compilation errors.
type UnsafeMusicFSServer interface {
mustEmbedUnimplementedMusicFSServer()
}
func RegisterMusicFSServer(s grpc.ServiceRegistrar, srv MusicFSServer) {
// If the following call panics, it indicates UnimplementedMusicFSServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&MusicFS_ServiceDesc, srv)
}
func _MusicFS_Search_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SearchRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).Search(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_Search_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).Search(ctx, req.(*SearchRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_SearchStream_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SearchRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MusicFSServer).SearchStream(m, &grpc.GenericServerStream[SearchRequest, SearchResult]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_SearchStreamServer = grpc.ServerStreamingServer[SearchResult]
func _MusicFS_GetStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).GetStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_GetStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).GetStatus(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_Shutdown_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ShutdownRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).Shutdown(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_Shutdown_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).Shutdown(ctx, req.(*ShutdownRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_GetCacheStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).GetCacheStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_GetCacheStats_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).GetCacheStats(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_ClearCache_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClearCacheRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).ClearCache(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_ClearCache_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).ClearCache(ctx, req.(*ClearCacheRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_Prefetch_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(PrefetchRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MusicFSServer).Prefetch(m, &grpc.GenericServerStream[PrefetchRequest, PrefetchProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_PrefetchServer = grpc.ServerStreamingServer[PrefetchProgress]
func _MusicFS_ListOrigins_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).ListOrigins(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_ListOrigins_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).ListOrigins(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_GetOriginHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(OriginRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MusicFSServer).GetOriginHealth(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MusicFS_GetOriginHealth_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MusicFSServer).GetOriginHealth(ctx, req.(*OriginRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MusicFS_RescanOrigin_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(OriginRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MusicFSServer).RescanOrigin(m, &grpc.GenericServerStream[OriginRequest, SyncProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_RescanOriginServer = grpc.ServerStreamingServer[SyncProgress]
func _MusicFS_SubscribeEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(EventFilter)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MusicFSServer).SubscribeEvents(m, &grpc.GenericServerStream[EventFilter, Event]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MusicFS_SubscribeEventsServer = grpc.ServerStreamingServer[Event]
// MusicFS_ServiceDesc is the grpc.ServiceDesc for MusicFS service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var MusicFS_ServiceDesc = grpc.ServiceDesc{
ServiceName: "musicfs.v1.MusicFS",
HandlerType: (*MusicFSServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Search",
Handler: _MusicFS_Search_Handler,
},
{
MethodName: "GetStatus",
Handler: _MusicFS_GetStatus_Handler,
},
{
MethodName: "Shutdown",
Handler: _MusicFS_Shutdown_Handler,
},
{
MethodName: "GetCacheStats",
Handler: _MusicFS_GetCacheStats_Handler,
},
{
MethodName: "ClearCache",
Handler: _MusicFS_ClearCache_Handler,
},
{
MethodName: "ListOrigins",
Handler: _MusicFS_ListOrigins_Handler,
},
{
MethodName: "GetOriginHealth",
Handler: _MusicFS_GetOriginHealth_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "SearchStream",
Handler: _MusicFS_SearchStream_Handler,
ServerStreams: true,
},
{
StreamName: "Prefetch",
Handler: _MusicFS_Prefetch_Handler,
ServerStreams: true,
},
{
StreamName: "RescanOrigin",
Handler: _MusicFS_RescanOrigin_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeEvents",
Handler: _MusicFS_SubscribeEvents_Handler,
ServerStreams: true,
},
},
Metadata: "musicfs.proto",
}
const (
MetadataService_GetMetadata_FullMethodName = "/musicfs.v1.MetadataService/GetMetadata"
MetadataService_UpdateMetadata_FullMethodName = "/musicfs.v1.MetadataService/UpdateMetadata"
MetadataService_ClearOverlay_FullMethodName = "/musicfs.v1.MetadataService/ClearOverlay"
MetadataService_BatchUpdateMetadata_FullMethodName = "/musicfs.v1.MetadataService/BatchUpdateMetadata"
MetadataService_ImportMetadata_FullMethodName = "/musicfs.v1.MetadataService/ImportMetadata"
)
// MetadataServiceClient is the client API for MetadataService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type MetadataServiceClient interface {
GetMetadata(ctx context.Context, in *GetMetadataRequest, opts ...grpc.CallOption) (*MetadataResponse, error)
UpdateMetadata(ctx context.Context, in *UpdateMetadataRequest, opts ...grpc.CallOption) (*UpdateMetadataResponse, error)
ClearOverlay(ctx context.Context, in *ClearOverlayRequest, opts ...grpc.CallOption) (*ClearOverlayResponse, error)
BatchUpdateMetadata(ctx context.Context, in *BatchUpdateRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[BatchUpdateProgress], error)
ImportMetadata(ctx context.Context, in *ImportMetadataRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ImportProgress], error)
}
type metadataServiceClient struct {
cc grpc.ClientConnInterface
}
func NewMetadataServiceClient(cc grpc.ClientConnInterface) MetadataServiceClient {
return &metadataServiceClient{cc}
}
func (c *metadataServiceClient) GetMetadata(ctx context.Context, in *GetMetadataRequest, opts ...grpc.CallOption) (*MetadataResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MetadataResponse)
err := c.cc.Invoke(ctx, MetadataService_GetMetadata_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) UpdateMetadata(ctx context.Context, in *UpdateMetadataRequest, opts ...grpc.CallOption) (*UpdateMetadataResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateMetadataResponse)
err := c.cc.Invoke(ctx, MetadataService_UpdateMetadata_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) ClearOverlay(ctx context.Context, in *ClearOverlayRequest, opts ...grpc.CallOption) (*ClearOverlayResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ClearOverlayResponse)
err := c.cc.Invoke(ctx, MetadataService_ClearOverlay_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *metadataServiceClient) BatchUpdateMetadata(ctx context.Context, in *BatchUpdateRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[BatchUpdateProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MetadataService_ServiceDesc.Streams[0], MetadataService_BatchUpdateMetadata_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[BatchUpdateRequest, BatchUpdateProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MetadataService_BatchUpdateMetadataClient = grpc.ServerStreamingClient[BatchUpdateProgress]
func (c *metadataServiceClient) ImportMetadata(ctx context.Context, in *ImportMetadataRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ImportProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MetadataService_ServiceDesc.Streams[1], MetadataService_ImportMetadata_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[ImportMetadataRequest, ImportProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MetadataService_ImportMetadataClient = grpc.ServerStreamingClient[ImportProgress]
// MetadataServiceServer is the server API for MetadataService service.
// All implementations must embed UnimplementedMetadataServiceServer
// for forward compatibility.
type MetadataServiceServer interface {
GetMetadata(context.Context, *GetMetadataRequest) (*MetadataResponse, error)
UpdateMetadata(context.Context, *UpdateMetadataRequest) (*UpdateMetadataResponse, error)
ClearOverlay(context.Context, *ClearOverlayRequest) (*ClearOverlayResponse, error)
BatchUpdateMetadata(*BatchUpdateRequest, grpc.ServerStreamingServer[BatchUpdateProgress]) error
ImportMetadata(*ImportMetadataRequest, grpc.ServerStreamingServer[ImportProgress]) error
mustEmbedUnimplementedMetadataServiceServer()
}
// UnimplementedMetadataServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedMetadataServiceServer struct{}
func (UnimplementedMetadataServiceServer) GetMetadata(context.Context, *GetMetadataRequest) (*MetadataResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetMetadata not implemented")
}
func (UnimplementedMetadataServiceServer) UpdateMetadata(context.Context, *UpdateMetadataRequest) (*UpdateMetadataResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateMetadata not implemented")
}
func (UnimplementedMetadataServiceServer) ClearOverlay(context.Context, *ClearOverlayRequest) (*ClearOverlayResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ClearOverlay not implemented")
}
func (UnimplementedMetadataServiceServer) BatchUpdateMetadata(*BatchUpdateRequest, grpc.ServerStreamingServer[BatchUpdateProgress]) error {
return status.Error(codes.Unimplemented, "method BatchUpdateMetadata not implemented")
}
func (UnimplementedMetadataServiceServer) ImportMetadata(*ImportMetadataRequest, grpc.ServerStreamingServer[ImportProgress]) error {
return status.Error(codes.Unimplemented, "method ImportMetadata not implemented")
}
func (UnimplementedMetadataServiceServer) mustEmbedUnimplementedMetadataServiceServer() {}
func (UnimplementedMetadataServiceServer) testEmbeddedByValue() {}
// UnsafeMetadataServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to MetadataServiceServer will
// result in compilation errors.
type UnsafeMetadataServiceServer interface {
mustEmbedUnimplementedMetadataServiceServer()
}
func RegisterMetadataServiceServer(s grpc.ServiceRegistrar, srv MetadataServiceServer) {
// If the following call panics, it indicates UnimplementedMetadataServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&MetadataService_ServiceDesc, srv)
}
func _MetadataService_GetMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMetadataRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).GetMetadata(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_GetMetadata_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).GetMetadata(ctx, req.(*GetMetadataRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_UpdateMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateMetadataRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).UpdateMetadata(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_UpdateMetadata_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).UpdateMetadata(ctx, req.(*UpdateMetadataRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_ClearOverlay_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClearOverlayRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MetadataServiceServer).ClearOverlay(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MetadataService_ClearOverlay_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MetadataServiceServer).ClearOverlay(ctx, req.(*ClearOverlayRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MetadataService_BatchUpdateMetadata_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(BatchUpdateRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MetadataServiceServer).BatchUpdateMetadata(m, &grpc.GenericServerStream[BatchUpdateRequest, BatchUpdateProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MetadataService_BatchUpdateMetadataServer = grpc.ServerStreamingServer[BatchUpdateProgress]
func _MetadataService_ImportMetadata_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(ImportMetadataRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MetadataServiceServer).ImportMetadata(m, &grpc.GenericServerStream[ImportMetadataRequest, ImportProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MetadataService_ImportMetadataServer = grpc.ServerStreamingServer[ImportProgress]
// MetadataService_ServiceDesc is the grpc.ServiceDesc for MetadataService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var MetadataService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "musicfs.v1.MetadataService",
HandlerType: (*MetadataServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetMetadata",
Handler: _MetadataService_GetMetadata_Handler,
},
{
MethodName: "UpdateMetadata",
Handler: _MetadataService_UpdateMetadata_Handler,
},
{
MethodName: "ClearOverlay",
Handler: _MetadataService_ClearOverlay_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "BatchUpdateMetadata",
Handler: _MetadataService_BatchUpdateMetadata_Handler,
ServerStreams: true,
},
{
StreamName: "ImportMetadata",
Handler: _MetadataService_ImportMetadata_Handler,
ServerStreams: true,
},
},
Metadata: "musicfs.proto",
}
-174
View File
@@ -1,174 +0,0 @@
module homelab.lan/music-agregator
go 1.26.2
require (
github.com/anacrolix/torrent v1.61.0
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3
github.com/jackc/pgx/v5 v5.9.2
github.com/mewkiz/flac v1.0.13
github.com/prometheus/client_golang v1.23.2
github.com/riverqueue/river v0.35.1
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.35.1
github.com/rs/zerolog v1.35.1
github.com/stretchr/testify v1.11.1
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300
github.com/testcontainers/testcontainers-go v0.42.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0
)
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/RoaringBitmap/roaring v1.2.3 // indirect
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
github.com/anacrolix/btree v0.0.0-20251201064447-d86c3fa41bd8 // indirect
github.com/anacrolix/chansync v0.7.0 // indirect
github.com/anacrolix/dht/v2 v2.23.0 // indirect
github.com/anacrolix/envpprof v1.4.0 // indirect
github.com/anacrolix/generics v0.1.1-0.20251125230353-15d98d46693b // indirect
github.com/anacrolix/go-libutp v1.3.2 // indirect
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb // indirect
github.com/anacrolix/missinggo v1.3.0 // indirect
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
github.com/anacrolix/missinggo/v2 v2.10.0 // indirect
github.com/anacrolix/mmsg v1.0.1 // indirect
github.com/anacrolix/multiless v0.4.0 // indirect
github.com/anacrolix/stm v0.5.0 // indirect
github.com/anacrolix/sync v0.5.5-0.20251119100342-d78dd1f686f1 // indirect
github.com/anacrolix/upnp v0.1.4 // indirect
github.com/anacrolix/utp v0.1.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.2.2 // indirect
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.3.2+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect
github.com/go-llsqlite/crawshaw v0.5.6-0.20250312230104-194977a03421 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/icza/bitio v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d // indirect
github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/moby/api v1.54.1 // indirect
github.com/moby/moby/client v0.4.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-varint v0.0.6 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pion/datachannel v1.5.9 // indirect
github.com/pion/dtls/v3 v3.0.3 // indirect
github.com/pion/ice/v4 v4.0.2 // indirect
github.com/pion/interceptor v0.1.40 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.18 // indirect
github.com/pion/sctp v1.8.33 // indirect
github.com/pion/sdp/v3 v3.0.9 // indirect
github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pion/webrtc/v4 v4.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/protolambda/ctxlock v0.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/riverqueue/river/riverdriver v0.35.1 // indirect
github.com/riverqueue/river/rivershared v0.35.1 // indirect
github.com/riverqueue/river/rivertype v0.35.1 // indirect
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tidwall/btree v1.8.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/wlynxg/anet v0.0.3 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.uber.org/goleak v1.3.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.1.6 // indirect
modernc.org/libc v1.22.3 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.21.1 // indirect
zombiezen.com/go/sqlite v0.13.1 // indirect
)
require (
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.36.0 // indirect
google.golang.org/grpc v1.81.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v2 v2.4.0
)
-710
View File
@@ -1,710 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o=
github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anacrolix/btree v0.0.0-20251201064447-d86c3fa41bd8 h1:c02PsmoaChabVqAFm7pqPI1UIkDdDAjUaWa6ZmfxybQ=
github.com/anacrolix/btree v0.0.0-20251201064447-d86c3fa41bd8/go.mod h1:7stWJ39LeusmMI8mjJuhFNRqep//vx0AsaySRoK9or0=
github.com/anacrolix/chansync v0.7.0 h1:wgwxbsJRmOqNjil4INpxHrDp4rlqQhECxR8/WBP4Et0=
github.com/anacrolix/chansync v0.7.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
github.com/anacrolix/dht/v2 v2.23.0 h1:EuD17ykTTEkAMPLjBsS5QjGOwuBgLTdQhds6zPAjeVY=
github.com/anacrolix/dht/v2 v2.23.0/go.mod h1:seXRz6HLw8zEnxlysf9ye2eQbrKUmch6PyOHpe/Nb/U=
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
github.com/anacrolix/envpprof v1.4.0 h1:QHeIcrgHcRChhnxR8l6rlaLlRQx9zd7Q2NII6Zbt83w=
github.com/anacrolix/envpprof v1.4.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
github.com/anacrolix/generics v0.1.1-0.20251125230353-15d98d46693b h1:Kuvx/A/TTJuT9x8mn7DeGx2KW9tWn1LI8bira67xdT0=
github.com/anacrolix/generics v0.1.1-0.20251125230353-15d98d46693b/go.mod h1:NGehhfeXJPBujPx0s6cstSj8B+TERsTY32Xckfx5ftc=
github.com/anacrolix/go-libutp v1.3.2 h1:WswiaxTIogchbkzNgGHuHRfbrYLpv4o290mlvcx+++M=
github.com/anacrolix/go-libutp v1.3.2/go.mod h1:fCUiEnXJSe3jsPG554A200Qv+45ZzIIyGEvE56SHmyA=
github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb h1:nGNLCQbxFQZz7/9PXLGQ9GmavI/W+eX66pSwVeUwugU=
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb/go.mod h1:YjBZbwe2v3RsU7WdoBlVSPVpfKuOAno9SRQ/8tIl+hk=
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
github.com/anacrolix/lsan v0.1.0 h1:TbgB8fdVXgBwrNsJGHtht9+9FepNFu5H7dU8ek6XYAY=
github.com/anacrolix/lsan v0.1.0/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo=
github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo=
github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y=
github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw=
github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc=
github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy6BqESAJVw=
github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ=
github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
github.com/anacrolix/missinggo/v2 v2.10.0 h1:pg0iO4Z/UhP2MAnmGcaMtp5ZP9kyWsusENWN9aolrkY=
github.com/anacrolix/missinggo/v2 v2.10.0/go.mod h1:nCRMW6bRCMOVcw5z9BnSYKF+kDbtenx+hQuphf4bK8Y=
github.com/anacrolix/mmsg v1.0.1 h1:TxfpV7kX70m3f/O7ielL/2I3OFkMPjrRCPo7+4X5AWw=
github.com/anacrolix/mmsg v1.0.1/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
github.com/anacrolix/multiless v0.4.0 h1:lqSszHkliMsZd2hsyrDvHOw4AbYWa+ijQ66LzbjqWjM=
github.com/anacrolix/multiless v0.4.0/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
github.com/anacrolix/stm v0.5.0 h1:9df1KBpttF0TzLgDq51Z+TEabZKMythqgx89f1FQJt8=
github.com/anacrolix/stm v0.5.0/go.mod h1:MOwrSy+jCm8Y7HYfMAwPj7qWVu7XoVvjOiYwJmpeB/M=
github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
github.com/anacrolix/sync v0.5.5-0.20251119100342-d78dd1f686f1 h1:oLCfNgEOR3/Z98mSwmwTM1pcqCDb/1zIjxCNn7dzVaE=
github.com/anacrolix/sync v0.5.5-0.20251119100342-d78dd1f686f1/go.mod h1:21cUWerw9eiu/3T3kyoChu37AVO+YFue1/H15qqubS0=
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
github.com/anacrolix/torrent v1.61.0 h1:vxo+B4SwnoP5AQWbhvnTYIaTgPSX+llYUVuQVsN4Jg8=
github.com/anacrolix/torrent v1.61.0/go.mod h1:yKUKuZSSDdyOsCbuH+rDOpswl/g546gICapdrU7aUmQ=
github.com/anacrolix/upnp v0.1.4 h1:+2t2KA6QOhm/49zeNyeVwDu1ZYS9dB9wfxyVvh/wk7U=
github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d h1:2qVb9bsAMtmAfnxXltm+6eBzrrS7SZ52c3SedsulaMI=
github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk=
github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA=
github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 h1:OyQmpAN302wAopDgwVjgs2HkFawP9ahIEqkUYz7V7CA=
github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU=
github.com/go-llsqlite/crawshaw v0.5.6-0.20250312230104-194977a03421 h1:GClwZI0at7xwV0TpgUMTYr/DoTE7TJZ/tc29LcPcs7o=
github.com/go-llsqlite/crawshaw v0.5.6-0.20250312230104-194977a03421/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0=
github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
github.com/mewkiz/flac v1.0.13 h1:6wF8rRQKBFW159Daqx6Ro7K5ZnlVhHUKfS5aTsC4oXs=
github.com/mewkiz/flac v1.0.13/go.mod h1:HfPYDA+oxjyuqMu2V+cyKcxF51KM6incpw5eZXmfA6k=
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d h1:IL2tii4jXLdhCeQN69HNzYYW1kl0meSG0wt5+sLwszU=
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d/go.mod h1:SIpumAnUWSy0q9RzKD3pyH3g1t5vdawUAPcW5tQrUtI=
github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985 h1:h8O1byDZ1uk6RUXMhj1QJU3VXFKXHDZxr4TXRPGeBa8=
github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985/go.mod h1:uiPmbdUbdt1NkGApKl7htQjZ8S7XaGUAVulJUJ9v6q4=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM=
github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU=
github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s=
github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg=
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8=
github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/riverqueue/river v0.35.1 h1:TK1LLGRdTWL7ARPbIUB+TqMnTYJ0GiCoy5Q/yEf5yBE=
github.com/riverqueue/river v0.35.1/go.mod h1:jDt0LimObI+5e6FVy7LyuIWfHftmV0wARmiK7W+9D64=
github.com/riverqueue/river/riverdriver v0.35.1 h1:zJx8SaQdMP7zVEfd8SDoe8KjVHCXoXoFfzt6v+SJtQg=
github.com/riverqueue/river/riverdriver v0.35.1/go.mod h1:Y+rQzz0uvh+pQI+mzJh3qgAGGNxestOWgjKa7mob87w=
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.35.1 h1:GL+ztwpXgIqBin/3wNzq8h1/H8befxl61/DlLvVCAAY=
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.35.1/go.mod h1:5Llh5ONCFsW67dLm5+OelSWTKhliQ989JLbVMwyuN2U=
github.com/riverqueue/river/rivershared v0.35.1 h1:XEHf7yj35p5Os5r6K08q9BVaAKsvWhP9hfxEr+MwXqg=
github.com/riverqueue/river/rivershared v0.35.1/go.mod h1:YqVk7bZoojLsx58kyQ6ZU2FHP91HP4whVj6MTCtih/c=
github.com/riverqueue/river/rivertype v0.35.1 h1:7SfjZ3Hkr7gRjItMHAUzJBAHIqx41yS/4yjVPQVtNfM=
github.com/riverqueue/river/rivertype v0.35.1/go.mod h1:D1Ad+EaZiaXbQbJcJcfeicXJMBKno0n6UcfKI5Q7DIQ=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vAjVY4qbSr8rQ610YzTkWcxzxSI=
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI=
github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo=
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs=
github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c=
lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
zombiezen.com/go/sqlite v0.13.1 h1:qDzxyWWmMtSSEH5qxamqBFmqA2BLSSbtODi3ojaE02o=
zombiezen.com/go/sqlite v0.13.1/go.mod h1:Ht/5Rg3Ae2hoyh1I7gbWtWAl89CNocfqeb/aAMTkJr4=
-285
View File
@@ -1,285 +0,0 @@
package analysis
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/rs/zerolog/log"
"homelab.lan/music-agregator/internal/audio"
"homelab.lan/music-agregator/internal/database"
)
var audioExtensions = map[string]bool{
".flac": true, ".mp3": true, ".aac": true, ".m4a": true,
".ape": true, ".wv": true, ".ogg": true, ".wav": true, ".alac": true,
}
var trackNumberRegex = regexp.MustCompile(`^(\d+)[\s\-._]+`)
type ReleaseAnalyzer struct {
downloads *database.DownloadRepository
downloadFiles *database.DownloadFileRepository
albumReleases *database.AlbumReleaseRepository
trackReleases *database.TrackReleaseRepository
}
func NewReleaseAnalyzer(db *database.DB) *ReleaseAnalyzer {
return &ReleaseAnalyzer{
downloads: database.NewDownloadRepository(db.Pool),
downloadFiles: database.NewDownloadFileRepository(db.Pool),
albumReleases: database.NewAlbumReleaseRepository(db.Pool),
trackReleases: database.NewTrackReleaseRepository(db.Pool),
}
}
func (a *ReleaseAnalyzer) Analyze(ctx context.Context, downloadID string, contentPath string) (*database.AlbumRelease, []*database.TrackRelease, error) {
download, err := a.downloads.GetByID(ctx, downloadID)
if err != nil {
return nil, nil, fmt.Errorf("getting download: %w", err)
}
files, err := a.downloadFiles.GetByDownloadID(ctx, downloadID)
if err != nil || len(files) == 0 {
log.Info().Str("download_id", downloadID).Msg("no download files in DB, scanning filesystem")
scanned, scanErr := ScanAndHashFiles(contentPath)
if scanErr != nil {
return nil, nil, fmt.Errorf("scanning files: %w", scanErr)
}
for _, f := range scanned {
f.DownloadID = downloadID
}
if err := a.downloadFiles.CreateBatch(ctx, scanned); err != nil {
return nil, nil, fmt.Errorf("persisting scanned files: %w", err)
}
files = scanned
}
var audioFiles []*database.DownloadFile
var hasCoverArt, hasCueSheet, hasRipLog bool
for _, f := range files {
if audioExtensions["."+f.FileType] {
audioFiles = append(audioFiles, f)
}
switch f.FileType {
case "jpg", "jpeg", "png", "gif", "webp":
hasCoverArt = true
case "cue":
hasCueSheet = true
case "log":
hasRipLog = true
}
}
if len(audioFiles) == 0 {
return nil, nil, fmt.Errorf("no audio files found for download %s", downloadID)
}
var trackReleases []*database.TrackRelease
var totalSize int64
var totalDuration int
formatCounts := make(map[string]int)
var firstBitDepth, firstSampleRate, firstChannels int
var firstIsLossless bool
for i, f := range audioFiles {
fullPath := filepath.Join(contentPath, f.FilePath)
info, err := audio.Analyze(fullPath)
if err != nil {
log.Warn().Err(err).Str("path", f.FilePath).Msg("failed to analyze audio file")
info = &audio.TrackInfo{
Format: strings.ToUpper(f.FileType),
}
}
if i == 0 {
firstBitDepth = info.BitDepth
firstSampleRate = info.SampleRate
firstChannels = info.Channels
firstIsLossless = info.IsLossless
}
formatCounts[info.Format]++
totalSize += f.FileSize
totalDuration += info.DurationMs
trackNum := extractTrackNumber(f.FilePath)
title := extractTitle(f.FilePath)
tr := &database.TrackRelease{
Title: title,
TrackNumber: trackNum,
DiscNumber: 1,
Format: info.Format,
Channels: info.Channels,
FileSize: f.FileSize,
FilePath: f.FilePath,
}
if info.DurationMs > 0 {
dur := info.DurationMs
tr.DurationMs = &dur
}
if info.BitDepth > 0 {
bd := info.BitDepth
tr.BitDepth = &bd
}
if info.SampleRate > 0 {
sr := info.SampleRate
tr.SampleRate = &sr
}
if info.BitrateKbps > 0 {
br := info.BitrateKbps
tr.BitrateKbps = &br
}
trackReleases = append(trackReleases, tr)
}
dominantFormat := ""
maxCount := 0
for format, count := range formatCounts {
if count > maxCount {
dominantFormat = format
maxCount = count
}
}
var source *string
if hasRipLog {
s := "CD"
source = &s
}
release := &database.AlbumRelease{
AlbumID: download.AlbumID,
DownloadID: downloadID,
Format: dominantFormat,
Channels: firstChannels,
IsLossless: firstIsLossless,
Source: source,
TotalSize: totalSize,
TotalDurationMs: totalDuration,
TrackCount: len(audioFiles),
HasCoverArt: hasCoverArt,
HasCueSheet: hasCueSheet,
HasRipLog: hasRipLog,
Path: contentPath,
}
if firstBitDepth > 0 {
release.BitDepth = &firstBitDepth
}
if firstSampleRate > 0 {
release.SampleRate = &firstSampleRate
}
return release, trackReleases, nil
}
func (a *ReleaseAnalyzer) AnalyzeAndPersist(ctx context.Context, downloadID string, contentPath string) (*database.AlbumRelease, []*database.TrackRelease, error) {
release, trackReleases, err := a.Analyze(ctx, downloadID, contentPath)
if err != nil {
return nil, nil, err
}
if err := a.albumReleases.DeleteByDownloadID(ctx, downloadID); err != nil {
log.Warn().Err(err).Str("download_id", downloadID).Msg("failed to delete existing album release")
}
if err := a.albumReleases.Create(ctx, release); err != nil {
return nil, nil, fmt.Errorf("creating album release: %w", err)
}
for _, tr := range trackReleases {
tr.AlbumReleaseID = release.ID
}
if err := a.trackReleases.CreateBatch(ctx, trackReleases); err != nil {
return nil, nil, fmt.Errorf("creating track releases: %w", err)
}
return release, trackReleases, nil
}
func extractTrackNumber(filePath string) int {
base := filepath.Base(filePath)
matches := trackNumberRegex.FindStringSubmatch(base)
if len(matches) >= 2 {
var num int
fmt.Sscanf(matches[1], "%d", &num)
return num
}
return 0
}
func extractTitle(filePath string) string {
base := filepath.Base(filePath)
ext := filepath.Ext(base)
name := strings.TrimSuffix(base, ext)
name = trackNumberRegex.ReplaceAllString(name, "")
return strings.TrimSpace(name)
}
func IsAudioExtension(ext string) bool {
return audioExtensions[ext]
}
func ScanAndHashFiles(rootPath string) ([]*database.DownloadFile, error) {
var files []*database.DownloadFile
err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
ext := strings.ToLower(filepath.Ext(path))
relPath, _ := filepath.Rel(rootPath, path)
fileType := strings.TrimPrefix(ext, ".")
if fileType == "" {
return nil
}
df := &database.DownloadFile{
FilePath: relPath,
FileSize: info.Size(),
FileType: fileType,
}
if IsAudioExtension(ext) || ext == ".cue" || ext == ".log" {
hash, err := hashFile(path)
if err != nil {
log.Warn().Err(err).Str("path", path).Msg("failed to hash file")
} else {
df.SHA256Hash = hash
now := time.Now()
df.VerifiedAt = &now
}
}
files = append(files, df)
return nil
})
return files, err
}
func hashFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("opening file: %w", err)
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", fmt.Errorf("hashing file: %w", err)
}
return hex.EncodeToString(h.Sum(nil)), nil
}
-35
View File
@@ -1,35 +0,0 @@
package audio
import (
"fmt"
"path/filepath"
"strings"
)
type TrackInfo struct {
Format string
BitDepth int
SampleRate int
Channels int
DurationMs int
BitrateKbps int
IsLossless bool
}
func Analyze(filePath string) (*TrackInfo, error) {
ext := strings.ToLower(filepath.Ext(filePath))
switch ext {
case ".flac":
return analyzeFLAC(filePath)
case ".mp3":
return analyzeMP3(filePath)
case ".aac", ".m4a", ".ogg", ".wav", ".ape", ".wv", ".alac":
return &TrackInfo{
Format: strings.ToUpper(strings.TrimPrefix(ext, ".")),
IsLossless: ext == ".wav" || ext == ".ape" || ext == ".wv" || ext == ".alac",
}, nil
default:
return nil, fmt.Errorf("unsupported audio format: %s", ext)
}
}
-27
View File
@@ -1,27 +0,0 @@
package audio
import (
"fmt"
"github.com/mewkiz/flac"
)
func analyzeFLAC(filePath string) (*TrackInfo, error) {
stream, err := flac.ParseFile(filePath)
if err != nil {
return nil, fmt.Errorf("parsing FLAC: %w", err)
}
defer stream.Close()
info := stream.Info
durationMs := int(info.NSamples * 1000 / uint64(info.SampleRate))
return &TrackInfo{
Format: "FLAC",
BitDepth: int(info.BitsPerSample),
SampleRate: int(info.SampleRate),
Channels: int(info.NChannels),
DurationMs: durationMs,
IsLossless: true,
}, nil
}
-54
View File
@@ -1,54 +0,0 @@
package audio
import (
"fmt"
"os"
"time"
"github.com/tcolgate/mp3"
)
func analyzeMP3(filePath string) (*TrackInfo, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("opening MP3: %w", err)
}
defer f.Close()
decoder := mp3.NewDecoder(f)
var frame mp3.Frame
var skipped int
var totalDuration time.Duration
var sampleRate, channels, bitrate int
var frameCount int
for {
err := decoder.Decode(&frame, &skipped)
if err != nil {
break
}
if frameCount == 0 {
sampleRate = int(frame.Header().SampleRate())
channels = channelCount(frame.Header().ChannelMode())
bitrate = int(frame.Header().BitRate()) / 1000
}
totalDuration += frame.Duration()
frameCount++
}
return &TrackInfo{
Format: "MP3",
SampleRate: sampleRate,
Channels: channels,
DurationMs: int(totalDuration.Milliseconds()),
BitrateKbps: bitrate,
IsLossless: false,
}, nil
}
func channelCount(mode mp3.FrameChannelMode) int {
if mode == mp3.SingleChannel {
return 1
}
return 2
}
-85
View File
@@ -1,85 +0,0 @@
package config
import (
"fmt"
"time"
)
const (
IndexerTypeJackett IndexerType = "jackett"
)
const (
TorrentClientQbittorrent TorrentClientType = "qbittorrent"
)
type IndexerType string
type TorrentClientType string
type Config struct {
App struct {
Host string `yaml:"host"`
Port string `yaml:"port"`
} `yaml:"app"`
Database struct {
URL string `yaml:"url"`
} `yaml:"database"`
Indexer struct {
Url string `yaml:"url"`
Port string `yaml:"port"`
Type IndexerType `yaml:"type"`
ApiKey string `yaml:"api_key"`
Cache CacheConfig `yaml:"cache"`
} `yaml:"indexer"`
Torrent struct {
ClientType TorrentClientType `yaml:"client_type"`
Url string `yaml:"url"`
Username string `yaml:"username"`
Password string `yaml:"password"`
ContainerName string `yaml:"container_name"`
} `yaml:"torrent"`
Metadata struct {
Endpoint string `yaml:"endpoint"`
} `yaml:"metadata"`
MusicFS struct {
Enabled bool `yaml:"enabled"`
Endpoint string `yaml:"endpoint"`
OriginID string `yaml:"origin_id"`
OriginRoot string `yaml:"origin_root"`
TimeoutSeconds int `yaml:"timeout_seconds"`
} `yaml:"musicfs"`
}
type CacheConfig struct {
Enabled bool `yaml:"enabled"`
RefreshInterval time.Duration `yaml:"refresh_interval"`
TTL time.Duration `yaml:"ttl"`
}
func (t *IndexerType) UnmarshalYAML(unmarshal func(any) error) error {
var value string
if err := unmarshal(&value); err != nil {
return err
}
switch IndexerType(value) {
case IndexerTypeJackett:
*t = IndexerType(value)
return nil
default:
return fmt.Errorf("unknown indexer type: %s", value)
}
}
func NewConfig() *Config {
config := Config{}
config.App.Host = "localhost"
config.App.Port = "8080"
return &config
}
-116
View File
@@ -1,116 +0,0 @@
package database
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type AlbumEvent struct {
ID string
Seq int64
WorkflowRunID string
AlbumID string
EventType string
Step string
Message string
DataJSON []byte
CreatedAt time.Time
}
type AlbumEventRepository struct {
pool *pgxpool.Pool
}
func NewAlbumEventRepository(pool *pgxpool.Pool) *AlbumEventRepository {
return &AlbumEventRepository{pool: pool}
}
func (r *AlbumEventRepository) Create(ctx context.Context, event *AlbumEvent) error {
err := r.pool.QueryRow(ctx,
`INSERT INTO album_events (workflow_run_id, album_id, event_type, step, message, data_json)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, seq, created_at`,
event.WorkflowRunID, event.AlbumID, event.EventType, event.Step, event.Message, event.DataJSON,
).Scan(&event.ID, &event.Seq, &event.CreatedAt)
if err != nil {
return fmt.Errorf("creating album event: %w", err)
}
return nil
}
func (r *AlbumEventRepository) GetByWorkflowRun(ctx context.Context, workflowRunID string) ([]*AlbumEvent, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, seq, workflow_run_id, album_id, event_type, step, message, data_json, created_at
FROM album_events WHERE workflow_run_id = $1 ORDER BY seq`, workflowRunID,
)
if err != nil {
return nil, fmt.Errorf("listing album events by workflow run: %w", err)
}
defer rows.Close()
var events []*AlbumEvent
for rows.Next() {
event := &AlbumEvent{}
if err := rows.Scan(&event.ID, &event.Seq, &event.WorkflowRunID, &event.AlbumID, &event.EventType, &event.Step, &event.Message, &event.DataJSON, &event.CreatedAt); err != nil {
return nil, fmt.Errorf("scanning album event: %w", err)
}
events = append(events, event)
}
return events, nil
}
func (r *AlbumEventRepository) GetByAlbum(ctx context.Context, albumID string, afterSeq int64, limit int) ([]*AlbumEvent, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, seq, workflow_run_id, album_id, event_type, step, message, data_json, created_at
FROM album_events WHERE album_id = $1 AND seq > $2 ORDER BY seq LIMIT $3`, albumID, afterSeq, limit,
)
if err != nil {
return nil, fmt.Errorf("listing album events by album: %w", err)
}
defer rows.Close()
var events []*AlbumEvent
for rows.Next() {
event := &AlbumEvent{}
if err := rows.Scan(&event.ID, &event.Seq, &event.WorkflowRunID, &event.AlbumID, &event.EventType, &event.Step, &event.Message, &event.DataJSON, &event.CreatedAt); err != nil {
return nil, fmt.Errorf("scanning album event: %w", err)
}
events = append(events, event)
}
return events, nil
}
func (r *AlbumEventRepository) GetLatestSeq(ctx context.Context) (int64, error) {
var seq int64
err := r.pool.QueryRow(ctx,
`SELECT COALESCE(MAX(seq), 0) FROM album_events`,
).Scan(&seq)
if err != nil {
return 0, fmt.Errorf("getting latest album event seq: %w", err)
}
return seq, nil
}
func (r *AlbumEventRepository) GetAfterSeq(ctx context.Context, afterSeq int64) ([]*AlbumEvent, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, seq, workflow_run_id, album_id, event_type, step, message, data_json, created_at
FROM album_events WHERE seq > $1 ORDER BY seq LIMIT 1000`, afterSeq,
)
if err != nil {
return nil, fmt.Errorf("listing album events after seq: %w", err)
}
defer rows.Close()
var events []*AlbumEvent
for rows.Next() {
event := &AlbumEvent{}
if err := rows.Scan(&event.ID, &event.Seq, &event.WorkflowRunID, &event.AlbumID, &event.EventType, &event.Step, &event.Message, &event.DataJSON, &event.CreatedAt); err != nil {
return nil, fmt.Errorf("scanning album event: %w", err)
}
events = append(events, event)
}
return events, nil
}
@@ -1,91 +0,0 @@
package database
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type AlbumRelease struct {
ID string
AlbumID string
DownloadID string
Format string
BitDepth *int
SampleRate *int
Channels int
IsLossless bool
Source *string
TotalSize int64
TotalDurationMs int
TrackCount int
HasCoverArt bool
HasCueSheet bool
HasRipLog bool
Path string
CreatedAt time.Time
}
type AlbumReleaseRepository struct {
pool *pgxpool.Pool
}
func NewAlbumReleaseRepository(pool *pgxpool.Pool) *AlbumReleaseRepository {
return &AlbumReleaseRepository{pool: pool}
}
func (r *AlbumReleaseRepository) Create(ctx context.Context, ar *AlbumRelease) error {
err := r.pool.QueryRow(ctx,
`INSERT INTO album_releases (album_id, download_id, format, bit_depth, sample_rate, channels, is_lossless, source, total_size, total_duration_ms, track_count, has_cover_art, has_cue_sheet, has_rip_log, path)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
RETURNING id, created_at`,
ar.AlbumID, ar.DownloadID, ar.Format, ar.BitDepth, ar.SampleRate, ar.Channels, ar.IsLossless, ar.Source, ar.TotalSize, ar.TotalDurationMs, ar.TrackCount, ar.HasCoverArt, ar.HasCueSheet, ar.HasRipLog, ar.Path,
).Scan(&ar.ID, &ar.CreatedAt)
if err != nil {
return fmt.Errorf("creating album release: %w", err)
}
return nil
}
func (r *AlbumReleaseRepository) GetByAlbumID(ctx context.Context, albumID string) ([]*AlbumRelease, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, album_id, download_id, format, bit_depth, sample_rate, channels, is_lossless, source, total_size, total_duration_ms, track_count, has_cover_art, has_cue_sheet, has_rip_log, path, created_at
FROM album_releases WHERE album_id = $1 ORDER BY created_at DESC`, albumID,
)
if err != nil {
return nil, fmt.Errorf("listing album releases: %w", err)
}
defer rows.Close()
var releases []*AlbumRelease
for rows.Next() {
ar := &AlbumRelease{}
if err := rows.Scan(&ar.ID, &ar.AlbumID, &ar.DownloadID, &ar.Format, &ar.BitDepth, &ar.SampleRate, &ar.Channels, &ar.IsLossless, &ar.Source, &ar.TotalSize, &ar.TotalDurationMs, &ar.TrackCount, &ar.HasCoverArt, &ar.HasCueSheet, &ar.HasRipLog, &ar.Path, &ar.CreatedAt); err != nil {
return nil, fmt.Errorf("scanning album release: %w", err)
}
releases = append(releases, ar)
}
return releases, nil
}
func (r *AlbumReleaseRepository) GetByDownloadID(ctx context.Context, downloadID string) (*AlbumRelease, error) {
ar := &AlbumRelease{}
err := r.pool.QueryRow(ctx,
`SELECT id, album_id, download_id, format, bit_depth, sample_rate, channels, is_lossless, source, total_size, total_duration_ms, track_count, has_cover_art, has_cue_sheet, has_rip_log, path, created_at
FROM album_releases WHERE download_id = $1`, downloadID,
).Scan(&ar.ID, &ar.AlbumID, &ar.DownloadID, &ar.Format, &ar.BitDepth, &ar.SampleRate, &ar.Channels, &ar.IsLossless, &ar.Source, &ar.TotalSize, &ar.TotalDurationMs, &ar.TrackCount, &ar.HasCoverArt, &ar.HasCueSheet, &ar.HasRipLog, &ar.Path, &ar.CreatedAt)
if err != nil {
return nil, fmt.Errorf("getting album release by download: %w", err)
}
return ar, nil
}
func (r *AlbumReleaseRepository) DeleteByDownloadID(ctx context.Context, downloadID string) error {
_, err := r.pool.Exec(ctx, `DELETE FROM album_releases WHERE download_id = $1`, downloadID)
if err != nil {
return fmt.Errorf("deleting album release by download: %w", err)
}
return nil
}
-178
View File
@@ -1,178 +0,0 @@
package database
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type Album struct {
ID string
ExternalID string
ArtistID string
Title string
AlbumType string
ReleaseDate *time.Time
TotalTracks int
TotalDiscs int
Label string
Genres []string
CoverURL string
MonitorState MonitorState
CreatedAt time.Time
UpdatedAt time.Time
}
type AlbumRepository struct {
pool *pgxpool.Pool
}
func NewAlbumRepository(pool *pgxpool.Pool) *AlbumRepository {
return &AlbumRepository{pool: pool}
}
func (r *AlbumRepository) Create(ctx context.Context, a *Album) error {
_, err := r.pool.Exec(ctx,
`INSERT INTO albums (external_id, artist_id, title, album_type, release_date, total_tracks, total_discs, label, genres, cover_url, monitor_state)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (external_id) DO UPDATE SET
title = EXCLUDED.title,
album_type = EXCLUDED.album_type,
release_date = EXCLUDED.release_date,
total_tracks = EXCLUDED.total_tracks,
total_discs = EXCLUDED.total_discs,
label = EXCLUDED.label,
genres = EXCLUDED.genres,
cover_url = EXCLUDED.cover_url,
monitor_state = CASE
WHEN albums.monitor_state = 'excluded' THEN albums.monitor_state
WHEN albums.monitor_state = 'monitored' THEN albums.monitor_state
ELSE EXCLUDED.monitor_state
END,
updated_at = NOW()`,
a.ExternalID, a.ArtistID, a.Title, a.AlbumType, a.ReleaseDate, a.TotalTracks, a.TotalDiscs, a.Label, a.Genres, a.CoverURL, a.MonitorState,
)
if err != nil {
return fmt.Errorf("creating album: %w", err)
}
return nil
}
func (r *AlbumRepository) CreateBatch(ctx context.Context, albums []*Album) error {
if len(albums) == 0 {
return nil
}
batch := &pgx.Batch{}
for _, a := range albums {
batch.Queue(
`INSERT INTO albums (external_id, artist_id, title, album_type, release_date, total_tracks, total_discs, label, genres, cover_url, monitor_state)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (external_id) DO UPDATE SET
title = EXCLUDED.title,
album_type = EXCLUDED.album_type,
release_date = EXCLUDED.release_date,
total_tracks = EXCLUDED.total_tracks,
total_discs = EXCLUDED.total_discs,
label = EXCLUDED.label,
genres = EXCLUDED.genres,
cover_url = EXCLUDED.cover_url,
monitor_state = CASE
WHEN albums.monitor_state = 'excluded' THEN albums.monitor_state
WHEN albums.monitor_state = 'monitored' THEN albums.monitor_state
ELSE EXCLUDED.monitor_state
END,
updated_at = NOW()`,
a.ExternalID, a.ArtistID, a.Title, a.AlbumType, a.ReleaseDate, a.TotalTracks, a.TotalDiscs, a.Label, a.Genres, a.CoverURL, a.MonitorState,
)
}
results := r.pool.SendBatch(ctx, batch)
defer results.Close()
for range albums {
if _, err := results.Exec(); err != nil {
return fmt.Errorf("batch creating album: %w", err)
}
}
return nil
}
func (r *AlbumRepository) GetByExternalID(ctx context.Context, externalID string) (*Album, error) {
a := &Album{}
err := r.pool.QueryRow(ctx,
`SELECT id, external_id, artist_id, title, album_type, release_date, total_tracks, total_discs, label, genres, cover_url, monitor_state, created_at, updated_at
FROM albums WHERE external_id = $1`, externalID,
).Scan(&a.ID, &a.ExternalID, &a.ArtistID, &a.Title, &a.AlbumType, &a.ReleaseDate, &a.TotalTracks, &a.TotalDiscs, &a.Label, &a.Genres, &a.CoverURL, &a.MonitorState, &a.CreatedAt, &a.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("getting album: %w", err)
}
return a, nil
}
func (r *AlbumRepository) GetByID(ctx context.Context, id string) (*Album, error) {
a := &Album{}
err := r.pool.QueryRow(ctx,
`SELECT id, external_id, artist_id, title, album_type, release_date, total_tracks, total_discs, label, genres, cover_url, monitor_state, created_at, updated_at
FROM albums WHERE id = $1`, id,
).Scan(&a.ID, &a.ExternalID, &a.ArtistID, &a.Title, &a.AlbumType, &a.ReleaseDate, &a.TotalTracks, &a.TotalDiscs, &a.Label, &a.Genres, &a.CoverURL, &a.MonitorState, &a.CreatedAt, &a.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("getting album: %w", err)
}
return a, nil
}
func (r *AlbumRepository) GetByArtistID(ctx context.Context, artistID string) ([]*Album, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, external_id, artist_id, title, album_type, release_date, total_tracks, total_discs, label, genres, cover_url, monitor_state, created_at, updated_at
FROM albums WHERE artist_id = $1 ORDER BY release_date DESC`, artistID,
)
if err != nil {
return nil, fmt.Errorf("listing albums: %w", err)
}
defer rows.Close()
var albums []*Album
for rows.Next() {
a := &Album{}
if err := rows.Scan(&a.ID, &a.ExternalID, &a.ArtistID, &a.Title, &a.AlbumType, &a.ReleaseDate, &a.TotalTracks, &a.TotalDiscs, &a.Label, &a.Genres, &a.CoverURL, &a.MonitorState, &a.CreatedAt, &a.UpdatedAt); err != nil {
return nil, fmt.Errorf("scanning album: %w", err)
}
albums = append(albums, a)
}
return albums, nil
}
func (r *AlbumRepository) GetMonitored(ctx context.Context) ([]*Album, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, external_id, artist_id, title, album_type, release_date, total_tracks, total_discs, label, genres, cover_url, monitor_state, created_at, updated_at
FROM albums WHERE monitor_state = 'monitored' ORDER BY release_date DESC`,
)
if err != nil {
return nil, fmt.Errorf("listing monitored albums: %w", err)
}
defer rows.Close()
var albums []*Album
for rows.Next() {
a := &Album{}
if err := rows.Scan(&a.ID, &a.ExternalID, &a.ArtistID, &a.Title, &a.AlbumType, &a.ReleaseDate, &a.TotalTracks, &a.TotalDiscs, &a.Label, &a.Genres, &a.CoverURL, &a.MonitorState, &a.CreatedAt, &a.UpdatedAt); err != nil {
return nil, fmt.Errorf("scanning album: %w", err)
}
albums = append(albums, a)
}
return albums, nil
}
func (r *AlbumRepository) SetMonitorState(ctx context.Context, id string, state MonitorState) error {
_, err := r.pool.Exec(ctx,
`UPDATE albums SET monitor_state = $1, updated_at = NOW() WHERE id = $2`, state, id,
)
if err != nil {
return fmt.Errorf("updating monitor state: %w", err)
}
return nil
}
-100
View File
@@ -1,100 +0,0 @@
package database
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type Artist struct {
ID string
ExternalID string
Name string
ArtistType string
Country string
Genres []string
ImageURL string
MonitorState MonitorState
CreatedAt time.Time
UpdatedAt time.Time
}
type ArtistRepository struct {
pool *pgxpool.Pool
}
func NewArtistRepository(pool *pgxpool.Pool) *ArtistRepository {
return &ArtistRepository{pool: pool}
}
func (r *ArtistRepository) Create(ctx context.Context, a *Artist) error {
_, err := r.pool.Exec(ctx,
`INSERT INTO artists (external_id, name, artist_type, country, genres, image_url, monitor_state)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (external_id) DO UPDATE SET
name = EXCLUDED.name,
artist_type = EXCLUDED.artist_type,
country = EXCLUDED.country,
genres = EXCLUDED.genres,
image_url = EXCLUDED.image_url,
monitor_state = CASE
WHEN artists.monitor_state = 'excluded' THEN artists.monitor_state
WHEN artists.monitor_state = 'monitored' THEN artists.monitor_state
ELSE EXCLUDED.monitor_state
END,
updated_at = NOW()
RETURNING id, created_at, updated_at`,
a.ExternalID, a.Name, a.ArtistType, a.Country, a.Genres, a.ImageURL, a.MonitorState,
)
if err != nil {
return fmt.Errorf("creating artist: %w", err)
}
return nil
}
func (r *ArtistRepository) GetByExternalID(ctx context.Context, externalID string) (*Artist, error) {
a := &Artist{}
err := r.pool.QueryRow(ctx,
`SELECT id, external_id, name, artist_type, country, genres, image_url, monitor_state, created_at, updated_at
FROM artists WHERE external_id = $1`, externalID,
).Scan(&a.ID, &a.ExternalID, &a.Name, &a.ArtistType, &a.Country, &a.Genres, &a.ImageURL, &a.MonitorState, &a.CreatedAt, &a.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("getting artist: %w", err)
}
return a, nil
}
func (r *ArtistRepository) GetAll(ctx context.Context) ([]*Artist, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, external_id, name, artist_type, country, genres, image_url, monitor_state, created_at, updated_at
FROM artists ORDER BY name ASC`,
)
if err != nil {
return nil, fmt.Errorf("listing artists: %w", err)
}
defer rows.Close()
var artists []*Artist
for rows.Next() {
a := &Artist{}
if err := rows.Scan(&a.ID, &a.ExternalID, &a.Name, &a.ArtistType, &a.Country, &a.Genres, &a.ImageURL, &a.MonitorState, &a.CreatedAt, &a.UpdatedAt); err != nil {
return nil, fmt.Errorf("scanning artist: %w", err)
}
artists = append(artists, a)
}
return artists, nil
}
func (r *ArtistRepository) GetByID(ctx context.Context, id string) (*Artist, error) {
a := &Artist{}
err := r.pool.QueryRow(ctx,
`SELECT id, external_id, name, artist_type, country, genres, image_url, monitor_state, created_at, updated_at
FROM artists WHERE id = $1`, id,
).Scan(&a.ID, &a.ExternalID, &a.Name, &a.ArtistType, &a.Country, &a.Genres, &a.ImageURL, &a.MonitorState, &a.CreatedAt, &a.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("getting artist: %w", err)
}
return a, nil
}
-40
View File
@@ -1,40 +0,0 @@
package database
import (
"context"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/rs/zerolog/log"
)
type MonitorState string
const (
Monitored MonitorState = "monitored"
Unmonitored MonitorState = "unmonitored"
Excluded MonitorState = "excluded"
)
type DB struct {
Pool *pgxpool.Pool
}
func New(ctx context.Context, databaseURL string) (*DB, error) {
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
return nil, fmt.Errorf("connecting to database: %w", err)
}
if err := pool.Ping(ctx); err != nil {
return nil, fmt.Errorf("pinging database: %w", err)
}
log.Info().Str("url", databaseURL).Msg("database connected")
return &DB{Pool: pool}, nil
}
func (db *DB) Close() {
db.Pool.Close()
}
@@ -1,104 +0,0 @@
package database
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type DownloadFile struct {
ID string
DownloadID string
TrackID *string
FilePath string
FileSize int64
FileType string
SHA256Hash string
VerifiedAt *time.Time
CreatedAt time.Time
}
type DownloadFileRepository struct {
pool *pgxpool.Pool
}
func NewDownloadFileRepository(pool *pgxpool.Pool) *DownloadFileRepository {
return &DownloadFileRepository{pool: pool}
}
func (r *DownloadFileRepository) Create(ctx context.Context, f *DownloadFile) error {
err := r.pool.QueryRow(ctx,
`INSERT INTO download_files (download_id, track_id, file_path, file_size, file_type, sha256_hash)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, created_at`,
f.DownloadID, f.TrackID, f.FilePath, f.FileSize, f.FileType, f.SHA256Hash,
).Scan(&f.ID, &f.CreatedAt)
if err != nil {
return fmt.Errorf("creating download file: %w", err)
}
return nil
}
func (r *DownloadFileRepository) CreateBatch(ctx context.Context, files []*DownloadFile) error {
for _, f := range files {
if err := r.Create(ctx, f); err != nil {
return err
}
}
return nil
}
func (r *DownloadFileRepository) GetByDownloadID(ctx context.Context, downloadID string) ([]*DownloadFile, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, download_id, track_id, file_path, file_size, file_type, sha256_hash, verified_at, created_at
FROM download_files WHERE download_id = $1 ORDER BY file_path`, downloadID,
)
if err != nil {
return nil, fmt.Errorf("listing download files: %w", err)
}
defer rows.Close()
var files []*DownloadFile
for rows.Next() {
f := &DownloadFile{}
if err := rows.Scan(&f.ID, &f.DownloadID, &f.TrackID, &f.FilePath, &f.FileSize, &f.FileType, &f.SHA256Hash, &f.VerifiedAt, &f.CreatedAt); err != nil {
return nil, fmt.Errorf("scanning download file: %w", err)
}
files = append(files, f)
}
return files, nil
}
func (r *DownloadFileRepository) SetHash(ctx context.Context, id string, hash string) error {
_, err := r.pool.Exec(ctx,
`UPDATE download_files SET sha256_hash = $1, verified_at = NOW() WHERE id = $2`, hash, id,
)
if err != nil {
return fmt.Errorf("setting file hash: %w", err)
}
return nil
}
func (r *DownloadFileRepository) GetUnverified(ctx context.Context) ([]*DownloadFile, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, download_id, track_id, file_path, file_size, file_type, sha256_hash, verified_at, created_at
FROM download_files WHERE sha256_hash IS NULL OR verified_at < NOW() - INTERVAL '30 days'
ORDER BY created_at`,
)
if err != nil {
return nil, fmt.Errorf("listing unverified files: %w", err)
}
defer rows.Close()
var files []*DownloadFile
for rows.Next() {
f := &DownloadFile{}
if err := rows.Scan(&f.ID, &f.DownloadID, &f.TrackID, &f.FilePath, &f.FileSize, &f.FileType, &f.SHA256Hash, &f.VerifiedAt, &f.CreatedAt); err != nil {
return nil, fmt.Errorf("scanning download file: %w", err)
}
files = append(files, f)
}
return files, nil
}
-227
View File
@@ -1,227 +0,0 @@
package database
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type Download struct {
ID string
TorrentID string
AlbumID string
Format string
Quality string
State string
QbitHash string
SavePath *string
ErrorMessage *string
QueuedAt time.Time
StartedAt *time.Time
CompletedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
type DownloadRepository struct {
pool *pgxpool.Pool
}
func NewDownloadRepository(pool *pgxpool.Pool) *DownloadRepository {
return &DownloadRepository{pool: pool}
}
func (r *DownloadRepository) Create(ctx context.Context, d *Download) error {
err := r.pool.QueryRow(ctx,
`INSERT INTO downloads (torrent_id, album_id, format, quality, state, qbit_hash, save_path)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, queued_at, created_at, updated_at`,
d.TorrentID, d.AlbumID, d.Format, d.Quality, d.State, d.QbitHash, d.SavePath,
).Scan(&d.ID, &d.QueuedAt, &d.CreatedAt, &d.UpdatedAt)
if err != nil {
return fmt.Errorf("creating download: %w", err)
}
return nil
}
func (r *DownloadRepository) UpdateState(ctx context.Context, id string, state string) error {
_, err := r.pool.Exec(ctx,
`UPDATE downloads SET state = $1, updated_at = NOW() WHERE id = $2`, state, id,
)
if err != nil {
return fmt.Errorf("updating download state: %w", err)
}
return nil
}
func (r *DownloadRepository) SetStarted(ctx context.Context, id string) error {
_, err := r.pool.Exec(ctx,
`UPDATE downloads SET state = 'downloading', started_at = NOW(), updated_at = NOW() WHERE id = $1`, id,
)
if err != nil {
return fmt.Errorf("setting download started: %w", err)
}
return nil
}
func (r *DownloadRepository) SetCompleted(ctx context.Context, id string, savePath string) error {
_, err := r.pool.Exec(ctx,
`UPDATE downloads SET state = 'completed', save_path = $1, completed_at = NOW(), updated_at = NOW() WHERE id = $2`, savePath, id,
)
if err != nil {
return fmt.Errorf("setting download completed: %w", err)
}
return nil
}
func (r *DownloadRepository) SetFailed(ctx context.Context, id string, errorMsg string) error {
_, err := r.pool.Exec(ctx,
`UPDATE downloads SET state = 'failed', error_message = $1, updated_at = NOW() WHERE id = $2`, errorMsg, id,
)
if err != nil {
return fmt.Errorf("setting download failed: %w", err)
}
return nil
}
func (r *DownloadRepository) SetCancelled(ctx context.Context, id string) error {
_, err := r.pool.Exec(ctx,
`UPDATE downloads SET state = 'cancelled', updated_at = NOW() WHERE id = $1`, id,
)
if err != nil {
return fmt.Errorf("setting download cancelled: %w", err)
}
return nil
}
func (r *DownloadRepository) SetCancelledByQbitHash(ctx context.Context, hash string) error {
_, err := r.pool.Exec(ctx,
`UPDATE downloads SET state = 'cancelled', updated_at = NOW() WHERE qbit_hash = $1 AND state NOT IN ('completed', 'failed', 'cancelled')`, hash,
)
if err != nil {
return fmt.Errorf("setting download cancelled by hash: %w", err)
}
return nil
}
func (r *DownloadRepository) GetByAlbumID(ctx context.Context, albumID string) ([]*Download, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, torrent_id, album_id, format, quality, state, qbit_hash, save_path, error_message, queued_at, started_at, completed_at, created_at, updated_at
FROM downloads WHERE album_id = $1 ORDER BY created_at DESC`, albumID,
)
if err != nil {
return nil, fmt.Errorf("listing downloads: %w", err)
}
defer rows.Close()
var downloads []*Download
for rows.Next() {
d := &Download{}
if err := rows.Scan(&d.ID, &d.TorrentID, &d.AlbumID, &d.Format, &d.Quality, &d.State, &d.QbitHash, &d.SavePath, &d.ErrorMessage, &d.QueuedAt, &d.StartedAt, &d.CompletedAt, &d.CreatedAt, &d.UpdatedAt); err != nil {
return nil, fmt.Errorf("scanning download: %w", err)
}
downloads = append(downloads, d)
}
return downloads, nil
}
func (r *DownloadRepository) GetActiveByTorrentID(ctx context.Context, torrentID string) (*Download, error) {
d := &Download{}
err := r.pool.QueryRow(ctx,
`SELECT id, torrent_id, album_id, format, quality, state, qbit_hash, save_path, error_message, queued_at, started_at, completed_at, created_at, updated_at
FROM downloads WHERE torrent_id = $1 AND state NOT IN ('failed')
ORDER BY created_at DESC LIMIT 1`, torrentID,
).Scan(&d.ID, &d.TorrentID, &d.AlbumID, &d.Format, &d.Quality, &d.State, &d.QbitHash, &d.SavePath, &d.ErrorMessage, &d.QueuedAt, &d.StartedAt, &d.CompletedAt, &d.CreatedAt, &d.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("getting active download by torrent: %w", err)
}
return d, nil
}
func (r *DownloadRepository) GetActive(ctx context.Context) ([]*Download, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, torrent_id, album_id, format, quality, state, qbit_hash, save_path, error_message, queued_at, started_at, completed_at, created_at, updated_at
FROM downloads WHERE state IN ('pending', 'downloading') ORDER BY created_at`,
)
if err != nil {
return nil, fmt.Errorf("listing active downloads: %w", err)
}
defer rows.Close()
var downloads []*Download
for rows.Next() {
d := &Download{}
if err := rows.Scan(&d.ID, &d.TorrentID, &d.AlbumID, &d.Format, &d.Quality, &d.State, &d.QbitHash, &d.SavePath, &d.ErrorMessage, &d.QueuedAt, &d.StartedAt, &d.CompletedAt, &d.CreatedAt, &d.UpdatedAt); err != nil {
return nil, fmt.Errorf("scanning download: %w", err)
}
downloads = append(downloads, d)
}
return downloads, nil
}
func (r *DownloadRepository) GetByID(ctx context.Context, id string) (*Download, error) {
d := &Download{}
err := r.pool.QueryRow(ctx,
`SELECT id, torrent_id, album_id, format, quality, state, qbit_hash, save_path, error_message, queued_at, started_at, completed_at, created_at, updated_at
FROM downloads WHERE id = $1`, id,
).Scan(&d.ID, &d.TorrentID, &d.AlbumID, &d.Format, &d.Quality, &d.State, &d.QbitHash, &d.SavePath, &d.ErrorMessage, &d.QueuedAt, &d.StartedAt, &d.CompletedAt, &d.CreatedAt, &d.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("getting download by id: %w", err)
}
return d, nil
}
func (r *DownloadRepository) GetLatestCompleted(ctx context.Context, albumID string) (*Download, error) {
d := &Download{}
err := r.pool.QueryRow(ctx,
`SELECT id, torrent_id, album_id, format, quality, state, qbit_hash, save_path, error_message, queued_at, started_at, completed_at, created_at, updated_at
FROM downloads WHERE album_id = $1 AND state = 'completed'
ORDER BY completed_at DESC LIMIT 1`, albumID,
).Scan(&d.ID, &d.TorrentID, &d.AlbumID, &d.Format, &d.Quality, &d.State, &d.QbitHash, &d.SavePath, &d.ErrorMessage, &d.QueuedAt, &d.StartedAt, &d.CompletedAt, &d.CreatedAt, &d.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("getting latest completed download: %w", err)
}
return d, nil
}
func (r *DownloadRepository) GetByAlbumIDs(ctx context.Context, albumIDs []string) (map[string][]*Download, error) {
if len(albumIDs) == 0 {
return nil, nil
}
rows, err := r.pool.Query(ctx,
`SELECT id, torrent_id, album_id, format, quality, state, qbit_hash, save_path, error_message, queued_at, started_at, completed_at, created_at, updated_at
FROM downloads WHERE album_id = ANY($1) AND state NOT IN ('failed', 'cancelled')
ORDER BY album_id, created_at DESC`, albumIDs,
)
if err != nil {
return nil, fmt.Errorf("batch listing downloads: %w", err)
}
defer rows.Close()
result := make(map[string][]*Download, len(albumIDs))
for rows.Next() {
d := &Download{}
if err := rows.Scan(&d.ID, &d.TorrentID, &d.AlbumID, &d.Format, &d.Quality, &d.State, &d.QbitHash, &d.SavePath, &d.ErrorMessage, &d.QueuedAt, &d.StartedAt, &d.CompletedAt, &d.CreatedAt, &d.UpdatedAt); err != nil {
return nil, fmt.Errorf("scanning download: %w", err)
}
result[d.AlbumID] = append(result[d.AlbumID], d)
}
return result, nil
}
func (r *DownloadRepository) HasAlbumInQuality(ctx context.Context, albumID string, format string, quality string) (bool, error) {
var exists bool
err := r.pool.QueryRow(ctx,
`SELECT EXISTS(
SELECT 1 FROM downloads
WHERE album_id = $1 AND format = $2 AND quality = $3 AND state IN ('completed', 'seeding')
)`, albumID, format, quality,
).Scan(&exists)
if err != nil {
return false, fmt.Errorf("checking album quality: %w", err)
}
return exists, nil
}
-104
View File
@@ -1,104 +0,0 @@
package database
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type Torrent struct {
ID string
AlbumID string
InfoHash string
Tracker string
Title string
Format string
Quality string
Source string
BitDepth int
SampleRate int
Seeders int
Peers int
Size int64
TrackCount int
HasCoverArt bool
HasCueSheet bool
HasRipLog bool
DownloadLink string
TorrentFile []byte
CreatedAt time.Time
UpdatedAt time.Time
}
type TorrentRepository struct {
pool *pgxpool.Pool
}
func NewTorrentRepository(pool *pgxpool.Pool) *TorrentRepository {
return &TorrentRepository{pool: pool}
}
func (r *TorrentRepository) Create(ctx context.Context, t *Torrent) error {
_, err := r.pool.Exec(ctx,
`INSERT INTO torrents (album_id, info_hash, tracker, title, format, quality, source, bit_depth, sample_rate, seeders, peers, size, track_count, has_cover_art, has_cue_sheet, has_rip_log, download_link, torrent_file)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
ON CONFLICT (info_hash) DO UPDATE SET
seeders = EXCLUDED.seeders,
peers = EXCLUDED.peers,
updated_at = NOW()`,
t.AlbumID, t.InfoHash, t.Tracker, t.Title, t.Format, t.Quality, t.Source, t.BitDepth, t.SampleRate, t.Seeders, t.Peers, t.Size, t.TrackCount, t.HasCoverArt, t.HasCueSheet, t.HasRipLog, t.DownloadLink, t.TorrentFile,
)
if err != nil {
return fmt.Errorf("creating torrent: %w", err)
}
return nil
}
func (r *TorrentRepository) GetByInfoHash(ctx context.Context, infoHash string) (*Torrent, error) {
t := &Torrent{}
err := r.pool.QueryRow(ctx,
`SELECT id, album_id, info_hash, tracker, title, format, quality, source, bit_depth, sample_rate, seeders, peers, size, track_count, has_cover_art, has_cue_sheet, has_rip_log, download_link, torrent_file, created_at, updated_at
FROM torrents WHERE info_hash = $1`, infoHash,
).Scan(&t.ID, &t.AlbumID, &t.InfoHash, &t.Tracker, &t.Title, &t.Format, &t.Quality, &t.Source, &t.BitDepth, &t.SampleRate, &t.Seeders, &t.Peers, &t.Size, &t.TrackCount, &t.HasCoverArt, &t.HasCueSheet, &t.HasRipLog, &t.DownloadLink, &t.TorrentFile, &t.CreatedAt, &t.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("getting torrent: %w", err)
}
return t, nil
}
func (r *TorrentRepository) GetByAlbumID(ctx context.Context, albumID string) ([]*Torrent, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, album_id, info_hash, tracker, title, format, quality, source, bit_depth, sample_rate, seeders, peers, size, track_count, has_cover_art, has_cue_sheet, has_rip_log, download_link, torrent_file, created_at, updated_at
FROM torrents WHERE album_id = $1 ORDER BY seeders DESC`, albumID,
)
if err != nil {
return nil, fmt.Errorf("listing torrents: %w", err)
}
defer rows.Close()
var torrents []*Torrent
for rows.Next() {
t := &Torrent{}
if err := rows.Scan(&t.ID, &t.AlbumID, &t.InfoHash, &t.Tracker, &t.Title, &t.Format, &t.Quality, &t.Source, &t.BitDepth, &t.SampleRate, &t.Seeders, &t.Peers, &t.Size, &t.TrackCount, &t.HasCoverArt, &t.HasCueSheet, &t.HasRipLog, &t.DownloadLink, &t.TorrentFile, &t.CreatedAt, &t.UpdatedAt); err != nil {
return nil, fmt.Errorf("scanning torrent: %w", err)
}
torrents = append(torrents, t)
}
return torrents, nil
}
func (r *TorrentRepository) HasAlbumInFormat(ctx context.Context, albumID string, format string) (bool, error) {
var exists bool
err := r.pool.QueryRow(ctx,
`SELECT EXISTS(
SELECT 1 FROM downloads
WHERE album_id = $1 AND format = $2 AND state IN ('completed', 'seeding')
)`, albumID, format,
).Scan(&exists)
if err != nil {
return false, fmt.Errorf("checking album format: %w", err)
}
return exists, nil
}
@@ -1,79 +0,0 @@
package database
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type TrackRelease struct {
ID string
AlbumReleaseID string
TrackID *string
DownloadFileID *string
Title string
TrackNumber int
DiscNumber int
DurationMs *int
Format string
BitDepth *int
SampleRate *int
Channels int
BitrateKbps *int
FileSize int64
FilePath string
CreatedAt time.Time
}
type TrackReleaseRepository struct {
pool *pgxpool.Pool
}
func NewTrackReleaseRepository(pool *pgxpool.Pool) *TrackReleaseRepository {
return &TrackReleaseRepository{pool: pool}
}
func (r *TrackReleaseRepository) Create(ctx context.Context, tr *TrackRelease) error {
err := r.pool.QueryRow(ctx,
`INSERT INTO track_releases (album_release_id, track_id, download_file_id, title, track_number, disc_number, duration_ms, format, bit_depth, sample_rate, channels, bitrate_kbps, file_size, file_path)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
RETURNING id, created_at`,
tr.AlbumReleaseID, tr.TrackID, tr.DownloadFileID, tr.Title, tr.TrackNumber, tr.DiscNumber, tr.DurationMs, tr.Format, tr.BitDepth, tr.SampleRate, tr.Channels, tr.BitrateKbps, tr.FileSize, tr.FilePath,
).Scan(&tr.ID, &tr.CreatedAt)
if err != nil {
return fmt.Errorf("creating track release: %w", err)
}
return nil
}
func (r *TrackReleaseRepository) CreateBatch(ctx context.Context, tracks []*TrackRelease) error {
for _, tr := range tracks {
if err := r.Create(ctx, tr); err != nil {
return err
}
}
return nil
}
func (r *TrackReleaseRepository) GetByAlbumReleaseID(ctx context.Context, albumReleaseID string) ([]*TrackRelease, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, album_release_id, track_id, download_file_id, title, track_number, disc_number, duration_ms, format, bit_depth, sample_rate, channels, bitrate_kbps, file_size, file_path, created_at
FROM track_releases WHERE album_release_id = $1 ORDER BY disc_number, track_number`, albumReleaseID,
)
if err != nil {
return nil, fmt.Errorf("listing track releases: %w", err)
}
defer rows.Close()
var tracks []*TrackRelease
for rows.Next() {
tr := &TrackRelease{}
if err := rows.Scan(&tr.ID, &tr.AlbumReleaseID, &tr.TrackID, &tr.DownloadFileID, &tr.Title, &tr.TrackNumber, &tr.DiscNumber, &tr.DurationMs, &tr.Format, &tr.BitDepth, &tr.SampleRate, &tr.Channels, &tr.BitrateKbps, &tr.FileSize, &tr.FilePath, &tr.CreatedAt); err != nil {
return nil, fmt.Errorf("scanning track release: %w", err)
}
tracks = append(tracks, tr)
}
return tracks, nil
}
-68
View File
@@ -1,68 +0,0 @@
package database
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type Track struct {
ID string
ExternalID string
AlbumID string
Title string
DurationMS int
ISRC string
DiscNumber int
TrackNumber int
CreatedAt time.Time
}
type TrackRepository struct {
pool *pgxpool.Pool
}
func NewTrackRepository(pool *pgxpool.Pool) *TrackRepository {
return &TrackRepository{pool: pool}
}
func (r *TrackRepository) Create(ctx context.Context, t *Track) error {
_, err := r.pool.Exec(ctx,
`INSERT INTO tracks (external_id, album_id, title, duration_ms, isrc, disc_number, track_number)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (external_id) DO UPDATE SET
title = EXCLUDED.title,
duration_ms = EXCLUDED.duration_ms,
isrc = EXCLUDED.isrc,
disc_number = EXCLUDED.disc_number,
track_number = EXCLUDED.track_number`,
t.ExternalID, t.AlbumID, t.Title, t.DurationMS, t.ISRC, t.DiscNumber, t.TrackNumber,
)
if err != nil {
return fmt.Errorf("creating track: %w", err)
}
return nil
}
func (r *TrackRepository) GetByAlbumID(ctx context.Context, albumID string) ([]*Track, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, external_id, album_id, title, duration_ms, isrc, disc_number, track_number, created_at
FROM tracks WHERE album_id = $1 ORDER BY disc_number, track_number`, albumID,
)
if err != nil {
return nil, fmt.Errorf("listing tracks: %w", err)
}
defer rows.Close()
var tracks []*Track
for rows.Next() {
t := &Track{}
if err := rows.Scan(&t.ID, &t.ExternalID, &t.AlbumID, &t.Title, &t.DurationMS, &t.ISRC, &t.DiscNumber, &t.TrackNumber, &t.CreatedAt); err != nil {
return nil, fmt.Errorf("scanning track: %w", err)
}
tracks = append(tracks, t)
}
return tracks, nil
}
@@ -1,123 +0,0 @@
package database
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var ErrWorkflowAlreadyRunning = fmt.Errorf("workflow already running for this album and quality")
type WorkflowRun struct {
ID string
AlbumID string
Quality string
Status string
ErrorMessage *string
StartedAt time.Time
CompletedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
type WorkflowRunRepository struct {
pool *pgxpool.Pool
}
func NewWorkflowRunRepository(pool *pgxpool.Pool) *WorkflowRunRepository {
return &WorkflowRunRepository{pool: pool}
}
func (r *WorkflowRunRepository) Create(ctx context.Context, run *WorkflowRun) error {
err := r.pool.QueryRow(ctx,
`INSERT INTO workflow_runs (album_id, quality, status) VALUES ($1, $2, 'running')
ON CONFLICT ON CONSTRAINT idx_workflow_runs_active DO NOTHING
RETURNING id, started_at, created_at, updated_at`,
run.AlbumID, run.Quality,
).Scan(&run.ID, &run.StartedAt, &run.CreatedAt, &run.UpdatedAt)
if err != nil {
if err == pgx.ErrNoRows {
return ErrWorkflowAlreadyRunning
}
return fmt.Errorf("creating workflow run: %w", err)
}
return nil
}
func (r *WorkflowRunRepository) SetCompleted(ctx context.Context, id string) error {
_, err := r.pool.Exec(ctx,
`UPDATE workflow_runs SET status = 'completed', completed_at = NOW(), updated_at = NOW() WHERE id = $1`, id,
)
if err != nil {
return fmt.Errorf("setting workflow run completed: %w", err)
}
return nil
}
func (r *WorkflowRunRepository) SetFailed(ctx context.Context, id string, errorMsg string) error {
_, err := r.pool.Exec(ctx,
`UPDATE workflow_runs SET status = 'failed', error_message = $1, completed_at = NOW(), updated_at = NOW() WHERE id = $2`, errorMsg, id,
)
if err != nil {
return fmt.Errorf("setting workflow run failed: %w", err)
}
return nil
}
func (r *WorkflowRunRepository) SetCancelled(ctx context.Context, id string) error {
_, err := r.pool.Exec(ctx,
`UPDATE workflow_runs SET status = 'cancelled', completed_at = NOW(), updated_at = NOW() WHERE id = $1`, id,
)
if err != nil {
return fmt.Errorf("setting workflow run cancelled: %w", err)
}
return nil
}
func (r *WorkflowRunRepository) GetByAlbumAndQuality(ctx context.Context, albumID string, quality string) (*WorkflowRun, error) {
run := &WorkflowRun{}
err := r.pool.QueryRow(ctx,
`SELECT id, album_id, quality, status, error_message, started_at, completed_at, created_at, updated_at
FROM workflow_runs WHERE album_id = $1 AND quality = $2 AND status = 'running' LIMIT 1`, albumID, quality,
).Scan(&run.ID, &run.AlbumID, &run.Quality, &run.Status, &run.ErrorMessage, &run.StartedAt, &run.CompletedAt, &run.CreatedAt, &run.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("getting workflow run by album and quality: %w", err)
}
return run, nil
}
func (r *WorkflowRunRepository) GetRunning(ctx context.Context) ([]*WorkflowRun, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, album_id, quality, status, error_message, started_at, completed_at, created_at, updated_at
FROM workflow_runs WHERE status = 'running' ORDER BY started_at`,
)
if err != nil {
return nil, fmt.Errorf("listing running workflow runs: %w", err)
}
defer rows.Close()
var runs []*WorkflowRun
for rows.Next() {
run := &WorkflowRun{}
if err := rows.Scan(&run.ID, &run.AlbumID, &run.Quality, &run.Status, &run.ErrorMessage, &run.StartedAt, &run.CompletedAt, &run.CreatedAt, &run.UpdatedAt); err != nil {
return nil, fmt.Errorf("scanning workflow run: %w", err)
}
runs = append(runs, run)
}
return runs, nil
}
func (r *WorkflowRunRepository) GetByID(ctx context.Context, id string) (*WorkflowRun, error) {
run := &WorkflowRun{}
err := r.pool.QueryRow(ctx,
`SELECT id, album_id, quality, status, error_message, started_at, completed_at, created_at, updated_at
FROM workflow_runs WHERE id = $1`, id,
).Scan(&run.ID, &run.AlbumID, &run.Quality, &run.Status, &run.ErrorMessage, &run.StartedAt, &run.CompletedAt, &run.CreatedAt, &run.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("getting workflow run by id: %w", err)
}
return run, nil
}
-260
View File
@@ -1,260 +0,0 @@
package internal
import (
"context"
"encoding/json"
"sync"
"github.com/rs/zerolog/log"
pb "homelab.lan/music-agregator/gen/music_agregator/v1"
"homelab.lan/music-agregator/internal/database"
"homelab.lan/music-agregator/internal/eventbus"
)
type EventPublisher interface {
PublishStatus(ctx context.Context, step pb.MonitorStep, msg string, data interface{}) error
PublishError(ctx context.Context, step pb.MonitorStep, err error, recoverable bool) error
PublishResult(ctx context.Context, result *pb.MonitorAlbumResponse) error
SetAlbumID(albumID string)
SetWorkflowRunID(id string)
}
type dbEventPublisher struct {
mu sync.Mutex
workflowRunID string
albumID string
quality string
events *database.AlbumEventRepository
bus *eventbus.EventBus
topic string
}
func newDBEventPublisher(albumID, quality string, events *database.AlbumEventRepository, bus *eventbus.EventBus, topic string) *dbEventPublisher {
return &dbEventPublisher{
albumID: albumID,
quality: quality,
events: events,
bus: bus,
topic: topic,
}
}
func (p *dbEventPublisher) SetAlbumID(albumID string) {
p.mu.Lock()
defer p.mu.Unlock()
p.albumID = albumID
}
func (p *dbEventPublisher) getAlbumID() string {
p.mu.Lock()
defer p.mu.Unlock()
return p.albumID
}
func (p *dbEventPublisher) SetWorkflowRunID(id string) {
p.mu.Lock()
defer p.mu.Unlock()
p.workflowRunID = id
}
func (p *dbEventPublisher) getWorkflowRunID() string {
p.mu.Lock()
defer p.mu.Unlock()
return p.workflowRunID
}
func (p *dbEventPublisher) PublishStatus(ctx context.Context, step pb.MonitorStep, msg string, data interface{}) error {
var dataJSON []byte
if data != nil {
var err error
dataJSON, err = json.Marshal(data)
if err != nil {
log.Warn().Err(err).Msg("failed to marshal status data to JSON")
dataJSON = nil
}
}
albumID := p.getAlbumID()
workflowRunID := p.getWorkflowRunID()
var seq int64
if albumID != "" {
event := &database.AlbumEvent{
WorkflowRunID: workflowRunID,
AlbumID: albumID,
EventType: "status",
Step: step.String(),
Message: msg,
DataJSON: dataJSON,
}
if err := p.events.Create(ctx, event); err != nil {
log.Error().Err(err).Msg("failed to persist status event")
} else {
seq = event.Seq
}
}
p.bus.Publish(p.topic, &eventbus.Event{
Seq: seq,
WorkflowRunID: workflowRunID,
AlbumID: albumID,
Quality: p.quality,
EventType: "status",
Step: step.String(),
Message: msg,
Data: data,
})
return nil
}
func (p *dbEventPublisher) PublishError(ctx context.Context, step pb.MonitorStep, err error, recoverable bool) error {
albumID := p.getAlbumID()
workflowRunID := p.getWorkflowRunID()
var seq int64
if albumID != "" {
event := &database.AlbumEvent{
WorkflowRunID: workflowRunID,
AlbumID: albumID,
EventType: "error",
Step: step.String(),
Message: err.Error(),
}
if dbErr := p.events.Create(ctx, event); dbErr != nil {
log.Error().Err(dbErr).Msg("failed to persist error event")
} else {
seq = event.Seq
}
}
p.bus.Publish(p.topic, &eventbus.Event{
Seq: seq,
WorkflowRunID: workflowRunID,
AlbumID: albumID,
Quality: p.quality,
EventType: "error",
Step: step.String(),
Message: err.Error(),
Data: map[string]bool{"recoverable": recoverable},
})
return nil
}
func (p *dbEventPublisher) PublishResult(ctx context.Context, result *pb.MonitorAlbumResponse) error {
var dataJSON []byte
if result != nil {
var err error
dataJSON, err = json.Marshal(result)
if err != nil {
log.Warn().Err(err).Msg("failed to marshal result to JSON")
dataJSON = nil
}
}
albumID := p.getAlbumID()
workflowRunID := p.getWorkflowRunID()
var seq int64
if albumID != "" {
event := &database.AlbumEvent{
WorkflowRunID: workflowRunID,
AlbumID: albumID,
EventType: "result",
Step: pb.MonitorStep_MONITOR_STEP_COMPLETE.String(),
Message: "workflow completed",
DataJSON: dataJSON,
}
if err := p.events.Create(ctx, event); err != nil {
log.Error().Err(err).Msg("failed to persist result event")
} else {
seq = event.Seq
}
}
p.bus.Publish(p.topic, &eventbus.Event{
Seq: seq,
WorkflowRunID: workflowRunID,
AlbumID: albumID,
Quality: p.quality,
EventType: "result",
Step: pb.MonitorStep_MONITOR_STEP_COMPLETE.String(),
Message: "workflow completed",
Data: result,
})
return nil
}
type streamEventPublisher struct {
*dbEventPublisher
stream pb.MusicAgregatorService_MonitorAlbumStreamServer
}
func newStreamEventPublisher(db *dbEventPublisher, stream pb.MusicAgregatorService_MonitorAlbumStreamServer) *streamEventPublisher {
return &streamEventPublisher{
dbEventPublisher: db,
stream: stream,
}
}
func (p *streamEventPublisher) PublishStatus(ctx context.Context, step pb.MonitorStep, msg string, data interface{}) error {
if err := p.dbEventPublisher.PublishStatus(ctx, step, msg, data); err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
default:
}
status := &pb.StatusUpdate{
Step: step,
Message: msg,
}
switch v := data.(type) {
case *pb.StreamAlbumInfo:
status.Data = &pb.StatusUpdate_AlbumInfo{AlbumInfo: v}
case *pb.TorrentList:
status.Data = &pb.StatusUpdate_Torrents{Torrents: v}
case *pb.ReleaseInfo:
status.Data = &pb.StatusUpdate_ReleaseInfo{ReleaseInfo: v}
}
return p.stream.Send(&pb.MonitorAlbumStreamResponse{
Message: &pb.MonitorAlbumStreamResponse_Status{Status: status},
})
}
func (p *streamEventPublisher) PublishError(ctx context.Context, step pb.MonitorStep, err error, recoverable bool) error {
if dbErr := p.dbEventPublisher.PublishError(ctx, step, err, recoverable); dbErr != nil {
return dbErr
}
return p.stream.Send(&pb.MonitorAlbumStreamResponse{
Message: &pb.MonitorAlbumStreamResponse_Error{
Error: &pb.ErrorUpdate{
FailedStep: step,
Message: err.Error(),
Recoverable: recoverable,
},
},
})
}
func (p *streamEventPublisher) PublishResult(ctx context.Context, result *pb.MonitorAlbumResponse) error {
if err := p.dbEventPublisher.PublishResult(ctx, result); err != nil {
return err
}
return p.stream.Send(&pb.MonitorAlbumStreamResponse{
Message: &pb.MonitorAlbumStreamResponse_Result{Result: result},
})
}
-116
View File
@@ -1,116 +0,0 @@
package eventbus
import "sync"
type Event struct {
Seq int64
WorkflowRunID string
AlbumID string
Quality string
EventType string
Step string
Message string
Data interface{}
}
type Subscription struct {
Ring *RingBuffer[*Event]
C chan struct{}
done chan struct{}
once sync.Once
}
type EventBus struct {
mu sync.RWMutex
topics map[string]map[*Subscription]struct{}
global map[*Subscription]struct{}
}
func New() *EventBus {
return &EventBus{
topics: make(map[string]map[*Subscription]struct{}),
global: make(map[*Subscription]struct{}),
}
}
func (b *EventBus) Publish(topic string, event *Event) {
b.mu.RLock()
defer b.mu.RUnlock()
if subs, ok := b.topics[topic]; ok {
for sub := range subs {
sub.Ring.Push(event)
select {
case sub.C <- struct{}{}:
default:
}
}
}
for sub := range b.global {
sub.Ring.Push(event)
select {
case sub.C <- struct{}{}:
default:
}
}
}
func (b *EventBus) Subscribe(topic string) (*Subscription, func()) {
sub := &Subscription{
Ring: NewRingBuffer[*Event](256),
C: make(chan struct{}, 1),
done: make(chan struct{}),
}
b.mu.Lock()
if b.topics[topic] == nil {
b.topics[topic] = make(map[*Subscription]struct{})
}
b.topics[topic][sub] = struct{}{}
b.mu.Unlock()
cleanup := func() {
sub.once.Do(func() {
b.mu.Lock()
delete(b.topics[topic], sub)
if len(b.topics[topic]) == 0 {
delete(b.topics, topic)
}
b.mu.Unlock()
close(sub.done)
})
}
return sub, cleanup
}
func (b *EventBus) SubscribeGlobal() (*Subscription, func()) {
sub := &Subscription{
Ring: NewRingBuffer[*Event](256),
C: make(chan struct{}, 1),
done: make(chan struct{}),
}
b.mu.Lock()
b.global[sub] = struct{}{}
b.mu.Unlock()
cleanup := func() {
sub.once.Do(func() {
b.mu.Lock()
delete(b.global, sub)
b.mu.Unlock()
close(sub.done)
})
}
return sub, cleanup
}
func (b *EventBus) HasTopic(topic string) bool {
b.mu.RLock()
defer b.mu.RUnlock()
_, ok := b.topics[topic]
return ok
}
-168
View File
@@ -1,168 +0,0 @@
package eventbus
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEventBus_PublishSubscribe(t *testing.T) {
bus := New()
sub, cleanup := bus.Subscribe("test-topic")
defer cleanup()
event := &Event{Seq: 1, EventType: "status", Message: "hello"}
bus.Publish("test-topic", event)
got, ok := sub.Ring.Pop()
require.True(t, ok)
assert.Equal(t, event, got)
}
func TestEventBus_MultipleSubscribers(t *testing.T) {
bus := New()
sub1, cleanup1 := bus.Subscribe("topic")
defer cleanup1()
sub2, cleanup2 := bus.Subscribe("topic")
defer cleanup2()
sub3, cleanup3 := bus.Subscribe("topic")
defer cleanup3()
event := &Event{Seq: 1, EventType: "status"}
bus.Publish("topic", event)
got1, ok := sub1.Ring.Pop()
require.True(t, ok)
assert.Equal(t, event, got1)
got2, ok := sub2.Ring.Pop()
require.True(t, ok)
assert.Equal(t, event, got2)
got3, ok := sub3.Ring.Pop()
require.True(t, ok)
assert.Equal(t, event, got3)
}
func TestEventBus_GlobalSubscriber(t *testing.T) {
bus := New()
sub, cleanup := bus.SubscribeGlobal()
defer cleanup()
bus.Publish("topic-a", &Event{Seq: 1})
bus.Publish("topic-b", &Event{Seq: 2})
bus.Publish("topic-c", &Event{Seq: 3})
got, ok := sub.Ring.Pop()
require.True(t, ok)
assert.Equal(t, int64(1), got.Seq)
got, ok = sub.Ring.Pop()
require.True(t, ok)
assert.Equal(t, int64(2), got.Seq)
got, ok = sub.Ring.Pop()
require.True(t, ok)
assert.Equal(t, int64(3), got.Seq)
}
func TestEventBus_TopicIsolation(t *testing.T) {
bus := New()
subA, cleanupA := bus.Subscribe("topic-a")
defer cleanupA()
bus.Publish("topic-b", &Event{Seq: 1})
_, ok := subA.Ring.Pop()
assert.False(t, ok)
}
func TestEventBus_Notification(t *testing.T) {
bus := New()
sub, cleanup := bus.Subscribe("topic")
defer cleanup()
bus.Publish("topic", &Event{Seq: 1})
select {
case <-sub.C:
case <-time.After(100 * time.Millisecond):
t.Fatal("expected notification on channel")
}
}
func TestEventBus_Unsubscribe(t *testing.T) {
bus := New()
sub, cleanup := bus.Subscribe("topic")
bus.Publish("topic", &Event{Seq: 1})
_, ok := sub.Ring.Pop()
require.True(t, ok)
cleanup()
bus.Publish("topic", &Event{Seq: 2})
_, ok = sub.Ring.Pop()
assert.False(t, ok)
}
func TestEventBus_SlowSubscriber(t *testing.T) {
bus := New()
sub, cleanup := bus.Subscribe("topic")
defer cleanup()
for i := 0; i < 500; i++ {
bus.Publish("topic", &Event{Seq: int64(i)})
}
assert.Equal(t, 256, sub.Ring.Len())
first, ok := sub.Ring.Pop()
require.True(t, ok)
assert.Equal(t, int64(244), first.Seq)
}
func TestEventBus_HasTopic(t *testing.T) {
bus := New()
assert.False(t, bus.HasTopic("topic"))
sub, cleanup := bus.Subscribe("topic")
_ = sub
assert.True(t, bus.HasTopic("topic"))
cleanup()
assert.False(t, bus.HasTopic("topic"))
}
func TestEventBus_ConcurrentPublishSubscribe(t *testing.T) {
bus := New()
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
sub, cleanup := bus.Subscribe("topic")
defer cleanup()
for j := 0; j < 100; j++ {
sub.Ring.Pop()
}
}(i)
}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 100; j++ {
bus.Publish("topic", &Event{Seq: int64(id*100 + j)})
}
}(i)
}
wg.Wait()
}
-56
View File
@@ -1,56 +0,0 @@
package eventbus
import "sync"
type RingBuffer[T any] struct {
mu sync.Mutex
buf []T
head int
tail int
count int
cap int
}
func NewRingBuffer[T any](capacity int) *RingBuffer[T] {
return &RingBuffer[T]{
buf: make([]T, capacity),
cap: capacity,
}
}
func (r *RingBuffer[T]) Push(item T) {
r.mu.Lock()
defer r.mu.Unlock()
r.buf[r.head] = item
r.head = (r.head + 1) % r.cap
if r.count == r.cap {
r.tail = (r.tail + 1) % r.cap
} else {
r.count++
}
}
func (r *RingBuffer[T]) Pop() (T, bool) {
r.mu.Lock()
defer r.mu.Unlock()
var zero T
if r.count == 0 {
return zero, false
}
item := r.buf[r.tail]
r.buf[r.tail] = zero
r.tail = (r.tail + 1) % r.cap
r.count--
return item, true
}
func (r *RingBuffer[T]) Len() int {
r.mu.Lock()
defer r.mu.Unlock()
return r.count
}
-109
View File
@@ -1,109 +0,0 @@
package eventbus
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRingBuffer_PushPop(t *testing.T) {
ring := NewRingBuffer[int](5)
ring.Push(1)
ring.Push(2)
ring.Push(3)
v, ok := ring.Pop()
require.True(t, ok)
assert.Equal(t, 1, v)
v, ok = ring.Pop()
require.True(t, ok)
assert.Equal(t, 2, v)
v, ok = ring.Pop()
require.True(t, ok)
assert.Equal(t, 3, v)
}
func TestRingBuffer_Empty(t *testing.T) {
ring := NewRingBuffer[int](5)
v, ok := ring.Pop()
assert.False(t, ok)
assert.Equal(t, 0, v)
}
func TestRingBuffer_OverwriteOldest(t *testing.T) {
ring := NewRingBuffer[int](4)
ring.Push(1)
ring.Push(2)
ring.Push(3)
ring.Push(4)
ring.Push(5)
ring.Push(6)
var values []int
for {
v, ok := ring.Pop()
if !ok {
break
}
values = append(values, v)
}
assert.Equal(t, []int{3, 4, 5, 6}, values)
}
func TestRingBuffer_Len(t *testing.T) {
ring := NewRingBuffer[int](5)
assert.Equal(t, 0, ring.Len())
ring.Push(1)
assert.Equal(t, 1, ring.Len())
ring.Push(2)
ring.Push(3)
assert.Equal(t, 3, ring.Len())
ring.Pop()
assert.Equal(t, 2, ring.Len())
ring.Push(4)
ring.Push(5)
ring.Push(6)
ring.Push(7)
assert.Equal(t, 5, ring.Len())
ring.Push(8)
assert.Equal(t, 5, ring.Len())
}
func TestRingBuffer_Concurrent(t *testing.T) {
ring := NewRingBuffer[int](100)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 100; j++ {
ring.Push(id*100 + j)
}
}(i)
}
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 500; i++ {
ring.Pop()
}
}()
wg.Wait()
}
-32
View File
@@ -1,32 +0,0 @@
package hello
import (
"context"
"google.golang.org/grpc"
pb "homelab.lan/music-agregator/gen/music_agregator/hello/v1"
)
type HelloServer struct {
pb.UnimplementedHelloServiceServer
}
func NewHelloServer() *HelloServer {
return &HelloServer{}
}
func (server *HelloServer) Ping(ctx context.Context, req *pb.PingRequest) (*pb.PongResponse, error) {
return &pb.PongResponse{}, nil
}
func (server *HelloServer) Echo(ctx context.Context, req *pb.EchoRequest) (*pb.EchoResponse, error) {
response := &pb.EchoResponse{
Response: req.GetMsg(),
}
return response, nil
}
func (s *HelloServer) Register(server *grpc.Server) {
pb.RegisterHelloServiceServer(server, s)
}
-79
View File
@@ -1,79 +0,0 @@
package indexer
import (
"sync"
"time"
"github.com/rs/zerolog/log"
)
type CacheEntry struct {
Key string
URL string
Result SearchResult
CreatedAt time.Time
TTL time.Duration
RefreshInterval time.Duration
}
func (e *CacheEntry) IsExpired() bool {
return time.Now().After(e.CreatedAt.Add(e.TTL))
}
type IndexerCache struct {
entries map[string]*CacheEntry
mu sync.RWMutex
}
func NewIndexerCache() *IndexerCache {
return &IndexerCache{
entries: make(map[string]*CacheEntry),
}
}
func (c *IndexerCache) Get(key string) (*CacheEntry, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.entries[key]
if !ok {
log.Trace().Str("key", key).Msg("cache miss")
return nil, false
}
if entry.IsExpired() {
log.Trace().Str("key", key).Msg("cache expired")
return nil, false
}
log.Trace().Str("key", key).Int("items", len(entry.Result.Items)).Msg("cache hit")
return entry, true
}
func (c *IndexerCache) Add(entry CacheEntry) {
c.mu.Lock()
defer c.mu.Unlock()
c.entries[entry.Key] = &entry
log.Debug().Str("key", entry.Key).Int("items", len(entry.Result.Items)).Dur("ttl", entry.TTL).Dur("refresh", entry.RefreshInterval).Msg("cache entry added")
}
func (c *IndexerCache) Update(key string, result SearchResult) {
c.mu.Lock()
defer c.mu.Unlock()
if entry, ok := c.entries[key]; ok {
entry.Result = result
log.Debug().Str("key", key).Int("items", len(result.Items)).Msg("cache entry updated")
} else {
log.Warn().Str("key", key).Msg("cache update for missing entry")
}
}
func (c *IndexerCache) Remove(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.entries, key)
log.Debug().Str("key", key).Msg("cache entry removed")
}
-71
View File
@@ -1,71 +0,0 @@
package indexer
import (
"context"
"time"
"github.com/jackc/pgx/v5"
"github.com/riverqueue/river"
"github.com/rs/zerolog/log"
)
type CacheRefreshArgs struct {
Key string `json:"key"`
URL string `json:"url"`
TTLExpires time.Time `json:"ttl_expires"`
RefreshInterval time.Duration `json:"refresh_interval"`
}
func (CacheRefreshArgs) Kind() string { return "indexer_cache_refresh" }
type CacheRefreshWorker struct {
river.WorkerDefaults[CacheRefreshArgs]
Cache *IndexerCache
Indexer Indexer
RiverClient *river.Client[pgx.Tx]
}
func (w *CacheRefreshWorker) Work(ctx context.Context, job *river.Job[CacheRefreshArgs]) error {
args := job.Args
if w.Cache == nil || w.Indexer == nil {
log.Trace().Str("key", args.Key).Msg("cache disabled, discarding refresh job")
return nil
}
log.Trace().Str("key", args.Key).Int64("job_id", job.ID).Time("ttl_expires", args.TTLExpires).Msg("cache refresh worker started")
if time.Now().After(args.TTLExpires) {
w.Cache.Remove(args.Key)
log.Debug().Str("key", args.Key).Msg("cache entry TTL expired, removed")
return nil
}
log.Trace().Str("key", args.Key).Str("url", args.URL).Msg("fetching fresh data from indexer")
start := time.Now()
result, err := w.Indexer.FetchURL(args.URL)
if err != nil {
retryAt := time.Now().Add(5 * time.Minute)
log.Error().Err(err).Str("key", args.Key).Time("retry_at", retryAt).Msg("cache refresh failed, scheduling retry")
w.RiverClient.Insert(ctx, args, &river.InsertOpts{
ScheduledAt: retryAt,
})
return nil
}
log.Trace().Str("key", args.Key).Int("items", len(result.Items)).Dur("duration", time.Since(start)).Msg("fresh data fetched")
w.Cache.Update(args.Key, result)
nextRefresh := time.Now().Add(args.RefreshInterval)
_, err = w.RiverClient.Insert(ctx, args, &river.InsertOpts{
ScheduledAt: nextRefresh,
})
if err != nil {
log.Error().Err(err).Str("key", args.Key).Msg("failed to schedule next cache refresh")
} else {
log.Trace().Str("key", args.Key).Time("next_refresh", nextRefresh).Msg("next refresh scheduled")
}
log.Debug().Str("key", args.Key).Int("items", len(result.Items)).Msg("cache refreshed")
return nil
}
-90
View File
@@ -1,90 +0,0 @@
package indexer
import (
"context"
"time"
"github.com/jackc/pgx/v5"
"github.com/riverqueue/river"
"github.com/rs/zerolog/log"
"homelab.lan/music-agregator/internal/config"
)
type CachedIndexer struct {
inner Indexer
cache *IndexerCache
riverClient *river.Client[pgx.Tx]
cfg config.CacheConfig
}
func NewCachedIndexer(inner Indexer, cache *IndexerCache, riverClient *river.Client[pgx.Tx], cfg config.CacheConfig) *CachedIndexer {
return &CachedIndexer{
inner: inner,
cache: cache,
riverClient: riverClient,
cfg: cfg,
}
}
func (c *CachedIndexer) Search(query string, limit int32, tracker string) (SearchResult, error) {
key := query + "|" + tracker
log.Trace().Str("key", key).Str("query", query).Str("tracker", tracker).Msg("cached indexer search")
if entry, ok := c.cache.Get(key); ok {
log.Debug().Str("key", key).Int("items", len(entry.Result.Items)).Msg("returning cached result")
return entry.Result, nil
}
log.Trace().Str("key", key).Msg("cache miss, fetching from indexer")
result, err := c.inner.Search(query, limit, tracker)
if err != nil {
log.Error().Err(err).Str("key", key).Msg("cached indexer fetch failed")
return SearchResult{}, err
}
url := c.inner.BuildSearchURL(query, limit, tracker)
log.Trace().Str("key", key).Str("url", url).Int("items", len(result.Items)).Msg("caching result")
c.cache.Add(CacheEntry{
Key: key,
URL: url,
Result: result,
CreatedAt: time.Now(),
TTL: c.cfg.TTL,
RefreshInterval: c.cfg.RefreshInterval,
})
scheduleAt := time.Now().Add(c.cfg.RefreshInterval)
_, err = c.riverClient.Insert(context.Background(), CacheRefreshArgs{
Key: key,
URL: url,
TTLExpires: time.Now().Add(c.cfg.TTL),
RefreshInterval: c.cfg.RefreshInterval,
}, &river.InsertOpts{
ScheduledAt: scheduleAt,
})
if err != nil {
log.Error().Err(err).Str("key", key).Msg("failed to schedule cache refresh job")
} else {
log.Debug().Str("key", key).Time("scheduled_at", scheduleAt).Msg("cache refresh job scheduled")
}
log.Debug().Str("key", key).Dur("ttl", c.cfg.TTL).Dur("refresh", c.cfg.RefreshInterval).Int("items", len(result.Items)).Msg("cached indexer search complete")
return result, nil
}
func (c *CachedIndexer) FetchURL(url string) (SearchResult, error) {
log.Trace().Str("url", url).Msg("cached indexer fetch URL passthrough")
return c.inner.FetchURL(url)
}
func (c *CachedIndexer) BuildSearchURL(query string, limit int32, tracker string) string {
return c.inner.BuildSearchURL(query, limit, tracker)
}
func (c *CachedIndexer) Capabilities(indexerName string) (IndexerCapabilities, error) {
log.Trace().Str("indexer", indexerName).Msg("cached indexer capabilities passthrough")
return c.inner.Capabilities(indexerName)
}
-5
View File
@@ -1,5 +0,0 @@
package indexer
type Filter interface {
IsKnownCategory(categories []string) bool
}
-8
View File
@@ -1,8 +0,0 @@
package indexer
type Indexer interface {
Search(query string, limit int32, indexer string) (SearchResult, error)
FetchURL(url string) (SearchResult, error)
BuildSearchURL(query string, limit int32, tracker string) string
Capabilities(indexerName string) (IndexerCapabilities, error)
}
-103
View File
@@ -1,103 +0,0 @@
package indexer
import (
"encoding/xml"
"strings"
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
)
type IndexerCapabilities struct {
XMLName xml.Name `xml:"caps"`
Server Server `xml:"server"`
Limits Limits `xml:"limits"`
Searching Searching `xml:"searching"`
Categories []Category `xml:"categories>category"`
}
type Server struct {
Title string `xml:"title,attr"`
}
type Limits struct {
Default int `xml:"default,attr"`
Max int `xml:"max,attr"`
}
type Searching struct {
Search SearchCapability `xml:"search"`
TvSearch SearchCapability `xml:"tv-search"`
MovieSearch SearchCapability `xml:"movie-search"`
MusicSearch SearchCapability `xml:"music-search"`
AudioSearch SearchCapability `xml:"audio-search"`
BookSearch SearchCapability `xml:"book-search"`
}
type SearchCapability struct {
Available string `xml:"available,attr"`
SupportedParams string `xml:"supportedParams,attr"`
SearchEngine string `xml:"searchEngine,attr"`
}
type Category struct {
ID int `xml:"id,attr"`
Name string `xml:"name,attr"`
Subcats []Subcat `xml:"subcat"`
}
type Subcat struct {
ID int `xml:"id,attr"`
Name string `xml:"name,attr"`
}
func (c *IndexerCapabilities) ToProto() *pb.CapabilitiesResponse {
return &pb.CapabilitiesResponse{
Server: &pb.Server{
Title: c.Server.Title,
},
Limits: &pb.Limits{
Default: int32(c.Limits.Default),
Max: int32(c.Limits.Max),
},
Searching: &pb.Searching{
Search: c.Searching.Search.toProto(),
TvSearch: c.Searching.TvSearch.toProto(),
MovieSearch: c.Searching.MovieSearch.toProto(),
MusicSearch: c.Searching.MusicSearch.toProto(),
AudioSearch: c.Searching.AudioSearch.toProto(),
BookSearch: c.Searching.BookSearch.toProto(),
},
Categories: c.categoriesToProto(),
}
}
func (s *SearchCapability) toProto() *pb.SearchCapability {
var params []string
if s.SupportedParams != "" {
params = strings.Split(s.SupportedParams, ",")
}
return &pb.SearchCapability{
Available: s.Available == "yes",
SupportedParams: params,
SearchEngine: s.SearchEngine,
}
}
func (c *IndexerCapabilities) categoriesToProto() []*pb.Category {
categories := make([]*pb.Category, len(c.Categories))
for i, cat := range c.Categories {
subcats := make([]*pb.Subcat, len(cat.Subcats))
for j, sub := range cat.Subcats {
subcats[j] = &pb.Subcat{
Id: int32(sub.ID),
Name: sub.Name,
}
}
categories[i] = &pb.Category{
Id: int32(cat.ID),
Name: cat.Name,
Subcats: subcats,
}
}
return categories
}
-139
View File
@@ -1,139 +0,0 @@
package indexer
import (
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/rs/zerolog/log"
"homelab.lan/music-agregator/internal/config"
)
type JacketIndexer struct {
cfg config.Config
client *http.Client
}
func NewIndexer(cfg config.Config) Indexer {
return &JacketIndexer{
cfg: cfg,
client: &http.Client{
Timeout: 60 * time.Second,
},
}
}
func (indexer *JacketIndexer) BuildSearchURL(query string, limit int32, tracker string) string {
searchTracker := "all"
if len(tracker) != 0 {
searchTracker = tracker
}
uri := fmt.Sprintf("%v/api/v2.0/indexers/%v/results/torznab?apikey=%v&cat=3010,3040&q=%v&t=search",
indexer.cfg.Indexer.Url, searchTracker, indexer.cfg.Indexer.ApiKey, url.QueryEscape(query))
if limit > 0 {
uri += fmt.Sprintf("&limit=%d", limit)
}
return uri
}
func (indexer *JacketIndexer) Search(query string, limit int32, tracker string) (SearchResult, error) {
uri := indexer.BuildSearchURL(query, limit, tracker)
return indexer.FetchURL(uri)
}
type JackettError struct {
Code string `xml:"code,attr"`
Description string `xml:"description,attr"`
}
func (e *JackettError) Error() string {
return fmt.Sprintf("jackett error %s: %s", e.Code, e.Description)
}
func (indexer *JacketIndexer) FetchURL(uri string) (SearchResult, error) {
log.Trace().Str("uri", uri).Msg("jackett request")
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
log.Error().Err(err).Msg("error creating request")
return SearchResult{}, err
}
start := time.Now()
resp, err := indexer.client.Do(req)
if err != nil {
log.Error().Err(err).Msg("error making search request")
return SearchResult{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("error reading search response body")
return SearchResult{}, err
}
log.Trace().
Int("status", resp.StatusCode).
Int("body_bytes", len(body)).
Dur("duration", time.Since(start)).
Msg("jackett response")
if resp.StatusCode != http.StatusOK {
var jackettErr JackettError
if xmlErr := xml.Unmarshal(body, &jackettErr); xmlErr == nil && jackettErr.Code != "" {
log.Error().Str("code", jackettErr.Code).Str("description", jackettErr.Description).Msg("jackett returned error")
return SearchResult{}, &jackettErr
}
return SearchResult{}, fmt.Errorf("jackett returned HTTP %d", resp.StatusCode)
}
var searchResult SearchResult
if err := xml.Unmarshal(body, &searchResult); err != nil {
log.Error().Err(err).Msg("error parsing search XML")
return SearchResult{}, err
}
log.Trace().Int("items", len(searchResult.Items)).Msg("jackett XML parsed")
return searchResult, nil
}
func (indexer *JacketIndexer) Capabilities(indexerName string) (IndexerCapabilities, error) {
url := indexer.cfg.Indexer.Url
uri := fmt.Sprintf("%v/api/v2.0/indexers/%v/results/torznab/api?apikey=%v&t=caps", url, indexerName, indexer.cfg.Indexer.ApiKey)
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
log.Error().Err(err).Msg("Error creating request")
return IndexerCapabilities{}, err
}
resp, err := indexer.client.Do(req)
if err != nil {
log.Error().Err(err).Msg("Error making capabilities request")
return IndexerCapabilities{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("Error reading response body")
return IndexerCapabilities{}, err
}
var capabilities IndexerCapabilities
if err := xml.Unmarshal(body, &capabilities); err != nil {
log.Error().Err(err).Msg("Error parsing capabilities XML")
return IndexerCapabilities{}, err
}
log.Debug().Str("server", capabilities.Server.Title).Msg("Parsed capabilities")
return capabilities, nil
}
-140
View File
@@ -1,140 +0,0 @@
package indexer
import (
"encoding/xml"
"strconv"
"github.com/rs/zerolog/log"
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
"homelab.lan/music-agregator/internal/release"
"homelab.lan/music-agregator/internal/tracker"
)
type SearchResult struct {
XMLName xml.Name `xml:"rss"`
Items []Item `xml:"channel>item"`
}
type JackettIndexer struct {
ID string `xml:"id,attr"`
}
type Item struct {
Title string `xml:"title"`
Link string `xml:"link"`
Guid string `xml:"guid"`
PubDate string `xml:"pubDate"`
Size int64 `xml:"size"`
Description string `xml:"description"`
Categories []string `xml:"category"`
Enclosure Enclosure `xml:"enclosure"`
TorznabAttrs []TorznabAttr `xml:"attr"`
JackettIndexer JackettIndexer `xml:"jackettindexer"`
}
type Enclosure struct {
URL string `xml:"url,attr"`
Length int64 `xml:"length,attr"`
Type string `xml:"type,attr"`
}
type TorznabAttr struct {
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
}
type SearchItemResult struct {
Title string
DownloadLink string
TorrentPageUrl string
PubDate string
Size int64
Description string
Categories []string
Tracker string
Seeders int
Peers int
Attrs map[string]string
Release *release.Release
}
func (si *SearchItemResult) ToProto() *pb.SearchItem {
var pbAttrs []*pb.TorznabAttr
for k, v := range si.Attrs {
pbAttrs = append(pbAttrs, &pb.TorznabAttr{Name: k, Value: v})
}
return &pb.SearchItem{
Title: si.Title,
DownloadLink: si.DownloadLink,
TorrentPageUrl: si.TorrentPageUrl,
PubDate: si.PubDate,
Size: si.Size,
Description: si.Description,
Categories: si.Categories,
TorznabAttrs: pbAttrs,
Release: si.Release.ToProto(),
}
}
type SearchResponse struct {
Items []*SearchItemResult
}
func (sr *SearchResponse) ToProto() *pb.SearchResponse {
pbItems := make([]*pb.SearchItem, len(sr.Items))
for i, item := range sr.Items {
pbItems[i] = item.ToProto()
}
return &pb.SearchResponse{Result: pbItems}
}
var genericParser = tracker.NewGenericParser()
func (sr *SearchResult) ToSearchResponse() *SearchResponse {
var items []*SearchItemResult
for _, item := range sr.Items {
rel := genericParser.Parse(item.Title)
log.Trace().
Str("tracker", item.JackettIndexer.ID).
Str("title", item.Title).
Str("artist", rel.Artist).
Str("album", rel.Album).
Int("year", rel.Year).
Bool("parsed", rel.ParsedSuccessfully).
Msg("parsed item")
attrs := make(map[string]string, len(item.TorznabAttrs))
for _, attr := range item.TorznabAttrs {
attrs[attr.Name] = attr.Value
}
seeders, _ := strconv.Atoi(attrs["seeders"])
peers, _ := strconv.Atoi(attrs["peers"])
items = append(items, &SearchItemResult{
Title: item.Title,
DownloadLink: item.Link,
TorrentPageUrl: item.Guid,
PubDate: item.PubDate,
Size: item.Size,
Description: item.Description,
Categories: item.Categories,
Tracker: item.JackettIndexer.ID,
Seeders: seeders,
Peers: peers,
Attrs: attrs,
Release: rel,
})
}
log.Trace().
Int("total", len(sr.Items)).
Int("items", len(items)).
Msg("conversion complete")
return &SearchResponse{Items: items}
}
-67
View File
@@ -1,67 +0,0 @@
package indexer
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/riverqueue/river"
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
"homelab.lan/music-agregator/internal/config"
)
type IndexerServer struct {
service *IndexerService
pb.UnimplementedIndexerServiceServer
}
func NewIndexerServer(cfg config.Config, riverClient *river.Client[pgx.Tx], cacheWorker *CacheRefreshWorker) (*IndexerServer, error) {
service, err := NewIndexerService(cfg, riverClient, cacheWorker)
if err != nil {
log.Err(err).Msg("failed to initialize IndexerService")
return nil, err
}
return &IndexerServer{service: service}, nil
}
func (server *IndexerServer) Search(ctx context.Context, req *pb.SearchRequest) (*pb.SearchResponse, error) {
log.Debug().
Str("query", req.GetQuery()).
Int32("limit", req.GetLimit()).
Str("tracker", req.GetTracker()).
Msg("search started")
log.Trace().Str("query", req.GetQuery()).Msg("fetching results from indexer")
resp, err := server.service.Search(req.GetQuery(), req.GetLimit(), req.GetTracker())
if err != nil {
log.Error().Err(err).Str("query", req.GetQuery()).Msg("search failed")
return nil, err
}
log.Debug().
Str("query", req.GetQuery()).
Int("results", len(resp.Items)).
Msg("search completed")
return resp.ToProto(), nil
}
func (server *IndexerServer) Capabilities(ctx context.Context, req *pb.CapabilitiesRequest) (*pb.CapabilitiesResponse, error) {
log.Debug().Str("indexer", req.GetIndexer()).Msg("capabilities requested")
resp, err := server.service.Capabilities(req)
if err != nil {
log.Error().Err(err).Str("indexer", req.GetIndexer()).Msg("capabilities failed")
return nil, err
}
return resp, nil
}
func (s *IndexerServer) Register(server *grpc.Server) {
pb.RegisterIndexerServiceServer(server, s)
}
-66
View File
@@ -1,66 +0,0 @@
package indexer
import (
"fmt"
"github.com/jackc/pgx/v5"
"github.com/riverqueue/river"
"github.com/rs/zerolog/log"
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
"homelab.lan/music-agregator/internal/config"
)
type Searcher interface {
Search(query string, limit int32, indexer string) (*SearchResponse, error)
}
type IndexerService struct {
indexer Indexer
}
func NewIndexerService(cfg config.Config, riverClient *river.Client[pgx.Tx], cacheWorker *CacheRefreshWorker) (*IndexerService, error) {
var idx Indexer
switch cfg.Indexer.Type {
case config.IndexerTypeJackett:
idx = NewIndexer(cfg)
default:
return nil, fmt.Errorf("unable to create the indexer for type: %v", cfg.Indexer.Type)
}
if cfg.Indexer.Cache.Enabled && riverClient != nil {
cache := NewIndexerCache()
idx = NewCachedIndexer(idx, cache, riverClient, cfg.Indexer.Cache)
if cacheWorker != nil {
cacheWorker.Cache = cache
cacheWorker.Indexer = idx
}
log.Info().Dur("ttl", cfg.Indexer.Cache.TTL).Dur("refresh", cfg.Indexer.Cache.RefreshInterval).Msg("indexer cache enabled")
}
return &IndexerService{indexer: idx}, nil
}
func (service *IndexerService) Search(query string, limit int32, indexer string) (*SearchResponse, error) {
searchResult, err := service.indexer.Search(query, limit, indexer)
if err != nil {
log.Error().Err(err).Msg("failed to search in indexer")
return nil, err
}
log.Trace().Int("raw_items", len(searchResult.Items)).Msg("indexer returned results")
return searchResult.ToSearchResponse(), nil
}
func (service *IndexerService) Capabilities(req *pb.CapabilitiesRequest) (*pb.CapabilitiesResponse, error) {
capabilities, err := service.indexer.Capabilities(req.GetIndexer())
if err != nil {
log.Error().Err(err).Msg("Failed to get capabilities from indexer")
return nil, err
}
return capabilities.ToProto(), nil
}
-135
View File
@@ -1,135 +0,0 @@
package matcher
import (
"regexp"
"strings"
)
const MatchThreshold = 0.7
var (
punctuationRe = regexp.MustCompile(`[^\w\s]`)
yearRe = regexp.MustCompile(`\b(19|20)\d{2}\b`)
whitespaceRe = regexp.MustCompile(`\s+`)
noiseWords = map[string]struct{}{
"remastered": {}, "remaster": {}, "deluxe": {}, "edition": {},
"expanded": {}, "anniversary": {}, "special": {}, "limited": {},
"collectors": {}, "collector": {}, "bonus": {}, "tracks": {},
"standard": {}, "super": {},
"flac": {}, "mp3": {}, "aac": {}, "ogg": {}, "wav": {},
"24bit": {}, "16bit": {}, "48khz": {}, "44khz": {}, "96khz": {}, "192khz": {},
"cd": {}, "vinyl": {}, "lp": {}, "web": {}, "sacd": {},
"box": {}, "set": {}, "japan": {}, "shm": {},
"singles": {}, "single": {},
"pmedia": {},
}
)
func normalize(s string) string {
s = strings.ToLower(s)
s = punctuationRe.ReplaceAllString(s, " ")
s = yearRe.ReplaceAllString(s, " ")
s = whitespaceRe.ReplaceAllString(s, " ")
return strings.TrimSpace(s)
}
func tokenize(s string) []string {
var tokens []string
for _, word := range strings.Fields(s) {
if _, noise := noiseWords[word]; !noise && len(word) > 1 {
tokens = append(tokens, word)
}
}
return tokens
}
func AlbumMatchScore(albumTitle, torrentTitle, artistName string) float64 {
normAlbum := normalize(albumTitle)
normTorrent := normalize(torrentTitle)
if artistName != "" {
normArtist := normalize(artistName)
normTorrent = strings.Replace(normTorrent, normArtist, "", 1)
normTorrent = strings.TrimSpace(whitespaceRe.ReplaceAllString(normTorrent, " "))
}
if normAlbum == "" {
return 0
}
if strings.Contains(normTorrent, normAlbum) {
return 1.0
}
albumTokens := tokenize(normAlbum)
if len(albumTokens) == 0 {
return 0
}
torrentTokens := make(map[string]struct{}, len(albumTokens))
for _, t := range tokenize(normTorrent) {
torrentTokens[t] = struct{}{}
}
var matches int
for _, token := range albumTokens {
if _, found := torrentTokens[token]; found {
matches++
continue
}
for tt := range torrentTokens {
if levenshteinClose(token, tt) {
matches++
break
}
}
}
return float64(matches) / float64(len(albumTokens))
}
func levenshteinClose(a, b string) bool {
if len(a) < 3 || len(b) < 3 {
return false
}
maxDist := 1
if len(a) > 6 {
maxDist = 2
}
return levenshtein(a, b) <= maxDist
}
func levenshtein(a, b string) int {
la, lb := len(a), len(b)
if la == 0 {
return lb
}
if lb == 0 {
return la
}
prev := make([]int, lb+1)
curr := make([]int, lb+1)
for j := 0; j <= lb; j++ {
prev[j] = j
}
for i := 1; i <= la; i++ {
curr[0] = i
for j := 1; j <= lb; j++ {
cost := 1
if a[i-1] == b[j-1] {
cost = 0
}
curr[j] = min(curr[j-1]+1, min(prev[j]+1, prev[j-1]+cost))
}
prev, curr = curr, prev
}
return prev[lb]
}
func IsAlbumMatch(albumTitle, torrentTitle, artistName string) bool {
return AlbumMatchScore(albumTitle, torrentTitle, artistName) >= MatchThreshold
}
-95
View File
@@ -1,95 +0,0 @@
package matcher
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAlbumMatchScore(t *testing.T) {
tests := []struct {
album string
torrent string
artist string
wantMin float64
wantMax float64
match bool
}{
{
album: "Ain't My Bitch",
torrent: "Metallica - Ain't My Bitch (2025) [FLAC]",
artist: "Metallica",
wantMin: 0.9, wantMax: 1.0, match: true,
},
{
album: "Ain't My Bitch",
torrent: "Metallica - Aint My Bitch Remastered 2025 FLAC",
artist: "Metallica",
wantMin: 0.9, wantMax: 1.0, match: true,
},
{
album: "72 Seasons",
torrent: "Metallica - 72 Seasons (2023) [24Bit-48kHz] FLAC [PMEDIA] ⭐️",
artist: "Metallica",
wantMin: 0.9, wantMax: 1.0, match: true,
},
{
album: "Kill 'Em All",
torrent: "Metallica - Kill Em All [1983] FLAC",
artist: "Metallica",
wantMin: 0.9, wantMax: 1.0, match: true,
},
{
album: "...And Justice for All",
torrent: "Metallica - And Justice For All (Remastered) FLAC",
artist: "Metallica",
wantMin: 0.7, wantMax: 1.0, match: true,
},
{
album: "Master of Puppets",
torrent: "Metallica - Master of Puppets (Remastered Deluxe Box Set) FLAC",
artist: "Metallica",
wantMin: 0.9, wantMax: 1.0, match: true,
},
{
album: "Ain't My Bitch",
torrent: "Metallica - Load 1996 (Japan SHM-CD)",
artist: "Metallica",
wantMin: 0.0, wantMax: 0.3, match: false,
},
{
album: "Ain't My Bitch",
torrent: "Metallica - The Memory Remains (Singles, 2026 remastered)",
artist: "Metallica",
wantMin: 0.0, wantMax: 0.3, match: false,
},
{
album: "Ain't My Bitch",
torrent: "Metallica - 72 Seasons (2023) [24Bit-48kHz] FLAC",
artist: "Metallica",
wantMin: 0.0, wantMax: 0.3, match: false,
},
{
album: "Hardwired...to Self-Destruct",
torrent: "Metallica - Hardwired To Self-Destruct (Deluxe 2016) [FLAC]",
artist: "Metallica",
wantMin: 0.7, wantMax: 1.0, match: true,
},
{
album: "S&M",
torrent: "Metallica - S&M (1999) [FLAC]",
artist: "Metallica",
wantMin: 0.9, wantMax: 1.0, match: true,
},
}
for _, tt := range tests {
t.Run(tt.album+" vs "+tt.torrent, func(t *testing.T) {
score := AlbumMatchScore(tt.album, tt.torrent, tt.artist)
assert.GreaterOrEqual(t, score, tt.wantMin, "score too low: %.2f", score)
assert.LessOrEqual(t, score, tt.wantMax, "score too high: %.2f", score)
assert.Equal(t, tt.match, IsAlbumMatch(tt.album, tt.torrent, tt.artist),
"match mismatch for score %.2f", score)
})
}
}
-24
View File
@@ -1,24 +0,0 @@
package metadata
import (
"fmt"
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "homelab.lan/music-agregator/gen/metadata/v1"
)
func NewMetadataClient(endpoint string) (pb.MetadataServiceClient, *grpc.ClientConn, error) {
log.Trace().Str("endpoint", endpoint).Msg("connecting to metadata service")
conn, err := grpc.NewClient(endpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, nil, fmt.Errorf("connecting to metadata service: %w", err)
}
log.Info().Str("endpoint", endpoint).Msg("metadata service connected")
return pb.NewMetadataServiceClient(conn), conn, nil
}
-77
View File
@@ -1,77 +0,0 @@
package metadata
import (
"context"
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
pb "homelab.lan/music-agregator/gen/metadata/v1"
"homelab.lan/music-agregator/internal/config"
)
type MetadataServer struct {
client pb.MetadataServiceClient
conn *grpc.ClientConn
pb.UnimplementedMetadataServiceServer
}
func NewMetadataServer(cfg config.Config) (*MetadataServer, error) {
client, conn, err := NewMetadataClient(cfg.Metadata.Endpoint)
if err != nil {
log.Err(err).Msg("failed to initialize MetadataServer")
return nil, err
}
return &MetadataServer{client: client, conn: conn}, nil
}
func (s *MetadataServer) Close() {
if s.conn != nil {
s.conn.Close()
}
}
func (s *MetadataServer) Register(server *grpc.Server) {
pb.RegisterMetadataServiceServer(server, s)
}
func (s *MetadataServer) GetArtist(ctx context.Context, req *pb.GetArtistRequest) (*pb.GetArtistResponse, error) {
log.Debug().Msg("metadata GetArtist")
return s.client.GetArtist(ctx, req)
}
func (s *MetadataServer) SearchArtists(ctx context.Context, req *pb.SearchArtistsRequest) (*pb.SearchArtistsResponse, error) {
log.Debug().Str("query", req.GetQuery()).Int32("limit", req.GetLimit()).Msg("metadata SearchArtists")
return s.client.SearchArtists(ctx, req)
}
func (s *MetadataServer) GetAlbum(ctx context.Context, req *pb.GetAlbumRequest) (*pb.GetAlbumResponse, error) {
log.Debug().Msg("metadata GetAlbum")
return s.client.GetAlbum(ctx, req)
}
func (s *MetadataServer) GetArtistAlbums(ctx context.Context, req *pb.GetArtistAlbumsRequest) (*pb.GetArtistAlbumsResponse, error) {
log.Debug().Str("artist_id", req.GetArtistId()).Msg("metadata GetArtistAlbums")
return s.client.GetArtistAlbums(ctx, req)
}
func (s *MetadataServer) GetTrack(ctx context.Context, req *pb.GetTrackRequest) (*pb.GetTrackResponse, error) {
log.Debug().Msg("metadata GetTrack")
return s.client.GetTrack(ctx, req)
}
func (s *MetadataServer) GetAlbumTracks(ctx context.Context, req *pb.GetAlbumTracksRequest) (*pb.GetAlbumTracksResponse, error) {
log.Debug().Str("album_id", req.GetAlbumId()).Msg("metadata GetAlbumTracks")
return s.client.GetAlbumTracks(ctx, req)
}
func (s *MetadataServer) SearchAlbums(ctx context.Context, req *pb.SearchAlbumsRequest) (*pb.SearchAlbumsResponse, error) {
log.Debug().Str("query", req.GetQuery()).Str("artist", req.GetArtist()).Msg("metadata SearchAlbums")
return s.client.SearchAlbums(ctx, req)
}
func (s *MetadataServer) SyncArtist(ctx context.Context, req *pb.SyncArtistRequest) (*pb.SyncArtistResponse, error) {
log.Debug().Msg("metadata SyncArtist")
return s.client.SyncArtist(ctx, req)
}
-235
View File
@@ -1,235 +0,0 @@
package metadata
import (
"context"
"fmt"
"github.com/rs/zerolog/log"
metadataPb "homelab.lan/music-agregator/gen/metadata/v1"
"homelab.lan/music-agregator/internal/database"
)
type MetadataService struct {
client metadataPb.MetadataServiceClient
artists *database.ArtistRepository
albums *database.AlbumRepository
tracks *database.TrackRepository
}
func NewMetadataService(client metadataPb.MetadataServiceClient, db *database.DB) *MetadataService {
return &MetadataService{
client: client,
artists: database.NewArtistRepository(db.Pool),
albums: database.NewAlbumRepository(db.Pool),
tracks: database.NewTrackRepository(db.Pool),
}
}
func (s *MetadataService) GetAlbum(ctx context.Context, albumID string) (*metadataPb.Album, error) {
resp, err := s.client.GetAlbum(ctx, &metadataPb.GetAlbumRequest{
Identifier: &metadataPb.GetAlbumRequest_Id{Id: albumID},
})
if err != nil {
return nil, fmt.Errorf("fetching album: %w", err)
}
album := resp.GetAlbum()
if _, err := s.albums.GetByExternalID(ctx, album.GetId()); err != nil {
s.PersistArtist(ctx, album, database.Monitored)
s.PersistAlbum(ctx, album, database.Monitored)
}
return album, nil
}
func (s *MetadataService) GetArtistAlbums(ctx context.Context, artistExternalID string) ([]*metadataPb.Album, error) {
resp, err := s.client.GetArtistAlbums(ctx, &metadataPb.GetArtistAlbumsRequest{
ArtistId: artistExternalID,
})
if err != nil {
return nil, fmt.Errorf("fetching artist albums: %w", err)
}
return resp.GetAlbums(), nil
}
func (s *MetadataService) SearchArtists(ctx context.Context, query string, limit, offset int32) (*metadataPb.SearchArtistsResponse, error) {
resp, err := s.client.SearchArtists(ctx, &metadataPb.SearchArtistsRequest{
Query: query,
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, fmt.Errorf("searching artists: %w", err)
}
return resp, nil
}
func (s *MetadataService) GetArtistAlbumsWithPagination(ctx context.Context, artistID string, limit, offset int32) (*metadataPb.GetArtistAlbumsResponse, error) {
resp, err := s.client.GetArtistAlbums(ctx, &metadataPb.GetArtistAlbumsRequest{
ArtistId: artistID,
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, fmt.Errorf("fetching artist albums: %w", err)
}
return resp, nil
}
func (s *MetadataService) GetAlbumTracks(ctx context.Context, albumExternalID string) ([]*metadataPb.Track, error) {
resp, err := s.client.GetAlbumTracks(ctx, &metadataPb.GetAlbumTracksRequest{
AlbumId: albumExternalID,
})
if err != nil {
return nil, fmt.Errorf("fetching album tracks: %w", err)
}
return resp.GetTracks(), nil
}
func (s *MetadataService) GetArtistByExternalID(ctx context.Context, externalID string) (*database.Artist, error) {
return s.artists.GetByExternalID(ctx, externalID)
}
func (s *MetadataService) GetAlbumByID(ctx context.Context, id string) (*database.Album, error) {
return s.albums.GetByID(ctx, id)
}
func (s *MetadataService) GetAlbumByExternalID(ctx context.Context, externalID string) (*database.Album, error) {
return s.albums.GetByExternalID(ctx, externalID)
}
func (s *MetadataService) GetAlbumsByArtistID(ctx context.Context, artistID string) ([]*database.Album, error) {
return s.albums.GetByArtistID(ctx, artistID)
}
func (s *MetadataService) GetTracksByAlbumID(ctx context.Context, albumID string) ([]*database.Track, error) {
return s.tracks.GetByAlbumID(ctx, albumID)
}
func (s *MetadataService) SetAlbumMonitorState(ctx context.Context, id string, state database.MonitorState) error {
return s.albums.SetMonitorState(ctx, id, state)
}
func (s *MetadataService) PersistArtist(ctx context.Context, album *metadataPb.Album, state database.MonitorState) {
if len(album.GetArtists()) == 0 {
return
}
artist := album.GetArtists()[0].GetArtist()
var genres []string
for _, g := range artist.GetGenres() {
genres = append(genres, g.GetName())
}
err := s.artists.Create(ctx, &database.Artist{
ExternalID: artist.GetId(),
Name: artist.GetName(),
ArtistType: artist.GetArtistType(),
Country: artist.GetCountry(),
Genres: genres,
ImageURL: artist.GetImageUrl(),
MonitorState: state,
})
if err != nil {
log.Warn().Err(err).Str("name", artist.GetName()).Msg("failed to persist artist")
}
}
func (s *MetadataService) PersistAlbum(ctx context.Context, album *metadataPb.Album, state database.MonitorState) {
s.PersistAlbumForArtist(ctx, album, "", state)
}
func (s *MetadataService) PersistAlbumsForArtist(ctx context.Context, metadataAlbums []*metadataPb.Album, artistDBID string, state database.MonitorState) {
if len(metadataAlbums) == 0 || artistDBID == "" {
return
}
dbAlbums := make([]*database.Album, 0, len(metadataAlbums))
for _, album := range metadataAlbums {
var genres []string
for _, g := range album.GetGenres() {
genres = append(genres, g.GetName())
}
labelName := ""
if album.GetLabel() != nil {
labelName = album.GetLabel().GetName()
}
dbAlbums = append(dbAlbums, &database.Album{
ExternalID: album.GetId(),
ArtistID: artistDBID,
Title: album.GetTitle(),
AlbumType: album.GetAlbumType(),
TotalTracks: int(album.GetTotalTracks()),
TotalDiscs: int(album.GetTotalDiscs()),
Label: labelName,
Genres: genres,
CoverURL: album.GetCoverUrl(),
MonitorState: state,
})
}
if err := s.albums.CreateBatch(ctx, dbAlbums); err != nil {
log.Warn().Err(err).Int("count", len(dbAlbums)).Msg("failed to batch persist albums")
}
}
func (s *MetadataService) PersistAlbumForArtist(ctx context.Context, album *metadataPb.Album, artistDBID string, state database.MonitorState) {
if artistDBID == "" {
if len(album.GetArtists()) > 0 {
a, err := s.artists.GetByExternalID(ctx, album.GetArtists()[0].GetArtist().GetId())
if err == nil {
artistDBID = a.ID
}
}
}
if artistDBID == "" {
log.Trace().Str("album", album.GetTitle()).Msg("skipping album persist, no artist in DB")
return
}
var genres []string
for _, g := range album.GetGenres() {
genres = append(genres, g.GetName())
}
labelName := ""
if album.GetLabel() != nil {
labelName = album.GetLabel().GetName()
}
err := s.albums.Create(ctx, &database.Album{
ExternalID: album.GetId(),
ArtistID: artistDBID,
Title: album.GetTitle(),
AlbumType: album.GetAlbumType(),
TotalTracks: int(album.GetTotalTracks()),
TotalDiscs: int(album.GetTotalDiscs()),
Label: labelName,
Genres: genres,
CoverURL: album.GetCoverUrl(),
MonitorState: state,
})
if err != nil {
log.Warn().Err(err).Str("title", album.GetTitle()).Msg("failed to persist album")
}
}
func (s *MetadataService) PersistTracks(ctx context.Context, albumDBID string, tracks []*metadataPb.Track) {
for _, t := range tracks {
err := s.tracks.Create(ctx, &database.Track{
ExternalID: t.GetId(),
AlbumID: albumDBID,
Title: t.GetTitle(),
DurationMS: int(t.GetDurationMs()),
ISRC: t.GetIsrc(),
DiscNumber: int(t.GetDiscNumber()),
TrackNumber: int(t.GetTrackNumber()),
})
if err != nil {
log.Warn().Err(err).Str("title", t.GetTitle()).Msg("failed to persist track")
}
}
}
-456
View File
@@ -1,456 +0,0 @@
package internal
import (
"context"
"fmt"
"sync"
"time"
"github.com/rs/zerolog/log"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "homelab.lan/music-agregator/gen/music_agregator/v1"
"homelab.lan/music-agregator/internal/database"
)
var MaxPromptTimeout = 300 * time.Second
type monitorWorkflow struct {
mode pb.InteractionMode
req *pb.StartMonitorRequest
service *MusicAgregatorService
publisher EventPublisher
stream pb.MusicAgregatorService_MonitorAlbumStreamServer
decisions chan *pb.UserDecision
cancel context.CancelFunc
addedHash string
workflowRunID string
mu sync.Mutex
promptID int
}
func (w *monitorWorkflow) nextPromptID() string {
w.mu.Lock()
defer w.mu.Unlock()
w.promptID++
return fmt.Sprintf("prompt-%d", w.promptID)
}
func (w *monitorWorkflow) promptAndWait(ctx context.Context, prompt *pb.PromptForDecision) (*pb.UserDecision, error) {
if w.mode == pb.InteractionMode_INTERACTION_MODE_AUTOMATIC {
return w.defaultDecision(prompt), nil
}
if w.stream == nil {
return w.defaultDecision(prompt), nil
}
if err := w.stream.Send(&pb.MonitorAlbumStreamResponse{
Message: &pb.MonitorAlbumStreamResponse_Prompt{Prompt: prompt},
}); err != nil {
return nil, err
}
timeout := time.Duration(prompt.TimeoutSeconds) * time.Second
if timeout == 0 || timeout > MaxPromptTimeout {
timeout = MaxPromptTimeout
}
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
select {
case decision := <-w.decisions:
if decision.PromptId != prompt.PromptId {
return nil, status.Error(codes.InvalidArgument, "prompt_id mismatch")
}
return decision, nil
case <-timeoutCtx.Done():
return w.defaultDecision(prompt), nil
}
}
func (w *monitorWorkflow) defaultDecision(prompt *pb.PromptForDecision) *pb.UserDecision {
decision := &pb.UserDecision{PromptId: prompt.PromptId}
switch prompt.Type {
case pb.PromptType_PROMPT_TYPE_CONFIRM:
decision.Decision = &pb.UserDecision_Confirm{
Confirm: prompt.GetConfirm().GetDefaultValue(),
}
case pb.PromptType_PROMPT_TYPE_SELECT_ONE:
decision.Decision = &pb.UserDecision_SelectedId{
SelectedId: prompt.GetSelectOne().GetDefaultId(),
}
case pb.PromptType_PROMPT_TYPE_SELECT_MANY:
decision.Decision = &pb.UserDecision_SelectedIds{
SelectedIds: &pb.SelectedIds{Ids: prompt.GetSelectMany().GetDefaultIds()},
}
}
return decision
}
func (w *monitorWorkflow) receiveDecisions(ctx context.Context) {
if w.stream == nil {
return
}
for {
select {
case <-ctx.Done():
return
default:
}
msg, err := w.stream.Recv()
if err != nil {
return
}
if msg.GetCancel() != nil {
if w.cancel != nil {
w.cancel()
}
return
}
if decision := msg.GetDecision(); decision != nil {
select {
case w.decisions <- decision:
default:
}
}
}
}
func (w *monitorWorkflow) run(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA, "Fetching album metadata...", nil)
album, err := w.service.metadata.GetAlbum(ctx, w.req.AlbumId)
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
log.Error().Err(err).Str("album_id", w.req.AlbumId).Msg("failed to get album")
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA, err, false)
return err
}
artistName := ""
if len(album.GetArtists()) > 0 {
artistName = album.GetArtists()[0].GetArtist().GetName()
}
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA,
fmt.Sprintf("Got metadata: %s - %s", artistName, album.GetTitle()),
&pb.StreamAlbumInfo{
Artist: artistName,
Title: album.GetTitle(),
ReleaseDate: album.GetReleaseDate(),
})
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_CHECKING_OWNED, "Checking if already owned...", nil)
dbAlbum, _ := w.service.metadata.GetAlbumByExternalID(ctx, album.GetId())
if dbAlbum != nil {
w.publisher.SetAlbumID(dbAlbum.ID)
w.service.metadata.SetAlbumMonitorState(ctx, dbAlbum.ID, database.Monitored)
dbAlbum.MonitorState = database.Monitored
if w.workflowRunID == "" {
run := &database.WorkflowRun{AlbumID: dbAlbum.ID, Quality: w.req.Quality.String()}
if err := w.service.workflowRuns.Create(ctx, run); err != nil && err != database.ErrWorkflowAlreadyRunning {
log.Warn().Err(err).Msg("failed to create workflow run")
} else if err == nil {
w.workflowRunID = run.ID
w.publisher.SetWorkflowRunID(run.ID)
}
}
qualityStr := normalizeQuality(w.req.Quality, 0, 0)
owned, err := w.service.downloads.HasAlbumInQuality(ctx, dbAlbum.ID, w.req.Quality.String(), qualityStr)
if err == nil && owned {
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_CHECKING_OWNED,
fmt.Sprintf("Already owned in %s quality", qualityStr), nil)
if w.mode == pb.InteractionMode_INTERACTION_MODE_MANUAL {
decision, err := w.promptAndWait(ctx, &pb.PromptForDecision{
PromptId: w.nextPromptID(),
Type: pb.PromptType_PROMPT_TYPE_CONFIRM,
Message: "Album already owned. Download anyway?",
Options: &pb.PromptForDecision_Confirm{
Confirm: &pb.ConfirmPrompt{
ConfirmLabel: "Download anyway",
CancelLabel: "Skip",
DefaultValue: false,
},
},
})
if err != nil {
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_CHECKING_OWNED, err, false)
return err
}
if !decision.GetConfirm() {
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_COMPLETE, "Skipped - already owned", nil)
return w.publisher.PublishResult(ctx, w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
}
} else {
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_COMPLETE, "Already owned", nil)
return w.publisher.PublishResult(ctx, w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
}
}
}
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_SEARCHING_INDEXER,
fmt.Sprintf("Searching indexers for %s - %s...", artistName, album.GetTitle()), nil)
searchResult, err := w.service.searchIndexer(album, w.req.IndexerOptions.GetTracker())
if err != nil {
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_SEARCHING_INDEXER, err, true)
return err
}
parsed := w.service.parseSearchResults(ctx, searchResult, album, func(phase string, current, total int) {
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS,
fmt.Sprintf("%s: %d/%d", phase, current, total), nil)
})
if len(parsed) > 0 {
summaries := make([]*pb.TorrentSummary, len(parsed))
for i, p := range parsed {
summaries[i] = &pb.TorrentSummary{
Id: fmt.Sprintf("torrent-%d", i),
Title: p.item.Title,
Tracker: p.item.Tracker,
Seeders: int32(p.item.Seeders),
Format: p.rel.Format.String(),
Lossless: p.rel.Format.IsLossless(),
}
}
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS,
fmt.Sprintf("Parsed %d from %d torrents", len(parsed), len(searchResult.Items)),
&pb.TorrentList{Torrents: summaries})
} else {
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS,
fmt.Sprintf("Found %d torrents, none parseable", len(searchResult.Items)), nil)
}
if len(parsed) == 0 {
err := fmt.Errorf("no torrents found for %s", album.GetTitle())
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS, err, false)
return err
}
parsed = filterByAlbumMatch(parsed, album.GetTitle(), artistName)
if len(parsed) == 0 {
err := fmt.Errorf("no torrents match album title %q", album.GetTitle())
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS, err, false)
return err
}
if w.mode == pb.InteractionMode_INTERACTION_MODE_MANUAL && len(parsed) > 1 {
options := make([]*pb.SelectOption, len(parsed))
defaultIDs := make([]string, len(parsed))
for i, p := range parsed {
id := fmt.Sprintf("torrent-%d", i)
options[i] = &pb.SelectOption{
Id: id,
Label: p.item.Title,
Description: fmt.Sprintf("%s - %d seeders", p.item.Tracker, p.item.Seeders),
}
defaultIDs[i] = id
}
decision, err := w.promptAndWait(ctx, &pb.PromptForDecision{
PromptId: w.nextPromptID(),
Type: pb.PromptType_PROMPT_TYPE_SELECT_MANY,
Message: "Select torrents to consider",
Options: &pb.PromptForDecision_SelectMany{
SelectMany: &pb.SelectManyPrompt{
Options: options,
DefaultIds: defaultIDs,
MinSelections: 1,
MaxSelections: int32(len(parsed)),
},
},
})
if err != nil {
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS, err, false)
return err
}
selectedIDs := make(map[string]bool)
if ids := decision.GetSelectedIds(); ids != nil {
for _, id := range ids.GetIds() {
selectedIDs[id] = true
}
}
var selected []parsedItem
for i, p := range parsed {
id := fmt.Sprintf("torrent-%d", i)
if selectedIDs[id] {
selected = append(selected, p)
}
}
if len(selected) > 0 {
parsed = selected
}
}
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_FILTERING_QUALITY,
fmt.Sprintf("Filtering %d results by quality...", len(parsed)), nil)
filtered := filterByQuality(parsed, w.req.Quality)
if len(filtered) == 0 {
err := fmt.Errorf("no releases match quality filter for %s", album.GetTitle())
log.Warn().Str("album", album.GetTitle()).Str("quality", w.req.Quality.String()).Msg("no releases match quality filter")
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_FILTERING_QUALITY, err, false)
return err
}
filtered = filterByActiveSeeders(filtered)
if len(filtered) == 0 {
err := fmt.Errorf("no releases with active seeders for %s", album.GetTitle())
log.Warn().Str("album", album.GetTitle()).Msg("no releases with active seeders")
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_FILTERING_QUALITY, err, false)
return err
}
var best parsedItem
if w.mode == pb.InteractionMode_INTERACTION_MODE_MANUAL && len(filtered) > 1 {
options := make([]*pb.SelectOption, len(filtered))
for i, p := range filtered {
options[i] = &pb.SelectOption{
Id: fmt.Sprintf("release-%d", i),
Label: p.item.Title,
Description: fmt.Sprintf("%s - %d seeders - %s", p.item.Tracker, p.item.Seeders, p.rel.Format.String()),
}
}
bestIdx := 0
for i, p := range filtered {
if p.item.Seeders > filtered[bestIdx].item.Seeders {
bestIdx = i
}
}
decision, err := w.promptAndWait(ctx, &pb.PromptForDecision{
PromptId: w.nextPromptID(),
Type: pb.PromptType_PROMPT_TYPE_SELECT_ONE,
Message: "Select release",
Options: &pb.PromptForDecision_SelectOne{
SelectOne: &pb.SelectOnePrompt{
Options: options,
DefaultId: fmt.Sprintf("release-%d", bestIdx),
},
},
})
if err != nil {
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_SELECTING_RELEASE, err, false)
return err
}
selectedIdx := 0
if id := decision.GetSelectedId(); id != "" {
for i := range filtered {
if fmt.Sprintf("release-%d", i) == id {
selectedIdx = i
break
}
}
}
best = filtered[selectedIdx]
} else {
best = selectBestRelease(filtered, album.GetTitle())
}
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_SELECTING_RELEASE,
fmt.Sprintf("Selected: %s (%d seeders)", best.item.Title, best.item.Seeders),
&pb.ReleaseInfo{
InfoHash: best.rel.InfoHash,
Format: best.rel.Format.String(),
BitDepth: int32(best.rel.BitDepth),
SampleRate: int32(best.rel.SampleRate),
Seeders: int32(best.item.Seeders),
Tracker: best.item.Tracker,
})
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_ADDING_TORRENT,
fmt.Sprintf("Adding torrent: %s...", best.item.Title), nil)
if w.mode == pb.InteractionMode_INTERACTION_MODE_MANUAL {
decision, err := w.promptAndWait(ctx, &pb.PromptForDecision{
PromptId: w.nextPromptID(),
Type: pb.PromptType_PROMPT_TYPE_CONFIRM,
Message: fmt.Sprintf("Add torrent '%s' to client?", best.item.Title),
Options: &pb.PromptForDecision_Confirm{
Confirm: &pb.ConfirmPrompt{
ConfirmLabel: "Add",
CancelLabel: "Skip",
DefaultValue: true,
},
},
})
if err != nil {
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_ADDING_TORRENT, err, false)
return err
}
if !decision.GetConfirm() {
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_COMPLETE, "Skipped by user", nil)
return w.publisher.PublishResult(ctx, w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
}
}
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_SAVING, "Saving to database...", nil)
dbAlbum, _ = w.service.metadata.GetAlbumByExternalID(ctx, album.GetId())
if dbAlbum == nil {
w.service.metadata.PersistArtist(ctx, album, database.Monitored)
w.service.metadata.PersistAlbum(ctx, album, database.Monitored)
dbAlbum, _ = w.service.metadata.GetAlbumByExternalID(ctx, album.GetId())
}
if dbAlbum != nil {
w.publisher.SetAlbumID(dbAlbum.ID)
w.service.saveTorrentAndDownload(ctx, dbAlbum.ID, w.workflowRunID, best)
} else {
log.Warn().Str("album_id", w.req.AlbumId).Msg("album not in DB after persist attempt, skipping torrent/download persistence")
}
w.addedHash = best.rel.InfoHash
if err := w.service.addToTorrentClient(best); err != nil {
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_ADDING_TORRENT, err, true)
return err
}
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_COMPLETE, "Done!", nil)
return w.publisher.PublishResult(ctx, w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, &best))
}
func (w *monitorWorkflow) cleanup(ctx context.Context) {
if w.addedHash != "" {
if err := w.service.torrentClient.DeleteTorrent(w.addedHash); err != nil {
log.Warn().Err(err).Str("hash", w.addedHash).Msg("failed to delete torrent during cancel cleanup")
}
if err := w.service.downloads.SetCancelledByQbitHash(ctx, w.addedHash); err != nil {
log.Warn().Err(err).Str("hash", w.addedHash).Msg("failed to cancel download during cleanup")
}
}
}
-32
View File
@@ -1,32 +0,0 @@
package musicfs
import (
"fmt"
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "homelab.lan/music-agregator/gen/musicfs/v1"
)
type Client struct {
MusicFS pb.MusicFSClient
Metadata pb.MetadataServiceClient
}
func NewClient(endpoint string) (*Client, *grpc.ClientConn, error) {
log.Trace().Str("endpoint", endpoint).Msg("connecting to musicfs")
conn, err := grpc.NewClient(endpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, nil, fmt.Errorf("connecting to musicfs: %w", err)
}
log.Info().Str("endpoint", endpoint).Msg("musicfs connected")
return &Client{
MusicFS: pb.NewMusicFSClient(conn),
Metadata: pb.NewMetadataServiceClient(conn),
}, conn, nil
}
-92
View File
@@ -1,92 +0,0 @@
package musicfs
import (
"context"
"fmt"
"io"
"github.com/rs/zerolog/log"
metadataPb "homelab.lan/music-agregator/gen/metadata/v1"
pb "homelab.lan/music-agregator/gen/musicfs/v1"
)
func (c *Client) EnrichFiles(ctx context.Context, files []SyncedFile, album *metadataPb.Album) error {
if len(files) == 0 || album == nil {
return nil
}
genre := firstGenreName(album.GetGenres())
label := labelName(album.GetLabel())
albumType := album.GetAlbumType()
coverURL := album.GetCoverUrl()
var items []*pb.BatchUpdateItem
for _, f := range files {
meta := &pb.UpdateMetadataRequest{
FileId: f.FileID,
}
if genre != "" {
meta.Genre = &genre
}
if label != "" {
meta.Label = &label
}
if albumType != "" {
meta.AlbumType = &albumType
}
if coverURL != "" {
meta.CoverUrl = &coverURL
}
items = append(items, &pb.BatchUpdateItem{
FileId: f.FileID,
Metadata: meta,
})
}
stream, err := c.Metadata.BatchUpdateMetadata(ctx, &pb.BatchUpdateRequest{Items: items})
if err != nil {
return fmt.Errorf("batch update: %w", err)
}
var updated, failed int
for {
progress, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("batch update stream: %w", err)
}
if progress.ErrorMessage != nil {
log.Warn().
Int64("file_id", progress.GetCurrentFileId()).
Str("error", progress.GetErrorMessage()).
Msg("batch update item failed")
failed++
} else {
updated++
}
}
log.Info().
Int("updated", updated).
Int("failed", failed).
Msg("enrichment batch complete")
return nil
}
func firstGenreName(genres []*metadataPb.Genre) string {
if len(genres) == 0 {
return ""
}
return genres[0].GetName()
}
func labelName(label *metadataPb.Label) string {
if label == nil {
return ""
}
return label.GetName()
}
-20
View File
@@ -1,20 +0,0 @@
package musicfs
import (
"path/filepath"
"strings"
"github.com/rs/zerolog/log"
)
func DeriveSubdir(originRoot, contentPath string) string {
rel, err := filepath.Rel(originRoot, contentPath)
if err != nil || strings.HasPrefix(rel, "..") {
log.Warn().
Str("origin_root", originRoot).
Str("content_path", contentPath).
Msg("content path outside origin root, falling back to full rescan")
return ""
}
return rel
}

Some files were not shown because too many files have changed in this diff Show More