feat(dl): add Transmission RPC client

This commit is contained in:
Miguel Palhas
2026-08-22 19:46:16 +01:00
parent 7b57c387fb
commit 16a22b8712
6 changed files with 930 additions and 23 deletions
Generated
+562 -17
View File
@@ -45,10 +45,24 @@ dependencies = [
[[package]]
name = "arr-dl"
version = "0.1.0"
dependencies = [
"base64",
"reqwest",
"serde",
"serde_json",
"thiserror",
"tokio",
"url",
]
[[package]]
name = "arr-e2e"
version = "0.1.0"
dependencies = [
"arr-dl",
"tokio",
"uuid",
]
[[package]]
name = "arr-indexer"
@@ -75,6 +89,12 @@ dependencies = [
"num-traits",
]
[[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"
@@ -145,6 +165,23 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chacha20"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
name = "chrono"
version = "0.4.45"
@@ -177,6 +214,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.4.0"
@@ -429,8 +475,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@@ -440,8 +488,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"rand_core 0.10.1",
"wasm-bindgen",
]
[[package]]
@@ -509,6 +560,104 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "http"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "http-body"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
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 = "hyper"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"httparse",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
@@ -647,6 +796,12 @@ dependencies = [
"hashbrown 0.17.1",
]
[[package]]
name = "ipnet"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
[[package]]
name = "itoa"
version = "1.0.18"
@@ -735,6 +890,12 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "md-5"
version = "0.10.6"
@@ -773,7 +934,7 @@ dependencies = [
"num-integer",
"num-iter",
"num-traits",
"rand",
"rand 0.8.7",
"smallvec",
"zeroize",
]
@@ -923,6 +1084,62 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quinn"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
dependencies = [
"bytes",
"getrandom 0.4.3",
"lru-slab",
"rand 0.10.2",
"rand_pcg",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.61.2",
]
[[package]]
name = "quote"
version = "1.0.47"
@@ -946,7 +1163,18 @@ checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom 0.4.3",
"rand_core 0.10.1",
]
[[package]]
@@ -956,7 +1184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
@@ -968,6 +1196,21 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_pcg"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -986,6 +1229,58 @@ dependencies = [
"bitflags",
]
[[package]]
name = "reqwest"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rsa"
version = "0.9.10"
@@ -999,13 +1294,19 @@ dependencies = [
"num-traits",
"pkcs1",
"pkcs8",
"rand_core",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rustc-hash"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
[[package]]
name = "rustix"
version = "1.1.4"
@@ -1019,6 +1320,41 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.23"
@@ -1099,7 +1435,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -1110,7 +1446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -1137,7 +1473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
@@ -1300,7 +1636,7 @@ dependencies = [
"memchr",
"once_cell",
"percent-encoding",
"rand",
"rand 0.8.7",
"rsa",
"serde",
"sha1",
@@ -1339,7 +1675,7 @@ dependencies = [
"md-5",
"memchr",
"once_cell",
"rand",
"rand 0.8.7",
"serde",
"serde_json",
"sha2",
@@ -1421,6 +1757,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
version = "0.13.2"
@@ -1517,6 +1862,16 @@ dependencies = [
"syn 3.0.3",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.19"
@@ -1528,6 +1883,51 @@ dependencies = [
"tokio",
]
[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
"bitflags",
"bytes",
"futures-util",
"http",
"http-body",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
"url",
]
[[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"
@@ -1560,6 +1960,12 @@ 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 = "typenum"
version = "1.20.1"
@@ -1593,6 +1999,12 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.8"
@@ -1611,6 +2023,18 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc"
dependencies = [
"getrandom 0.4.3",
"js-sys",
"serde_core",
"wasm-bindgen",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -1623,6 +2047,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[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"
@@ -1648,6 +2081,16 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.127"
@@ -1680,6 +2123,35 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "whoami"
version = "1.6.1"
@@ -1755,7 +2227,16 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
@@ -1773,13 +2254,29 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"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]]
@@ -1788,42 +2285,90 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[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_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "writeable"
version = "0.6.4"
+2
View File
@@ -26,6 +26,7 @@ axum = "0.8"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "time", "net", "io-util", "fs", "signal", "process"] }
tower-http = { version = "0.6", features = ["trace"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
base64 = "0.22"
# Persistence
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "sqlite", "macros", "migrate", "chrono", "json"] }
@@ -51,6 +52,7 @@ clap = { version = "4.5", features = ["derive", "env"] }
thiserror = "2"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
url = "2"
uuid = { version = "1", features = ["v4", "serde"] }
# Test-only
+7
View File
@@ -7,6 +7,13 @@ repository.workspace = true
publish = false
[dependencies]
base64 = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
url = { workspace = true }
[lints]
workspace = true
+297 -1
View File
@@ -1 +1,297 @@
//! arr-dl — see DESIGN.md.
//! Transmission RPC client for download lifecycle operations.
use std::path::PathBuf;
use std::sync::Arc;
use base64::Engine as _;
use reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tokio::sync::RwLock;
const SESSION_HEADER: &str = "x-transmission-session-id";
const MAX_SESSION_NEGOTIATIONS: usize = 4;
/// A magnet URI or the bytes of a `.torrent` file.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TorrentSource {
Magnet(String),
Metainfo(Vec<u8>),
}
/// Settings applied atomically when a torrent is added.
#[derive(Clone, Debug, PartialEq)]
pub struct AddTorrent {
pub source: TorrentSource,
pub label: String,
pub download_dir: PathBuf,
pub seed_ratio_limit: f64,
/// Transmission expresses its idle limit in minutes.
pub seed_idle_limit_minutes: u64,
}
/// The identity returned by Transmission for an added or existing torrent.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AddedTorrent {
pub id: i64,
pub name: String,
pub hash: String,
pub was_duplicate: bool,
}
/// Transmission's torrent activity state.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TorrentState {
Stopped,
QueuedToVerify,
Verifying,
QueuedToDownload,
Downloading,
QueuedToSeed,
Seeding,
}
/// Current torrent state, read directly from Transmission.
#[derive(Clone, Debug, PartialEq)]
pub struct Torrent {
pub id: i64,
pub name: String,
pub hash: String,
pub state: TorrentState,
pub progress: f64,
pub download_dir: PathBuf,
pub labels: Vec<String>,
}
/// A Transmission RPC failure.
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("invalid Transmission RPC URL: {0}")]
InvalidUrl(#[from] url::ParseError),
#[error("Transmission request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("Transmission did not return a session ID after {0} attempts")]
SessionNegotiation(usize),
#[error("Transmission RPC failed: {0}")]
Rpc(String),
#[error("invalid Transmission response: {0}")]
InvalidResponse(String),
}
/// Client for the unauthenticated Transmission JSON-RPC endpoint.
#[derive(Clone, Debug)]
pub struct TransmissionClient {
endpoint: Url,
http: reqwest::Client,
session_id: Arc<RwLock<Option<String>>>,
}
impl TransmissionClient {
/// Create a client for a full RPC URL, such as
/// `http://host:9091/transmission/rpc`.
///
/// # Errors
///
/// Returns an error when `endpoint` is not a valid URL.
pub fn new(endpoint: &str) -> Result<Self, Error> {
Ok(Self {
endpoint: Url::parse(endpoint)?,
http: reqwest::Client::new(),
session_id: Arc::new(RwLock::new(None)),
})
}
/// Add a torrent with its label, destination, and tracker seed limits.
///
/// # Errors
///
/// Returns an error for transport failures or rejected/malformed RPC
/// responses.
pub async fn add_torrent(&self, request: AddTorrent) -> Result<AddedTorrent, Error> {
let mut arguments = json!({
"download-dir": request.download_dir,
"labels": [request.label],
"seedRatioLimit": request.seed_ratio_limit,
"seedRatioMode": 1,
"seedIdleLimit": request.seed_idle_limit_minutes,
"seedIdleMode": 1,
});
match request.source {
TorrentSource::Magnet(uri) => arguments["filename"] = json!(uri),
TorrentSource::Metainfo(bytes) => {
arguments["metainfo"] =
json!(base64::engine::general_purpose::STANDARD.encode(bytes));
}
}
let arguments = self.call("torrent-add", arguments).await?;
let (torrent, was_duplicate) = if let Some(value) = arguments.get("torrent-added") {
(value.clone(), false)
} else if let Some(value) = arguments.get("torrent-duplicate") {
(value.clone(), true)
} else {
return Err(Error::InvalidResponse(
"torrent-add omitted torrent-added and torrent-duplicate".into(),
));
};
let torrent: RpcAddedTorrent = serde_json::from_value(torrent)
.map_err(|error| Error::InvalidResponse(error.to_string()))?;
Ok(AddedTorrent {
id: torrent.id,
name: torrent.name,
hash: torrent.hash,
was_duplicate,
})
}
/// List torrents with their current state and completion progress.
///
/// No torrent state is cached: every result is authoritative from
/// Transmission, including the first call after process startup.
///
/// # Errors
///
/// Returns an error for transport failures or rejected/malformed RPC
/// responses.
pub async fn list_torrents(&self) -> Result<Vec<Torrent>, Error> {
let arguments = self
.call(
"torrent-get",
json!({
"fields": [
"id", "name", "hashString", "status", "percentDone",
"downloadDir", "labels"
]
}),
)
.await?;
let response: RpcTorrentList = serde_json::from_value(arguments)
.map_err(|error| Error::InvalidResponse(error.to_string()))?;
response
.torrents
.into_iter()
.map(Torrent::try_from)
.collect()
}
/// Remove one torrent, optionally deleting its downloaded data.
///
/// # Errors
///
/// Returns an error for transport failures or rejected RPC responses.
pub async fn remove_torrent(&self, id: i64, delete_data: bool) -> Result<(), Error> {
self.call(
"torrent-remove",
json!({"ids": [id], "delete-local-data": delete_data}),
)
.await?;
Ok(())
}
async fn call(&self, method: &str, arguments: Value) -> Result<Value, Error> {
let body = RpcRequest { method, arguments };
for _ in 0..MAX_SESSION_NEGOTIATIONS {
let session_id = self.session_id.read().await.clone();
let mut request = self.http.post(self.endpoint.clone()).json(&body);
if let Some(session_id) = session_id {
request = request.header(SESSION_HEADER, session_id);
}
let response = request.send().await?;
if response.status() == StatusCode::CONFLICT {
let session_id = response
.headers()
.get(SESSION_HEADER)
.and_then(|value| value.to_str().ok())
.ok_or_else(|| {
Error::InvalidResponse("409 response omitted session ID".into())
})?;
*self.session_id.write().await = Some(session_id.to_owned());
continue;
}
let response = response.error_for_status()?.json::<RpcResponse>().await?;
if response.result != "success" {
return Err(Error::Rpc(response.result));
}
return Ok(response.arguments);
}
Err(Error::SessionNegotiation(MAX_SESSION_NEGOTIATIONS))
}
}
#[derive(Debug, Serialize)]
struct RpcRequest<'a> {
method: &'a str,
arguments: Value,
}
#[derive(Debug, Deserialize)]
struct RpcResponse {
result: String,
arguments: Value,
}
#[derive(Debug, Deserialize)]
struct RpcAddedTorrent {
id: i64,
name: String,
#[serde(rename = "hashString")]
hash: String,
}
#[derive(Debug, Deserialize)]
struct RpcTorrentList {
torrents: Vec<RpcTorrent>,
}
#[derive(Debug, Deserialize)]
struct RpcTorrent {
id: i64,
name: String,
#[serde(rename = "hashString")]
hash: String,
status: u8,
#[serde(rename = "percentDone")]
progress: f64,
#[serde(rename = "downloadDir")]
download_dir: PathBuf,
#[serde(default)]
labels: Vec<String>,
}
impl TryFrom<RpcTorrent> for Torrent {
type Error = Error;
fn try_from(value: RpcTorrent) -> Result<Self, Self::Error> {
let state = match value.status {
0 => TorrentState::Stopped,
1 => TorrentState::QueuedToVerify,
2 => TorrentState::Verifying,
3 => TorrentState::QueuedToDownload,
4 => TorrentState::Downloading,
5 => TorrentState::QueuedToSeed,
6 => TorrentState::Seeding,
status => {
return Err(Error::InvalidResponse(format!(
"unknown torrent status {status}"
)));
}
};
Ok(Self {
id: value.id,
name: value.name,
hash: value.hash,
state,
progress: value.progress,
download_dir: value.download_dir,
labels: value.labels,
})
}
}
+5
View File
@@ -8,5 +8,10 @@ publish = false
[dependencies]
[dev-dependencies]
arr-dl = { workspace = true }
tokio = { workspace = true }
uuid = { workspace = true }
[lints]
workspace = true
+57 -5
View File
@@ -5,9 +5,61 @@
#[cfg(test)]
mod tests {
/// Placeholder, same reason as the one in `arr-core`: `cargo nextest`
/// fails a zero-test run, and `--no-tests=pass` would let a broken filter
/// go green later. Replaced by the real harness in the e2e issue.
#[test]
fn e2e_crate_builds() {}
use std::path::PathBuf;
use arr_dl::{AddTorrent, TorrentSource, TransmissionClient};
#[tokio::test]
async fn transmission_add_list_and_remove() {
let endpoint = std::env::var("TRANSMISSION_RPC_URL")
.unwrap_or_else(|_| "http://127.0.0.1:9091/transmission/rpc".into());
let client = TransmissionClient::new(&endpoint).expect("valid endpoint");
let name = format!("arr-e2e-{}", uuid::Uuid::new_v4());
let metainfo = torrent_with_name(&name);
let download_dir = PathBuf::from("/tmp/arr-e2e");
let request = || AddTorrent {
source: TorrentSource::Metainfo(metainfo.clone()),
label: "movies-main".into(),
download_dir: download_dir.clone(),
seed_ratio_limit: 1.5,
seed_idle_limit_minutes: 60,
};
let first = client.add_torrent(request()).await.expect("add torrent");
assert!(!first.was_duplicate);
let torrents = client.list_torrents().await.expect("list torrents");
let listed = torrents
.iter()
.find(|torrent| torrent.id == first.id)
.expect("added torrent is authoritative in list");
assert_eq!(listed.name, name);
assert_eq!(listed.hash, first.hash);
assert_eq!(listed.download_dir, download_dir);
assert_eq!(listed.labels, ["movies-main"]);
assert!((0.0..=1.0).contains(&listed.progress));
client
.remove_torrent(first.id, false)
.await
.expect("remove without data");
let second = client.add_torrent(request()).await.expect("add again");
client
.remove_torrent(second.id, true)
.await
.expect("remove with data");
let torrents = client.list_torrents().await.expect("list after remove");
assert!(torrents.iter().all(|torrent| torrent.id != second.id));
}
fn torrent_with_name(name: &str) -> Vec<u8> {
let piece_hash = [0_u8; 20];
let mut bytes = format!("d4:infod6:lengthi1e4:name{}:{name}", name.len()).into_bytes();
bytes.extend_from_slice(b"12:piece lengthi16384e6:pieces20:");
bytes.extend_from_slice(&piece_hash);
bytes.extend_from_slice(b"ee");
bytes
}
}