Compare commits
2 Commits
abd399aacd
...
70932900cd
Author | SHA1 | Date | |
---|---|---|---|
70932900cd | |||
393f704e52 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -255,4 +255,7 @@ public/robots.txt
|
||||
public/sitemap*.xml
|
||||
|
||||
# Custom
|
||||
/data
|
||||
/data
|
||||
|
||||
patches/*
|
||||
!patches/.gitkeep
|
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
@ -94,31 +94,31 @@
|
||||
"name": "Next.js: debug client-side (app)",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000"
|
||||
"url": "http://localhost:5000"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side (web)",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3001"
|
||||
"url": "http://localhost:5001"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side (api)",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3002"
|
||||
"url": "http://localhost:5002"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side (email)",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3003"
|
||||
"url": "http://localhost:5003"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side (app)",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3004"
|
||||
"url": "http://localhost:5004"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug full stack",
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -28,5 +28,6 @@
|
||||
"emmet.showExpandedAbbreviation": "never",
|
||||
"prettier.enable": false,
|
||||
"tailwindCSS.experimental.configFile": "./packages/tailwind-config/config.ts",
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"rust-analyzer.cargo.features": ["testcontainers"]
|
||||
}
|
||||
|
335
Cargo.lock
generated
335
Cargo.lock
generated
@ -246,6 +246,15 @@ dependencies = [
|
||||
"quick-xml 0.37.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@ -550,9 +559,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bollard"
|
||||
version = "0.17.1"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d41711ad46fda47cd701f6908e59d1bd6b9a2b7464c0d0aeab95c6d37096ff8a"
|
||||
checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bollard-stubs",
|
||||
@ -579,7 +588,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"serde_urlencoded",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.9",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
@ -589,9 +598,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bollard-stubs"
|
||||
version = "1.45.0-rc.26.0.1"
|
||||
version = "1.47.1-rc.27.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d7c5415e3a6bc6d3e99eff6268e488fd4ee25e7b28c10f08fa6760bd9de16e4"
|
||||
checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_repr",
|
||||
@ -887,6 +896,21 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "conquer-once"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d008a441c0f269f36ca13712528069a86a3e60dffee1d98b976eb3b0b2160b4"
|
||||
dependencies = [
|
||||
"conquer-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "conquer-util"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e763eef8846b13b380f37dfecda401770b0ca4e56e95170237bd7c25c7db3582"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.10"
|
||||
@ -956,6 +980,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@ -1471,6 +1505,18 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.92",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@ -1536,6 +1582,22 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "figment"
|
||||
version = "0.10.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
|
||||
dependencies = [
|
||||
"atomic",
|
||||
"pear",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"toml",
|
||||
"uncased",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.25"
|
||||
@ -1936,6 +1998,51 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hickory-proto"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "447afdcdb8afb9d0a852af6dc65d9b285ce720ed7a59e42a8bf2e931c67bc1b5"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
"enum-as-inner",
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"idna 1.0.3",
|
||||
"ipnet",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"thiserror 1.0.69",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a2e2aba9c389ce5267d31cf1e4dace82390ae276b0b364ea55630b1fa1b44b4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"hickory-proto",
|
||||
"ipconfig",
|
||||
"lru-cache",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"rand",
|
||||
"resolv-conf",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
@ -1963,6 +2070,17 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"match_cfg",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.4.0"
|
||||
@ -2122,6 +2240,7 @@ dependencies = [
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
@ -2431,6 +2550,12 @@ dependencies = [
|
||||
"syn 2.0.92",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inlinable_string"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.41.1"
|
||||
@ -2459,6 +2584,18 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipconfig"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
|
||||
dependencies = [
|
||||
"socket2",
|
||||
"widestring",
|
||||
"windows-sys 0.48.0",
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.10.1"
|
||||
@ -2524,6 +2661,17 @@ version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||
|
||||
[[package]]
|
||||
name = "java-properties"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37bf6f484471c451f2b51eabd9e66b3fa7274550c5ec4b6c3d6070840945117f"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.32"
|
||||
@ -2753,7 +2901,7 @@ dependencies = [
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hostname",
|
||||
"hostname 0.4.0",
|
||||
"httpdate",
|
||||
"idna 1.0.3",
|
||||
"mime",
|
||||
@ -3024,6 +3172,15 @@ version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "lru-cache"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@ -3050,6 +3207,12 @@ dependencies = [
|
||||
"tendril",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "match_cfg"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@ -3196,7 +3359,7 @@ dependencies = [
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework 2.11.1",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
@ -3636,6 +3799,29 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pear"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
|
||||
dependencies = [
|
||||
"inlinable_string",
|
||||
"pear_codegen",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pear_codegen"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"quote",
|
||||
"syn 2.0.92",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.4"
|
||||
@ -3976,7 +4162,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "qbit-rs"
|
||||
version = "0.4.6"
|
||||
source = "git+https://github.com/lonelyhentxi/qbit.git?rev=a2c70aa#a2c70aa391d5edc2ab79c92fa8dcfec00d0d714b"
|
||||
source = "git+https://github.com/lonelyhentxi/qbit.git?rev=72d53138ebe#72d53138ebe1e3de49be46edc213ea9cb7345e55"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"mod_use",
|
||||
@ -4008,6 +4194,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.36.2"
|
||||
@ -4088,6 +4280,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -4175,24 +4368,31 @@ dependencies = [
|
||||
name = "recorder"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-auth",
|
||||
"bollard",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"eyre",
|
||||
"fancy-regex",
|
||||
"figment",
|
||||
"futures",
|
||||
"html-escape",
|
||||
"insta",
|
||||
"itertools 0.13.0",
|
||||
"jwt-authorizer",
|
||||
"lazy_static",
|
||||
"leaky-bucket",
|
||||
"librqbit-core",
|
||||
"lightningcss",
|
||||
"loco-rs",
|
||||
"log",
|
||||
"maplit",
|
||||
"once_cell",
|
||||
"opendal",
|
||||
"qbit-rs",
|
||||
"quirks_path",
|
||||
"regex",
|
||||
"reqwest",
|
||||
@ -4207,9 +4407,10 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"serial_test",
|
||||
"testcontainers",
|
||||
"testcontainers-modules",
|
||||
"thiserror 2.0.9",
|
||||
"tokio",
|
||||
"torrent",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
@ -4340,6 +4541,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"hickory-resolver",
|
||||
"http 1.2.0",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
@ -4358,6 +4560,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
@ -4443,6 +4646,16 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resolv-conf"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
|
||||
dependencies = [
|
||||
"hostname 0.3.1",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "retry-policies"
|
||||
version = "0.4.0"
|
||||
@ -4630,15 +4843,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.7.3"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
|
||||
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework 3.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4936,7 +5148,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81d3f8c9bfcc3cbb6b0179eb57042d75b1582bdc65c3cb95f3fa999509c03cbc"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation 0.10.0",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
@ -4986,6 +5211,17 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-java-properties"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8b5db85b934578ef8a8acc8ef7956b313d9e920d4d4160ef7862bd4c85d4bc7"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"java-properties",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-value"
|
||||
version = "0.7.0"
|
||||
@ -5216,6 +5452,16 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
@ -5755,7 +6001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
@ -5841,13 +6087,13 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "testcontainers"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f40cc2bd72e17f328faf8ca7687fe337e61bccd8acf9674fa78dd3792b045e1"
|
||||
source = "git+https://github.com/testcontainers/testcontainers-rs.git?rev=af21727#af2172714bbb79c6ce648b699135922f85cafc0c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bollard",
|
||||
"bollard-stubs",
|
||||
"bytes",
|
||||
"conquer-once",
|
||||
"docker_credential",
|
||||
"either",
|
||||
"etcetera",
|
||||
@ -5856,14 +6102,18 @@ dependencies = [
|
||||
"memchr",
|
||||
"parse-display",
|
||||
"pin-project-lite",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde-java-properties",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"thiserror 1.0.69",
|
||||
"signal-hook",
|
||||
"thiserror 2.0.9",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-tar",
|
||||
"tokio-util",
|
||||
"ulid",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -6125,30 +6375,6 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "torrent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"eyre",
|
||||
"futures",
|
||||
"itertools 0.13.0",
|
||||
"lazy_static",
|
||||
"librqbit-core",
|
||||
"qbit-rs",
|
||||
"quirks_path",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"testcontainers",
|
||||
"testcontainers-modules",
|
||||
"thiserror 2.0.9",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
@ -6376,6 +6602,15 @@ dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uncased"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
@ -6767,6 +7002,12 @@ dependencies = [
|
||||
"wasite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@ -7004,6 +7245,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "write16"
|
||||
version = "1.0.0"
|
||||
|
@ -1,3 +1,9 @@
|
||||
[workspace]
|
||||
members = ["apps/recorder", "packages/quirks-path", "packages/torrent"]
|
||||
members = ["apps/recorder", "packages/quirks-path"]
|
||||
resolver = "2"
|
||||
|
||||
[patch.crates-io]
|
||||
testcontainers = { git = "https://github.com/testcontainers/testcontainers-rs.git", rev = "af21727" }
|
||||
|
||||
# [patch."https://github.com/lonelyhentxi/qbit.git"]
|
||||
# qbit-rs = { path = "./patches/qbit-rs" }
|
||||
|
@ -1,14 +0,0 @@
|
||||
# Server
|
||||
DATABASE_URL="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu"
|
||||
BETTERSTACK_API_KEY=""
|
||||
BETTERSTACK_URL=""
|
||||
FLAGS_SECRET=""
|
||||
ARCJET_KEY=""
|
||||
SVIX_TOKEN=""
|
||||
LIVEBLOCKS_SECRET=""
|
||||
|
||||
# Client
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_WEB_URL="http://localhost:3001"
|
||||
NEXT_PUBLIC_DOCS_URL="http://localhost:3004"
|
||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://webui.konobangu.com"
|
@ -1,15 +0,0 @@
|
||||
# Server
|
||||
BETTER_AUTH_SECRET=""
|
||||
DATABASE_URL=""
|
||||
BETTERSTACK_API_KEY=""
|
||||
BETTERSTACK_URL=""
|
||||
FLAGS_SECRET=""
|
||||
ARCJET_KEY=""
|
||||
SVIX_TOKEN=""
|
||||
LIVEBLOCKS_SECRET=""
|
||||
|
||||
# Client
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_WEB_URL="http://localhost:3001"
|
||||
NEXT_PUBLIC_DOCS_URL="http://localhost:3004"
|
||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3000"
|
45
apps/api/.gitignore
vendored
45
apps/api/.gitignore
vendored
@ -1,45 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# prisma
|
||||
.env
|
||||
|
||||
# react.email
|
||||
.react-email
|
||||
|
||||
# Sentry
|
||||
.sentryclirc
|
Binary file not shown.
Before Width: | Height: | Size: 216 B |
@ -1,29 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@konobangu/design-system/components/ui/button';
|
||||
import { fonts } from '@konobangu/design-system/lib/fonts';
|
||||
import { captureException } from '@sentry/nextjs';
|
||||
import type NextError from 'next/error';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type GlobalErrorProperties = {
|
||||
readonly error: NextError & { digest?: string };
|
||||
readonly reset: () => void;
|
||||
};
|
||||
|
||||
const GlobalError = ({ error, reset }: GlobalErrorProperties) => {
|
||||
useEffect(() => {
|
||||
captureException(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html lang="en" className={fonts}>
|
||||
<body>
|
||||
<h1>Oops, something went wrong</h1>
|
||||
<Button onClick={() => reset()}>Try again</Button>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalError;
|
Binary file not shown.
Before Width: | Height: | Size: 96 B |
@ -1,13 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type RootLayoutProperties = {
|
||||
readonly children: ReactNode;
|
||||
};
|
||||
|
||||
const RootLayout = ({ children }: RootLayoutProperties) => (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
export default RootLayout;
|
Binary file not shown.
Before Width: | Height: | Size: 57 KiB |
@ -1,3 +0,0 @@
|
||||
import { initializeSentry } from '@konobangu/next-config/instrumentation';
|
||||
|
||||
export const register = initializeSentry();
|
@ -1,15 +0,0 @@
|
||||
import { env } from '@konobangu/env';
|
||||
import { config, withAnalyzer, withSentry } from '@konobangu/next-config';
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
let nextConfig: NextConfig = { ...config };
|
||||
|
||||
if (env.VERCEL) {
|
||||
nextConfig = withSentry(nextConfig);
|
||||
}
|
||||
|
||||
if (env.ANALYZE === 'true') {
|
||||
nextConfig = withAnalyzer(nextConfig);
|
||||
}
|
||||
|
||||
export default nextConfig;
|
@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "api",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "concurrently \"pnpm:next\"",
|
||||
"next": "next dev -p 3002 --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"analyze": "ANALYZE=true pnpm build",
|
||||
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||
},
|
||||
"dependencies": {
|
||||
"@konobangu/analytics": "workspace:*",
|
||||
"@konobangu/auth": "workspace:*",
|
||||
"@konobangu/database": "workspace:*",
|
||||
"@konobangu/design-system": "workspace:*",
|
||||
"@konobangu/env": "workspace:*",
|
||||
"@konobangu/next-config": "workspace:*",
|
||||
"@konobangu/observability": "workspace:*",
|
||||
"@sentry/nextjs": "^8.43.0",
|
||||
"import-in-the-middle": "^1.11.3",
|
||||
"next": "^15.1.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"require-in-the-middle": "^7.4.0",
|
||||
"svix": "^1.43.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@konobangu/typescript-config": "workspace:*",
|
||||
"@types/node": "22.10.1",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"concurrently": "^9.1.0",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* This file configures the initialization of Sentry on the client.
|
||||
* The config you add here will be used whenever a users loads a page in their browser.
|
||||
* https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
*/
|
||||
|
||||
import { init, replayIntegration } from '@sentry/nextjs';
|
||||
|
||||
init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
|
||||
replaysOnErrorSampleRate: 1,
|
||||
|
||||
/*
|
||||
* This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
* in development and sample at a lower rate in production
|
||||
*/
|
||||
replaysSessionSampleRate: 0.1,
|
||||
|
||||
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
|
||||
integrations: [
|
||||
replayIntegration({
|
||||
// Additional Replay configuration goes in here, for example:
|
||||
maskAllText: true,
|
||||
blockAllMedia: true,
|
||||
}),
|
||||
],
|
||||
});
|
@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": "@konobangu/typescript-config/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@konobangu/*": ["../../packages/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"next.config.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
]
|
||||
}
|
@ -23,7 +23,7 @@ SVIX_TOKEN=""
|
||||
LIVEBLOCKS_SECRET=""
|
||||
|
||||
# Client
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_WEB_URL="http://localhost:3001"
|
||||
NEXT_PUBLIC_DOCS_URL="http://localhost:3004"
|
||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://webui.konobangu.com"
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:5000"
|
||||
NEXT_PUBLIC_WEB_URL="http://localhost:5001"
|
||||
NEXT_PUBLIC_DOCS_URL="http://localhost:5004"
|
||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://konobangu.com"
|
@ -23,7 +23,7 @@ SVIX_TOKEN=""
|
||||
LIVEBLOCKS_SECRET=""
|
||||
|
||||
# WEBUI
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_WEB_URL="http://localhost:3001"
|
||||
NEXT_PUBLIC_DOCS_URL="http://localhost:3004"
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:5000"
|
||||
NEXT_PUBLIC_WEB_URL="http://localhost:5001"
|
||||
NEXT_PUBLIC_DOCS_URL="http://localhost:5004"
|
||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://konobangu.com"
|
@ -2,7 +2,7 @@
|
||||
"name": "app",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000 --turbopack",
|
||||
"dev": "next dev -p 5000 --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"analyze": "ANALYZE=true pnpm build",
|
||||
|
@ -28,11 +28,11 @@ Step 2. Go to the docs are located (where you can find `mint.json`) and run the
|
||||
mintlify dev
|
||||
```
|
||||
|
||||
The documentation website is now available at `http://localhost:3000`.
|
||||
The documentation website is now available at `http://localhost:5000`.
|
||||
|
||||
### Custom Ports
|
||||
|
||||
Mintlify uses port 3000 by default. You can use the `--port` flag to customize the port Mintlify runs on. For example, use this command to run in port 3333:
|
||||
Mintlify uses port 5000 by default. You can use the `--port` flag to customize the port Mintlify runs on. For example, use this command to run in port 3333:
|
||||
|
||||
```bash
|
||||
mintlify dev --port 3333
|
||||
@ -41,7 +41,7 @@ mintlify dev --port 3333
|
||||
You will see an error like this if you try to run Mintlify in a port that's already taken:
|
||||
|
||||
```md
|
||||
Error: listen EADDRINUSE: address already in use :::3000
|
||||
Error: listen EADDRINUSE: address already in use :::5000
|
||||
```
|
||||
|
||||
## Mintlify Versions
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "docs",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npx --yes mintlify dev --port 3004",
|
||||
"dev": "npx --yes mintlify dev --port 5004",
|
||||
"lint": "npx --yes mintlify broken-links"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "email build",
|
||||
"dev": "email dev --port 3003",
|
||||
"dev": "email dev --port 5003",
|
||||
"export": "email export",
|
||||
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||
|
@ -1,2 +1,2 @@
|
||||
^https://webui.konobangu.com/*** http://127.0.0.1:3000/$1
|
||||
^wss://webui.konobangu.com/*** ws://127.0.0.1:3000/$1
|
||||
^https://konobangu.com/*** http://127.0.0.1:5000/$1
|
||||
^wss://konobangu.com/*** ws://127.0.0.1:5000/$1
|
@ -1,9 +1,7 @@
|
||||
{
|
||||
"name": "Konobangu Recorder",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"forwardPorts": [
|
||||
3001
|
||||
]
|
||||
}
|
||||
"name": "Konobangu Recorder",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"forwardPorts": [5001]
|
||||
}
|
||||
|
@ -13,14 +13,21 @@ name = "recorder_cli"
|
||||
path = "src/bin/main.rs"
|
||||
required-features = []
|
||||
|
||||
[features]
|
||||
default = []
|
||||
testcontainers = [
|
||||
"dep:testcontainers",
|
||||
"dep:testcontainers-modules",
|
||||
"dep:bollard",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
quirks_path = { path = "../../packages/quirks-path" }
|
||||
torrent = { path = "../../packages/torrent" }
|
||||
loco-rs = { version = "0.13" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
eyre = "0.6"
|
||||
tokio = { version = "1.42", default-features = false }
|
||||
tokio = { version = "1.42", features = ["macros", "fs", "rt-multi-thread"] }
|
||||
async-trait = "0.1.83"
|
||||
tracing = "0.1"
|
||||
chrono = "0.4"
|
||||
@ -29,8 +36,9 @@ sea-orm = { version = "1", features = [
|
||||
"sqlx-postgres",
|
||||
"runtime-tokio-rustls",
|
||||
"macros",
|
||||
"debug-print"
|
||||
"debug-print",
|
||||
] }
|
||||
figment = { version = "0.10", features = ["toml", "json", "env", "yaml"] }
|
||||
|
||||
axum = "0.7.9"
|
||||
uuid = { version = "1.6.0", features = ["v4"] }
|
||||
@ -59,6 +67,25 @@ leaky-bucket = "1.1.2"
|
||||
serde_with = "3"
|
||||
jwt-authorizer = "0.15.0"
|
||||
axum-auth = "0.7.0"
|
||||
futures = "0.3.31"
|
||||
librqbit-core = "4"
|
||||
qbit-rs = { git = "https://github.com/lonelyhentxi/qbit.git", rev = "72d53138ebe", features = [
|
||||
"default",
|
||||
"builder",
|
||||
] }
|
||||
testcontainers = { version = "0.23.1", features = [
|
||||
"default",
|
||||
"properties-config",
|
||||
"watchdog",
|
||||
"http_wait",
|
||||
"reusable-containers",
|
||||
], optional = true }
|
||||
testcontainers-modules = { version = "0.11.4", optional = true }
|
||||
|
||||
log = "0.4.22"
|
||||
anyhow = "1.0.95"
|
||||
bollard = { version = "0.18", optional = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "3"
|
||||
|
@ -34,7 +34,7 @@ pub trait AppContextExt {
|
||||
}
|
||||
|
||||
fn get_auth_service(&self) -> &AppAuthService {
|
||||
&AppAuthService::app_instance()
|
||||
AppAuthService::app_instance()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,7 +93,7 @@ impl AuthService for OidcAuthService {
|
||||
|
||||
let token_data = self.authorizer.check_auth(&token).await?;
|
||||
let claims = token_data.claims;
|
||||
if !claims.sub.as_deref().is_some_and(|s| !s.trim().is_empty()) {
|
||||
if claims.sub.as_deref().is_none_or(|s| s.trim().is_empty()) {
|
||||
return Err(AuthError::OidcSubMissingError);
|
||||
}
|
||||
if !claims.contains_audience(&config.audience) {
|
||||
@ -103,7 +103,7 @@ impl AuthService for OidcAuthService {
|
||||
let found_scopes = claims.scopes().collect::<HashSet<_>>();
|
||||
if !expected_scopes
|
||||
.iter()
|
||||
.all(|es| found_scopes.contains(&es as &str))
|
||||
.all(|es| found_scopes.contains(es as &str))
|
||||
{
|
||||
return Err(AuthError::OidcExtraScopesMatchError {
|
||||
expected: expected_scopes.iter().join(","),
|
||||
|
@ -107,7 +107,7 @@ impl Initializer for AppAuthServiceInitializer {
|
||||
|
||||
let service = AppAuthService::from_conf(auth_conf)
|
||||
.await
|
||||
.map_err(|e| loco_rs::Error::wrap(e))?;
|
||||
.map_err(loco_rs::Error::wrap)?;
|
||||
|
||||
APP_AUTH_SERVICE.get_or_init(|| service);
|
||||
|
||||
|
@ -1,12 +1,18 @@
|
||||
use figment::{
|
||||
providers::{Format, Json, Yaml},
|
||||
Figment,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
use crate::{auth::AppAuthConfig, dal::config::AppDalConfig, extract::mikan::AppMikanConfig};
|
||||
|
||||
const DEFAULT_APP_SETTINGS_MIXIN: &str = include_str!("./settings_mixin.yaml");
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
pub auth: AppAuthConfig,
|
||||
pub dal: Option<AppDalConfig>,
|
||||
pub mikan: Option<AppMikanConfig>,
|
||||
pub dal: AppDalConfig,
|
||||
pub mikan: AppMikanConfig,
|
||||
}
|
||||
|
||||
pub fn deserialize_key_path_from_json_value<T: DeserializeOwned>(
|
||||
@ -42,10 +48,19 @@ pub trait AppConfigExt {
|
||||
fn get_root_conf(&self) -> &loco_rs::config::Config;
|
||||
|
||||
fn get_app_conf(&self) -> loco_rs::Result<AppConfig> {
|
||||
Ok(
|
||||
deserialize_key_path_from_app_config(self.get_root_conf(), &[])?
|
||||
.expect("app config must be present"),
|
||||
)
|
||||
let settings_str = self
|
||||
.get_root_conf()
|
||||
.settings
|
||||
.as_ref()
|
||||
.map(serde_json::to_string)
|
||||
.unwrap_or_else(|| Ok(String::new()))?;
|
||||
|
||||
let app_config = Figment::from(Json::string(&settings_str))
|
||||
.merge(Yaml::string(DEFAULT_APP_SETTINGS_MIXIN))
|
||||
.extract()
|
||||
.map_err(loco_rs::Error::wrap)?;
|
||||
|
||||
Ok(app_config)
|
||||
}
|
||||
}
|
||||
|
||||
|
12
apps/recorder/src/config/settings_mixin.yaml
Normal file
12
apps/recorder/src/config/settings_mixin.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
dal:
|
||||
data_dir: ./data
|
||||
|
||||
mikan:
|
||||
http_client:
|
||||
exponential_backoff_max_retries: 3
|
||||
leaky_bucket_max_tokens: 3
|
||||
leaky_bucket_initial_tokens: 0
|
||||
leaky_bucket_refill_tokens: 1
|
||||
leaky_bucket_refill_interval: 500
|
||||
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"
|
||||
base_url: "https://mikanani.me/"
|
@ -194,7 +194,7 @@ impl Initializer for AppDalInitalizer {
|
||||
let config = &app_context.config;
|
||||
let app_dal_conf = config.get_app_conf()?.dal;
|
||||
|
||||
APP_DAL_CLIENT.get_or_init(|| AppDalClient::new(app_dal_conf.unwrap_or_default()));
|
||||
APP_DAL_CLIENT.get_or_init(|| AppDalClient::new(app_dal_conf));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use std::ops::Deref;
|
||||
use loco_rs::app::{AppContext, Initializer};
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
use super::{AppMikanConfig, MIKAN_BASE_URL};
|
||||
use super::AppMikanConfig;
|
||||
use crate::{config::AppConfigExt, fetch::HttpClient};
|
||||
|
||||
static APP_MIKAN_CLIENT: OnceCell<AppMikanClient> = OnceCell::new();
|
||||
@ -14,12 +14,10 @@ pub struct AppMikanClient {
|
||||
}
|
||||
|
||||
impl AppMikanClient {
|
||||
pub fn new(mut config: AppMikanConfig) -> loco_rs::Result<Self> {
|
||||
pub fn new(config: AppMikanConfig) -> loco_rs::Result<Self> {
|
||||
let http_client =
|
||||
HttpClient::new(config.http_client.take()).map_err(loco_rs::Error::wrap)?;
|
||||
let base_url = config
|
||||
.base_url
|
||||
.unwrap_or_else(|| String::from(MIKAN_BASE_URL));
|
||||
HttpClient::from_config(config.http_client).map_err(loco_rs::Error::wrap)?;
|
||||
let base_url = config.base_url;
|
||||
Ok(Self {
|
||||
http_client,
|
||||
base_url,
|
||||
@ -55,7 +53,7 @@ impl Initializer for AppMikanClientInitializer {
|
||||
|
||||
async fn before_run(&self, app_context: &AppContext) -> loco_rs::Result<()> {
|
||||
let config = &app_context.config;
|
||||
let app_mikan_conf = config.get_app_conf()?.mikan.unwrap_or_default();
|
||||
let app_mikan_conf = config.get_app_conf()?.mikan;
|
||||
|
||||
APP_MIKAN_CLIENT.get_or_try_init(|| AppMikanClient::new(app_mikan_conf))?;
|
||||
|
||||
|
@ -2,8 +2,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::fetch::HttpClientConfig;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AppMikanConfig {
|
||||
pub http_client: Option<HttpClientConfig>,
|
||||
pub base_url: Option<String>,
|
||||
pub http_client: HttpClientConfig,
|
||||
pub base_url: String,
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
pub const MIKAN_BUCKET_KEY: &str = "mikan";
|
||||
pub const MIKAN_BASE_URL: &str = "https://mikanani.me/";
|
||||
pub const MIKAN_UNKNOWN_FANSUB_NAME: &str = "生肉/不明字幕";
|
||||
pub const MIKAN_UNKNOWN_FANSUB_ID: &str = "202";
|
||||
|
@ -6,7 +6,7 @@ pub mod web_parser;
|
||||
|
||||
pub use client::{AppMikanClient, AppMikanClientInitializer};
|
||||
pub use config::AppMikanConfig;
|
||||
pub use constants::{MIKAN_BASE_URL, MIKAN_BUCKET_KEY};
|
||||
pub use constants::MIKAN_BUCKET_KEY;
|
||||
pub use rss_parser::{
|
||||
build_mikan_bangumi_rss_link, build_mikan_subscriber_aggregation_rss_link,
|
||||
parse_mikan_bangumi_id_from_rss_link, parse_mikan_rss_channel_from_rss_link,
|
||||
|
@ -4,14 +4,19 @@ use chrono::DateTime;
|
||||
use itertools::Itertools;
|
||||
use reqwest::IntoUrl;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use torrent::core::BITTORRENT_MIME_TYPE;
|
||||
use url::Url;
|
||||
|
||||
use super::{
|
||||
web_parser::{parse_mikan_episode_id_from_homepage, MikanEpisodeHomepage},
|
||||
AppMikanClient,
|
||||
use crate::{
|
||||
extract::{
|
||||
errors::ParseError,
|
||||
mikan::{
|
||||
web_parser::{parse_mikan_episode_id_from_homepage, MikanEpisodeHomepage},
|
||||
AppMikanClient,
|
||||
},
|
||||
},
|
||||
fetch::bytes::fetch_bytes,
|
||||
sync::core::BITTORRENT_MIME_TYPE,
|
||||
};
|
||||
use crate::{extract::errors::ParseError, fetch::bytes::download_bytes_with_client};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct MikanRssItem {
|
||||
@ -228,7 +233,7 @@ pub async fn parse_mikan_rss_channel_from_rss_link(
|
||||
url: impl IntoUrl,
|
||||
) -> eyre::Result<MikanRssChannel> {
|
||||
let http_client = client.map(|s| s.deref());
|
||||
let bytes = download_bytes_with_client(http_client, url.as_str()).await?;
|
||||
let bytes = fetch_bytes(http_client, url.as_str()).await?;
|
||||
|
||||
let channel = rss::Channel::read_from(&bytes[..])?;
|
||||
|
||||
@ -297,11 +302,12 @@ pub async fn parse_mikan_rss_channel_from_rss_link(
|
||||
mod tests {
|
||||
use std::assert_matches::assert_matches;
|
||||
|
||||
use torrent::core::BITTORRENT_MIME_TYPE;
|
||||
|
||||
use crate::extract::mikan::{
|
||||
parse_mikan_rss_channel_from_rss_link, MikanBangumiAggregationRssChannel,
|
||||
MikanBangumiRssChannel, MikanRssChannel,
|
||||
use crate::{
|
||||
extract::mikan::{
|
||||
parse_mikan_rss_channel_from_rss_link, MikanBangumiAggregationRssChannel,
|
||||
MikanBangumiRssChannel, MikanRssChannel,
|
||||
},
|
||||
sync::core::BITTORRENT_MIME_TYPE,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -18,7 +18,7 @@ use crate::{
|
||||
app::AppContextExt,
|
||||
dal::DalContentCategory,
|
||||
extract::html::parse_style_attr,
|
||||
fetch::{html::download_html_with_client, image::download_image_with_client},
|
||||
fetch::{html::fetch_html, image::fetch_image},
|
||||
models::subscribers,
|
||||
};
|
||||
|
||||
@ -95,7 +95,7 @@ pub async fn parse_mikan_bangumi_poster_from_origin_poster_src(
|
||||
origin_poster_src: Url,
|
||||
) -> eyre::Result<MikanBangumiPosterMeta> {
|
||||
let http_client = client.map(|s| s.deref());
|
||||
let poster_data = download_image_with_client(http_client, origin_poster_src.clone()).await?;
|
||||
let poster_data = fetch_image(http_client, origin_poster_src.clone()).await?;
|
||||
Ok(MikanBangumiPosterMeta {
|
||||
origin_poster_src,
|
||||
poster_data: Some(poster_data),
|
||||
@ -127,8 +127,7 @@ pub async fn parse_mikan_bangumi_poster_from_origin_poster_src_with_cache(
|
||||
});
|
||||
}
|
||||
|
||||
let poster_data =
|
||||
download_image_with_client(Some(mikan_client.deref()), origin_poster_src.clone()).await?;
|
||||
let poster_data = fetch_image(Some(mikan_client.deref()), origin_poster_src.clone()).await?;
|
||||
|
||||
let poster_str = dal_client
|
||||
.store_object(
|
||||
@ -153,7 +152,7 @@ pub async fn parse_mikan_bangumi_meta_from_mikan_homepage(
|
||||
) -> eyre::Result<MikanBangumiMeta> {
|
||||
let http_client = client.map(|s| s.deref());
|
||||
let url_host = url.origin().unicode_serialization();
|
||||
let content = download_html_with_client(http_client, url.as_str()).await?;
|
||||
let content = fetch_html(http_client, url.as_str()).await?;
|
||||
let html = Html::parse_document(&content);
|
||||
|
||||
let bangumi_fansubs = html
|
||||
@ -276,7 +275,7 @@ pub async fn parse_mikan_episode_meta_from_mikan_homepage(
|
||||
) -> eyre::Result<MikanEpisodeMeta> {
|
||||
let http_client = client.map(|s| s.deref());
|
||||
let url_host = url.origin().unicode_serialization();
|
||||
let content = download_html_with_client(http_client, url.as_str()).await?;
|
||||
let content = fetch_html(http_client, url.as_str()).await?;
|
||||
|
||||
let html = Html::parse_document(&content);
|
||||
|
||||
@ -401,6 +400,8 @@ pub async fn parse_mikan_episode_meta_from_mikan_homepage(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn parse_mikan_bangumis_from_user_home(_client: Option<&AppMikanClient>, _url: Url) {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::assert_matches::assert_matches;
|
||||
|
@ -1,24 +1,11 @@
|
||||
use bytes::Bytes;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
use super::{core::DEFAULT_HTTP_CLIENT_USER_AGENT, HttpClient};
|
||||
use super::HttpClient;
|
||||
|
||||
pub async fn download_bytes<T: IntoUrl>(url: T) -> eyre::Result<Bytes> {
|
||||
let request_client = reqwest::Client::builder()
|
||||
.user_agent(DEFAULT_HTTP_CLIENT_USER_AGENT)
|
||||
.build()?;
|
||||
let bytes = request_client.get(url).send().await?.bytes().await?;
|
||||
pub async fn fetch_bytes<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> eyre::Result<Bytes> {
|
||||
let client = client.unwrap_or_default();
|
||||
|
||||
let bytes = client.get(url).send().await?.bytes().await?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub async fn download_bytes_with_client<T: IntoUrl>(
|
||||
client: Option<&HttpClient>,
|
||||
url: T,
|
||||
) -> eyre::Result<Bytes> {
|
||||
if let Some(client) = client {
|
||||
let bytes = client.get(url).send().await?.bytes().await?;
|
||||
Ok(bytes)
|
||||
} else {
|
||||
download_bytes(url).await
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ use std::{ops::Deref, time::Duration};
|
||||
|
||||
use axum::http::Extensions;
|
||||
use leaky_bucket::RateLimiter;
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::{ClientBuilder, Request, Response};
|
||||
use reqwest_middleware::{
|
||||
ClientBuilder as ClientWithMiddlewareBuilder, ClientWithMiddleware, Next,
|
||||
@ -11,7 +12,7 @@ use reqwest_tracing::TracingMiddleware;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
|
||||
use super::DEFAULT_HTTP_CLIENT_USER_AGENT;
|
||||
use crate::fetch::DEFAULT_HTTP_CLIENT_USER_AGENT;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
@ -27,6 +28,13 @@ pub struct HttpClientConfig {
|
||||
|
||||
pub struct HttpClient {
|
||||
client: ClientWithMiddleware,
|
||||
pub config: HttpClientConfig,
|
||||
}
|
||||
|
||||
impl Into<ClientWithMiddleware> for HttpClient {
|
||||
fn into(self) -> ClientWithMiddleware {
|
||||
self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for HttpClient {
|
||||
@ -55,42 +63,73 @@ impl reqwest_middleware::Middleware for RateLimiterMiddleware {
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
pub fn new(config: Option<HttpClientConfig>) -> reqwest::Result<Self> {
|
||||
let mut config = config.unwrap_or_default();
|
||||
let retry_policy = ExponentialBackoff::builder()
|
||||
.build_with_max_retries(config.exponential_backoff_max_retries.take().unwrap_or(3));
|
||||
let rate_limiter = RateLimiter::builder()
|
||||
.max(config.leaky_bucket_max_tokens.take().unwrap_or(3) as usize)
|
||||
.initial(
|
||||
config
|
||||
.leaky_bucket_initial_tokens
|
||||
.take()
|
||||
.unwrap_or_default() as usize,
|
||||
)
|
||||
.refill(config.leaky_bucket_refill_tokens.take().unwrap_or(1) as usize)
|
||||
.interval(
|
||||
config
|
||||
.leaky_bucket_refill_interval
|
||||
.take()
|
||||
.unwrap_or_else(|| Duration::from_millis(500)),
|
||||
)
|
||||
.build();
|
||||
pub fn from_config(config: HttpClientConfig) -> reqwest::Result<Self> {
|
||||
let reqwest_client_builder = ClientBuilder::new().user_agent(
|
||||
config
|
||||
.user_agent
|
||||
.as_deref()
|
||||
.unwrap_or(DEFAULT_HTTP_CLIENT_USER_AGENT),
|
||||
);
|
||||
|
||||
let client = ClientBuilder::new()
|
||||
.user_agent(
|
||||
config
|
||||
.user_agent
|
||||
.take()
|
||||
.unwrap_or_else(|| DEFAULT_HTTP_CLIENT_USER_AGENT.to_owned()),
|
||||
)
|
||||
.build()?;
|
||||
let reqwest_client = reqwest_client_builder.build()?;
|
||||
|
||||
let mut reqwest_with_middleware_builder =
|
||||
ClientWithMiddlewareBuilder::new(reqwest_client).with(TracingMiddleware::default());
|
||||
|
||||
if let Some(ref x) = config.exponential_backoff_max_retries {
|
||||
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(*x);
|
||||
|
||||
reqwest_with_middleware_builder = reqwest_with_middleware_builder
|
||||
.with(RetryTransientMiddleware::new_with_policy(retry_policy));
|
||||
}
|
||||
|
||||
if let (None, None, None, None) = (
|
||||
config.leaky_bucket_initial_tokens.as_ref(),
|
||||
config.leaky_bucket_refill_tokens.as_ref(),
|
||||
config.leaky_bucket_refill_interval.as_ref(),
|
||||
config.leaky_bucket_max_tokens.as_ref(),
|
||||
) {
|
||||
} else {
|
||||
let mut rate_limiter_builder = RateLimiter::builder();
|
||||
|
||||
if let Some(ref x) = config.leaky_bucket_max_tokens {
|
||||
rate_limiter_builder.max(*x as usize);
|
||||
}
|
||||
if let Some(ref x) = config.leaky_bucket_initial_tokens {
|
||||
rate_limiter_builder.initial(*x as usize);
|
||||
}
|
||||
if let Some(ref x) = config.leaky_bucket_refill_tokens {
|
||||
rate_limiter_builder.refill(*x as usize);
|
||||
}
|
||||
if let Some(ref x) = config.leaky_bucket_refill_interval {
|
||||
rate_limiter_builder.interval(*x);
|
||||
}
|
||||
|
||||
let rate_limiter = rate_limiter_builder.build();
|
||||
|
||||
reqwest_with_middleware_builder =
|
||||
reqwest_with_middleware_builder.with(RateLimiterMiddleware { rate_limiter });
|
||||
}
|
||||
|
||||
let reqwest_with_middleware = reqwest_with_middleware_builder.build();
|
||||
|
||||
Ok(Self {
|
||||
client: ClientWithMiddlewareBuilder::new(client)
|
||||
.with(TracingMiddleware::default())
|
||||
.with(RateLimiterMiddleware { rate_limiter })
|
||||
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
|
||||
.build(),
|
||||
client: reqwest_with_middleware,
|
||||
config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
static DEFAULT_HTTP_CLIENT: OnceCell<HttpClient> = OnceCell::new();
|
||||
|
||||
impl Default for HttpClient {
|
||||
fn default() -> Self {
|
||||
HttpClient::from_config(Default::default()).expect("Failed to create default HttpClient")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for &HttpClient {
|
||||
fn default() -> Self {
|
||||
DEFAULT_HTTP_CLIENT.get_or_init(HttpClient::default)
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,10 @@
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
use super::{core::DEFAULT_HTTP_CLIENT_USER_AGENT, HttpClient};
|
||||
use super::HttpClient;
|
||||
|
||||
pub async fn fetch_html<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> eyre::Result<String> {
|
||||
let client = client.unwrap_or_default();
|
||||
let content = client.get(url).send().await?.text().await?;
|
||||
|
||||
pub async fn download_html<U: IntoUrl>(url: U) -> eyre::Result<String> {
|
||||
let request_client = reqwest::Client::builder()
|
||||
.user_agent(DEFAULT_HTTP_CLIENT_USER_AGENT)
|
||||
.build()?;
|
||||
let content = request_client.get(url).send().await?.text().await?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
pub async fn download_html_with_client<T: IntoUrl>(
|
||||
client: Option<&HttpClient>,
|
||||
url: T,
|
||||
) -> eyre::Result<String> {
|
||||
if let Some(client) = client {
|
||||
let content = client.get(url).send().await?.text().await?;
|
||||
Ok(content)
|
||||
} else {
|
||||
download_html(url).await
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,8 @@
|
||||
use bytes::Bytes;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
use super::{
|
||||
bytes::{download_bytes, download_bytes_with_client},
|
||||
HttpClient,
|
||||
};
|
||||
use super::{bytes::fetch_bytes, HttpClient};
|
||||
|
||||
pub async fn download_image<U: IntoUrl>(url: U) -> eyre::Result<Bytes> {
|
||||
download_bytes(url).await
|
||||
}
|
||||
|
||||
pub async fn download_image_with_client<T: IntoUrl>(
|
||||
client: Option<&HttpClient>,
|
||||
url: T,
|
||||
) -> eyre::Result<Bytes> {
|
||||
download_bytes_with_client(client, url).await
|
||||
pub async fn fetch_image<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> eyre::Result<Bytes> {
|
||||
fetch_bytes(client, url).await
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ pub mod image;
|
||||
|
||||
pub use core::DEFAULT_HTTP_CLIENT_USER_AGENT;
|
||||
|
||||
pub use bytes::download_bytes;
|
||||
pub use bytes::fetch_bytes;
|
||||
pub use client::{HttpClient, HttpClientConfig};
|
||||
pub use image::download_image;
|
||||
pub use html::fetch_html;
|
||||
pub use image::fetch_image;
|
||||
|
@ -1,6 +1,7 @@
|
||||
#![feature(duration_constructors, assert_matches, unboxed_closures)]
|
||||
|
||||
pub mod app;
|
||||
pub mod auth;
|
||||
pub mod config;
|
||||
pub mod controllers;
|
||||
pub mod dal;
|
||||
@ -8,7 +9,9 @@ pub mod extract;
|
||||
pub mod fetch;
|
||||
pub mod migrations;
|
||||
pub mod models;
|
||||
pub mod sync;
|
||||
pub mod tasks;
|
||||
#[cfg(test)]
|
||||
pub mod test_utils;
|
||||
pub mod views;
|
||||
pub mod workers;
|
||||
pub mod auth;
|
||||
|
@ -1,4 +1,5 @@
|
||||
use bytes::Bytes;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
use librqbit_core::{
|
||||
@ -7,23 +8,14 @@ use librqbit_core::{
|
||||
};
|
||||
use quirks_path::{Path, PathBuf};
|
||||
use regex::Regex;
|
||||
use reqwest::IntoUrl;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::{QbitTorrent, QbitTorrentContent, TorrentDownloadError};
|
||||
use super::{QbitTorrent, QbitTorrentContent, TorrentDownloadError};
|
||||
use crate::fetch::{fetch_bytes, HttpClient};
|
||||
|
||||
pub const BITTORRENT_MIME_TYPE: &str = "application/x-bittorrent";
|
||||
pub const MAGNET_SCHEMA: &str = "magnet";
|
||||
pub const DEFAULT_TORRENT_USER_AGENT: &str = "Wget/1.13.4 (linux-gnu)";
|
||||
|
||||
async fn download_torrent_file<T: IntoUrl>(url: T) -> eyre::Result<Bytes> {
|
||||
let request_client = reqwest::Client::builder()
|
||||
.user_agent(DEFAULT_TORRENT_USER_AGENT)
|
||||
.build()?;
|
||||
let bytes = request_client.get(url).send().await?.bytes().await?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@ -46,7 +38,7 @@ lazy_static! {
|
||||
static ref TORRENT_EXT_RE: Regex = Regex::new(r"\.torrent$").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum TorrentSource {
|
||||
MagnetUrl {
|
||||
url: Url,
|
||||
@ -64,7 +56,7 @@ pub enum TorrentSource {
|
||||
}
|
||||
|
||||
impl TorrentSource {
|
||||
pub async fn parse(url: &str) -> eyre::Result<Self> {
|
||||
pub async fn parse(client: Option<&HttpClient>, url: &str) -> eyre::Result<Self> {
|
||||
let url = Url::parse(url)?;
|
||||
let source = if url.scheme() == MAGNET_SCHEMA {
|
||||
TorrentSource::from_magnet_url(url)?
|
||||
@ -79,11 +71,11 @@ impl TorrentSource {
|
||||
) {
|
||||
TorrentSource::from_torrent_url(url, match_hash.as_str().to_string())?
|
||||
} else {
|
||||
let contents = download_torrent_file(url).await?;
|
||||
let contents = fetch_bytes(client, url).await?;
|
||||
TorrentSource::from_torrent_file(contents.to_vec(), Some(basename.to_string()))?
|
||||
}
|
||||
} else {
|
||||
let contents = download_torrent_file(url).await?;
|
||||
let contents = fetch_bytes(client, url).await?;
|
||||
TorrentSource::from_torrent_file(contents.to_vec(), None)?
|
||||
};
|
||||
Ok(source)
|
||||
@ -137,6 +129,24 @@ impl TorrentSource {
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for TorrentSource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TorrentSource::MagnetUrl { url, .. } => {
|
||||
write!(f, "MagnetUrl {{ url: {} }}", url.as_str())
|
||||
}
|
||||
TorrentSource::TorrentUrl { url, .. } => {
|
||||
write!(f, "TorrentUrl {{ url: {} }}", url.as_str())
|
||||
}
|
||||
TorrentSource::TorrentFile { name, hash, .. } => write!(
|
||||
f,
|
||||
"TorrentFile {{ name: \"{}\", hash: \"{hash}\" }}",
|
||||
name.as_deref().unwrap_or_default()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TorrentContent {
|
||||
fn get_name(&self) -> &str;
|
||||
|
@ -2,7 +2,10 @@ pub mod core;
|
||||
pub mod error;
|
||||
pub mod qbit;
|
||||
|
||||
pub use core::{Torrent, TorrentContent, TorrentDownloader, TorrentFilter, TorrentSource};
|
||||
pub use core::{
|
||||
Torrent, TorrentContent, TorrentDownloader, TorrentFilter, TorrentSource, BITTORRENT_MIME_TYPE,
|
||||
MAGNET_SCHEMA,
|
||||
};
|
||||
|
||||
pub use error::TorrentDownloadError;
|
||||
pub use qbit::{
|
@ -14,9 +14,10 @@ use qbit_rs::{
|
||||
};
|
||||
use quirks_path::{path_equals_as_file_url, Path, PathBuf};
|
||||
use tokio::time::sleep;
|
||||
use tracing::instrument;
|
||||
use url::Url;
|
||||
|
||||
use crate::{Torrent, TorrentDownloadError, TorrentDownloader, TorrentFilter, TorrentSource};
|
||||
use super::{Torrent, TorrentDownloadError, TorrentDownloader, TorrentFilter, TorrentSource};
|
||||
|
||||
impl From<TorrentSource> for QbitTorrentSource {
|
||||
fn from(value: TorrentSource) -> Self {
|
||||
@ -81,7 +82,9 @@ impl QBittorrentDownloader {
|
||||
) -> Result<Self, TorrentDownloadError> {
|
||||
let endpoint_url =
|
||||
Url::parse(&creation.endpoint).map_err(TorrentDownloadError::InvalidUrlParse)?;
|
||||
|
||||
let credential = Credential::new(creation.username, creation.password);
|
||||
|
||||
let client = Qbit::new(endpoint_url.clone(), credential);
|
||||
|
||||
client
|
||||
@ -100,6 +103,7 @@ impl QBittorrentDownloader {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn api_version(&self) -> eyre::Result<String> {
|
||||
let result = self.client.get_webapi_version().await?;
|
||||
Ok(result)
|
||||
@ -116,8 +120,9 @@ impl QBittorrentDownloader {
|
||||
H: FnOnce() -> E,
|
||||
G: Fn(Arc<Qbit>, E) -> Fut,
|
||||
Fut: Future<Output = eyre::Result<D>>,
|
||||
F: FnMut(D) -> bool,
|
||||
F: FnMut(&D) -> bool,
|
||||
E: Clone,
|
||||
D: Debug + serde::Serialize,
|
||||
{
|
||||
let mut next_wait_ms = 32u64;
|
||||
let mut all_wait_ms = 0u64;
|
||||
@ -129,9 +134,10 @@ impl QBittorrentDownloader {
|
||||
if all_wait_ms >= timeout.as_millis() as u64 {
|
||||
// full update
|
||||
let sync_data = fetch_data_fn(self.client.clone(), env.clone()).await?;
|
||||
if stop_wait_fn(sync_data) {
|
||||
if stop_wait_fn(&sync_data) {
|
||||
break;
|
||||
} else {
|
||||
tracing::warn!(name = "wait_until timeout", sync_data = serde_json::to_string(&sync_data).unwrap(), timeout = ?timeout);
|
||||
return Err(TorrentDownloadError::TimeoutError {
|
||||
action: Cow::Borrowed("QBittorrentDownloader::wait_unit"),
|
||||
timeout,
|
||||
@ -140,7 +146,7 @@ impl QBittorrentDownloader {
|
||||
}
|
||||
}
|
||||
let sync_data = fetch_data_fn(self.client.clone(), env.clone()).await?;
|
||||
if stop_wait_fn(sync_data) {
|
||||
if stop_wait_fn(&sync_data) {
|
||||
break;
|
||||
}
|
||||
next_wait_ms *= 2;
|
||||
@ -148,6 +154,7 @@ impl QBittorrentDownloader {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip(self, stop_wait_fn))]
|
||||
pub async fn wait_torrents_until<F>(
|
||||
&self,
|
||||
arg: GetTorrentListArg,
|
||||
@ -155,7 +162,7 @@ impl QBittorrentDownloader {
|
||||
timeout: Option<Duration>,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
F: FnMut(Vec<QbitTorrent>) -> bool,
|
||||
F: FnMut(&Vec<QbitTorrent>) -> bool,
|
||||
{
|
||||
self.wait_until(
|
||||
|| arg,
|
||||
@ -171,7 +178,8 @@ impl QBittorrentDownloader {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn wait_sync_until<F: FnMut(SyncData) -> bool>(
|
||||
#[instrument(level = "debug", skip(self, stop_wait_fn))]
|
||||
pub async fn wait_sync_until<F: FnMut(&SyncData) -> bool>(
|
||||
&self,
|
||||
stop_wait_fn: F,
|
||||
timeout: Option<Duration>,
|
||||
@ -188,7 +196,8 @@ impl QBittorrentDownloader {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn wait_torrent_contents_until<F: FnMut(Vec<QbitTorrentContent>) -> bool>(
|
||||
#[instrument(level = "debug", skip(self, stop_wait_fn))]
|
||||
async fn wait_torrent_contents_until<F: FnMut(&Vec<QbitTorrentContent>) -> bool>(
|
||||
&self,
|
||||
hash: &str,
|
||||
stop_wait_fn: F,
|
||||
@ -211,6 +220,7 @@ impl QBittorrentDownloader {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TorrentDownloader for QBittorrentDownloader {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_torrents_info(
|
||||
&self,
|
||||
status_filter: TorrentFilter,
|
||||
@ -239,6 +249,7 @@ impl TorrentDownloader for QBittorrentDownloader {
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn add_torrents(
|
||||
&self,
|
||||
source: TorrentSource,
|
||||
@ -268,6 +279,7 @@ impl TorrentDownloader for QBittorrentDownloader {
|
||||
|sync_data| {
|
||||
sync_data
|
||||
.torrents
|
||||
.as_ref()
|
||||
.is_some_and(|t| t.contains_key(source_hash))
|
||||
},
|
||||
None,
|
||||
@ -276,6 +288,7 @@ impl TorrentDownloader for QBittorrentDownloader {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn delete_torrents(&self, hashes: Vec<String>) -> eyre::Result<()> {
|
||||
self.client
|
||||
.delete_torrents(hashes.clone(), Some(true))
|
||||
@ -291,6 +304,7 @@ impl TorrentDownloader for QBittorrentDownloader {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn rename_torrent_file(
|
||||
&self,
|
||||
hash: &str,
|
||||
@ -304,7 +318,11 @@ impl TorrentDownloader for QBittorrentDownloader {
|
||||
hash,
|
||||
|contents| -> bool {
|
||||
contents.iter().any(|c| {
|
||||
path_equals_as_file_url(save_path.join(&c.name), &new_path).unwrap_or(false)
|
||||
path_equals_as_file_url(save_path.join(&c.name), &new_path)
|
||||
.inspect_err(|error| {
|
||||
tracing::warn!(name = "path_equals_as_file_url", error = ?error);
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
},
|
||||
None,
|
||||
@ -313,19 +331,23 @@ impl TorrentDownloader for QBittorrentDownloader {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn move_torrents(&self, hashes: Vec<String>, new_path: &str) -> eyre::Result<()> {
|
||||
self.client
|
||||
.set_torrent_location(hashes.clone(), new_path)
|
||||
.await?;
|
||||
|
||||
self.wait_torrents_until(
|
||||
GetTorrentListArg::builder()
|
||||
.hashes(hashes.join("|"))
|
||||
.build(),
|
||||
|torrents| -> bool {
|
||||
torrents.iter().all(|t| {
|
||||
t.save_path
|
||||
.as_ref()
|
||||
.is_some_and(|p| path_equals_as_file_url(p, new_path).unwrap_or(false))
|
||||
torrents.iter().flat_map(|t| t.save_path.as_ref()).any(|p| {
|
||||
path_equals_as_file_url(p, new_path)
|
||||
.inspect_err(|error| {
|
||||
tracing::warn!(name = "path_equals_as_file_url", error = ?error);
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
},
|
||||
None,
|
||||
@ -346,11 +368,13 @@ impl TorrentDownloader for QBittorrentDownloader {
|
||||
Ok(torrent.save_path.take())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn check_connection(&self) -> eyre::Result<()> {
|
||||
self.api_version().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn set_torrents_category(&self, hashes: Vec<String>, category: &str) -> eyre::Result<()> {
|
||||
let result = self
|
||||
.client
|
||||
@ -379,6 +403,7 @@ impl TorrentDownloader for QBittorrentDownloader {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn add_torrent_tags(&self, hashes: Vec<String>, tags: Vec<String>) -> eyre::Result<()> {
|
||||
if tags.is_empty() {
|
||||
return Err(eyre::eyre!("add torrent tags can not be empty"));
|
||||
@ -408,6 +433,7 @@ impl TorrentDownloader for QBittorrentDownloader {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn add_category(&self, category: &str) -> eyre::Result<()> {
|
||||
self.client
|
||||
.add_category(
|
||||
@ -419,6 +445,7 @@ impl TorrentDownloader for QBittorrentDownloader {
|
||||
|sync_data| {
|
||||
sync_data
|
||||
.categories
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.contains_key(category))
|
||||
},
|
||||
None,
|
||||
@ -445,11 +472,12 @@ impl Debug for QBittorrentDownloader {
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use itertools::Itertools;
|
||||
use testcontainers_modules::testcontainers::ImageExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn get_tmp_qbit_test_folder() -> &'static str {
|
||||
if cfg!(windows) {
|
||||
if cfg!(all(windows, not(feature = "testcontainers"))) {
|
||||
"C:\\Windows\\Temp\\konobangu\\qbit"
|
||||
} else {
|
||||
"/tmp/konobangu/qbit"
|
||||
@ -457,65 +485,117 @@ pub mod tests {
|
||||
}
|
||||
|
||||
#[cfg(feature = "testcontainers")]
|
||||
pub fn create_qbit_testcontainer() -> testcontainers::RunnableImage<testcontainers::GenericImage>
|
||||
{
|
||||
let image = testcontainers::RunnableImage::from(
|
||||
testcontainers::GenericImage::new("linuxserver/qbittorrent", "latest")
|
||||
.with_wait_for(testcontainers::core::WaitFor::message_on_stderr(
|
||||
"Connection to localhost",
|
||||
))
|
||||
.with_env_var("WEBUI_PORT", "8080")
|
||||
.with_env_var("TZ", "Asia/Singapore")
|
||||
.with_env_var("TORRENTING_PORT", "6881")
|
||||
.with_exposed_port(8080)
|
||||
.with_exposed_port(6881),
|
||||
);
|
||||
pub async fn create_qbit_testcontainer(
|
||||
) -> eyre::Result<testcontainers::ContainerRequest<testcontainers::GenericImage>> {
|
||||
use testcontainers::{
|
||||
core::{
|
||||
ContainerPort,
|
||||
// ReuseDirective,
|
||||
WaitFor,
|
||||
},
|
||||
GenericImage,
|
||||
};
|
||||
|
||||
image
|
||||
use crate::test_utils::testcontainers::ContainerRequestEnhancedExt;
|
||||
|
||||
let container = GenericImage::new("linuxserver/qbittorrent", "latest")
|
||||
.with_wait_for(WaitFor::message_on_stderr("Connection to localhost"))
|
||||
.with_env_var("WEBUI_PORT", "8080")
|
||||
.with_env_var("TZ", "Asia/Singapore")
|
||||
.with_env_var("TORRENTING_PORT", "6881")
|
||||
.with_mapped_port(6881, ContainerPort::Tcp(6881))
|
||||
.with_mapped_port(8080, ContainerPort::Tcp(8080))
|
||||
// .with_reuse(ReuseDirective::Always)
|
||||
.with_default_log_consumer()
|
||||
.with_prune_existed_label("qbit-downloader", true, true)
|
||||
.await?;
|
||||
|
||||
Ok(container)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "testcontainers"))]
|
||||
#[tokio::test]
|
||||
async fn test_qbittorrent_downloader() {
|
||||
test_qbittorrent_downloader_impl().await;
|
||||
test_qbittorrent_downloader_impl(None, None).await;
|
||||
}
|
||||
|
||||
// @TODO: not support now, testcontainers crate not support to read logs to get
|
||||
// password
|
||||
#[cfg(feature = "testcontainers")]
|
||||
#[tokio::test]
|
||||
async fn test_qbittorrent_downloader() {
|
||||
let docker = testcontainers::clients::Cli::default();
|
||||
let image = create_qbit_testcontainer();
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_qbittorrent_downloader() -> eyre::Result<()> {
|
||||
use testcontainers::runners::AsyncRunner;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
let _container = docker.run(image);
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::DEBUG)
|
||||
.with_test_writer()
|
||||
.init();
|
||||
|
||||
test_qbittorrent_downloader_impl().await;
|
||||
let image = create_qbit_testcontainer().await?;
|
||||
|
||||
let container = image.start().await?;
|
||||
|
||||
let mut logs = String::new();
|
||||
|
||||
container.stdout(false).read_to_string(&mut logs).await?;
|
||||
|
||||
let username = logs
|
||||
.lines()
|
||||
.find_map(|line| {
|
||||
if line.contains("The WebUI administrator username is") {
|
||||
line.split_whitespace().last()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.expect("should have username")
|
||||
.trim();
|
||||
|
||||
let password = logs
|
||||
.lines()
|
||||
.find_map(|line| {
|
||||
if line.contains("A temporary password is provided for this session") {
|
||||
line.split_whitespace().last()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.expect("should have password")
|
||||
.trim();
|
||||
|
||||
tracing::info!(username, password);
|
||||
|
||||
test_qbittorrent_downloader_impl(Some(username), Some(password)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_qbittorrent_downloader_impl() {
|
||||
async fn test_qbittorrent_downloader_impl(
|
||||
username: Option<&str>,
|
||||
password: Option<&str>,
|
||||
) -> eyre::Result<()> {
|
||||
let base_save_path = Path::new(get_tmp_qbit_test_folder());
|
||||
|
||||
let downloader = QBittorrentDownloader::from_creation(QBittorrentDownloaderCreation {
|
||||
endpoint: "http://localhost:8080".to_string(),
|
||||
password: "".to_string(),
|
||||
username: "".to_string(),
|
||||
let mut downloader = QBittorrentDownloader::from_creation(QBittorrentDownloaderCreation {
|
||||
endpoint: "http://127.0.0.1:8080".to_string(),
|
||||
password: password.unwrap_or_default().to_string(),
|
||||
username: username.unwrap_or_default().to_string(),
|
||||
subscriber_id: 0,
|
||||
save_path: base_save_path.to_string(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
downloader.check_connection().await.unwrap();
|
||||
downloader.wait_sync_timeout = Duration::from_secs(3);
|
||||
|
||||
downloader.check_connection().await?;
|
||||
|
||||
downloader
|
||||
.delete_torrents(vec!["47ee2d69e7f19af783ad896541a07b012676f858".to_string()])
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
let torrent_source = TorrentSource::parse(
|
||||
None,
|
||||
"https://mikanani.me/Download/20240301/47ee2d69e7f19af783ad896541a07b012676f858.torrent"
|
||||
).await.unwrap();
|
||||
).await?;
|
||||
|
||||
let save_path = base_save_path.join(format!(
|
||||
"test_add_torrents_{}",
|
||||
@ -524,8 +604,7 @@ pub mod tests {
|
||||
|
||||
downloader
|
||||
.add_torrents(torrent_source, save_path.to_string(), Some("bangumi"))
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
let get_torrent = async || -> eyre::Result<Torrent> {
|
||||
let torrent_infos = downloader
|
||||
@ -540,7 +619,7 @@ pub mod tests {
|
||||
Ok(result)
|
||||
};
|
||||
|
||||
let target_torrent = get_torrent().await.unwrap();
|
||||
let target_torrent = get_torrent().await?;
|
||||
|
||||
let files = target_torrent.iter_files().collect_vec();
|
||||
assert!(!files.is_empty());
|
||||
@ -558,10 +637,9 @@ pub mod tests {
|
||||
vec!["47ee2d69e7f19af783ad896541a07b012676f858".to_string()],
|
||||
vec![test_tag.clone()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
let target_torrent = get_torrent().await.unwrap();
|
||||
let target_torrent = get_torrent().await?;
|
||||
|
||||
assert!(target_torrent.get_tags().iter().any(|s| s == &test_tag));
|
||||
|
||||
@ -572,10 +650,9 @@ pub mod tests {
|
||||
vec!["47ee2d69e7f19af783ad896541a07b012676f858".to_string()],
|
||||
&test_category,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
let target_torrent = get_torrent().await.unwrap();
|
||||
let target_torrent = get_torrent().await?;
|
||||
|
||||
assert_eq!(Some(test_category.as_str()), target_torrent.get_category());
|
||||
|
||||
@ -589,10 +666,9 @@ pub mod tests {
|
||||
vec!["47ee2d69e7f19af783ad896541a07b012676f858".to_string()],
|
||||
moved_save_path.as_str(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
let target_torrent = get_torrent().await.unwrap();
|
||||
let target_torrent = get_torrent().await?;
|
||||
|
||||
let content_path = target_torrent.iter_files().next().unwrap().get_name();
|
||||
|
||||
@ -604,10 +680,9 @@ pub mod tests {
|
||||
content_path,
|
||||
new_content_path,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
let target_torrent = get_torrent().await.unwrap();
|
||||
let target_torrent = get_torrent().await?;
|
||||
|
||||
let content_path = target_torrent.iter_files().next().unwrap().get_name();
|
||||
|
||||
@ -615,14 +690,14 @@ pub mod tests {
|
||||
|
||||
downloader
|
||||
.delete_torrents(vec!["47ee2d69e7f19af783ad896541a07b012676f858".to_string()])
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
let torrent_infos1 = downloader
|
||||
.get_torrents_info(TorrentFilter::All, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
assert!(torrent_infos1.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
107
apps/recorder/src/test_utils.rs
Normal file
107
apps/recorder/src/test_utils.rs
Normal file
@ -0,0 +1,107 @@
|
||||
#[cfg(feature = "testcontainers")]
|
||||
pub mod testcontainers {
|
||||
use bollard::container::ListContainersOptions;
|
||||
use itertools::Itertools;
|
||||
use testcontainers::{
|
||||
core::logs::consumer::logging_consumer::LoggingConsumer, ContainerRequest, Image, ImageExt,
|
||||
};
|
||||
|
||||
pub const TESTCONTAINERS_PROJECT_KEY: &str = "tech.enfw.testcontainers.project";
|
||||
pub const TESTCONTAINERS_CONTAINER_KEY: &str = "tech.enfw.testcontainers.container";
|
||||
pub const TESTCONTAINERS_PRUNE_KEY: &str = "tech.enfw.testcontainers.prune";
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ContainerRequestEnhancedExt<I>: Sized + ImageExt<I>
|
||||
where
|
||||
I: Image,
|
||||
{
|
||||
async fn with_prune_existed_label(
|
||||
self,
|
||||
container_label: &str,
|
||||
prune: bool,
|
||||
force: bool,
|
||||
) -> eyre::Result<Self>;
|
||||
|
||||
fn with_default_log_consumer(self) -> Self;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<I> ContainerRequestEnhancedExt<I> for ContainerRequest<I>
|
||||
where
|
||||
I: Image,
|
||||
{
|
||||
async fn with_prune_existed_label(
|
||||
self,
|
||||
container_label: &str,
|
||||
prune: bool,
|
||||
force: bool,
|
||||
) -> eyre::Result<Self> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bollard::container::PruneContainersOptions;
|
||||
use testcontainers::core::client::docker_client_instance;
|
||||
|
||||
if prune {
|
||||
let client = docker_client_instance().await?;
|
||||
|
||||
let mut filters = HashMap::<String, Vec<String>>::new();
|
||||
|
||||
filters.insert(
|
||||
String::from("label"),
|
||||
vec![
|
||||
format!("{TESTCONTAINERS_PRUNE_KEY}=true"),
|
||||
format!("{}={}", TESTCONTAINERS_PROJECT_KEY, "konobangu"),
|
||||
format!("{}={}", TESTCONTAINERS_CONTAINER_KEY, container_label),
|
||||
],
|
||||
);
|
||||
|
||||
if force {
|
||||
let result = client
|
||||
.list_containers(Some(ListContainersOptions {
|
||||
all: false,
|
||||
filters: filters.clone(),
|
||||
..Default::default()
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let remove_containers = result
|
||||
.iter()
|
||||
.filter(|c| matches!(c.state.as_deref(), Some("running")))
|
||||
.flat_map(|c| c.id.as_deref())
|
||||
.collect_vec();
|
||||
|
||||
futures::future::try_join_all(
|
||||
remove_containers
|
||||
.iter()
|
||||
.map(|c| client.stop_container(c, None)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::warn!(name = "stop running containers", result = ?remove_containers);
|
||||
}
|
||||
|
||||
let result = client
|
||||
.prune_containers(Some(PruneContainersOptions { filters }))
|
||||
.await?;
|
||||
|
||||
tracing::warn!(name = "prune existed containers", result = ?result);
|
||||
}
|
||||
|
||||
let result = self.with_labels([
|
||||
(TESTCONTAINERS_PRUNE_KEY, "true"),
|
||||
(TESTCONTAINERS_PROJECT_KEY, "konobangu"),
|
||||
(TESTCONTAINERS_CONTAINER_KEY, container_label),
|
||||
]);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn with_default_log_consumer(self) -> Self {
|
||||
self.with_log_consumer(
|
||||
LoggingConsumer::new()
|
||||
.with_stdout_level(log::Level::Info)
|
||||
.with_stderr_level(log::Level::Error),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -14,11 +14,11 @@ pnpm dev
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
Open [http://localhost:5000](http://localhost:5000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||
[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:5000/api/hello](http://localhost:5000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages.
|
||||
|
||||
|
@ -9,7 +9,7 @@ SVIX_TOKEN=""
|
||||
LIVEBLOCKS_SECRET=""
|
||||
|
||||
# Client
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_WEB_URL="http://localhost:3001"
|
||||
NEXT_PUBLIC_DOCS_URL="http://localhost:3004"
|
||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://webui.konobangu.com"
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:5000"
|
||||
NEXT_PUBLIC_WEB_URL="http://localhost:5001"
|
||||
NEXT_PUBLIC_DOCS_URL="http://localhost:5004"
|
||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://konobangu.com"
|
@ -9,7 +9,7 @@ SVIX_TOKEN=""
|
||||
LIVEBLOCKS_SECRET=""
|
||||
|
||||
# Client
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_WEB_URL="http://localhost:3001"
|
||||
NEXT_PUBLIC_DOCS_URL="http://localhost:3004"
|
||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:5000"
|
||||
NEXT_PUBLIC_WEB_URL="http://localhost:5001"
|
||||
NEXT_PUBLIC_DOCS_URL="http://localhost:5004"
|
||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://konobangu.com"
|
@ -2,7 +2,7 @@
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3001 --turbopack",
|
||||
"dev": "next dev -p 5001 --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"analyze": "ANALYZE=true pnpm build",
|
||||
|
@ -17,7 +17,7 @@ logger:
|
||||
# Web server configuration
|
||||
server:
|
||||
# Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
|
||||
port: 3001
|
||||
port: 5001
|
||||
# The UI hostname or IP address that mailers will point to.
|
||||
host: http://webui.konobangu.com
|
||||
# Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
|
||||
|
@ -4,7 +4,7 @@ services:
|
||||
webui:
|
||||
image: node:22-alpine
|
||||
ports:
|
||||
- '3000:3000'
|
||||
- '5000:5000'
|
||||
volumes:
|
||||
- ./apps/webui:/home/node/app
|
||||
- node_modules:/home/node/app/node_modules
|
||||
|
@ -8,4 +8,8 @@ nom = "7.1.3"
|
||||
percent-encoding = "2.3.1"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
thiserror = "1.0.57"
|
||||
tracing = "0.1.41"
|
||||
url = "2.5.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tracing = "0.1.41"
|
||||
|
@ -1702,11 +1702,11 @@ mod tests {
|
||||
);
|
||||
assert!(matches!(
|
||||
test_fn(r"\\?\abc\path"),
|
||||
Err(PathToUrlError::UrlNotSupportedPrefix { .. })
|
||||
Err(PathToUrlError::NotSupportedPrefixError { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
test_fn(r"\\.\device\path"),
|
||||
Err(PathToUrlError::UrlNotSupportedPrefix { .. })
|
||||
Err(PathToUrlError::NotSupportedPrefixError { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
test_fn(r"~/a"),
|
||||
|
@ -10,12 +10,20 @@ const URL_PATH_SEGMENT: &AsciiSet = &URL_PATH.add(b'/').add(b'%');
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum PathToUrlError {
|
||||
#[error("Path not absolute: {path}")]
|
||||
#[error(transparent)]
|
||||
UrlParseError(#[from] url::ParseError),
|
||||
#[error("PathNotAbsoluteError {{ path = {path} }}")]
|
||||
PathNotAbsoluteError { path: Cow<'static, str> },
|
||||
#[error("Invalid UNC path")]
|
||||
ParseUrlError(#[from] ::url::ParseError),
|
||||
#[error("Path prefix can not be a url: {path}")]
|
||||
UrlNotSupportedPrefix { path: Cow<'static, str> },
|
||||
#[error("NotSupportedPrefixError {{ path = {path}, prefix = {prefix} }}")]
|
||||
NotSupportedPrefixError {
|
||||
path: Cow<'static, str>,
|
||||
prefix: Cow<'static, str>,
|
||||
},
|
||||
#[error("NotSupportedFirstComponentError {{ path = {path}, comp = {comp} }}")]
|
||||
NotSupportedFirstComponentError {
|
||||
path: Cow<'static, str>,
|
||||
comp: Cow<'static, str>,
|
||||
},
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@ -60,15 +68,41 @@ pub(crate) fn path_to_file_url_segments(
|
||||
serialization.extend(percent_encode(share.as_bytes(), URL_PATH_SEGMENT));
|
||||
}
|
||||
_ => {
|
||||
return Err(PathToUrlError::UrlNotSupportedPrefix {
|
||||
return Err(PathToUrlError::NotSupportedPrefixError {
|
||||
path: Cow::Owned(path.as_str().to_string()),
|
||||
})
|
||||
prefix: Cow::Owned(p.as_str().to_string()),
|
||||
});
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(PathToUrlError::UrlNotSupportedPrefix {
|
||||
Some(Component::RootDir(_)) => {
|
||||
let host_end = to_u32(serialization.len()).unwrap();
|
||||
let mut empty = true;
|
||||
for component in components {
|
||||
empty = false;
|
||||
serialization.push('/');
|
||||
|
||||
serialization.extend(percent_encode(
|
||||
component.as_str().as_bytes(),
|
||||
URL_PATH_SEGMENT,
|
||||
));
|
||||
}
|
||||
|
||||
if empty {
|
||||
serialization.push('/');
|
||||
}
|
||||
return Ok((host_end, None));
|
||||
}
|
||||
Some(comp) => {
|
||||
return Err(PathToUrlError::NotSupportedFirstComponentError {
|
||||
path: Cow::Owned(path.as_str().to_string()),
|
||||
})
|
||||
comp: Cow::Owned(comp.as_str().to_string()),
|
||||
});
|
||||
}
|
||||
None => {
|
||||
return Err(PathToUrlError::NotSupportedFirstComponentError {
|
||||
path: Cow::Owned(path.as_str().to_string()),
|
||||
comp: Cow::Borrowed("null"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
17
packages/torrent/.gitignore
vendored
17
packages/torrent/.gitignore
vendored
@ -1,17 +0,0 @@
|
||||
**/config/local.yaml
|
||||
**/config/*.local.yaml
|
||||
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
@ -1,38 +0,0 @@
|
||||
[package]
|
||||
name = "torrent"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
name = "torrent"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
testcontainers = []
|
||||
|
||||
[dependencies]
|
||||
quirks_path = { path = "../quirks-path" }
|
||||
async-trait = "0.1.83"
|
||||
chrono = "0.4.39"
|
||||
eyre = "0.6.12"
|
||||
futures = "0.3.31"
|
||||
itertools = "0.13.0"
|
||||
lazy_static = "1.5.0"
|
||||
librqbit-core = "4"
|
||||
qbit-rs = { git = "https://github.com/lonelyhentxi/qbit.git", rev = "a2c70aa", features = [
|
||||
"default",
|
||||
"builder",
|
||||
] }
|
||||
regex = "1.11.1"
|
||||
serde = "1.0.216"
|
||||
thiserror = "2.0.9"
|
||||
tokio = "1.42.0"
|
||||
url = "2.5.4"
|
||||
reqwest = "0.12.11"
|
||||
bytes = "1.9.0"
|
||||
|
||||
[dev-dependencies]
|
||||
testcontainers = { version = "0.23.1" }
|
||||
testcontainers-modules = { version = "0.11.4" }
|
0
patches/.gitkeep
Normal file
0
patches/.gitkeep
Normal file
127
pnpm-lock.yaml
generated
127
pnpm-lock.yaml
generated
@ -49,70 +49,6 @@ importers:
|
||||
specifier: ^4.1.12
|
||||
version: 4.1.14
|
||||
|
||||
apps/api:
|
||||
dependencies:
|
||||
'@konobangu/analytics':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/analytics
|
||||
'@konobangu/auth':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/auth
|
||||
'@konobangu/database':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/database
|
||||
'@konobangu/design-system':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/design-system
|
||||
'@konobangu/env':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/env
|
||||
'@konobangu/next-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/next-config
|
||||
'@konobangu/observability':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/observability
|
||||
'@sentry/nextjs':
|
||||
specifier: ^8.43.0
|
||||
version: 8.47.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(next@15.1.3(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.97.1)
|
||||
import-in-the-middle:
|
||||
specifier: ^1.11.3
|
||||
version: 1.12.0
|
||||
next:
|
||||
specifier: ^15.1.3
|
||||
version: 15.1.3(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.0.0
|
||||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.0.0(react@19.0.0)
|
||||
require-in-the-middle:
|
||||
specifier: ^7.4.0
|
||||
version: 7.4.0
|
||||
svix:
|
||||
specifier: ^1.43.0
|
||||
version: 1.44.0
|
||||
devDependencies:
|
||||
'@konobangu/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/typescript-config
|
||||
'@types/node':
|
||||
specifier: 22.10.1
|
||||
version: 22.10.1
|
||||
'@types/react':
|
||||
specifier: 19.0.1
|
||||
version: 19.0.1
|
||||
'@types/react-dom':
|
||||
specifier: 19.0.2
|
||||
version: 19.0.2(@types/react@19.0.1)
|
||||
concurrently:
|
||||
specifier: ^9.1.0
|
||||
version: 9.1.1
|
||||
typescript:
|
||||
specifier: ^5.7.2
|
||||
version: 5.7.2
|
||||
|
||||
apps/app:
|
||||
dependencies:
|
||||
'@konobangu/analytics':
|
||||
@ -5476,10 +5412,6 @@ packages:
|
||||
cliui@6.0.0:
|
||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
clone@1.0.4:
|
||||
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
||||
engines: {node: '>=0.8'}
|
||||
@ -5586,11 +5518,6 @@ packages:
|
||||
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
|
||||
engines: {'0': node >= 0.8}
|
||||
|
||||
concurrently@9.1.1:
|
||||
resolution: {integrity: sha512-6VX8lrBIycgZKTwBsWS+bLrmkGRkDmvtGsYylRN9b93CygN6CbK46HmnQ3rdSOR8HRjdahDrxb5MqD9cEFOg5Q==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
console-browserify@1.2.0:
|
||||
resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==}
|
||||
|
||||
@ -9090,10 +9017,6 @@ packages:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shell-quote@1.8.2:
|
||||
resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
shelljs@0.8.5:
|
||||
resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==}
|
||||
engines: {node: '>=4'}
|
||||
@ -9596,10 +9519,6 @@ packages:
|
||||
resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tree-kill@1.2.2:
|
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
|
||||
trim-lines@3.0.1:
|
||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||
|
||||
@ -10221,10 +10140,6 @@ packages:
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yallist@2.1.2:
|
||||
resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==}
|
||||
|
||||
@ -10244,18 +10159,10 @@ packages:
|
||||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yargs@15.4.1:
|
||||
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
yargs@17.7.2:
|
||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yn@3.1.1:
|
||||
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
||||
engines: {node: '>=6'}
|
||||
@ -15191,12 +15098,6 @@ snapshots:
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
clone@1.0.4: {}
|
||||
|
||||
clone@2.1.2: {}
|
||||
@ -15289,16 +15190,6 @@ snapshots:
|
||||
readable-stream: 2.3.8
|
||||
typedarray: 0.0.6
|
||||
|
||||
concurrently@9.1.1:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
lodash: 4.17.21
|
||||
rxjs: 7.8.1
|
||||
shell-quote: 1.8.2
|
||||
supports-color: 8.1.1
|
||||
tree-kill: 1.2.2
|
||||
yargs: 17.7.2
|
||||
|
||||
console-browserify@1.2.0: {}
|
||||
|
||||
constant-case@2.0.0:
|
||||
@ -19605,8 +19496,6 @@ snapshots:
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
shell-quote@1.8.2: {}
|
||||
|
||||
shelljs@0.8.5:
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
@ -20266,8 +20155,6 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
tree-kill@1.2.2: {}
|
||||
|
||||
trim-lines@3.0.1: {}
|
||||
|
||||
trough@2.2.0: {}
|
||||
@ -21029,8 +20916,6 @@ snapshots:
|
||||
|
||||
y18n@4.0.3: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yallist@2.1.2: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
@ -21044,8 +20929,6 @@ snapshots:
|
||||
camelcase: 5.3.1
|
||||
decamelize: 1.2.0
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@15.4.1:
|
||||
dependencies:
|
||||
cliui: 6.0.0
|
||||
@ -21060,16 +20943,6 @@ snapshots:
|
||||
y18n: 4.0.3
|
||||
yargs-parser: 18.1.3
|
||||
|
||||
yargs@17.7.2:
|
||||
dependencies:
|
||||
cliui: 8.0.1
|
||||
escalade: 3.2.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
string-width: 4.2.3
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
yn@3.1.1: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
Loading…
Reference in New Issue
Block a user