mirror of
https://github.com/LLukas22/Jellyswarrm.git
synced 2025-12-23 22:47:47 -05:00
start massive cleanup
This commit is contained in:
357
Cargo.lock
generated
357
Cargo.lock
generated
@@ -2,21 +2,47 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -147,6 +173,16 @@ dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assert-json-diff"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.27"
|
||||
@@ -358,21 +394,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
@@ -493,7 +514,17 @@ dependencies = [
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -671,9 +702,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
@@ -758,6 +799,24 @@ dependencies = [
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"
|
||||
dependencies = [
|
||||
"deadpool-runtime",
|
||||
"lazy_static",
|
||||
"num_cpus",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool-runtime"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -974,6 +1033,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
@@ -1053,6 +1113,7 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
@@ -1102,10 +1163,14 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
@@ -1170,6 +1235,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
@@ -1260,13 +1331,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.6.0"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
|
||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
@@ -1274,6 +1346,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
@@ -1314,7 +1387,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@@ -1483,14 +1556,12 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.8"
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1515,6 +1586,20 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jellyfin-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jellyswarrm-macros"
|
||||
version = "0.1.5"
|
||||
@@ -1531,6 +1616,7 @@ dependencies = [
|
||||
name = "jellyswarrm-proxy"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"askama",
|
||||
"askama_axum",
|
||||
@@ -1549,6 +1635,7 @@ dependencies = [
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"indexmap 2.12.1",
|
||||
"jellyfin-api",
|
||||
"jellyswarrm-macros",
|
||||
"mime_guess",
|
||||
"mockall",
|
||||
@@ -1867,12 +1954,13 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
name = "num_cpus"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1881,6 +1969,12 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "2.10.1"
|
||||
@@ -2068,6 +2162,18 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
@@ -2146,7 +2252,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -2183,7 +2289,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -2491,12 +2597,6 @@ dependencies = [
|
||||
"ordered-multimap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
@@ -2849,6 +2949,16 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@@ -3288,29 +3398,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.46.1"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
|
||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2",
|
||||
"socket2 0.6.1",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3695,6 +3802,16 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@@ -3965,7 +4082,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
@@ -3998,13 +4115,19 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
@@ -4015,7 +4138,7 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4024,7 +4147,7 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4054,6 +4177,24 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
@@ -4078,13 +4219,30 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
@@ -4097,6 +4255,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
@@ -4109,6 +4273,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
@@ -4121,12 +4291,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
@@ -4139,6 +4321,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
@@ -4151,6 +4339,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
@@ -4163,6 +4357,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
@@ -4175,6 +4375,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.14"
|
||||
@@ -4184,6 +4390,29 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wiremock"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
|
||||
dependencies = [
|
||||
"assert-json-diff",
|
||||
"base64",
|
||||
"deadpool",
|
||||
"futures",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"log",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
members = [
|
||||
"crates/jellyswarrm-proxy",
|
||||
"crates/jellyswarrm-macros",
|
||||
"crates/jellyfin-api",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
||||
16
crates/jellyfin-api/Cargo.toml
Normal file
16
crates/jellyfin-api/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "jellyfin-api"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
url = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true }
|
||||
wiremock = "0.6"
|
||||
23
crates/jellyfin-api/src/error.rs
Normal file
23
crates/jellyfin-api/src/error.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Network error: {0}")]
|
||||
Network(#[from] reqwest::Error),
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
#[error("URL parse error: {0}")]
|
||||
UrlParse(#[from] url::ParseError),
|
||||
#[error("Authentication failed: {0}")]
|
||||
AuthenticationFailed(String),
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
#[error("Forbidden")]
|
||||
Forbidden,
|
||||
#[error("Not found")]
|
||||
NotFound,
|
||||
#[error("Server error: {0}")]
|
||||
ServerError(String),
|
||||
#[error("Invalid response: {0}")]
|
||||
InvalidResponse(String),
|
||||
}
|
||||
305
crates/jellyfin-api/src/lib.rs
Normal file
305
crates/jellyfin-api/src/lib.rs
Normal file
@@ -0,0 +1,305 @@
|
||||
pub mod error;
|
||||
pub mod models;
|
||||
|
||||
use error::Error;
|
||||
use models::{AuthResponse, MediaFoldersResponse, User};
|
||||
use reqwest::{header, Client, StatusCode};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::json;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClientInfo {
|
||||
pub client: String,
|
||||
pub device: String,
|
||||
pub device_id: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
impl Default for ClientInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
client: "Jellyfin API Client".to_string(),
|
||||
device: "Unknown".to_string(),
|
||||
device_id: "unknown-device-id".to_string(),
|
||||
version: "0.0.0".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JellyfinClient {
|
||||
base_url: Url,
|
||||
client_info: ClientInfo,
|
||||
http_client: Client,
|
||||
auth_token: Arc<RwLock<Option<String>>>,
|
||||
}
|
||||
|
||||
impl JellyfinClient {
|
||||
pub fn new(base_url: &str, client_info: ClientInfo) -> Result<Self, Error> {
|
||||
let mut url = Url::parse(base_url)?;
|
||||
// Ensure no trailing slash for consistent joining
|
||||
if url.path().ends_with('/') {
|
||||
url.path_segments_mut()
|
||||
.map_err(|_| Error::UrlParse(url::ParseError::EmptyHost))?
|
||||
.pop_if_empty();
|
||||
}
|
||||
|
||||
let http_client = Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()?;
|
||||
|
||||
Ok(Self {
|
||||
base_url: url,
|
||||
client_info,
|
||||
http_client,
|
||||
auth_token: Arc::new(RwLock::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_token(self, token: String) -> Self {
|
||||
*self.auth_token.write().unwrap() = Some(token);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_token(&self) -> Option<String> {
|
||||
self.auth_token.read().unwrap().clone()
|
||||
}
|
||||
|
||||
fn build_auth_header(&self) -> String {
|
||||
let mut header = format!(
|
||||
"MediaBrowser Client=\"{}\", Device=\"{}\", DeviceId=\"{}\", Version=\"{}\"",
|
||||
self.client_info.client,
|
||||
self.client_info.device,
|
||||
self.client_info.device_id,
|
||||
self.client_info.version
|
||||
);
|
||||
|
||||
if let Some(token) = self.auth_token.read().unwrap().as_ref() {
|
||||
header.push_str(&format!(", Token=\"{}\"", token));
|
||||
}
|
||||
|
||||
// println!("DEBUG HEADER: {}", header);
|
||||
header
|
||||
}
|
||||
|
||||
async fn request<T: DeserializeOwned>(
|
||||
&self,
|
||||
method: reqwest::Method,
|
||||
path: &str,
|
||||
body: Option<&serde_json::Value>,
|
||||
) -> Result<T, Error> {
|
||||
let url = self.base_url.join(path)?;
|
||||
let auth_header = self.build_auth_header();
|
||||
|
||||
let mut request = self
|
||||
.http_client
|
||||
.request(method, url)
|
||||
.header(header::AUTHORIZATION, auth_header);
|
||||
|
||||
if let Some(b) = body {
|
||||
request = request.json(b);
|
||||
}
|
||||
|
||||
let response = request.send().await?;
|
||||
let status = response.status();
|
||||
|
||||
if status.is_success() {
|
||||
let data = response.json::<T>().await?;
|
||||
Ok(data)
|
||||
} else {
|
||||
match status {
|
||||
StatusCode::UNAUTHORIZED => Err(Error::Unauthorized),
|
||||
StatusCode::FORBIDDEN => Err(Error::Forbidden),
|
||||
StatusCode::NOT_FOUND => Err(Error::NotFound),
|
||||
_ => {
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
Err(Error::ServerError(format!("{} - {}", status, text)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn request_no_content(
|
||||
&self,
|
||||
method: reqwest::Method,
|
||||
path: &str,
|
||||
body: Option<&serde_json::Value>,
|
||||
) -> Result<(), Error> {
|
||||
let url = self.base_url.join(path)?;
|
||||
let auth_header = self.build_auth_header();
|
||||
|
||||
let mut request = self
|
||||
.http_client
|
||||
.request(method, url)
|
||||
.header(header::AUTHORIZATION, auth_header);
|
||||
|
||||
if let Some(b) = body {
|
||||
request = request.json(b);
|
||||
}
|
||||
|
||||
let response = request.send().await?;
|
||||
let status = response.status();
|
||||
|
||||
if status.is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
match status {
|
||||
StatusCode::UNAUTHORIZED => Err(Error::Unauthorized),
|
||||
StatusCode::FORBIDDEN => Err(Error::Forbidden),
|
||||
StatusCode::NOT_FOUND => Err(Error::NotFound),
|
||||
_ => {
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
Err(Error::ServerError(format!("{} - {}", status, text)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn authenticate_by_name(
|
||||
&self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<User, Error> {
|
||||
let body = json!({
|
||||
"Username": username,
|
||||
"Pw": password
|
||||
});
|
||||
|
||||
let response: AuthResponse = self
|
||||
.request(
|
||||
reqwest::Method::POST,
|
||||
"Users/AuthenticateByName",
|
||||
Some(&body),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
Error::Unauthorized => {
|
||||
Error::AuthenticationFailed("Invalid credentials".to_string())
|
||||
}
|
||||
_ => e,
|
||||
})?;
|
||||
|
||||
*self.auth_token.write().unwrap() = Some(response.access_token);
|
||||
Ok(response.user)
|
||||
}
|
||||
|
||||
pub async fn logout(&self) -> Result<(), Error> {
|
||||
self.request_no_content(reqwest::Method::POST, "Sessions/Logout", None)
|
||||
.await?;
|
||||
*self.auth_token.write().unwrap() = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_me(&self) -> Result<User, Error> {
|
||||
self.request(reqwest::Method::GET, "Users/Me", None).await
|
||||
}
|
||||
|
||||
pub async fn get_media_folders(&self) -> Result<Vec<models::MediaFolder>, Error> {
|
||||
let response: MediaFoldersResponse = self
|
||||
.request(reqwest::Method::GET, "Library/MediaFolders", None)
|
||||
.await?;
|
||||
Ok(response.items)
|
||||
}
|
||||
|
||||
pub async fn get_public_system_info(&self) -> Result<models::PublicSystemInfo, Error> {
|
||||
self.request(reqwest::Method::GET, "System/Info/Public", None)
|
||||
.await
|
||||
}
|
||||
|
||||
// Admin methods
|
||||
|
||||
pub async fn get_users(&self) -> Result<Vec<User>, Error> {
|
||||
self.request(reqwest::Method::GET, "Users", None).await
|
||||
}
|
||||
|
||||
pub async fn create_user(&self, username: &str, password: Option<&str>) -> Result<User, Error> {
|
||||
let body = json!({
|
||||
"Name": username,
|
||||
"Password": password
|
||||
});
|
||||
|
||||
let user: User = self
|
||||
.request(reqwest::Method::POST, "Users/New", Some(&body))
|
||||
.await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn delete_user(&self, user_id: &str) -> Result<(), Error> {
|
||||
let path = format!("Users/{}", user_id);
|
||||
self.request_no_content(reqwest::Method::DELETE, &path, None)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wiremock::matchers::{method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_authenticate_success() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
let auth_response = json!({
|
||||
"AccessToken": "test_token",
|
||||
"User": {
|
||||
"Id": "user_id",
|
||||
"Name": "test_user",
|
||||
"ServerId": "server_id"
|
||||
}
|
||||
});
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/Users/AuthenticateByName"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(auth_response))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let client_info = ClientInfo::default();
|
||||
let client = JellyfinClient::new(&mock_server.uri(), client_info).unwrap();
|
||||
|
||||
let user = client
|
||||
.authenticate_by_name("test_user", "password")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(user.name, "test_user");
|
||||
assert_eq!(client.get_token().as_deref(), Some("test_token"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_media_folders() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
let folders_response = json!({
|
||||
"Items": [
|
||||
{
|
||||
"Name": "Movies",
|
||||
"CollectionType": "movies",
|
||||
"Id": "folder_1"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/Library/MediaFolders"))
|
||||
//.and(header("Authorization", "MediaBrowser Client=\"Jellyfin API Client\", Device=\"Unknown\", DeviceId=\"unknown-device-id\", Version=\"0.0.0\", Token=\"test_token\""))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(folders_response))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let client_info = ClientInfo::default();
|
||||
let client = JellyfinClient::new(&mock_server.uri(), client_info)
|
||||
.unwrap()
|
||||
.with_token("test_token".to_string());
|
||||
|
||||
let folders = client.get_media_folders().await.unwrap();
|
||||
|
||||
assert_eq!(folders.len(), 1);
|
||||
assert_eq!(folders[0].name, "Movies");
|
||||
}
|
||||
}
|
||||
67
crates/jellyfin-api/src/models.rs
Normal file
67
crates/jellyfin-api/src/models.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserPolicy {
|
||||
#[serde(rename = "IsAdministrator")]
|
||||
pub is_administrator: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
#[serde(rename = "Id")]
|
||||
pub id: String,
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
#[serde(rename = "ServerId")]
|
||||
pub server_id: Option<String>,
|
||||
#[serde(rename = "Policy")]
|
||||
pub policy: Option<UserPolicy>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthResponse {
|
||||
#[serde(rename = "AccessToken")]
|
||||
pub access_token: String,
|
||||
#[serde(rename = "User")]
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MediaFolder {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
#[serde(rename = "CollectionType")]
|
||||
pub collection_type: Option<String>,
|
||||
#[serde(rename = "Id")]
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MediaFoldersResponse {
|
||||
#[serde(rename = "Items")]
|
||||
pub items: Vec<MediaFolder>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NewUserRequest {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
#[serde(rename = "Password")]
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PublicSystemInfo {
|
||||
#[serde(rename = "LocalAddress")]
|
||||
pub local_address: Option<String>,
|
||||
#[serde(rename = "ServerName")]
|
||||
pub server_name: Option<String>,
|
||||
#[serde(rename = "Version")]
|
||||
pub version: Option<String>,
|
||||
#[serde(rename = "ProductName")]
|
||||
pub product_name: Option<String>,
|
||||
#[serde(rename = "Id")]
|
||||
pub id: Option<String>,
|
||||
#[serde(rename = "StartupWizardCompleted")]
|
||||
pub startup_wizard_completed: Option<bool>,
|
||||
}
|
||||
@@ -61,6 +61,7 @@ async-recursion = "1.1.1"
|
||||
moka = { version = "0.12.11", features = ["future"] }
|
||||
aes-gcm = "0.10.3"
|
||||
serde_with = "3.16.1"
|
||||
jellyfin-api = { path = "../jellyfin-api" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4.4"
|
||||
|
||||
@@ -9,12 +9,20 @@ use tower_sessions::cookie::Key;
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
|
||||
use jellyfin_api::ClientInfo;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use base64::prelude::*;
|
||||
|
||||
pub static MIGRATOR: Migrator = sqlx::migrate!();
|
||||
|
||||
pub static CLIENT_INFO: Lazy<ClientInfo> = Lazy::new(|| ClientInfo {
|
||||
client: "Jellyswarrm Proxy".to_string(),
|
||||
device: "Server".to_string(),
|
||||
device_id: "jellyswarrm-proxy".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
});
|
||||
|
||||
// Lazily-resolved data directory shared across the application.
|
||||
// Priority: env var JELLYSWARRM_DATA_DIR, else "./data" relative to current working dir.
|
||||
// The directory is created on first access.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::Deserialize;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{
|
||||
encryption::decrypt_password, server_storage::ServerStorageService,
|
||||
user_authorization_service::UserAuthorizationService, AppState,
|
||||
};
|
||||
use jellyfin_api::JellyfinClient;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SyncStatus {
|
||||
@@ -30,22 +30,14 @@ pub struct ServerSyncResult {
|
||||
pub struct FederatedUserService {
|
||||
server_storage: Arc<ServerStorageService>,
|
||||
user_authorization: Arc<UserAuthorizationService>,
|
||||
reqwest_client: reqwest::Client,
|
||||
config: Arc<tokio::sync::RwLock<crate::config::AppConfig>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct NewUserResponse {
|
||||
#[serde(rename = "Id")]
|
||||
id: String,
|
||||
}
|
||||
|
||||
impl FederatedUserService {
|
||||
pub fn new(state: &AppState) -> Self {
|
||||
Self {
|
||||
server_storage: state.server_storage.clone(),
|
||||
user_authorization: state.user_authorization.clone(),
|
||||
reqwest_client: state.reqwest_client.clone(),
|
||||
config: state.config.clone(),
|
||||
}
|
||||
}
|
||||
@@ -53,13 +45,11 @@ impl FederatedUserService {
|
||||
pub fn new_from_components(
|
||||
server_storage: Arc<ServerStorageService>,
|
||||
user_authorization: Arc<UserAuthorizationService>,
|
||||
reqwest_client: reqwest::Client,
|
||||
config: Arc<tokio::sync::RwLock<crate::config::AppConfig>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
server_storage,
|
||||
user_authorization,
|
||||
reqwest_client,
|
||||
config,
|
||||
}
|
||||
}
|
||||
@@ -116,16 +106,27 @@ impl FederatedUserService {
|
||||
}
|
||||
};
|
||||
|
||||
let client_info = crate::config::CLIENT_INFO.clone();
|
||||
|
||||
let client = match JellyfinClient::new(server.url.as_str(), client_info.clone()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to create jellyfin client: {}", e);
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status: SyncStatus::Failed,
|
||||
message: Some(format!("Client error: {}", e)),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Authenticate as admin to get token
|
||||
let auth_token = match self
|
||||
.authenticate_as_admin(
|
||||
server.url.as_ref(),
|
||||
&admin.username,
|
||||
&decrypted_admin_password,
|
||||
)
|
||||
match client
|
||||
.authenticate_by_name(&admin.username, &decrypted_admin_password)
|
||||
.await
|
||||
{
|
||||
Ok(token) => token,
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to authenticate as admin on server {}: {}",
|
||||
@@ -140,39 +141,86 @@ impl FederatedUserService {
|
||||
}
|
||||
};
|
||||
|
||||
// Check if user exists, if not create
|
||||
match self
|
||||
.create_user_on_server(server.url.as_ref(), &auth_token, username, password)
|
||||
.await
|
||||
{
|
||||
Ok((remote_user_id, created)) => {
|
||||
let (status, should_map) = if created {
|
||||
(SyncStatus::Created, true)
|
||||
} else {
|
||||
// User exists. Check if password matches.
|
||||
match self
|
||||
.check_user_password(server.url.as_ref(), username, password)
|
||||
.await
|
||||
{
|
||||
Ok(true) => (SyncStatus::AlreadyExists, true),
|
||||
Ok(false) => (SyncStatus::ExistsWithDifferentPassword, false),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to check password for existing user {} on {}: {}",
|
||||
username, server.name, e
|
||||
);
|
||||
// Assume mismatch or failure, don't map to be safe
|
||||
(SyncStatus::ExistsWithDifferentPassword, false)
|
||||
}
|
||||
}
|
||||
// Check if user exists
|
||||
let users = match client.get_users().await {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
error!("Failed to list users on server {}: {}", server.name, e);
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status: SyncStatus::Failed,
|
||||
message: Some(format!("Failed to list users: {}", e)),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let existing_user = users.iter().find(|u| u.name.eq_ignore_ascii_case(username));
|
||||
|
||||
if let Some(remote_user) = existing_user {
|
||||
// User exists. Check if password matches.
|
||||
// We need a new client to check user password
|
||||
let user_client =
|
||||
match JellyfinClient::new(server.url.as_str(), client_info.clone()) {
|
||||
Ok(c) => c,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
info!(
|
||||
"Synced user {} to server {} (Remote ID: {}, Status: {:?})",
|
||||
username, server.name, remote_user_id, status
|
||||
);
|
||||
let (status, should_map) =
|
||||
match user_client.authenticate_by_name(username, password).await {
|
||||
Ok(_) => (SyncStatus::AlreadyExists, true),
|
||||
Err(_) => (SyncStatus::ExistsWithDifferentPassword, false),
|
||||
};
|
||||
|
||||
info!(
|
||||
"Synced user {} to server {} (Remote ID: {}, Status: {:?})",
|
||||
username, server.name, remote_user.id, status
|
||||
);
|
||||
|
||||
if should_map {
|
||||
if let Err(e) = self
|
||||
.user_authorization
|
||||
.add_server_mapping(
|
||||
user_id,
|
||||
server.url.as_str(),
|
||||
username,
|
||||
password,
|
||||
Some(password), // Encrypt with their own password so they can use it
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to create local mapping for synced user on server {}: {}",
|
||||
server.name, e
|
||||
);
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status: SyncStatus::Failed,
|
||||
message: Some(format!("Failed to save local mapping: {}", e)),
|
||||
});
|
||||
} else {
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status,
|
||||
message: None,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status,
|
||||
message: Some("User exists with different password".to_string()),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Create user
|
||||
match client.create_user(username, Some(password)).await {
|
||||
Ok(new_user) => {
|
||||
info!(
|
||||
"Synced user {} to server {} (Remote ID: {}, Status: Created)",
|
||||
username, server.name, new_user.id
|
||||
);
|
||||
|
||||
if should_map {
|
||||
if let Err(e) = self
|
||||
.user_authorization
|
||||
.add_server_mapping(
|
||||
@@ -196,29 +244,23 @@ impl FederatedUserService {
|
||||
} else {
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status,
|
||||
status: SyncStatus::Created,
|
||||
message: None,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to sync user {} to server {}: {}",
|
||||
username, server.name, e
|
||||
);
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status,
|
||||
message: Some("User exists with different password".to_string()),
|
||||
status: SyncStatus::Failed,
|
||||
message: Some(format!("Sync failed: {}", e)),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to sync user {} to server {}: {}",
|
||||
username, server.name, e
|
||||
);
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status: SyncStatus::Failed,
|
||||
message: Some(format!("Sync failed: {}", e)),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
@@ -278,15 +320,26 @@ impl FederatedUserService {
|
||||
}
|
||||
};
|
||||
|
||||
let auth_token = match self
|
||||
.authenticate_as_admin(
|
||||
server.url.as_ref(),
|
||||
&admin.username,
|
||||
&decrypted_admin_password,
|
||||
)
|
||||
let client_info = crate::config::CLIENT_INFO.clone();
|
||||
|
||||
let client = match JellyfinClient::new(server.url.as_str(), client_info.clone()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to create jellyfin client: {}", e);
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status: SyncStatus::Failed,
|
||||
message: Some(format!("Client error: {}", e)),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match client
|
||||
.authenticate_by_name(&admin.username, &decrypted_admin_password)
|
||||
.await
|
||||
{
|
||||
Ok(token) => token,
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to authenticate as admin on server {}: {}",
|
||||
@@ -301,37 +354,56 @@ impl FederatedUserService {
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.delete_user_on_server(server.url.as_ref(), &auth_token, username)
|
||||
.await
|
||||
{
|
||||
Ok(deleted) => {
|
||||
let status = if deleted {
|
||||
SyncStatus::Deleted
|
||||
} else {
|
||||
SyncStatus::NotFound
|
||||
};
|
||||
info!(
|
||||
"Deleted user {} from server {} (Deleted: {})",
|
||||
username, server.name, deleted
|
||||
);
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status,
|
||||
message: None,
|
||||
});
|
||||
}
|
||||
// Find user ID
|
||||
let users = match client.get_users().await {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to delete user {} from server {}: {}",
|
||||
username, server.name, e
|
||||
);
|
||||
error!("Failed to list users on server {}: {}", server.name, e);
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status: SyncStatus::Failed,
|
||||
message: Some(format!("Delete failed: {}", e)),
|
||||
message: Some(format!("Failed to list users: {}", e)),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = users
|
||||
.iter()
|
||||
.find(|u| u.name.eq_ignore_ascii_case(username))
|
||||
.map(|u| u.id.clone());
|
||||
|
||||
if let Some(id) = user_id {
|
||||
match client.delete_user(&id).await {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"Deleted user {} from server {} (Deleted: true)",
|
||||
username, server.name
|
||||
);
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status: SyncStatus::Deleted,
|
||||
message: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to delete user {} from server {}: {}",
|
||||
username, server.name, e
|
||||
);
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status: SyncStatus::Failed,
|
||||
message: Some(format!("Delete failed: {}", e)),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status: SyncStatus::NotFound,
|
||||
message: None,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
results.push(ServerSyncResult {
|
||||
@@ -344,224 +416,4 @@ impl FederatedUserService {
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
async fn check_user_password(
|
||||
&self,
|
||||
server_url: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<bool, anyhow::Error> {
|
||||
let auth_url = format!(
|
||||
"{}/Users/AuthenticateByName",
|
||||
server_url.trim_end_matches('/')
|
||||
);
|
||||
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
|
||||
let response = self
|
||||
.reqwest_client
|
||||
.post(&auth_url)
|
||||
.header("Authorization", auth_header)
|
||||
.json(&serde_json::json!({
|
||||
"Username": username,
|
||||
"Pw": password
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(true)
|
||||
} else if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Authentication check failed: {}",
|
||||
response.status()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn authenticate_as_admin(
|
||||
&self,
|
||||
server_url: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<String, anyhow::Error> {
|
||||
let auth_url = format!(
|
||||
"{}/Users/AuthenticateByName",
|
||||
server_url.trim_end_matches('/')
|
||||
);
|
||||
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
|
||||
let response = self
|
||||
.reqwest_client
|
||||
.post(&auth_url)
|
||||
.header("Authorization", auth_header)
|
||||
.json(&serde_json::json!({
|
||||
"Username": username,
|
||||
"Pw": password
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Authentication failed: {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AuthResponse {
|
||||
#[serde(rename = "AccessToken")]
|
||||
access_token: String,
|
||||
}
|
||||
|
||||
let auth_response: AuthResponse = response.json().await?;
|
||||
Ok(auth_response.access_token)
|
||||
}
|
||||
|
||||
async fn create_user_on_server(
|
||||
&self,
|
||||
server_url: &str,
|
||||
token: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(String, bool), anyhow::Error> {
|
||||
let base_url = server_url.trim_end_matches('/');
|
||||
|
||||
// 1. Check if user exists
|
||||
let users_url = format!("{}/Users", base_url);
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\", Token=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
token
|
||||
);
|
||||
|
||||
let response = self
|
||||
.reqwest_client
|
||||
.get(&users_url)
|
||||
.header("Authorization", &auth_header)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let users: Vec<serde_json::Value> = response.json().await?;
|
||||
if let Some(existing) = users.iter().find(|u| {
|
||||
u.get("Name")
|
||||
.and_then(|n| n.as_str())
|
||||
.map(|n| n.eq_ignore_ascii_case(username))
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
// User exists, return their ID
|
||||
return Ok((
|
||||
existing
|
||||
.get("Id")
|
||||
.and_then(|i| i.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
false,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Create user
|
||||
let create_url = format!("{}/Users/New", base_url);
|
||||
let response = self
|
||||
.reqwest_client
|
||||
.post(&create_url)
|
||||
.header("Authorization", &auth_header)
|
||||
.json(&serde_json::json!({
|
||||
"Name": username,
|
||||
"Password": password
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to create user: {} - {}",
|
||||
status,
|
||||
text
|
||||
));
|
||||
}
|
||||
|
||||
let new_user: NewUserResponse = response.json().await?;
|
||||
|
||||
// Password should be set by the New endpoint if provided.
|
||||
|
||||
Ok((new_user.id, true))
|
||||
}
|
||||
|
||||
async fn delete_user_on_server(
|
||||
&self,
|
||||
server_url: &str,
|
||||
token: &str,
|
||||
username: &str,
|
||||
) -> Result<bool, anyhow::Error> {
|
||||
let base_url = server_url.trim_end_matches('/');
|
||||
|
||||
// 1. Find user ID
|
||||
let users_url = format!("{}/Users", base_url);
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\", Token=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
token
|
||||
);
|
||||
|
||||
let response = self
|
||||
.reqwest_client
|
||||
.get(&users_url)
|
||||
.header("Authorization", &auth_header)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let user_id = if response.status().is_success() {
|
||||
let users: Vec<serde_json::Value> = response.json().await?;
|
||||
users
|
||||
.iter()
|
||||
.find(|u| {
|
||||
u.get("Name")
|
||||
.and_then(|n| n.as_str())
|
||||
.map(|n| n.eq_ignore_ascii_case(username))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.and_then(|u| u.get("Id").and_then(|i| i.as_str()).map(|s| s.to_string()))
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to list users: {}",
|
||||
response.status()
|
||||
));
|
||||
};
|
||||
|
||||
if let Some(id) = user_id {
|
||||
// 2. Delete user
|
||||
let delete_url = format!("{}/Users/{}", base_url, id);
|
||||
let response = self
|
||||
.reqwest_client
|
||||
.delete(&delete_url)
|
||||
.header("Authorization", &auth_header)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to delete user: {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ impl AppState {
|
||||
let federated_users = Arc::new(FederatedUserService::new_from_components(
|
||||
data_context.server_storage.clone(),
|
||||
data_context.user_authorization.clone(),
|
||||
reqwest_client.clone(),
|
||||
data_context.config.clone(),
|
||||
));
|
||||
|
||||
@@ -113,6 +112,11 @@ impl AppState {
|
||||
config.url_prefix.as_ref().map(|prefix| prefix.to_string())
|
||||
}
|
||||
|
||||
pub async fn get_admin_password(&self) -> String {
|
||||
let config = self.config.read().await;
|
||||
config.password.clone()
|
||||
}
|
||||
|
||||
pub async fn remove_prefix_from_path<'a>(&self, path: &'a str) -> &'a str {
|
||||
let config = self.config.read().await;
|
||||
if let Some(prefix) = &config.url_prefix {
|
||||
|
||||
@@ -8,9 +8,7 @@ use axum::{
|
||||
use serde::Deserialize;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
encryption::encrypt_password, server_storage::Server, url_helper::join_server_url, AppState,
|
||||
};
|
||||
use crate::{encryption::encrypt_password, server_storage::Server, AppState};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin/servers.html")]
|
||||
@@ -48,24 +46,6 @@ pub struct AddServerAdminForm {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AuthResponse {
|
||||
#[serde(rename = "User")]
|
||||
user: JellyfinUser,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JellyfinUser {
|
||||
#[serde(rename = "Policy")]
|
||||
policy: UserPolicy,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UserPolicy {
|
||||
#[serde(rename = "IsAdministrator")]
|
||||
is_administrator: bool,
|
||||
}
|
||||
|
||||
async fn render_server_list(state: &AppState) -> Result<String, String> {
|
||||
match state.server_storage.list_servers().await {
|
||||
Ok(servers) => {
|
||||
@@ -275,44 +255,29 @@ pub async fn add_server_admin(
|
||||
};
|
||||
|
||||
// 2. Verify credentials with upstream Jellyfin and check admin status
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap();
|
||||
let client_info = crate::config::CLIENT_INFO.clone();
|
||||
|
||||
let auth_url = join_server_url(&server.url, "/Users/AuthenticateByName");
|
||||
let body = serde_json::json!({
|
||||
"Username": form.username,
|
||||
"Pw": form.password
|
||||
});
|
||||
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
let client = match jellyfin_api::JellyfinClient::new(server.url.as_str(), client_info) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to create jellyfin client: {}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Html("<div style=\"background-color: #e74c3c; color: white; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem;\">Client error</div>"),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
match client
|
||||
.post(auth_url.as_str())
|
||||
.header("Authorization", auth_header)
|
||||
.json(&body)
|
||||
.send()
|
||||
.authenticate_by_name(&form.username, &form.password)
|
||||
.await
|
||||
{
|
||||
Ok(response) if response.status().is_success() => {
|
||||
// Parse response to check if user is admin
|
||||
let auth_response = match response.json::<AuthResponse>().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Failed to parse auth response: {}", e);
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Html("<div style=\"background-color: #e74c3c; color: white; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem;\">Invalid server response</div>"),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
Ok(user) => {
|
||||
// Check if user is admin
|
||||
let is_admin = user.policy.map(|p| p.is_administrator).unwrap_or(false);
|
||||
|
||||
if !auth_response.user.policy.is_administrator {
|
||||
if !is_admin {
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Html("<div style=\"background-color: #e74c3c; color: white; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem;\">User is not an administrator on this server</div>"),
|
||||
@@ -361,24 +326,12 @@ pub async fn add_server_admin(
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
if status == StatusCode::UNAUTHORIZED {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Html("<div style=\"background-color: #e74c3c; color: white; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem;\">Invalid credentials</div>"),
|
||||
)
|
||||
.into_response()
|
||||
} else {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Html(format!(
|
||||
"<div style=\"background-color: #e74c3c; color: white; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem;\">Upstream error: {}</div>",
|
||||
status
|
||||
)),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(jellyfin_api::error::Error::AuthenticationFailed(_)) => {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Html("<div style=\"background-color: #e74c3c; color: white; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem;\">Invalid credentials</div>"),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to authenticate with upstream: {}", e);
|
||||
|
||||
@@ -6,12 +6,13 @@ use axum::{
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{url_helper::join_server_url, AppState};
|
||||
use crate::AppState;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin/server_status.html")]
|
||||
pub struct ServerStatusTemplate {
|
||||
pub error_message: Option<String>,
|
||||
pub server_version: Option<String>,
|
||||
}
|
||||
|
||||
/// Check server status
|
||||
@@ -22,31 +23,31 @@ pub async fn check_server_status(
|
||||
// Get the server details first
|
||||
match state.server_storage.get_server_by_id(server_id).await {
|
||||
Ok(Some(server)) => {
|
||||
// Test connection to the server
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap();
|
||||
let client_info = crate::config::CLIENT_INFO.clone();
|
||||
|
||||
let status_url = join_server_url(&server.url, "/system/info/public");
|
||||
|
||||
match client.get(status_url.as_str()).send().await {
|
||||
Ok(response) if response.status().is_success() => {
|
||||
let client = match jellyfin_api::JellyfinClient::new(server.url.as_str(), client_info) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
let template = ServerStatusTemplate {
|
||||
error_message: None,
|
||||
error_message: Some(format!("Client error: {}", e)),
|
||||
server_version: None,
|
||||
};
|
||||
|
||||
match template.render() {
|
||||
return match template.render() {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(e) => {
|
||||
error!("Failed to render status template: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Template error").into_response()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(response) => {
|
||||
};
|
||||
|
||||
match client.get_public_system_info().await {
|
||||
Ok(info) => {
|
||||
let template = ServerStatusTemplate {
|
||||
error_message: Some(format!("HTTP {}", response.status().as_u16())),
|
||||
error_message: None,
|
||||
server_version: info.version,
|
||||
};
|
||||
|
||||
match template.render() {
|
||||
@@ -58,16 +59,9 @@ pub async fn check_server_status(
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = if e.is_timeout() {
|
||||
"Connection timeout".to_string()
|
||||
} else if e.is_connect() {
|
||||
"Connection refused".to_string()
|
||||
} else {
|
||||
format!("Network error: {e}")
|
||||
};
|
||||
|
||||
let template = ServerStatusTemplate {
|
||||
error_message: Some(error_msg),
|
||||
error_message: Some(format!("Error: {}", e)),
|
||||
server_version: None,
|
||||
};
|
||||
|
||||
match template.render() {
|
||||
|
||||
@@ -5,5 +5,8 @@
|
||||
{% else %}
|
||||
<span class="status-chip" style="background: rgba(40, 167, 69, 0.15); color: #28a745; font-weight: bold; padding: 0.25rem 0.75rem; border-radius: 1rem; font-size: 0.85rem; display: inline-flex; align-items: center; gap: 0.25rem;" title="Server is responding">
|
||||
<i class="fas fa-check-circle"></i> Online
|
||||
{% if let Some(ver) = server_version %}
|
||||
<span style="font-weight: normal; opacity: 0.8; font-size: 0.75em;">({{ ver }})</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
{% if let Some(username) = username %}
|
||||
<span style="color: var(--pico-primary-color);">
|
||||
<i class="fas fa-check-circle" style="margin-right: 0.25rem;"></i>
|
||||
{{ username }}
|
||||
{{ username }} - ({{ server_version }})
|
||||
</span>
|
||||
{% else %}
|
||||
{% if needs_login %}
|
||||
<span style="color: #f39c12;" data-tooltip="Please login via the Web UI to sync your session">
|
||||
<i class="fas fa-exclamation-triangle" style="margin-right: 0.25rem;"></i>
|
||||
Online (Login required)
|
||||
</span>
|
||||
{% else if let Some(error) = error_message %}
|
||||
{% if let Some(error) = error_message %}
|
||||
<span style="color: var(--pico-del-color);">
|
||||
<i class="fas fa-exclamation-circle" style="margin-right: 0.25rem;"></i>
|
||||
{{ error }}
|
||||
|
||||
@@ -4,38 +4,10 @@ use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use jellyfin_api::models::MediaFolder;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{ui::auth::AuthenticatedUser, url_helper::join_server_url, AppState};
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct MediaFolder {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
#[serde(rename = "CollectionType")]
|
||||
pub collection_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MediaFoldersResponse {
|
||||
#[serde(rename = "Items")]
|
||||
items: Vec<MediaFolder>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AuthResponse {
|
||||
#[serde(rename = "AccessToken")]
|
||||
access_token: String,
|
||||
#[serde(rename = "User")]
|
||||
user: JellyfinUser,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JellyfinUser {
|
||||
#[serde(rename = "Id")]
|
||||
id: String,
|
||||
}
|
||||
use crate::{ui::auth::AuthenticatedUser, AppState};
|
||||
|
||||
pub struct ServerLibraries {
|
||||
pub server_name: String,
|
||||
@@ -62,10 +34,6 @@ pub async fn get_user_media(
|
||||
};
|
||||
|
||||
let mut server_libraries: Vec<ServerLibraries> = Vec::new();
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Get all sessions for user once
|
||||
let sessions = match state
|
||||
@@ -90,125 +58,106 @@ pub async fn get_user_media(
|
||||
.filter(|(_, s)| s.id == server.id)
|
||||
.max_by_key(|(auth, _)| auth.updated_at);
|
||||
|
||||
let mut token = session.map(|(auth, _)| auth.jellyfin_token.clone());
|
||||
let token = session.map(|(auth, _)| auth.jellyfin_token.clone());
|
||||
|
||||
let client_info = crate::config::CLIENT_INFO.clone();
|
||||
|
||||
let client = match jellyfin_api::JellyfinClient::new(server.url.as_str(), client_info) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
server_libraries.push(ServerLibraries {
|
||||
server_name: server.name.clone(),
|
||||
libraries: Vec::new(),
|
||||
error: Some(format!("Client error: {}", e)),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// 1. Try to use existing token if available
|
||||
if let Some(t) = &token {
|
||||
let url = join_server_url(&server.url, "/Library/MediaFolders");
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\", Token=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
t
|
||||
);
|
||||
let mut needs_reauth = true;
|
||||
|
||||
match client
|
||||
.get(url.as_str())
|
||||
.header("Authorization", auth_header)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
match resp.json::<MediaFoldersResponse>().await {
|
||||
Ok(folders) => {
|
||||
libraries = folders.items;
|
||||
}
|
||||
Err(e) => {
|
||||
error_msg = Some(format!("Failed to parse: {}", e));
|
||||
}
|
||||
}
|
||||
if let Some(t) = &token {
|
||||
let client_with_token = client.clone().with_token(t.clone());
|
||||
match client_with_token.get_media_folders().await {
|
||||
Ok(folders) => {
|
||||
libraries = folders;
|
||||
needs_reauth = false;
|
||||
}
|
||||
Ok(resp) if resp.status() == StatusCode::FORBIDDEN || resp.status() == StatusCode::UNAUTHORIZED => {
|
||||
// Token expired, clear it to trigger re-login
|
||||
token = None;
|
||||
}
|
||||
Ok(resp) => {
|
||||
error_msg = Some(format!("HTTP {}", resp.status()));
|
||||
Err(jellyfin_api::error::Error::AuthenticationFailed(_)) => {
|
||||
// Token expired, needs reauth
|
||||
needs_reauth = true;
|
||||
}
|
||||
Err(e) => {
|
||||
error_msg = Some(format!("Network error: {}", e));
|
||||
error_msg = Some(format!("Error: {}", e));
|
||||
needs_reauth = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If no token or expired, try to login using mapping
|
||||
if token.is_none() && (libraries.is_empty() && error_msg.is_none() || error_msg.as_deref() == Some("HTTP 401") || error_msg.as_deref() == Some("HTTP 403")) {
|
||||
// Clear previous error if we are retrying
|
||||
error_msg = None;
|
||||
|
||||
match state.user_authorization.get_server_mapping(&user.id, &server.url.as_str()).await {
|
||||
if needs_reauth && error_msg.is_none() {
|
||||
match state
|
||||
.user_authorization
|
||||
.get_server_mapping(&user.id, server.url.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(Some(mapping)) => {
|
||||
// Decrypt password
|
||||
let config = state.config.read().await;
|
||||
let admin_password = &config.password;
|
||||
|
||||
let decrypted_password = state.user_authorization.decrypt_server_mapping_password(
|
||||
&mapping,
|
||||
&user.password,
|
||||
admin_password
|
||||
);
|
||||
|
||||
// Perform login
|
||||
let auth_url = join_server_url(&server.url, "/Users/AuthenticateByName");
|
||||
let body = serde_json::json!({
|
||||
"Username": mapping.mapped_username,
|
||||
"Pw": decrypted_password
|
||||
});
|
||||
let decrypted_password = state
|
||||
.user_authorization
|
||||
.decrypt_server_mapping_password(&mapping, &user.password, admin_password);
|
||||
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
match client
|
||||
.authenticate_by_name(&mapping.mapped_username, &decrypted_password)
|
||||
.await
|
||||
{
|
||||
Ok(user_info) => {
|
||||
// Store new session
|
||||
let auth = crate::models::Authorization {
|
||||
client: "Jellyswarrm Proxy".to_string(),
|
||||
device: "Server".to_string(),
|
||||
device_id: "jellyswarrm-proxy".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
token: None,
|
||||
};
|
||||
|
||||
match client.post(auth_url.as_str()).header("Authorization", auth_header).json(&body).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
match resp.json::<AuthResponse>().await {
|
||||
Ok(auth_resp) => {
|
||||
// Store new session
|
||||
let auth = crate::models::Authorization {
|
||||
client: "Jellyswarrm Proxy".to_string(),
|
||||
device: "Server".to_string(),
|
||||
device_id: "jellyswarrm-proxy".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
token: None,
|
||||
};
|
||||
|
||||
if let Err(e) = state.user_authorization.store_authorization_session(
|
||||
if let Some(new_token) = client.get_token() {
|
||||
if let Err(e) = state
|
||||
.user_authorization
|
||||
.store_authorization_session(
|
||||
&user.id,
|
||||
server.url.as_str(),
|
||||
&auth,
|
||||
auth_resp.access_token.clone(),
|
||||
auth_resp.user.id,
|
||||
None
|
||||
).await {
|
||||
error!("Failed to store session: {}", e);
|
||||
new_token.to_string(),
|
||||
user_info.id,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to store session: {}", e);
|
||||
}
|
||||
|
||||
// Retry fetch libraries
|
||||
match client.get_media_folders().await {
|
||||
Ok(folders) => {
|
||||
libraries = folders;
|
||||
}
|
||||
|
||||
// Fetch libraries with new token
|
||||
let url = join_server_url(&server.url, "/Library/MediaFolders");
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\", Token=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
auth_resp.access_token
|
||||
);
|
||||
|
||||
match client.get(url.as_str()).header("Authorization", auth_header).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
match resp.json::<MediaFoldersResponse>().await {
|
||||
Ok(folders) => {
|
||||
libraries = folders.items;
|
||||
}
|
||||
Err(e) => error_msg = Some(format!("Failed to parse: {}", e)),
|
||||
}
|
||||
}
|
||||
Ok(resp) => error_msg = Some(format!("HTTP {}", resp.status())),
|
||||
Err(e) => error_msg = Some(format!("Network error: {}", e)),
|
||||
Err(e) => {
|
||||
error_msg = Some(format!(
|
||||
"Error fetching libraries after login: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => error_msg = Some(format!("Login response error: {}", e)),
|
||||
}
|
||||
}
|
||||
Ok(resp) => error_msg = Some(format!("Login failed: HTTP {}", resp.status())),
|
||||
Err(e) => error_msg = Some(format!("Login network error: {}", e)),
|
||||
Err(e) => {
|
||||
error_msg = Some(format!("Login failed: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderValue, StatusCode},
|
||||
response::{Html, IntoResponse},
|
||||
Form,
|
||||
};
|
||||
use hyper::{header::HeaderValue, StatusCode};
|
||||
use jellyfin_api::JellyfinClient;
|
||||
use serde::Deserialize;
|
||||
use tracing::{error, info};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{
|
||||
models::Authorization, server_storage::Server, ui::auth::AuthenticatedUser,
|
||||
url_helper::join_server_url, AppState,
|
||||
};
|
||||
use crate::{server_storage::Server, ui::auth::AuthenticatedUser, AppState};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "user/user_server_list.html")]
|
||||
@@ -33,23 +31,7 @@ pub struct ConnectServerForm {
|
||||
pub struct UserServerStatusTemplate {
|
||||
pub username: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
pub needs_login: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AuthResponse {
|
||||
#[serde(rename = "AccessToken")]
|
||||
access_token: String,
|
||||
#[serde(rename = "User")]
|
||||
user: JellyfinUser,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JellyfinUser {
|
||||
#[serde(rename = "Id")]
|
||||
id: String,
|
||||
#[serde(rename = "Name")]
|
||||
name: String,
|
||||
pub server_version: String,
|
||||
}
|
||||
|
||||
pub async fn get_user_servers(
|
||||
@@ -120,43 +102,24 @@ pub async fn connect_server(
|
||||
};
|
||||
|
||||
// Verify credentials with upstream Jellyfin
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap();
|
||||
let server_url = server.url.clone();
|
||||
|
||||
let auth_url = join_server_url(&server.url, "/Users/AuthenticateByName");
|
||||
let body = serde_json::json!({
|
||||
"Username": form.username,
|
||||
"Pw": form.password
|
||||
});
|
||||
let client_info = crate::config::CLIENT_INFO.clone();
|
||||
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
|
||||
match client
|
||||
.post(auth_url.as_str())
|
||||
.header("Authorization", auth_header)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) if response.status().is_success() => {
|
||||
// Parse response to get token and user ID
|
||||
let auth_response = match response.json::<AuthResponse>().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Failed to parse auth response: {}", e);
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Html("<div style=\"background-color: #e74c3c; color: white; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem;\">Invalid server response</div>"),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let client = match JellyfinClient::new(server_url.as_str(), client_info) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to create jellyfin client: {}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Html("<span style=\"color: #dc3545;\">Client error</span>"),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
match client.authenticate_by_name(&form.username, &form.password).await {
|
||||
Ok(_) => {
|
||||
// Credentials valid, create mapping
|
||||
match state
|
||||
.user_authorization
|
||||
@@ -165,7 +128,7 @@ pub async fn connect_server(
|
||||
server.url.as_str(),
|
||||
&form.username,
|
||||
&form.password,
|
||||
None,
|
||||
Some(&user.password),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -175,31 +138,6 @@ pub async fn connect_server(
|
||||
user.username, server.name
|
||||
);
|
||||
|
||||
// Create authorization session
|
||||
let auth = Authorization {
|
||||
client: "Jellyswarrm Proxy".to_string(),
|
||||
device: "Server".to_string(),
|
||||
device_id: "jellyswarrm-proxy".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
token: None,
|
||||
};
|
||||
|
||||
if let Err(e) = state
|
||||
.user_authorization
|
||||
.store_authorization_session(
|
||||
&user.id,
|
||||
server.url.as_str(),
|
||||
&auth,
|
||||
auth_response.access_token,
|
||||
auth_response.user.id,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to store session: {}", e);
|
||||
// Continue anyway, as mapping was created
|
||||
}
|
||||
|
||||
// Return HX-Redirect header for HTMX
|
||||
let mut response = StatusCode::OK.into_response();
|
||||
response.headers_mut().insert(
|
||||
@@ -207,32 +145,23 @@ pub async fn connect_server(
|
||||
HeaderValue::from_str(&format!("/{}", state.get_ui_route().await)).unwrap(),
|
||||
);
|
||||
response
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to create mapping: {}", e);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Html("<div style=\"background-color: #e74c3c; color: white; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem;\">Database error</div>"),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to create mapping: {}", e);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Html("<div style=\"background-color: #e74c3c; color: white; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem;\">Database error</div>"),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
if status == StatusCode::UNAUTHORIZED {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Html("<div style=\"background-color: #e74c3c; color: white; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem;\">Invalid credentials</div>"),
|
||||
)
|
||||
.into_response()
|
||||
} else {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Html(format!("<div style=\"background-color: #e74c3c; color: white; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem;\">Upstream error: {}</div>", status)),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(jellyfin_api::error::Error::AuthenticationFailed(_)) => {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Html("<div style=\"background-color: #e74c3c; color: white; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem;\">Invalid credentials</div>"),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to authenticate with upstream: {}", e);
|
||||
@@ -333,15 +262,45 @@ pub async fn check_user_server_status(
|
||||
}
|
||||
};
|
||||
|
||||
// Check for existing session
|
||||
let sessions = match state
|
||||
// Always check public system info first to get version and name
|
||||
let server_url = server.url.clone();
|
||||
let client_info = crate::config::CLIENT_INFO.clone();
|
||||
|
||||
let (public_info, client) = match JellyfinClient::new(server_url.as_str(), client_info) {
|
||||
Ok(c) => match c.get_public_system_info().await {
|
||||
Ok(info) => (info, c),
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Html("<span style=\"color: #dc3545;\">Server offline or unreachable</span>"),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to create jellyfin client: {}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Html("<span style=\"color: #dc3545;\">Client error</span>"),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// Check for mapping and try to authenticate
|
||||
let mapping = match state
|
||||
.user_authorization
|
||||
.get_user_sessions(&user.id, None)
|
||||
.get_server_mapping(&user.id, server.url.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(s) => s,
|
||||
Ok(Some(m)) => m,
|
||||
Ok(None) => return (
|
||||
StatusCode::OK,
|
||||
Html("<span style=\"color: #dc3545;\">No mapping found for user on this server</span>"),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => {
|
||||
error!("Failed to get sessions: {}", e);
|
||||
error!("Failed to get server mapping: {}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Html("<span style=\"color: #dc3545;\">Database error</span>"),
|
||||
@@ -350,80 +309,51 @@ pub async fn check_user_server_status(
|
||||
}
|
||||
};
|
||||
|
||||
// Find session for this server
|
||||
let session = sessions
|
||||
.iter()
|
||||
.filter(|(_, s)| s.id == server.id)
|
||||
.max_by_key(|(auth, _)| auth.updated_at);
|
||||
let admin_password = state.get_admin_password().await;
|
||||
|
||||
if let Some((auth, _)) = session {
|
||||
// Try to get profile with token
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap();
|
||||
let password = state.user_authorization.decrypt_server_mapping_password(
|
||||
&mapping,
|
||||
&user.password,
|
||||
&admin_password,
|
||||
);
|
||||
|
||||
let profile_url = join_server_url(&server.url, "/Users/Me");
|
||||
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\", Token=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
auth.jellyfin_token
|
||||
);
|
||||
|
||||
match client
|
||||
.get(profile_url.as_str())
|
||||
.header("Authorization", auth_header)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) if response.status().is_success() => {
|
||||
match response.json::<JellyfinUser>().await {
|
||||
Ok(profile) => {
|
||||
let template = UserServerStatusTemplate {
|
||||
username: Some(profile.name),
|
||||
error_message: None,
|
||||
needs_login: false,
|
||||
};
|
||||
match template.render() {
|
||||
Ok(html) => return Html(html).into_response(),
|
||||
Err(e) => error!("Template error: {}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Failed to parse profile: {}", e),
|
||||
match client
|
||||
.authenticate_by_name(&mapping.mapped_username, &password)
|
||||
.await
|
||||
{
|
||||
Ok(jellyfin_user) => {
|
||||
let template = UserServerStatusTemplate {
|
||||
username: Some(jellyfin_user.name),
|
||||
error_message: None,
|
||||
server_version: public_info.version.unwrap_or("unknown".to_string()),
|
||||
};
|
||||
if let Err(e) = client.logout().await {
|
||||
warn!("Failed to logout from upstream server: {}", e);
|
||||
}
|
||||
match template.render() {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(e) => {
|
||||
error!("Failed to render user server status template: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Html("<span style=\"color: #dc3545;\">Template error</span>"),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Token might be expired or invalid, fall through to public check
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Check if server is online (public info)
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let status_url = join_server_url(&server.url, "/system/info/public");
|
||||
|
||||
let (msg, needs_login) = match client.get(status_url.as_str()).send().await {
|
||||
Ok(resp) if resp.status().is_success() => ("Online".to_string(), true),
|
||||
Ok(resp) => (format!("HTTP {}", resp.status().as_u16()), false),
|
||||
Err(_) => ("Offline".to_string(), false),
|
||||
};
|
||||
|
||||
let template = UserServerStatusTemplate {
|
||||
username: None,
|
||||
error_message: Some(msg),
|
||||
needs_login,
|
||||
};
|
||||
|
||||
match template.render() {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(e) => {
|
||||
error!("Template error: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Template error").into_response()
|
||||
// Auth failed, log it but continue to check existing session
|
||||
tracing::warn!(
|
||||
"Failed to authenticate with mapped credentials for server {}: {}",
|
||||
server.id,
|
||||
e
|
||||
);
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Html("<span style=\"color: #dc3545;\">Failed to log in with provided credentials</span>"),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user