diff --git a/.gitignore b/.gitignore index 5c5e9c7..d588b01 100644 --- a/.gitignore +++ b/.gitignore @@ -27,12 +27,3 @@ data/ Thumbs.db .idea/ .vscode/ - -# Only the vendored blivedm crate source and its manifests are required to -# compile the local patch. Exclude its upstream docs, examples and tooling. -vendor/blivedm/* -!vendor/blivedm/Cargo.toml -!vendor/blivedm/Cargo.lock -!vendor/blivedm/LICENSE -!vendor/blivedm/src/ -!vendor/blivedm/src/** diff --git a/AGENTS.md b/AGENTS.md index 11a5779..7ec6c15 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Project Structure & Module Organization -`apps/server-rust/` contains the Axum backend, live-event adapters, authentication, persistence, and WebSocket delivery. Database migrations live in `apps/server-rust/migrations/`; Rust unit and async tests are normally kept beside the modules they exercise. `apps/overlay/src/` contains the React/Vite control and OBS interfaces, with PWA code in `apps/overlay/pwa/` and static assets in `apps/overlay/public/`. Architecture, protocol, security, and component notes belong in `docs/`; deployment support lives in `deploy/`, `Dockerfile`, and `compose.yaml`. `vendor/blivedm/` is an intentionally patched dependency—edit it only for a targeted upstream integration change. +`apps/server-rust/` contains the Axum backend, live-event adapters, authentication, persistence, and WebSocket delivery. Database migrations live in `apps/server-rust/migrations/`; Rust unit and async tests are normally kept beside the modules they exercise. `apps/overlay/src/` contains the React/Vite control and OBS interfaces, with PWA code in `apps/overlay/pwa/` and static assets in `apps/overlay/public/`. Architecture, protocol, security, and component notes belong in `docs/`; deployment support lives in `deploy/`, `Dockerfile`, and `compose.yaml`. The backend uses the sibling `../libilibili` checkout as a path dependency; coordinate API changes in that crate instead of copying it here. ## Build, Test, and Development Commands diff --git a/Dockerfile b/Dockerfile index de7f2ea..f78e7e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,10 +6,12 @@ COPY apps/overlay ./ RUN npm run build FROM rust:1.97-bookworm AS rust-build -RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev libasound2-dev && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY apps/server-rust/Cargo.toml apps/server-rust/Cargo.lock ./apps/server-rust/ -COPY vendor/blivedm ./vendor/blivedm +# `libilibili` is a sibling checkout in local development. Compose exposes it +# as a named build context so the image always compiles the same local source +# without copying a second, drifting crate into this repository. +COPY --from=libilibili . /libilibili COPY apps/server-rust/src ./apps/server-rust/src COPY apps/server-rust/migrations ./apps/server-rust/migrations RUN --mount=type=cache,target=/usr/local/cargo/registry \ @@ -18,11 +20,8 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ cp /app/target-cache/release/lxc-stream-server /app/lxc-stream-server FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl libasound2 libssl3 && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/* WORKDIR /app -# The upstream client logs full upstream HTTP responses at INFO, which can include -# account metadata and short-lived connection tokens. Keep application lifecycle -# logs while suppressing dependency INFO output by default. COPY --from=rust-build /app/lxc-stream-server /app/lxc-stream-server COPY --from=web-build /app/apps/overlay/dist /app/web EXPOSE 9719 diff --git a/README.md b/README.md index 566f01f..6966e24 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,10 @@ 这是一个多用户 Rust/Axum 直播组件后端。目前内建 `danmaku_overlay` 弹幕姬与 `song_request` 点歌姬,后端已经按“直播源 → 强类型事件 → 组件实例”的方式拆分,后续可以继续增加礼物展示等组件。镜像构建阶段会编译 React 前端,运行时由 Rust 同域托管控制台和 OBS 页面。 -直播连接使用 [`blivedm_rs`](https://github.com/isomoes/blivedm_rs) 的 `blivedm` crate。项目在 -`vendor/blivedm` -固定了保留原始 JSON、可观测发送失败和安全重连所需的小补丁,避免 UID、礼物价格和上游事件 ID 被简化消息结构丢弃,也让宿主在 socket 恢复失败后重建客户端。 +直播连接使用相邻目录中的 [`libilibili`](https://github.com/feliscafra/libilibili) +crate。它负责 Cookie/WBI +API、直播 WebSocket 认证、心跳以及 JSON、zlib、brotli 包解析;本项目的 provider +adapter 只负责把强类型 Bilibili 命令转换成稳定的领域事件。crate 尚未建模的少量兼容命令只在 provider 边界解析,原始包不会进入组件协议。 ## 文档索引 @@ -36,6 +37,14 @@ ## 部署 +源码目录需要保持为相邻 checkout,Compose 会把 `libilibili` 作为独立 BuildKit 上下文传入镜像: + +```text +source/ +├── libilibili/ +└── lxc-streamutils/ +``` + 先复制配置并生成独立密钥: ```sh @@ -82,8 +91,8 @@ git diff --check ``` `.editorconfig` 统一换行、缩进和文件末尾规则;`.prettierignore` 与 Docker/Git -ignore 会排除依赖、构建产物、第三方 vendor、PNG/SVG 和包含真实 Secret 的 `config.toml`。不要对 -`vendor/blivedm` 做无关的批量风格改写,以便继续审查上游补丁。 +ignore 会排除依赖、构建产物、PNG/SVG 和包含真实 Secret 的 `config.toml`。`libilibili` +是独立 crate;协议解析能力应在该项目中维护,本仓库只维护领域事件适配。 ## 首次初始化与登录 diff --git a/apps/server-rust/Cargo.lock b/apps/server-rust/Cargo.lock index 85279f4..fa02cd2 100644 --- a/apps/server-rust/Cargo.lock +++ b/apps/server-rust/Cargo.lock @@ -28,31 +28,18 @@ dependencies = [ ] [[package]] -name = "allocator-api2" -version = "0.2.21" +name = "alloc-no-stdlib" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] -name = "alsa" -version = "0.9.1" +name = "alloc-stdlib" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ - "alsa-sys", - "bitflags 2.13.0", - "cfg-if", - "libc", -] - -[[package]] -name = "alsa-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" -dependencies = [ - "libc", - "pkg-config", + "alloc-no-stdlib", ] [[package]] @@ -64,83 +51,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "arboard" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" -dependencies = [ - "clipboard-win", - "image", - "log", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "wl-clipboard-rs", - "x11rb", -] - -[[package]] -name = "arrayvec" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" - [[package]] name = "async-trait" version = "0.1.89" @@ -171,14 +81,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", - "base64 0.22.1", + "base64", "bytes", "form_urlencoded", "futures-util", - "http 1.4.2", - "http-body 1.1.0", + "http", + "http-body", "http-body-util", - "hyper 1.10.1", + "hyper", "hyper-util", "itoa", "matchit", @@ -191,9 +101,9 @@ dependencies = [ "serde_path_to_error", "serde_urlencoded", "sha1", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.29.0", "tower", "tower-layer", "tower-service", @@ -208,12 +118,12 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.2", - "http-body 1.1.0", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tower-layer", "tower-service", "tracing", @@ -225,78 +135,18 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.13.0", - "cexpr", - "clang-sys", - "itertools", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex 1.3.0", - "syn", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" -[[package]] -name = "blivedm" -version = "0.5.6" -dependencies = [ - "arboard", - "base64 0.21.7", - "brotlic", - "clap", - "clap_complete", - "crossterm", - "dirs", - "env_logger", - "futures", - "futures-channel", - "http 0.2.12", - "log", - "md5", - "native-tls", - "ratatui", - "reqwest 0.11.27", - "rodio", - "serde", - "serde_json", - "tokio", - "toml", - "tungstenite 0.20.1", - "unicode-width 0.2.0", - "url", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -316,21 +166,24 @@ dependencies = [ ] [[package]] -name = "brotlic" -version = "0.8.2" +name = "brotli" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f552f56f302af0006c32b50bfa2bdb4696fd6ba33c3ab9f6225fefdb1efdc680" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" dependencies = [ - "brotlic-sys", + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", ] [[package]] -name = "brotlic-sys" -version = "0.2.2" +name = "brotli-decompressor" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdec5c62bc97b56349053cf66ba503af5c2448591be61c3ad70a5f11b57e574" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" dependencies = [ - "cc", + "alloc-no-stdlib", + "alloc-stdlib", ] [[package]] @@ -363,21 +216,6 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" version = "1.2.67" @@ -385,24 +223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", - "shlex 2.0.1", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", + "shlex", ] [[package]] @@ -475,117 +296,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_complete" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b" -dependencies = [ - "clap", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "claxon" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" - -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - [[package]] name = "cmov" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "compact_str" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - [[package]] name = "const-oid" version = "0.10.2" @@ -598,103 +314,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" -[[package]] -name = "cookie" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "cookie_store" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" -dependencies = [ - "cookie", - "idna 0.3.0", - "log", - "publicsuffix", - "serde", - "serde_derive", - "serde_json", - "time", - "url", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "coreaudio-rs" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" -dependencies = [ - "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b4739a805a62757a83e5654fa3faabec0442666b263bb2287d5a8185bfd953" -dependencies = [ - "bindgen", -] - -[[package]] -name = "cpal" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" -dependencies = [ - "alsa", - "core-foundation-sys", - "coreaudio-rs", - "dasp_sample", - "jni", - "js-sys", - "libc", - "mach2", - "ndk", - "ndk-context", - "oboe", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -722,37 +347,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossterm" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" -dependencies = [ - "bitflags 2.13.0", - "crossterm_winapi", - "mio", - "parking_lot", - "rustix 0.38.44", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-common" version = "0.1.7" @@ -782,46 +376,6 @@ dependencies = [ "cmov", ] -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "dasp_sample" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" - [[package]] name = "data-encoding" version = "2.11.0" @@ -863,43 +417,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "defmt" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" -dependencies = [ - "defmt-parser", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "defmt-parser" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" -dependencies = [ - "thiserror 2.0.18", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" - [[package]] name = "digest" version = "0.10.7" @@ -923,37 +440,6 @@ dependencies = [ "ctutils", ] -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.0", - "objc2", -] - [[package]] name = "displaydoc" version = "0.2.6" @@ -965,50 +451,6 @@ dependencies = [ "syn", ] -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "env_filter" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1025,30 +467,12 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "error-code" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" - [[package]] name = "fallible-iterator" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "fax" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" - [[package]] name = "fdeflate" version = "0.3.7" @@ -1064,12 +488,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - [[package]] name = "flate2" version = "1.1.9" @@ -1080,33 +498,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1116,21 +507,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.32" @@ -1147,23 +523,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - [[package]] name = "futures-macro" version = "0.3.32" @@ -1193,13 +552,10 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "futures-channel", "futures-core", - "futures-io", "futures-macro", "futures-sink", "futures-task", - "memchr", "pin-project-lite", "slab", ] @@ -1214,16 +570,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.4", - "windows-link", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -1263,65 +609,12 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hermit-abi" version = "0.5.2" @@ -1346,23 +639,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.4.2" @@ -1373,17 +649,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.1.0" @@ -1391,7 +656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", - "http 1.4.2", + "http", ] [[package]] @@ -1402,8 +667,8 @@ checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", - "http 1.4.2", - "http-body 1.1.0", + "http", + "http-body", "pin-project-lite", ] @@ -1434,30 +699,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.10.1" @@ -1468,8 +709,8 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http 1.4.2", - "http-body 1.1.0", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -1479,32 +720,18 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", -] - [[package]] name = "hyper-rustls" version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.2", - "hyper 1.10.1", + "http", + "hyper", "hyper-util", - "rustls 0.23.42", + "rustls", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower-service", "webpki-roots 1.0.8", ] @@ -1515,18 +742,18 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-util", - "http 1.4.2", - "http-body 1.1.0", - "hyper 1.10.1", + "http", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.5", + "socket2", "tokio", "tower-service", "tracing", @@ -1544,7 +771,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -1638,22 +865,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "1.1.0" @@ -1686,7 +897,6 @@ dependencies = [ "moxcms", "num-traits", "png", - "tiff", ] [[package]] @@ -1696,16 +906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.1", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", + "hashbrown", ] [[package]] @@ -1717,125 +918,18 @@ dependencies = [ "generic-array", ] -[[package]] -name = "instability" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" -dependencies = [ - "darling", - "indoc", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jiff" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" -dependencies = [ - "defmt", - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "jobserver" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.3", - "libc", -] - [[package]] name = "js-sys" version = "0.3.103" @@ -1853,17 +947,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "tinyvec", -] - [[package]] name = "libc" version = "0.2.186" @@ -1871,13 +954,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +name = "libilibili" +version = "0.1.0" dependencies = [ - "cfg-if", - "windows-link", + "base64", + "brotli", + "flate2", + "futures-util", + "md-5 0.10.6", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-tungstenite 0.26.2", + "url", ] [[package]] @@ -1889,18 +980,6 @@ dependencies = [ "libc", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - [[package]] name = "litemap" version = "0.8.2" @@ -1922,15 +1001,6 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "lru-slab" version = "0.1.2" @@ -1943,14 +1013,13 @@ version = "0.1.0" dependencies = [ "async-trait", "axum", - "base64 0.22.1", - "blivedm", + "base64", "chacha20poly1305", "chrono", "deadpool-postgres", - "futures-channel", + "libilibili", "rand 0.9.5", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "sha2 0.10.9", @@ -1966,15 +1035,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - [[package]] name = "matchers" version = "0.2.0" @@ -1990,6 +1050,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "md-5" version = "0.11.0" @@ -2000,12 +1070,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - [[package]] name = "memchr" version = "2.8.3" @@ -2028,12 +1092,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2051,7 +1109,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", - "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -2066,71 +1123,6 @@ dependencies = [ "pxfm", ] -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "ndk" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" -dependencies = [ - "bitflags 2.13.0", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2140,23 +1132,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -2176,99 +1151,13 @@ dependencies = [ "libc", ] -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-core-graphics", - "objc2-foundation", -] - [[package]] name = "objc2-core-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.0", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.13.0", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-core-foundation", + "bitflags", ] [[package]] @@ -2280,115 +1169,18 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "oboe" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" -dependencies = [ - "jni", - "ndk", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" -dependencies = [ - "cc", -] - -[[package]] -name = "ogg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" -dependencies = [ - "byteorder", -] - [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - [[package]] name = "opaque-debug" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl" -version = "0.10.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" -dependencies = [ - "bitflags 2.13.0", - "cfg-if", - "foreign-types", - "libc", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "openssl-sys" -version = "0.9.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "os_pipe" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "parking_lot" version = "0.12.5" @@ -2412,29 +1204,12 @@ dependencies = [ "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset", - "hashbrown 0.15.5", - "indexmap", -] - [[package]] name = "phf" version = "0.13.1" @@ -2460,19 +1235,13 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - [[package]] name = "png" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.0", + "bitflags", "crc32fast", "fdeflate", "flate2", @@ -2490,33 +1259,18 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - [[package]] name = "postgres-protocol" version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" dependencies = [ - "base64 0.22.1", + "base64", "byteorder", "bytes", "fallible-iterator", "hmac 0.13.0", - "md-5", + "md-5 0.11.0", "memchr", "rand 0.10.2", "sha2 0.11.0", @@ -2547,12 +1301,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2562,15 +1310,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -2580,22 +1319,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "psl-types" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" - -[[package]] -name = "publicsuffix" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" -dependencies = [ - "idna 1.1.0", - "psl-types", -] - [[package]] name = "pxfm" version = "0.1.30" @@ -2614,26 +1337,11 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e3dd60f5b603f72c307455fc52deec52ada1ba53c7580918bb2a8e3247d4fe7" dependencies = [ - "base64 0.22.1", + "base64", "image", "qrcodegen", ] -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quick-xml" -version = "0.39.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" -dependencies = [ - "memchr", -] - [[package]] name = "quinn" version = "0.11.11" @@ -2646,9 +1354,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.42", - "socket2 0.6.5", - "thiserror 2.0.18", + "rustls", + "socket2", + "thiserror", "tokio", "tracing", "web-time", @@ -2667,10 +1375,10 @@ dependencies = [ "rand_pcg", "ring", "rustc-hash", - "rustls 0.23.42", + "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -2685,7 +1393,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.5", + "socket2", "tracing", "windows-sys 0.61.2", ] @@ -2711,24 +1419,13 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.5", ] @@ -2743,16 +1440,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_chacha" version = "0.9.0" @@ -2796,57 +1483,13 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "ratatui" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" -dependencies = [ - "bitflags 2.13.0", - "cassowary", - "compact_str", - "crossterm", - "indoc", - "instability", - "itertools", - "lru", - "paste", - "strum", - "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.2.0", -] - [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "regex" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "bitflags", ] [[package]] @@ -2866,79 +1509,34 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64 0.21.7", - "bytes", - "cookie", - "cookie_store", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-rustls 0.24.2", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls 0.21.12", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration", - "tokio", - "tokio-rustls 0.24.1", - "tokio-util", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots 0.25.4", - "winreg", -] - [[package]] name = "reqwest" version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-core", - "http 1.4.2", - "http-body 1.1.0", + "http", + "http-body", "http-body-util", - "hyper 1.10.1", - "hyper-rustls 0.27.9", + "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.42", + "rustls", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -2963,63 +1561,12 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rodio" -version = "0.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b1bb7b48ee48471f55da122c0044fcc7600cfcc85db88240b89cb832935e611" -dependencies = [ - "claxon", - "cpal", - "hound", - "lewton", - "symphonia", -] - [[package]] name = "rustc-hash" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.13.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.0", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.23.42" @@ -3029,20 +1576,11 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.13", + "rustls-webpki", "subtle", "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pki-types" version = "1.15.0" @@ -3053,16 +1591,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.103.13" @@ -3086,63 +1614,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags 2.13.0", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "serde" version = "1.0.228" @@ -3260,39 +1737,12 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3327,16 +1777,6 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.5" @@ -3353,12 +1793,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "stringprep" version = "0.1.5" @@ -3370,89 +1804,12 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn", -] - [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "symphonia" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" -dependencies = [ - "lazy_static", - "symphonia-bundle-mp3", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-bundle-mp3" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" -dependencies = [ - "lazy_static", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-core" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" -dependencies = [ - "arrayvec", - "bitflags 1.3.2", - "bytemuck", - "lazy_static", - "log", -] - -[[package]] -name = "symphonia-metadata" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" -dependencies = [ - "encoding_rs", - "lazy_static", - "log", - "symphonia-core", -] - [[package]] name = "syn" version = "2.0.118" @@ -3464,12 +1821,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3490,67 +1841,13 @@ dependencies = [ "syn", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -3573,50 +1870,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", -] - -[[package]] -name = "time" -version = "0.3.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tinystr" version = "0.8.3" @@ -3654,7 +1907,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.5", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -3690,32 +1943,38 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand 0.10.2", - "socket2 0.6.5", + "socket2", "tokio", "tokio-util", "whoami", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.42", + "rustls", "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite 0.26.2", + "webpki-roots 0.26.11", +] + [[package]] name = "tokio-tungstenite" version = "0.29.0" @@ -3749,8 +2008,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", + "toml_datetime", + "toml_edit", ] [[package]] @@ -3762,15 +2021,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_edit" version = "0.22.27" @@ -3780,30 +2030,9 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime 0.6.11", + "toml_datetime", "toml_write", - "winnow 0.7.15", -] - -[[package]] -name = "toml_edit" -version = "0.25.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" -dependencies = [ - "indexmap", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "winnow 1.0.4", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.4", + "winnow", ] [[package]] @@ -3839,7 +2068,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -3852,12 +2081,12 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.0", + "bitflags", "bytes", "futures-core", "futures-util", - "http 1.4.2", - "http-body 1.1.0", + "http", + "http-body", "http-body-util", "http-range-header", "httpdate", @@ -3960,17 +2189,6 @@ dependencies = [ "tracing-serde", ] -[[package]] -name = "tree_magic_mini" -version = "3.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" -dependencies = [ - "memchr", - "nom 8.0.0", - "petgraph", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -3979,20 +2197,20 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ - "byteorder", "bytes", "data-encoding", - "http 0.2.12", + "http", "httparse", "log", - "rand 0.8.7", + "rand 0.9.5", + "rustls", + "rustls-pki-types", "sha1", - "thiserror 1.0.69", - "url", + "thiserror", "utf-8", ] @@ -4004,12 +2222,12 @@ checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" dependencies = [ "bytes", "data-encoding", - "http 1.4.2", + "http", "httparse", "log", "rand 0.9.5", "sha1", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -4051,35 +2269,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-truncate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" -dependencies = [ - "itertools", - "unicode-segmentation", - "unicode-width 0.1.14", -] - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" - [[package]] name = "universal-hash" version = "0.5.1" @@ -4103,7 +2292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", - "idna 1.1.0", + "idna", "percent-encoding", "serde", ] @@ -4126,12 +2315,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "uuid" version = "1.23.5" @@ -4150,28 +2333,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -4269,89 +2436,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wayland-backend" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" -dependencies = [ - "cc", - "downcast-rs", - "rustix 1.1.4", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" -dependencies = [ - "bitflags 2.13.0", - "rustix 1.1.4", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" -dependencies = [ - "bitflags 2.13.0", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" -dependencies = [ - "bitflags 2.13.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" -dependencies = [ - "proc-macro2", - "quick-xml", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" -dependencies = [ - "pkg-config", -] - [[package]] name = "web-sys" version = "0.3.103" @@ -4374,9 +2458,12 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.4" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] [[package]] name = "webpki-roots" @@ -4387,12 +2474,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - [[package]] name = "whoami" version = "2.1.2" @@ -4406,57 +2487,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -4466,7 +2496,7 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link", - "windows-result 0.4.1", + "windows-result", "windows-strings", ] @@ -4498,15 +2528,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.4.1" @@ -4525,49 +2546,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "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]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -4579,249 +2564,70 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "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 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[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_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[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_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[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 = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.7.15" @@ -4831,72 +2637,18 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -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 = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wl-clipboard-rs" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" -dependencies = [ - "libc", - "log", - "os_pipe", - "rustix 1.1.4", - "thiserror 2.0.18", - "tree_magic_mini", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-wlr", -] - [[package]] name = "writeable" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "gethostname", - "rustix 1.1.4", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - [[package]] name = "yoke" version = "0.8.3" @@ -5019,18 +2771,3 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zune-core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" - -[[package]] -name = "zune-jpeg" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -] diff --git a/apps/server-rust/Cargo.toml b/apps/server-rust/Cargo.toml index d81e137..4fc3169 100644 --- a/apps/server-rust/Cargo.toml +++ b/apps/server-rust/Cargo.toml @@ -7,14 +7,12 @@ edition = "2024" axum = { version = "0.8", features = ["ws", "json"] } async-trait = "0.1" base64 = "0.22" -# Patched local copy of the published blivedm_rs crate. The patch preserves -# the upstream raw payload so the application can retain UID, price and event -# identifiers for atomic accounting and gift de-duplication. -blivedm = { path = "../../vendor/blivedm", default-features = false } chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } chacha20poly1305 = "0.10" deadpool-postgres = "0.14" -futures-channel = "0.3" +# Kept as a sibling checkout during development. Docker Compose supplies the +# same directory as a named BuildKit context at `/libilibili`. +libilibili = { path = "../../../libilibili" } rand = "0.9" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1", features = ["derive"] } diff --git a/apps/server-rust/README.md b/apps/server-rust/README.md index 3ca4fe9..ed4a4a5 100644 --- a/apps/server-rust/README.md +++ b/apps/server-rust/README.md @@ -27,7 +27,7 @@ crate 导出。 - handler 不能信任请求体中的 owner;owner 必须来自 session 或 source context。 - tenant table 查询必须在 `Db::set_tenant` 后的事务中执行。 - provider 只能输出 canonical、bounded、sanitized `LiveEvent`。 -- Bilibili listener 必须保持 20 秒心跳;socket 内部重连失败后由 adapter 重建完整客户端。 +- `libilibili` listener 必须保持 20 秒心跳;断线后由 adapter 重新获取弹幕 host/token 并重建 socket。 - projection 无副作用;可靠业务动作必须使用幂等 handler。 - token、邀请码和恢复码只存摘要,TOTP/CookieCloud Secret 只存认证加密密文。 - EventHub 的 channel key 是 component ID,不允许增加无权限的全局 receiver。 @@ -42,7 +42,8 @@ cargo clippy --manifest-path apps/server-rust/Cargo.toml --all-targets --no-deps cargo test --manifest-path apps/server-rust/Cargo.toml --all-targets ``` -第三方 `vendor/blivedm` 不作为本项目风格重写目标;项目只维护保留原始 JSON 所需的小补丁。 +直播协议、WBI 与 WebSocket 解包由相邻的 `libilibili` +crate 维护;本项目只测试强类型命令到领域事件的映射。 更多设计说明: diff --git a/apps/server-rust/src/config.rs b/apps/server-rust/src/config.rs index acaa5e8..8e5e0ee 100644 --- a/apps/server-rust/src/config.rs +++ b/apps/server-rust/src/config.rs @@ -218,7 +218,7 @@ impl Config { legacy_obs_access_token: file.obs.access_token, legacy_overlay_defaults: overlay_defaults(file.overlay), log_filter: file.logging.filter.unwrap_or_else(|| { - "lxc_stream_server=info,blivedm=warn,tokio_postgres=warn".into() + "lxc_stream_server=info,libilibili=warn,tokio_postgres=warn".into() }), gift_refresh_seconds: file .gifts diff --git a/apps/server-rust/src/live/bilibili.rs b/apps/server-rust/src/live/bilibili.rs index 24bf94f..354bf83 100644 --- a/apps/server-rust/src/live/bilibili.rs +++ b/apps/server-rust/src/live/bilibili.rs @@ -1,19 +1,21 @@ -//! Bilibili `blivedm_rs` adapter and raw-command normalization. +//! Bilibili `libilibili` adapter and canonical event normalization. //! //! The adapter authenticates with a CookieCloud-derived cookie, refreshes gift -//! and emoticon catalogs, and converts supported Bilibili commands into bounded -//! domain payloads. Unknown commands expose only sanitized metadata—never the -//! original unbounded packet or authentication material. +//! and emoticon catalogs, drives the asynchronous live WebSocket heartbeat, and +//! converts supported commands into bounded domain payloads. Commands not yet +//! modeled by `libilibili` are interpreted only inside this provider boundary; +//! unknown events expose sanitized command names, never raw packets or secrets. -use std::{ - sync::Arc, - thread, - time::{Duration, Instant}, -}; +use std::{sync::Arc, time::Duration}; use async_trait::async_trait; -use blivedm::client::{models::BiliMessage, websocket::BiliLiveClient}; -use futures_channel::mpsc as futures_mpsc; +use libilibili::{ + Client, Credentials, + websocket::{ + ComboSendMessage, DanmuMessage, GiftMessage, InteractWordMessage, LikeClickMessage, + LiveCommand, LiveEvent as UpstreamLiveEvent, LiveWebSocket, SuperChatMessage, + }, +}; use serde_json::{Value, json}; use tokio::sync::{mpsc, watch}; use tokio_util::sync::CancellationToken; @@ -146,12 +148,21 @@ impl BilibiliProvider { viewer, name, gift_id, + coin_type, battery, quantity, event_id, } => LiveEventPayload::Gift(GiftEvent { viewer, - gift: gift_details(&self.gift_catalog, name, gift_id, battery, quantity).await, + gift: gift_details( + &self.gift_catalog, + name, + gift_id, + coin_type, + battery, + quantity, + ) + .await, quantity: quantity.max(1), source_event_id: event_id, }), @@ -159,12 +170,21 @@ impl BilibiliProvider { viewer, name, gift_id, + coin_type, battery, quantity, combo_id, } => LiveEventPayload::GiftCombo(GiftComboEvent { viewer, - gift: gift_details(&self.gift_catalog, name, gift_id, battery, quantity).await, + gift: gift_details( + &self.gift_catalog, + name, + gift_id, + coin_type, + battery, + quantity, + ) + .await, quantity: quantity.max(1), combo_id, }), @@ -227,151 +247,149 @@ impl LiveProvider for BilibiliProvider { self.initial_catalogs(&context.room_id).await; self.spawn_catalog_refreshes(context.room_id.clone(), cancel.clone()); - let (raw_sender, mut raw_receiver) = mpsc::channel::(256); - let cookie = self.cookie.to_string(); - let room_id = context.room_id.clone(); - let listener_cancel = cancel.clone(); - let listener_status = status.clone(); - let listener = tokio::task::spawn_blocking(move || -> Result<(), String> { - // A full-client retry loop complements blivedm_rs' short socket - // reconnect loop. If all in-place attempts fail, recreating the - // client refreshes the danmaku host and authentication token. - while !listener_cancel.is_cancelled() { - let (upstream_sender, mut upstream_receiver) = futures_mpsc::channel(256); - let mut client = - match BiliLiveClient::new_auto(Some(&cookie), &room_id, upstream_sender) { - Ok(client) => client, - Err(error) => { - warn!(%error, %room_id, "Bilibili listener creation failed; retrying"); - let _ = listener_status.send(SourceStatus { - source_id: context.source_id, - room_id: room_id.clone(), - connected: false, - cookie_cloud: true, - detail: format!("Bilibili connection failed; retrying: {error}"), - }); - wait_for_retry(&listener_cancel, RECONNECT_BACKOFF); - continue; - } - }; - if let Err(error) = client.set_read_timeout(Some(Duration::from_secs(1))) { - client.close(); - return Err(format!("cannot configure Bilibili socket timeout: {error}")); - } - // Bilibili closes otherwise healthy danmaku sockets after - // roughly one minute without a heartbeat. The upstream CLI - // sends one every 20 seconds; embedded users must do the same. - if !client.send_auth() || !client.send_heart_beat() { - client.close(); - let _ = listener_status.send(SourceStatus { - source_id: context.source_id, - room_id: room_id.clone(), - connected: false, - cookie_cloud: true, - detail: "Bilibili authentication failed; retrying".into(), - }); - wait_for_retry(&listener_cancel, RECONNECT_BACKOFF); + let room_id = context + .room_id + .parse::() + .map_err(|_| "Bilibili room ID must be an unsigned integer".to_owned())?; + let client = Client::with_credentials(Credentials::from_cookie_header(&*self.cookie)) + .map_err(|error| format!("cannot create libilibili client: {error}"))?; + + 'provider: loop { + let connection = tokio::select! { + _ = cancel.cancelled() => break 'provider, + result = LiveWebSocket::connect(&client, room_id) => result, + }; + let mut socket = match connection { + Ok(socket) => socket, + Err(error) => { + warn!(%error, room_id = %context.room_id, "Bilibili connection failed; retrying"); + send_status( + &status, + &context, + false, + format!("Bilibili connection failed; retrying: {error}"), + ); + if wait_for_retry(&cancel).await { + break 'provider; + } continue; } - let _ = listener_status.send(SourceStatus { - source_id: context.source_id, - room_id: room_id.clone(), - connected: true, - cookie_cloud: true, - detail: "Bilibili socket connected; waiting for live events".into(), - }); - info!(%room_id, "Bilibili live socket connected and heartbeat started"); + }; - let mut last_heartbeat = Instant::now(); - let mut received_event = false; - let disconnect_error = loop { - if listener_cancel.is_cancelled() { - client.close(); - return Ok(()); - } - if last_heartbeat.elapsed() >= HEARTBEAT_INTERVAL { - if !client.send_heart_beat() { - break "heartbeat recovery failed".to_owned(); - } - last_heartbeat = Instant::now(); - } - if let Err(error) = client.receive() { - break error; - } - while let Ok(message) = upstream_receiver.try_recv() { - let Some(message) = normalize(message) else { - continue; - }; - if !received_event { - received_event = true; - info!(%room_id, "Bilibili listener received its first live event"); - let _ = listener_status.send(SourceStatus { - source_id: context.source_id, - room_id: room_id.clone(), - connected: true, - cookie_cloud: true, - detail: "Connected and receiving live events".into(), - }); - } - if raw_sender.blocking_send(message).is_err() { - client.close(); - return Ok(()); - } - } - }; - - client.close(); - warn!(error = %disconnect_error, %room_id, "Bilibili listener disconnected; rebuilding client"); - let _ = listener_status.send(SourceStatus { - source_id: context.source_id, - room_id: room_id.clone(), - connected: false, - cookie_cloud: true, - detail: format!("Bilibili disconnected; retrying: {disconnect_error}"), - }); - wait_for_retry(&listener_cancel, RECONNECT_BACKOFF); - } - Ok(()) - }); - - loop { - tokio::select! { - _ = cancel.cancelled() => break, - value = raw_receiver.recv() => { - let Some(value) = value else { break }; - let event = Arc::new(self.enrich(&context, value).await); - if events.send(event).await.is_err() { break; } + // `connect` sends the authentication packet. An immediate heartbeat + // matches the web client and starts the server's liveness window. + if let Err(error) = socket.heartbeat().await { + warn!(%error, room_id = %context.room_id, "initial Bilibili heartbeat failed"); + send_status( + &status, + &context, + false, + format!("Bilibili heartbeat failed; retrying: {error}"), + ); + if wait_for_retry(&cancel).await { + break 'provider; } + continue; + } + send_status( + &status, + &context, + true, + "Bilibili socket connected; waiting for live events", + ); + info!(room_id = %context.room_id, "libilibili live socket connected and heartbeat started"); + + let mut next_heartbeat = tokio::time::Instant::now() + HEARTBEAT_INTERVAL; + let mut received_event = false; + let disconnect_error = loop { + let until_heartbeat = + next_heartbeat.saturating_duration_since(tokio::time::Instant::now()); + let upstream = tokio::select! { + _ = cancel.cancelled() => break 'provider, + result = tokio::time::timeout(until_heartbeat, socket.next_event()) => result, + }; + let upstream = match upstream { + Err(_) => { + if let Err(error) = socket.heartbeat().await { + break format!("heartbeat failed: {error}"); + } + next_heartbeat = tokio::time::Instant::now() + HEARTBEAT_INTERVAL; + continue; + } + Ok(Err(error)) => break error.to_string(), + Ok(Ok(None)) => break "upstream closed the WebSocket".to_owned(), + Ok(Ok(Some(event))) => event, + }; + if let Some(error) = authentication_error(&upstream) { + break error; + } + let Some(raw) = normalize_upstream(upstream) else { + continue; + }; + if !received_event { + received_event = true; + info!(room_id = %context.room_id, "libilibili listener received its first live event"); + send_status( + &status, + &context, + true, + "Connected and receiving live events", + ); + } + let event = Arc::new(self.enrich(&context, raw).await); + if events.send(event).await.is_err() { + break 'provider; + } + }; + + warn!(error = %disconnect_error, room_id = %context.room_id, "Bilibili listener disconnected; rebuilding libilibili socket"); + send_status( + &status, + &context, + false, + format!("Bilibili disconnected; retrying: {disconnect_error}"), + ); + if wait_for_retry(&cancel).await { + break 'provider; } } cancel.cancel(); - listener - .await - .map_err(|error| format!("listener task failed: {error}"))??; - let _ = status.send(SourceStatus { - source_id: context.source_id, - room_id: context.room_id, - connected: false, - cookie_cloud: true, - detail: "Listener stopped".into(), - }); + send_status(&status, &context, false, "Listener stopped"); Ok(()) } } -/// Blocking listener tasks cannot await a cancellation token. Sleeping in -/// short slices keeps shutdown/reconfiguration latency bounded. -fn wait_for_retry(cancel: &CancellationToken, duration: Duration) { - let deadline = Instant::now() + duration; - while !cancel.is_cancelled() { - let remaining = deadline.saturating_duration_since(Instant::now()); - if remaining.is_zero() { - break; - } - thread::sleep(remaining.min(Duration::from_millis(200))); +fn send_status( + status: &watch::Sender, + context: &SourceContext, + connected: bool, + detail: impl Into, +) { + let _ = status.send(SourceStatus { + source_id: context.source_id, + room_id: context.room_id.clone(), + connected, + cookie_cloud: true, + detail: detail.into(), + }); +} + +/// Returns `true` when cancellation won the retry wait. +async fn wait_for_retry(cancel: &CancellationToken) -> bool { + tokio::select! { + _ = cancel.cancelled() => true, + _ = tokio::time::sleep(RECONNECT_BACKOFF) => false, } } +fn authentication_error(event: &UpstreamLiveEvent) -> Option { + let UpstreamLiveEvent::Auth(payload) = event else { + return None; + }; + let code = payload.get("code").and_then(Value::as_i64).unwrap_or(-1); + (code != 0).then(|| format!("Bilibili WebSocket authentication rejected with code {code}")) +} + #[derive(Clone, Debug)] struct EmoticonHint { text: String, @@ -397,6 +415,7 @@ enum ProviderEvent { viewer: PlatformViewer, name: String, gift_id: Option, + coin_type: Option, battery: i32, quantity: i32, event_id: String, @@ -405,6 +424,7 @@ enum ProviderEvent { viewer: PlatformViewer, name: String, gift_id: Option, + coin_type: Option, battery: i32, quantity: i32, combo_id: String, @@ -432,13 +452,249 @@ enum ProviderEvent { }, } -fn normalize(message: BiliMessage) -> Option { - let raw = match message { - BiliMessage::Raw(value) => value, - _ => return None, +fn normalize_upstream(event: UpstreamLiveEvent) -> Option { + let UpstreamLiveEvent::Command(command) = event else { + return None; }; + normalize_command(*command) +} + +fn normalize_command(command: LiveCommand) -> Option { + match command { + LiveCommand::Danmu(message) => normalize_danmaku(&message), + LiveCommand::Gift(message) => normalize_gift(&message), + LiveCommand::ComboSend(message) => normalize_combo(&message), + LiveCommand::LikeClick(message) => normalize_like(&message), + LiveCommand::LikeUpdate(message) => unknown(message.cmd), + LiveCommand::LikeNotice(message) => unknown(message.cmd), + LiveCommand::GuardBuy(message) => Some(ProviderEvent::Guard { + viewer: PlatformViewer { + uid: message.uid.to_string(), + name: message.username, + }, + name: message.gift_name, + quantity: bounded_i32(message.num).max(1), + price: message.extra.get("price").and_then(value_i64).unwrap_or(0), + }), + LiveCommand::SuperChat(message) => normalize_superchat(&message), + LiveCommand::InteractWord(message) => normalize_interaction(&message), + // Invalid/unknown commands retain raw JSON inside libilibili. We only + // use that fallback for legacy aliases or malformed versions of known + // commands; every other raw payload is discarded at this boundary. + LiveCommand::Unknown { command, raw } + | LiveCommand::Invalid { + command, + raw, + error: _, + } => normalize_raw(&raw).or_else(|| { + Some(ProviderEvent::Unknown { + command: sanitized_command(command.as_deref()), + }) + }), + LiveCommand::OnlineRankCount(message) => unknown(message.cmd), + LiveCommand::WatchedChange(message) => unknown(message.cmd), + LiveCommand::StopLiveRoomList(message) => unknown(message.cmd), + LiveCommand::GiftStarProcess(message) => unknown(message.cmd), + LiveCommand::OnlineRankV3(message) => unknown(message.cmd), + LiveCommand::InteractWordV2(message) => unknown(message.cmd), + LiveCommand::RoomChange(message) => unknown(message.cmd), + LiveCommand::Live(message) => unknown(message.cmd), + LiveCommand::Preparing(message) => unknown(message.cmd), + } +} + +fn normalize_danmaku(message: &DanmuMessage) -> Option { + let text = message.text.clone()?; + let uid = message.sender.uid?; + Some(ProviderEvent::Danmaku { + viewer: PlatformViewer { + uid: uid.to_string(), + name: message + .sender + .uname + .clone() + .unwrap_or_else(|| format!("UID {uid}")), + }, + emoticons: parse_danmaku_emoticons(message, &text), + text, + }) +} + +fn normalize_gift(message: &GiftMessage) -> Option { + let uid = message.data.uid?; + let quantity = bounded_i32(message.data.num.unwrap_or(1)).max(1); + let unit_price = message.data.price.or_else(|| { + message + .data + .total_coin + .map(|total| total / u64::try_from(quantity).unwrap_or(1)) + }); + let event_id = message + .message_id + .clone() + .or_else(|| { + message + .data + .extra + .get("tid") + .and_then(value_lossless_string) + }) + .unwrap_or_else(|| { + let stable_time = message + .data + .extra + .get("timestamp") + .and_then(value_lossless_string) + .or_else(|| message.sent_at.map(|value| value.to_string())) + .unwrap_or_else(|| "unknown-time".into()); + format!( + "gift-{uid}-{}-{stable_time}", + message.data.gift_id.unwrap_or_default() + ) + }); + Some(ProviderEvent::Gift { + viewer: PlatformViewer { + uid: uid.to_string(), + name: message + .data + .uname + .clone() + .unwrap_or_else(|| format!("UID {uid}")), + }, + name: message + .data + .gift_name + .clone() + .unwrap_or_else(|| "礼物".into()), + gift_id: message.data.gift_id.and_then(bounded_i64), + coin_type: message.data.coin_type.clone(), + battery: bounded_i32(unit_price.unwrap_or_default()), + quantity, + event_id, + }) +} + +fn normalize_combo(message: &ComboSendMessage) -> Option { + let uid = message.data.uid?; + let quantity = message + .data + .combo_num + .or(message.data.batch_combo_num) + .or(message.data.num) + .map(bounded_i32) + .unwrap_or(1) + .max(1); + let unit_price = message.data.price.or_else(|| { + message + .data + .total_coin + .map(|total| total / message.data.num.unwrap_or(1).max(1)) + }); + let gift_id = message.data.gift_id.and_then(bounded_i64); + let combo_id = message + .data + .combo_id + .clone() + .or_else(|| message.data.batch_combo_id.clone()) + .unwrap_or_else(|| format!("combo-{uid}-{}", message.data.gift_id.unwrap_or_default())); + Some(ProviderEvent::GiftCombo { + viewer: PlatformViewer { + uid: uid.to_string(), + name: message + .data + .uname + .clone() + .unwrap_or_else(|| format!("UID {uid}")), + }, + name: message + .data + .gift_name + .clone() + .unwrap_or_else(|| "礼物".into()), + gift_id, + coin_type: message.data.coin_type.clone(), + battery: bounded_i32(unit_price.unwrap_or_default()), + quantity, + combo_id, + }) +} + +fn normalize_like(message: &LikeClickMessage) -> Option { + if message.data.is_like == Some(false) { + return None; + } + let uid = message.data.uid?; + Some(ProviderEvent::Like { + viewer: PlatformViewer { + uid: uid.to_string(), + name: message + .data + .uname + .clone() + .unwrap_or_else(|| format!("UID {uid}")), + }, + }) +} + +fn normalize_superchat(message: &SuperChatMessage) -> Option { + let uid = message.data.uid; + let name = message + .data + .extra + .get("user_info") + .and_then(|value| value.get("uname")) + .and_then(Value::as_str) + .or_else(|| message.data.extra.get("uname").and_then(Value::as_str)) + .map(ToOwned::to_owned) + .unwrap_or_else(|| format!("UID {uid}")); + let event_id = message + .data + .extra + .get("id") + .and_then(value_lossless_string) + .unwrap_or_else(|| Uuid::new_v4().to_string()); + Some(ProviderEvent::SuperChat { + viewer: PlatformViewer { + uid: uid.to_string(), + name, + }, + message: message.data.message.clone(), + price: bounded_i64(message.data.price).unwrap_or(i64::MAX), + event_id, + }) +} + +fn normalize_interaction(message: &InteractWordMessage) -> Option { + let viewer = PlatformViewer { + uid: message.data.uid.to_string(), + name: message.data.uname.clone(), + }; + match message.data.msg_type { + 1 => Some(ProviderEvent::Enter { viewer }), + 3 => Some(ProviderEvent::Share { viewer }), + _ => unknown(message.cmd.clone()), + } +} + +fn unknown(command: String) -> Option { + Some(ProviderEvent::Unknown { + command: sanitized_command(Some(&command)), + }) +} + +fn sanitized_command(command: Option<&str>) -> String { + command + .and_then(|value| value.split(':').next()) + .filter(|value| !value.is_empty()) + .unwrap_or("UNKNOWN") + .chars() + .take(80) + .collect() +} + +fn normalize_raw(raw: &Value) -> Option { let command = raw.get("cmd")?.as_str()?.split(':').next()?.to_owned(); - let data = raw.get("data").unwrap_or(&raw); + let data = raw.get("data").unwrap_or(raw); let viewer = |uid: &Value, name: &Value| { Some(PlatformViewer { uid: uid @@ -458,15 +714,6 @@ fn normalize(message: BiliMessage) -> Option { ) }; match command.as_str() { - "DANMU_MSG" => { - let info = raw.get("info")?.as_array()?; - let text = info.get(1)?.as_str()?.to_owned(); - Some(ProviderEvent::Danmaku { - viewer: viewer(info.get(2)?.get(0)?, info.get(2)?.get(1)?)?, - emoticons: parse_danmaku_emoticons(info, &text), - text, - }) - } "SEND_GIFT" => Some(ProviderEvent::Gift { viewer: data_viewer(data)?, name: data @@ -478,12 +725,23 @@ fn normalize(message: BiliMessage) -> Option { .get("giftId") .or_else(|| data.get("gift_id")) .and_then(Value::as_i64), - battery: data.get("price").and_then(Value::as_i64).unwrap_or(0) as i32, - quantity: data.get("num").and_then(Value::as_i64).unwrap_or(1) as i32, + coin_type: data + .get("coin_type") + .and_then(Value::as_str) + .map(ToOwned::to_owned), + battery: data + .get("price") + .and_then(value_i64) + .map(bounded_signed_i32) + .unwrap_or(0), + quantity: data + .get("num") + .and_then(value_i64) + .map(bounded_signed_i32) + .unwrap_or(1), event_id: data .get("tid") - .and_then(Value::as_str) - .map(str::to_owned) + .and_then(value_lossless_string) .unwrap_or_else(|| { format!( "{}-{}", @@ -492,32 +750,6 @@ fn normalize(message: BiliMessage) -> Option { ) }), }), - "COMBO_SEND" => Some(ProviderEvent::GiftCombo { - viewer: data_viewer(data)?, - name: data - .get("gift_name") - .or_else(|| data.get("giftName")) - .and_then(Value::as_str) - .unwrap_or("礼物") - .to_owned(), - gift_id: data - .get("gift_id") - .or_else(|| data.get("giftId")) - .and_then(Value::as_i64), - battery: data.get("price").and_then(Value::as_i64).unwrap_or(0) as i32, - quantity: data - .get("combo_num") - .or_else(|| data.get("total_num")) - .and_then(Value::as_i64) - .unwrap_or(1) as i32, - combo_id: data - .get("combo_id") - .map(Value::to_string) - .unwrap_or_default(), - }), - "INTERACT_WORD" => Some(ProviderEvent::Enter { - viewer: data_viewer(data)?, - }), "GUARD_BUY" => Some(ProviderEvent::Guard { viewer: data_viewer(data)?, name: data @@ -526,8 +758,12 @@ fn normalize(message: BiliMessage) -> Option { .and_then(Value::as_str) .unwrap_or("舰长") .to_owned(), - quantity: data.get("num").and_then(Value::as_i64).unwrap_or(1) as i32, - price: data.get("price").and_then(Value::as_i64).unwrap_or(0), + quantity: data + .get("num") + .and_then(value_i64) + .map(bounded_signed_i32) + .unwrap_or(1), + price: data.get("price").and_then(value_i64).unwrap_or(0), }), "SUPER_CHAT_MESSAGE" | "SUPER_CHAT_MESSAGE_JPN" => Some(ProviderEvent::SuperChat { viewer: data_viewer(data)?, @@ -536,22 +772,50 @@ fn normalize(message: BiliMessage) -> Option { .and_then(Value::as_str) .unwrap_or("") .to_owned(), - price: data.get("price").and_then(Value::as_i64).unwrap_or(0), + price: data.get("price").and_then(value_i64).unwrap_or(0), event_id: data .get("id") - .map(Value::to_string) + .and_then(value_lossless_string) .unwrap_or_else(|| Uuid::new_v4().to_string()), }), - "LIKE_INFO_V3_CLICK" => Some(ProviderEvent::Like { - viewer: data_viewer(data)?, - }), "SHARE" => Some(ProviderEvent::Share { viewer: data_viewer(data)?, }), - _ => Some(ProviderEvent::Unknown { command }), + _ => None, } } +fn value_i64(value: &Value) -> Option { + value + .as_i64() + .or_else(|| value.as_u64().and_then(bounded_i64)) + .or_else(|| value.as_str()?.parse().ok()) +} + +fn value_lossless_string(value: &Value) -> Option { + value + .as_str() + .map(ToOwned::to_owned) + .or_else(|| value.as_i64().map(|number| number.to_string())) + .or_else(|| value.as_u64().map(|number| number.to_string())) +} + +fn bounded_i32(value: u64) -> i32 { + i32::try_from(value).unwrap_or(i32::MAX) +} + +fn bounded_signed_i32(value: i64) -> i32 { + i32::try_from(value).unwrap_or(if value.is_negative() { + i32::MIN + } else { + i32::MAX + }) +} + +fn bounded_i64(value: u64) -> Option { + i64::try_from(value).ok() +} + fn json_object(value: &Value) -> Option { match value { Value::Object(_) => Some(value.clone()), @@ -605,16 +869,15 @@ fn emoticon_hint(value: &Value, text: &str, standalone: bool) -> Option Vec { - let Some(header) = info.first().and_then(Value::as_array) else { - return Vec::new(); - }; +fn parse_danmaku_emoticons(message: &DanmuMessage, text: &str) -> Vec { let mut hints = Vec::new(); - if let Some(direct) = header - .get(13) + if let Some(direct) = message + .metadata + .extra + .get(&13) .and_then(|value| emoticon_hint(value, text, true)) .or_else(|| { - header.iter().find_map(|value| { + message.metadata.extra.values().find_map(|value| { let object = json_object(value)?; object.get("url")?; emoticon_hint(&object, text, true) @@ -623,43 +886,62 @@ fn parse_danmaku_emoticons(info: &[Value], text: &str) -> Vec { { hints.push(direct); } - let extra = header.iter().find_map(|value| { - let object = json_object(value)?; - json_object(object.get("extra")?) - }); - if let Some(extra) = extra { - if let Some(emoticons) = extra.get("emots").and_then(json_object) - && let Some(emoticons) = emoticons.as_object() - { - for (token, metadata) in emoticons { - if let Some(hint) = emoticon_hint(metadata, token, false) { - hints.push(hint); - } - } - } - if let Some(unique) = extra - .get("emoticon_unique") - .and_then(Value::as_str) - .filter(|value| !value.is_empty()) - && !hints - .iter() - .any(|hint| hint.unique.as_deref() == Some(unique)) - { - hints.push(EmoticonHint { - text: text.to_owned(), - unique: Some(unique.to_owned()), - url: None, - width: None, - height: None, - is_dynamic: false, - bulge_display: value_is_truthy(extra.get("bulge_display")), - standalone: extra.get("dm_type").and_then(Value::as_i64) == Some(1), - }); + + // libilibili preserves unmodeled metadata indices and extension JSON, so + // evolving emoticon layouts remain recoverable without leaking the raw + // DANMU_MSG array into the application domain. + for value in message.metadata.extra.values() { + let Some(object) = json_object(value) else { + continue; + }; + push_emoticon_extra(&object, text, &mut hints); + if let Some(extra) = object.get("extra").and_then(json_object) { + push_emoticon_extra(&extra, text, &mut hints); } } + if let Some(extra) = message + .extension + .as_ref() + .and_then(|extension| extension.extra_json.as_deref()) + .and_then(|value| serde_json::from_str::(value).ok()) + .and_then(|value| json_object(&value)) + { + push_emoticon_extra(&extra, text, &mut hints); + } hints } +fn push_emoticon_extra(extra: &Value, text: &str, hints: &mut Vec) { + if let Some(emoticons) = extra.get("emots").and_then(json_object) + && let Some(emoticons) = emoticons.as_object() + { + for (token, metadata) in emoticons { + if let Some(hint) = emoticon_hint(metadata, token, false) { + hints.push(hint); + } + } + } + if let Some(unique) = extra + .get("emoticon_unique") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + && !hints + .iter() + .any(|hint| hint.unique.as_deref() == Some(unique)) + { + hints.push(EmoticonHint { + text: text.to_owned(), + unique: Some(unique.to_owned()), + url: None, + width: None, + height: None, + is_dynamic: false, + bulge_display: value_is_truthy(extra.get("bulge_display")), + standalone: extra.get("dm_type").and_then(Value::as_i64) == Some(1), + }); + } +} + #[derive(Clone)] struct ResolvedEmoticon { text: String, @@ -782,6 +1064,7 @@ async fn gift_details( catalog: &GiftCatalog, name: String, gift_id: Option, + coin_type: Option, battery: i32, quantity: i32, ) -> GiftDetails { @@ -797,9 +1080,8 @@ async fn gift_details( .as_ref() .map(|gift| gift.name.clone()) .unwrap_or(name), - coin_type: metadata - .as_ref() - .map(|gift| gift.coin_type.clone()) + coin_type: coin_type + .or_else(|| metadata.as_ref().map(|gift| gift.coin_type.clone())) .unwrap_or_else(|| "gold".into()), unit_price, total_price, @@ -816,10 +1098,11 @@ async fn gift_details( #[cfg(test)] mod tests { use super::*; + use libilibili::websocket::parse_command; #[test] - fn normalizes_raw_danmaku_to_provider_event() { - let message = normalize(BiliMessage::Raw( + fn normalizes_libilibili_danmaku_to_provider_event() { + let message = normalize_command(parse_command( json!({"cmd":"DANMU_MSG:4:0:2","info":[[],"晚上好",[12345,"观众"]]}), )) .unwrap(); @@ -835,7 +1118,7 @@ mod tests { #[test] fn unknown_events_do_not_forward_raw_payload() { - let event = normalize(BiliMessage::Raw(json!({ + let event = normalize_command(parse_command(json!({ "cmd":"FUTURE_SECRET_EVENT", "data":{"cookie":"must-not-cross-provider-boundary"} }))) @@ -845,4 +1128,90 @@ mod tests { _ => panic!("expected unknown event"), } } + + #[test] + fn preserves_typed_gift_identity_value_and_coin_type() { + let event = normalize_command(parse_command(json!({ + "cmd":"SEND_GIFT", + "data":{ + "uid":123, + "uname":"送礼观众", + "giftId":42, + "giftName":"小花花", + "num":3, + "price":100, + "total_coin":300, + "coin_type":"gold", + "tid":"gift-event-1" + } + }))) + .unwrap(); + match event { + ProviderEvent::Gift { + viewer, + gift_id, + coin_type, + battery, + quantity, + event_id, + .. + } => { + assert_eq!(viewer.uid, "123"); + assert_eq!(gift_id, Some(42)); + assert_eq!(coin_type.as_deref(), Some("gold")); + assert_eq!(battery, 100); + assert_eq!(quantity, 3); + assert_eq!(event_id, "gift-event-1"); + } + _ => panic!("expected gift"), + } + } + + #[test] + fn normalizes_typed_combo_commands() { + let event = normalize_command(parse_command(json!({ + "cmd":"COMBO_SEND", + "data":{ + "uid":123, + "uname":"送礼观众", + "gift_id":42, + "gift_name":"小花花", + "combo_num":8, + "price":100, + "coin_type":"gold", + "combo_id":"combo-1" + } + }))) + .unwrap(); + match event { + ProviderEvent::GiftCombo { + quantity, combo_id, .. + } => { + assert_eq!(quantity, 8); + assert_eq!(combo_id, "combo-1"); + } + _ => panic!("expected gift combo"), + } + } + + #[test] + fn normalizes_typed_like_clicks_with_a_viewer() { + let event = normalize_command(parse_command(json!({ + "cmd":"LIKE_INFO_V3_CLICK", + "data":{ + "uid":456, + "uname":"点赞观众", + "is_like":true, + "like_count":12 + } + }))) + .unwrap(); + match event { + ProviderEvent::Like { viewer } => { + assert_eq!(viewer.uid, "456"); + assert_eq!(viewer.name, "点赞观众"); + } + _ => panic!("expected like"), + } + } } diff --git a/compose.yaml b/compose.yaml index 9dfc48d..2905a9f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,6 +1,10 @@ services: app: - build: . + build: + context: . + additional_contexts: + # Cargo resolves apps/server-rust/../../../libilibili to this checkout. + libilibili: ../libilibili restart: unless-stopped network_mode: host user: '${APP_UID:-1000}:${APP_GID:-1000}' diff --git a/config.toml.example b/config.toml.example index e94ef41..8a0bb48 100644 --- a/config.toml.example +++ b/config.toml.example @@ -115,7 +115,7 @@ guard = true like = false share = false -# 默认会记录本服务生命周期信息,并屏蔽 blivedm_rs 的认证响应日志。 -# 如需诊断可临时调高本服务级别;不要将 blivedm 设为 info。 +# 默认记录本服务生命周期信息,并保持依赖库仅输出警告。 +# libilibili 的凭据 Debug 已脱敏,但仍不要记录 Cookie 或原始认证载荷。 [logging] -filter = "lxc_stream_server=info,blivedm=warn,tokio_postgres=warn" +filter = "lxc_stream_server=info,libilibili=warn,tokio_postgres=warn" diff --git a/docs/architecture.md b/docs/architecture.md index a0068d5..04af659 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -30,8 +30,8 @@ flowchart LR ``` 1. `SourceSupervisor` 为每个 `source_id` 保持至多一个 provider task。 -2. `BilibiliProvider` 使用该用户加密保存的 CookieCloud 凭据获取 Cookie,并把原始消息转换成 - `LiveEvent`。 +2. `BilibiliProvider` 使用该用户加密保存的 CookieCloud 凭据构造 `libilibili` + 客户端;crate 负责 WBI、WebSocket 与压缩包解析,adapter 再把强类型命令转换成 `LiveEvent`。 3. `SourceEventRouter` 同时使用 `owner_id` 与 `source_id` 查找启用的组件,并再次检查组件归属。 4. 匹配订阅后,路由器先执行可持久化的 `EventHandler`,再执行无副作用的 `EventProjection`。 5. 投影结果只发布到该 `component_id` 的广播通道。没有全局 WebSocket 事件总线。 @@ -71,6 +71,7 @@ flowchart LR - 容器使用 host network,但默认只监听 `127.0.0.1:9719`。 - Nginx 负责公网 TLS、域名和 WebSocket upgrade。 - CookieCloud 与 PostgreSQL 是外部服务,不由本项目 Compose 创建。 +- `libilibili` 是源码树中的相邻 crate,由 Compose named build context 注入 Rust 构建阶段。 ## 代码导航 diff --git a/docs/security.md b/docs/security.md index 07123e7..00ac6b2 100644 --- a/docs/security.md +++ b/docs/security.md @@ -42,7 +42,7 @@ token。以下规则是实现约束,而不是可选部署建议。 - Key 编码成单一路径段,不能注入额外路径。 - HTTP 客户端禁止重定向,防止允许的地址跳转到内网目标。 - 浏览器只看到 host 和 `keyConfigured`/`passwordConfigured`,不会读回凭据。 -- 日志不得把 `blivedm` 调到可能打印认证响应的详细级别。 +- `libilibili::Credentials` 的 `Debug` 已脱敏,但应用仍不得记录 Cookie、原始认证响应或弹幕 token。 ## HTTP、WebSocket 与 OBS diff --git a/vendor/blivedm/Cargo.lock b/vendor/blivedm/Cargo.lock deleted file mode 100644 index b652988..0000000 --- a/vendor/blivedm/Cargo.lock +++ /dev/null @@ -1,3934 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "alsa" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" -dependencies = [ - "alsa-sys", - "bitflags 2.9.4", - "cfg-if", - "libc", -] - -[[package]] -name = "alsa-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" -dependencies = [ - "windows-sys 0.60.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.60.2", -] - -[[package]] -name = "arboard" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" -dependencies = [ - "clipboard-win", - "image", - "log", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "wl-clipboard-rs", - "x11rb", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.9.4", - "cexpr", - "clang-sys", - "itertools", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" - -[[package]] -name = "blivedm" -version = "0.5.6" -dependencies = [ - "arboard", - "base64", - "brotlic", - "chrono", - "clap", - "clap_complete", - "crossterm", - "directories", - "dirs", - "env_logger", - "futures", - "futures-channel", - "http", - "log", - "md5", - "native-tls", - "ratatui", - "reqwest", - "rodio", - "serde", - "serde_json", - "sqlite", - "tokio", - "toml", - "tungstenite", - "unicode-width 0.2.0", - "url", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "brotlic" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f552f56f302af0006c32b50bfa2bdb4696fd6ba33c3ab9f6225fefdb1efdc680" -dependencies = [ - "brotlic-sys", -] - -[[package]] -name = "brotlic-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdec5c62bc97b56349053cf66ba503af5c2448591be61c3ad70a5f11b57e574" -dependencies = [ - "cc", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cc" -version = "1.2.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", -] - -[[package]] -name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.5.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_complete" -version = "4.5.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" -dependencies = [ - "clap", -] - -[[package]] -name = "clap_derive" -version = "4.5.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" - -[[package]] -name = "claxon" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" - -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "compact_str" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - -[[package]] -name = "cookie" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "cookie_store" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" -dependencies = [ - "cookie", - "idna 0.3.0", - "log", - "publicsuffix", - "serde", - "serde_derive", - "serde_json", - "time", - "url", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "coreaudio-rs" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" -dependencies = [ - "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" -dependencies = [ - "bindgen", -] - -[[package]] -name = "cpal" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" -dependencies = [ - "alsa", - "core-foundation-sys", - "coreaudio-rs", - "dasp_sample", - "jni", - "js-sys", - "libc", - "mach2", - "ndk", - "ndk-context", - "oboe", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossterm" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" -dependencies = [ - "bitflags 2.9.4", - "crossterm_winapi", - "mio", - "parking_lot", - "rustix 0.38.44", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "dasp_sample" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "deranged" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "directories" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.9.4", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "env_filter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "error-code" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.2", - "windows-link", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http", - "hyper", - "rustls", - "tokio", - "tokio-rustls", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "moxcms", - "num-traits", - "png", - "tiff", -] - -[[package]] -name = "indexmap" -version = "2.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" -dependencies = [ - "equivalent", - "hashbrown 0.16.0", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "instability" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" -dependencies = [ - "darling", - "indoc", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jiff" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde", -] - -[[package]] -name = "jiff-static" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "tinyvec", -] - -[[package]] -name = "libc" -version = "0.2.176" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libredox" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" -dependencies = [ - "bitflags 2.9.4", - "libc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - -[[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "ndk" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" -dependencies = [ - "bitflags 2.9.4", - "jni-sys", - "log", - "ndk-sys", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.9.4", - "objc2", - "objc2-core-graphics", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.9.4", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.9.4", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.9.4", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.9.4", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "oboe" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" -dependencies = [ - "jni", - "ndk", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" -dependencies = [ - "cc", -] - -[[package]] -name = "ogg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" -dependencies = [ - "byteorder", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "openssl" -version = "0.10.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "os_pipe" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" -dependencies = [ - "libc", - "windows-sys 0.45.0", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset", - "hashbrown 0.15.5", - "indexmap", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.9.4", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit 0.23.6", -] - -[[package]] -name = "proc-macro2" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "psl-types" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" - -[[package]] -name = "publicsuffix" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" -dependencies = [ - "idna 1.1.0", - "psl-types", -] - -[[package]] -name = "pxfm" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "ratatui" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" -dependencies = [ - "bitflags 2.9.4", - "cassowary", - "compact_str", - "crossterm", - "indoc", - "instability", - "itertools", - "lru", - "paste", - "strum", - "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.2.0", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.9.4", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "regex" -version = "1.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" - -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64", - "bytes", - "cookie", - "cookie_store", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-rustls", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-rustls", - "tokio-util", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots", - "winreg", -] - -[[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.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rodio" -version = "0.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b1bb7b48ee48471f55da122c0044fcc7600cfcc85db88240b89cb832935e611" -dependencies = [ - "claxon", - "cpal", - "hound", - "lewton", - "symphonia", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.9.4", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags 2.9.4", - "errno", - "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki", - "sct", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.4", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" -dependencies = [ - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "sqlite" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6843ff5d46230ca95be07e59876757d7c4dd88f5204eeede7271dab03ac4bed3" -dependencies = [ - "sqlite3-sys", -] - -[[package]] -name = "sqlite3-src" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "174d4a6df77c27db281fb23de1a6d968f3aaaa4807c2a1afa8056b971f947b4a" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "sqlite3-sys" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3901ada7090c3c3584dc92ec7ef1b7091868d13bfe6d7de9f0bcaffee7d0ade5" -dependencies = [ - "sqlite3-src", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn", -] - -[[package]] -name = "symphonia" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" -dependencies = [ - "lazy_static", - "symphonia-bundle-mp3", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-bundle-mp3" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" -dependencies = [ - "lazy_static", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-core" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" -dependencies = [ - "arrayvec", - "bitflags 1.3.2", - "bytemuck", - "lazy_static", - "log", -] - -[[package]] -name = "symphonia-metadata" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" -dependencies = [ - "encoding_rs", - "lazy_static", - "log", - "symphonia-core", -] - -[[package]] -name = "syn" -version = "2.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom 0.3.3", - "once_cell", - "rustix 1.1.2", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" -dependencies = [ - "backtrace", - "bytes", - "io-uring", - "libc", - "mio", - "pin-project-lite", - "slab", - "socket2 0.6.0", - "tokio-macros", - "windows-sys 0.59.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_edit" -version = "0.23.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" -dependencies = [ - "indexmap", - "toml_datetime 0.7.2", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[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.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tree_magic_mini" -version = "3.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" -dependencies = [ - "memchr", - "nom 8.0.0", - "petgraph", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tungstenite" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand", - "sha1", - "thiserror 1.0.69", - "url", - "utf-8", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-ident" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" - -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-truncate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" -dependencies = [ - "itertools", - "unicode-segmentation", - "unicode-width 0.1.14", -] - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" - -[[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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna 1.1.0", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wayland-backend" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" -dependencies = [ - "cc", - "downcast-rs", - "rustix 1.1.2", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" -dependencies = [ - "bitflags 2.9.4", - "rustix 1.1.2", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" -dependencies = [ - "bitflags 2.9.4", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" -dependencies = [ - "bitflags 2.9.4", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" -dependencies = [ - "proc-macro2", - "quick-xml", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" -dependencies = [ - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result 0.4.1", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "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]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "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 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[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_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[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_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[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 = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -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 = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "wl-clipboard-rs" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" -dependencies = [ - "libc", - "log", - "os_pipe", - "rustix 1.1.2", - "thiserror 2.0.18", - "tree_magic_mini", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-wlr", -] - -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "gethostname", - "rustix 1.1.2", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zune-core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" - -[[package]] -name = "zune-jpeg" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" -dependencies = [ - "zune-core", -] diff --git a/vendor/blivedm/Cargo.toml b/vendor/blivedm/Cargo.toml deleted file mode 100644 index 26405e8..0000000 --- a/vendor/blivedm/Cargo.toml +++ /dev/null @@ -1,172 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2024" -name = "blivedm" -version = "0.5.6" -authors = ["isomo "] -build = false -publish = true -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Bilibili live room danmaku WebSocket client with TTS and plugin support" -readme = "README.md" -keywords = [ - "bilibili", - "danmaku", - "live", - "websocket", - "tts", -] -categories = [ - "command-line-utilities", - "network-programming", -] -license = "MIT OR Apache-2.0" -repository = "https://github.com/isomoes/blivedm_rs" - -[package.metadata.docs.rs] -all-features = true -rustdoc-args = [ - "--cfg", - "docsrs", -] - -[features] -browser_cookies = [ - "dep:sqlite", - "dep:directories", - "dep:chrono", -] -default = ["browser_cookies"] - -[lib] -name = "blivedm" -path = "src/lib.rs" - -[[bin]] -name = "blivedm" -path = "src/main.rs" - -[[example]] -name = "integration_bili_live_client" -path = "examples/integration_bili_live_client.rs" - -[[example]] -name = "simple_client" -path = "examples/simple_client.rs" - -[[example]] -name = "tts_example" -path = "examples/tts_example.rs" - -[dependencies.arboard] -version = "3.4" -features = ["wayland-data-control"] - -[dependencies.base64] -version = "0.21" - -[dependencies.brotlic] -version = "0.8.1" - -[dependencies.chrono] -version = "0.4" -optional = true - -[dependencies.clap] -version = "4.0" -features = ["derive"] - -[dependencies.clap_complete] -version = "4.0" - -[dependencies.crossterm] -version = "0.28" - -[dependencies.directories] -version = "5.0" -optional = true - -[dependencies.dirs] -version = "5.0" - -[dependencies.env_logger] -version = "0.11.8" - -[dependencies.futures] -version = "0.3" - -[dependencies.futures-channel] -version = "0.3.28" - -[dependencies.http] -version = "0.2.11" - -[dependencies.log] -version = "0.4" - -[dependencies.md5] -version = "0.7" - -[dependencies.native-tls] -version = "0.2.0" - -[dependencies.ratatui] -version = "0.29" - -[dependencies.reqwest] -version = "0.11.17" -features = [ - "blocking", - "cookies", - "rustls-tls", - "json", - "stream", -] -default-features = false - -[dependencies.rodio] -version = "0.17" - -[dependencies.serde] -version = "1.0" -features = ["derive"] - -[dependencies.serde_json] -version = "1.0" - -[dependencies.sqlite] -version = "0.36" -optional = true - -[dependencies.tokio] -version = "1" -features = [ - "rt-multi-thread", - "macros", -] - -[dependencies.toml] -version = "0.8" - -[dependencies.tungstenite] -version = "0.20.1" - -[dependencies.unicode-width] -version = "0.2.0" - -[dependencies.url] -version = "2.3.1" diff --git a/vendor/blivedm/LICENSE b/vendor/blivedm/LICENSE deleted file mode 100644 index 6112e15..0000000 --- a/vendor/blivedm/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 isomo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/blivedm/src/client/auth.rs b/vendor/blivedm/src/client/auth.rs deleted file mode 100644 index 06b866f..0000000 --- a/vendor/blivedm/src/client/auth.rs +++ /dev/null @@ -1,537 +0,0 @@ -// src/client/auth.rs -//! Authentication helpers for Bilibili live danmaku WebSocket client - -use md5; -use reqwest::StatusCode; -use reqwest::header::HeaderMap; -use serde::Deserialize; -use std::time::{SystemTime, UNIX_EPOCH}; - -// Add browser cookie support -#[cfg(feature = "browser_cookies")] -use crate::browser_cookies; - -/// Get Bilibili cookies from browser (preferred, newest), then fallback to provided cookie string -pub fn get_cookies_or_browser(provided_cookie: Option<&str>) -> Option { - #[cfg(feature = "browser_cookies")] - { - // First try browser cookies as they are the newest - log::info!("Searching for Bilibili cookies in browser (newest)..."); - if let Some(browser_cookie) = browser_cookies::find_bilibili_cookies_as_string() { - log::info!("Found Bilibili cookies in browser (using newest)"); - return Some(browser_cookie); - } - - log::info!("No Bilibili cookies found in browser, checking provided cookie..."); - } - - #[cfg(not(feature = "browser_cookies"))] - { - log::debug!("Browser cookie feature not enabled. Skip to find cookies in browser..."); - } - - if let Some(cookie) = provided_cookie { - if !cookie.is_empty() && cookie != "dummy_sessdata" && cookie.len() > 20 { - log::info!("Using provided cookie as fallback"); - return Some(cookie.to_string()); - } - } - - log::warn!("No valid Bilibili cookies found in browser or provided input"); - None -} - -pub fn init_uid(headers: HeaderMap) -> (StatusCode, String) { - let client = reqwest::blocking::Client::builder() - .https_only(true) - .build() - .unwrap(); - - let mut request_headers = headers; - request_headers.insert("user-agent", USER_AGENT.parse().unwrap()); - - let response = client.get(UID_INIT_URL).headers(request_headers).send(); - log::debug!("init uid response: {:?}", response); - let stat: StatusCode; - let body: String; - match response { - Ok(resp) => { - stat = resp.status(); - body = resp.text().unwrap(); - log::info!("init uid response: {:?}", body); - } - Err(_) => { - panic!("init uid failed"); - } - } - (stat, body) -} - -/// Initializes the buvid by sending a request and extracting the 'buvid3' cookie. -/// -/// Note: This function is not used for document creation. -/// -/// # Panics -/// -/// Panics if the request fails. -pub fn init_buvid(headers: HeaderMap) -> (StatusCode, String) { - // Not used for document creation. - let client = reqwest::blocking::Client::builder() - .https_only(true) - .build() - .unwrap(); - - let mut request_headers = headers; - request_headers.insert("user-agent", USER_AGENT.parse().unwrap()); - - let response = client.get(BUVID_INIT_URL).headers(request_headers).send(); - let stat: StatusCode; - let mut buvid: String = "".to_string(); - match response { - Ok(resp) => { - stat = resp.status(); - let cookies = resp.cookies(); - for i in cookies { - log::debug!("init buvid response cookie : {:?}", i); - if "buvid3".eq(i.name()) { - buvid = i.value().to_string(); - log::info!("init buvid response: {:?}", buvid); - } - } - } - Err(_) => { - panic!("init buvid failed"); - } - } - (stat, buvid) -} - -/// Initializes the room by sending a request with the given room ID. -/// -/// Note: This function should NOT be used for document creation. -/// -/// # Panics -/// -/// Panics if the request fails. -pub fn init_room(headers: HeaderMap, temp_room_id: &str) -> (StatusCode, String) { - let client = reqwest::blocking::Client::builder() - .https_only(true) - .build() - .unwrap(); - - let mut request_headers = headers; - request_headers.insert("user-agent", USER_AGENT.parse().unwrap()); - - let url = format!("{}?room_id={}", ROOM_INIT_URL, temp_room_id); - let response = client.get(url).headers(request_headers).send(); - let stat: StatusCode; - let body: String; - match response { - Ok(resp) => { - stat = resp.status(); - body = resp.text().unwrap(); - log::info!("init room response: {:?}", body); - } - Err(_) => { - panic!("init buvid failed"); - } - } - (stat, body) -} - -pub fn init_host_server(headers: HeaderMap, room_id: u64) -> (StatusCode, String) { - let client = reqwest::blocking::Client::builder() - .https_only(true) - .build() - .unwrap(); - - let mut request_headers = headers.clone(); - request_headers.insert("user-agent", USER_AGENT.parse().unwrap()); - - // Get WBI keys for signing - let wbi_keys = match get_wbi_keys(request_headers.clone()) { - Ok(keys) => keys, - Err(e) => { - log::error!("Failed to get WBI keys: {:?}", e); - panic!("Failed to get WBI keys"); - } - }; - - // Prepare parameters for signing - let params = vec![ - ("id", room_id.to_string()), - ("type", "0".to_string()), - ("web_location", "444.8".to_string()), - ]; - - // Generate signed query string - let signed_query = encode_wbi(params, wbi_keys); - - // Construct final URL - let url = format!("{}?{}", DANMAKU_SERVER_CONF_URL, signed_query); - - // debug log the total request - let response = client.get(url).headers(request_headers).send(); - log::debug!("init host server response: {:?}", response); - let stat: StatusCode; - let body: String; - match response { - Ok(resp) => { - stat = resp.status(); - body = resp.text().unwrap(); - log::info!("init host server response body: {:?}", body); - } - Err(_) => { - panic!("init host server failed"); - } - } - (stat, body) -} - -// WBI signing constants and functions -const MIXIN_KEY_ENC_TAB: [usize; 64] = [ - 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, - 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, - 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52, -]; - -#[derive(Deserialize)] -struct WbiImg { - img_url: String, - sub_url: String, -} - -#[derive(Deserialize)] -struct Data { - wbi_img: WbiImg, -} - -#[derive(Deserialize)] -struct ResWbi { - data: Data, -} - -// 对 imgKey 和 subKey 进行字符顺序打乱编码 -fn get_mixin_key(orig: &[u8]) -> String { - MIXIN_KEY_ENC_TAB - .iter() - .take(32) - .map(|&i| orig[i] as char) - .collect::() -} - -fn get_url_encoded(s: &str) -> String { - s.chars() - .filter_map(|c| match c.is_ascii_alphanumeric() || "-_.~".contains(c) { - true => Some(c.to_string()), - false => { - // 过滤 value 中的 "!'()*" 字符 - if "!'()*".contains(c) { - return None; - } - let encoded = c - .encode_utf8(&mut [0; 4]) - .bytes() - .fold("".to_string(), |acc, b| acc + &format!("%{:02X}", b)); - Some(encoded) - } - }) - .collect::() -} - -// 为请求参数进行 wbi 签名 -fn encode_wbi(params: Vec<(&str, String)>, (img_key, sub_key): (String, String)) -> String { - let cur_time = match SystemTime::now().duration_since(UNIX_EPOCH) { - Ok(t) => t.as_secs(), - Err(_) => panic!("SystemTime before UNIX EPOCH!"), - }; - _encode_wbi(params, (img_key, sub_key), cur_time) -} - -fn _encode_wbi( - mut params: Vec<(&str, String)>, - (img_key, sub_key): (String, String), - timestamp: u64, -) -> String { - let mixin_key = get_mixin_key((img_key + &sub_key).as_bytes()); - // 添加当前时间戳 - params.push(("wts", timestamp.to_string())); - // 重新排序 - params.sort_by(|a, b| a.0.cmp(b.0)); - // 拼接参数 - let query = params - .iter() - .map(|(k, v)| format!("{}={}", get_url_encoded(k), get_url_encoded(v))) - .collect::>() - .join("&"); - // 计算签名 - let web_sign = format!("{:x}", md5::compute(query.clone() + &mixin_key)); - // 返回最终的 query - query + &format!("&w_rid={}", web_sign) -} - -fn get_wbi_keys(headers: HeaderMap) -> Result<(String, String), reqwest::Error> { - let client = reqwest::blocking::Client::builder() - .https_only(true) - .build() - .unwrap(); - - let mut request_headers = headers; - request_headers.insert("user-agent", USER_AGENT.parse().unwrap()); - - let response = client - .get("https://api.bilibili.com/x/web-interface/nav") - .headers(request_headers) - .send()?; - - let res_wbi: ResWbi = response.json()?; - Ok(( - take_filename(res_wbi.data.wbi_img.img_url).unwrap(), - take_filename(res_wbi.data.wbi_img.sub_url).unwrap(), - )) -} - -fn take_filename(url: String) -> Option { - url.rsplit_once('/') - .and_then(|(_, s)| s.rsplit_once('.')) - .map(|(s, _)| s.to_string()) -} - -pub const UID_INIT_URL: &str = "https://api.bilibili.com/x/web-interface/nav"; -pub const BUVID_INIT_URL: &str = "https://data.bilibili.com/v/"; -pub const ROOM_INIT_URL: &str = - "https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom"; -pub const DANMAKU_SERVER_CONF_URL: &str = - "https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo"; -pub const USER_AGENT: &str = - "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0"; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_uid_url_constant() { - assert!(UID_INIT_URL.contains("bilibili.com")); - } - - #[test] - fn test_take_filename() { - assert_eq!( - take_filename( - "https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png".to_string() - ), - Some("7cd084941338484aae1ad9425b84077c".to_string()) - ); - - assert_eq!( - take_filename( - "https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png".to_string() - ), - Some("4932caff0ff746eab6f01bf08b70ac45".to_string()) - ); - - // Test edge case with no extension - assert_eq!( - take_filename("https://example.com/path/file".to_string()), - None - ); - } - - #[test] - fn test_encode_wbi_with_known_values() { - let params = vec![ - ("foo", String::from("114")), - ("bar", String::from("514")), - ("zab", String::from("1919810")), - ]; - - let result = _encode_wbi( - params, - ( - "7cd084941338484aae1ad9425b84077c".to_string(), - "4932caff0ff746eab6f01bf08b70ac45".to_string(), - ), - 1702204169, - ); - - assert_eq!( - result, - "bar=514&foo=114&wts=1702204169&zab=1919810&w_rid=8f6f2b5b3d485fe1886cec6a0be8c5d4" - ); - } - - #[test] - fn test_encode_wbi_bilibili_danmu_params() { - // Test with the actual Bilibili danmu parameters from the example - let params = vec![ - ("id", String::from("24779526")), - ("type", String::from("0")), - ("web_location", String::from("444.8")), - ]; - - // Using the timestamp from the example URL (1748308267) - let result = _encode_wbi( - params, - ( - "7cd084941338484aae1ad9425b84077c".to_string(), - "4932caff0ff746eab6f01bf08b70ac45".to_string(), - ), - 1748308267, - ); - - // The result should contain the correct parameters and w_rid - assert!(result.contains("id=24779526")); - assert!(result.contains("type=0")); - assert!(result.contains("web_location=444.8")); - assert!(result.contains("wts=1748308267")); - assert!(result.contains("w_rid=")); - - // Check the parameter order (should be alphabetical) - let expected_order = "id=24779526&type=0&web_location=444.8&wts=1748308267&w_rid="; - assert!(result.starts_with(expected_order)); - } - - #[test] - fn test_wbi_signature_consistency() { - // Test that the same parameters always generate the same signature - let params1 = vec![ - ("id", String::from("24779526")), - ("type", String::from("0")), - ("web_location", String::from("444.8")), - ]; - - let params2 = vec![ - ("id", String::from("24779526")), - ("type", String::from("0")), - ("web_location", String::from("444.8")), - ]; - - let keys = ( - "7cd084941338484aae1ad9425b84077c".to_string(), - "4932caff0ff746eab6f01bf08b70ac45".to_string(), - ); - - let timestamp = 1748308267; - - let result1 = _encode_wbi(params1, keys.clone(), timestamp); - let result2 = _encode_wbi(params2, keys, timestamp); - - assert_eq!(result1, result2); - } - - #[test] - fn test_wbi_parameter_sorting() { - // Test that parameters are properly sorted alphabetically - let params = vec![ - ("z_param", String::from("last")), - ("a_param", String::from("first")), - ("m_param", String::from("middle")), - ]; - - let result = _encode_wbi( - params, - ( - "7cd084941338484aae1ad9425b84077c".to_string(), - "4932caff0ff746eab6f01bf08b70ac45".to_string(), - ), - 1748308267, - ); - - // Check that parameters appear in alphabetical order - let parts: Vec<&str> = result.split('&').collect(); - assert!(parts[0].starts_with("a_param=")); - assert!(parts[1].starts_with("m_param=")); - assert!(parts[2].starts_with("wts=")); - assert!(parts[3].starts_with("z_param=")); - assert!(parts[4].starts_with("w_rid=")); - } - - #[test] - fn test_correct_bilibili_url_signature() { - // Test the exact URL from the working example: - // "https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=24779526&type=0&web_location=444.8&wts=1748308267&w_rid=884cf361b8ad4e239b4a9dbbb7134679" - // "https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=24779526&type=0&web_location=444.8&w_rid=d1e619744b4977f88ed67524a1f567cc&wts=1751072897" - - let params = vec![ - ("id", String::from("24779526")), - ("type", String::from("0")), - ("web_location", String::from("444.8")), - ]; - - let result = _encode_wbi( - params, - ( - "7cd084941338484aae1ad9425b84077c".to_string(), - "4932caff0ff746eab6f01bf08b70ac45".to_string(), - ), - 1751072897, - ); - - // Expected complete query string from working URL - let expected = "id=24779526&type=0&web_location=444.8&wts=1751072897&w_rid=d1e619744b4977f88ed67524a1f567cc"; - assert_eq!(result, expected); - - // Extract and verify the w_rid specifically - let w_rid = result.split("w_rid=").nth(1).unwrap(); - assert_eq!(w_rid, "d1e619744b4977f88ed67524a1f567cc"); - } - - #[test] - fn test_second_bilibili_url_signature() { - // Test the second URL example: - // "https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=24779526&type=0&web_location=444.8&w_rid=fa20533eb27334ba6f2ec7263721319a&wts=1748311635" - - let params = vec![ - ("id", String::from("24779526")), - ("type", String::from("0")), - ("web_location", String::from("444.8")), - ]; - - let result = _encode_wbi( - params, - ( - "7cd084941338484aae1ad9425b84077c".to_string(), - "4932caff0ff746eab6f01bf08b70ac45".to_string(), - ), - 1748311635, - ); - - // Expected complete query string from working URL - let expected = "id=24779526&type=0&web_location=444.8&wts=1748311635&w_rid=fa20533eb27334ba6f2ec7263721319a"; - assert_eq!(result, expected); - - // Extract and verify the w_rid specifically - let w_rid = result.split("w_rid=").nth(1).unwrap(); - assert_eq!(w_rid, "fa20533eb27334ba6f2ec7263721319a"); - } - - #[test] - fn test_third_bilibili_url_signature() { - // Test the third URL example from README: - // "https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=24779526&type=0&web_location=444.8&wts=1748312554&w_rid=30f250e8abd9effea1bcb88aab416507" - - let params = vec![ - ("id", String::from("24779526")), - ("type", String::from("0")), - ("web_location", String::from("444.8")), - ]; - - let result = _encode_wbi( - params, - ( - "7cd084941338484aae1ad9425b84077c".to_string(), - "4932caff0ff746eab6f01bf08b70ac45".to_string(), - ), - 1748312554, - ); - - // Expected complete query string from working URL - let expected = "id=24779526&type=0&web_location=444.8&wts=1748312554&w_rid=30f250e8abd9effea1bcb88aab416507"; - assert_eq!(result, expected); - - // Extract and verify the w_rid specifically - let w_rid = result.split("w_rid=").nth(1).unwrap(); - assert_eq!(w_rid, "30f250e8abd9effea1bcb88aab416507"); - } -} diff --git a/vendor/blivedm/src/client/browser_cookies.rs b/vendor/blivedm/src/client/browser_cookies.rs deleted file mode 100644 index 06a80bd..0000000 --- a/vendor/blivedm/src/client/browser_cookies.rs +++ /dev/null @@ -1,416 +0,0 @@ -// src/client/browser_cookies.rs -//! Browser cookie reading functionality for automatic SESSDATA detection - -use chrono::{DateTime, TimeZone, Utc}; -use directories::UserDirs; -use log::{debug, info, warn}; -use sqlite::Connection; -use std::collections::HashMap; -use std::fs; -use std::path::{Path, PathBuf}; - -#[derive(Debug, Clone)] -pub struct Cookie { - pub name: String, - pub value: String, - pub domain: String, - pub path: String, - pub expires: Option>, - pub secure: bool, - pub http_only: bool, -} - -#[derive(Debug)] -pub enum Browser { - Chrome, - Firefox, - Edge, - Chromium, - Opera, -} - -impl Browser { - pub fn get_cookie_db_path(&self) -> Option { - let user_dirs = UserDirs::new()?; - let home_dir = user_dirs.home_dir(); - - match self { - Browser::Chrome => { - #[cfg(target_os = "linux")] - { - Some(home_dir.join(".config/google-chrome/Default/Cookies")) - } - #[cfg(target_os = "macos")] - { - Some(home_dir.join("Library/Application Support/Google/Chrome/Default/Cookies")) - } - #[cfg(target_os = "windows")] - { - Some( - home_dir - .join("AppData/Local/Google/Chrome/User Data/Default/Network/Cookies"), - ) - } - } - Browser::Firefox => { - #[cfg(target_os = "linux")] - { - let firefox_dir = vec![ - home_dir.join(".mozilla/firefox"), - home_dir.join("snap/firefox/common/.mozilla/firefox"), - home_dir.join(".var/app/org.mozilla.firefox/.mozilla/firefox"), - home_dir.join("snap/firefox/current/.mozilla/firefox"), - ] - .into_iter() - .find(|p| p.exists())?; - - Self::find_firefox_profile_cookies(&firefox_dir) - } - #[cfg(target_os = "macos")] - { - let firefox_dir = home_dir.join("Library/Application Support/Firefox/Profiles"); - Self::find_firefox_profile_cookies(&firefox_dir) - } - #[cfg(target_os = "windows")] - { - let firefox_dir = home_dir.join("AppData/Roaming/Mozilla/Firefox/Profiles"); - Self::find_firefox_profile_cookies(&firefox_dir) - } - } - Browser::Edge => { - #[cfg(target_os = "linux")] - { - Some(home_dir.join(".config/microsoft-edge/Default/Cookies")) - } - #[cfg(target_os = "macos")] - { - Some( - home_dir.join("Library/Application Support/Microsoft Edge/Default/Cookies"), - ) - } - #[cfg(target_os = "windows")] - { - Some( - home_dir - .join("AppData/Local/Microsoft/Edge/User Data/Default/Network/Cookies"), - ) - } - } - Browser::Chromium => { - #[cfg(target_os = "linux")] - { - Some(home_dir.join(".config/chromium/Default/Cookies")) - } - #[cfg(target_os = "macos")] - { - Some(home_dir.join("Library/Application Support/Chromium/Default/Cookies")) - } - #[cfg(target_os = "windows")] - { - Some(home_dir.join("AppData/Local/Chromium/User Data/Default/Network/Cookies")) - } - } - Browser::Opera => { - #[cfg(target_os = "linux")] - { - Some(home_dir.join(".config/opera/Default/Cookies")) - } - #[cfg(target_os = "macos")] - { - Some(home_dir.join( - "Library/Application Support/com.operasoftware.Opera/Default/Cookies", - )) - } - #[cfg(target_os = "windows")] - { - Some( - home_dir - .join("AppData/Roaming/Opera Software/Opera Stable/Network/Cookies"), - ) - } - } - } - } - - fn find_firefox_profile_cookies(firefox_dir: &Path) -> Option { - if !firefox_dir.exists() { - return None; - } - - // Look for the default profile directory - let entries = fs::read_dir(firefox_dir).ok()?; - for entry in entries { - if let Ok(entry) = entry { - let path = entry.path(); - if path.is_dir() { - let dir_name = path.file_name()?.to_str()?; - if dir_name.contains(".default") || dir_name.contains(".default-release") { - let cookies_path = path.join("cookies.sqlite"); - if cookies_path.exists() { - return Some(cookies_path); - } - } - } - } - } - None - } - - pub fn get_all_supported() -> Vec { - vec![ - Browser::Chrome, - Browser::Firefox, - Browser::Edge, - Browser::Chromium, - Browser::Opera, - ] - } -} - -/// Read cookies from a browser's cookie database -pub fn read_cookies_from_browser( - browser: &Browser, - domain_filter: Option<&str>, -) -> Result, String> { - let db_path = browser - .get_cookie_db_path() - .ok_or_else(|| "Could not determine cookie database path".to_string())?; - - if !db_path.exists() { - return Err(format!("Cookie database not found at: {:?}", db_path)); - } - - debug!("Reading cookies from: {:?}", db_path); - - // Create a temporary copy of the database since browsers might have it locked - let temp_path = std::env::temp_dir().join(format!("temp_cookies_{}.db", std::process::id())); - if let Err(e) = fs::copy(&db_path, &temp_path) { - return Err(format!("Failed to copy cookie database: {}", e)); - } - - let result = match browser { - Browser::Firefox => read_firefox_cookies(&temp_path, domain_filter), - _ => read_chromium_cookies(&temp_path, domain_filter), - }; - - // Clean up temporary file - let _ = fs::remove_file(&temp_path); - - result -} - -fn read_chromium_cookies( - db_path: &Path, - domain_filter: Option<&str>, -) -> Result, String> { - let connection = - Connection::open(db_path).map_err(|e| format!("Failed to open cookie database: {}", e))?; - - let mut query = - "SELECT name, value, host_key, path, expires_utc, is_secure, is_httponly FROM cookies" - .to_string(); - - if let Some(domain) = domain_filter { - query.push_str(&format!(" WHERE host_key LIKE '%{}'", domain)); - } - - let mut cookies = Vec::new(); - - connection - .iterate(query, |pairs| { - let mut cookie_data = HashMap::new(); - for &(column, value) in pairs.iter() { - cookie_data.insert(column, value.unwrap_or("")); - } - - let expires = if let Some(expires_str) = cookie_data.get("expires_utc") { - if let Ok(expires_microseconds) = expires_str.parse::() { - // Chrome stores time as microseconds since Windows epoch (1601-01-01) - // Convert to Unix timestamp (seconds since 1970-01-01) - let windows_epoch_offset = 11644473600_i64; // seconds between 1601 and 1970 - let unix_timestamp = (expires_microseconds / 1_000_000) - windows_epoch_offset; - Utc.timestamp_opt(unix_timestamp, 0).single() - } else { - None - } - } else { - None - }; - - let cookie = Cookie { - name: cookie_data.get("name").unwrap_or(&"").to_string(), - value: cookie_data.get("value").unwrap_or(&"").to_string(), - domain: cookie_data.get("host_key").unwrap_or(&"").to_string(), - path: cookie_data.get("path").unwrap_or(&"").to_string(), - expires, - secure: cookie_data.get("is_secure").unwrap_or(&"0") == &"1", - http_only: cookie_data.get("is_httponly").unwrap_or(&"0") == &"1", - }; - - cookies.push(cookie); - true - }) - .map_err(|e| format!("Failed to query cookies: {}", e))?; - - Ok(cookies) -} - -fn read_firefox_cookies( - db_path: &Path, - domain_filter: Option<&str>, -) -> Result, String> { - let connection = - Connection::open(db_path).map_err(|e| format!("Failed to open cookie database: {}", e))?; - - let mut query = - "SELECT name, value, host, path, expiry, isSecure, isHttpOnly FROM moz_cookies".to_string(); - - if let Some(domain) = domain_filter { - query.push_str(&format!(" WHERE host LIKE '%{}'", domain)); - } - - let mut cookies = Vec::new(); - - connection - .iterate(query, |pairs| { - let mut cookie_data = HashMap::new(); - for &(column, value) in pairs.iter() { - cookie_data.insert(column, value.unwrap_or("")); - } - - let expires = if let Some(expires_str) = cookie_data.get("expiry") { - if let Ok(expires_timestamp) = expires_str.parse::() { - Utc.timestamp_opt(expires_timestamp, 0).single() - } else { - None - } - } else { - None - }; - - let cookie = Cookie { - name: cookie_data.get("name").unwrap_or(&"").to_string(), - value: cookie_data.get("value").unwrap_or(&"").to_string(), - domain: cookie_data.get("host").unwrap_or(&"").to_string(), - path: cookie_data.get("path").unwrap_or(&"").to_string(), - expires, - secure: cookie_data.get("isSecure").unwrap_or(&"0") == &"1", - http_only: cookie_data.get("isHttpOnly").unwrap_or(&"0") == &"1", - }; - - cookies.push(cookie); - true - }) - .map_err(|e| format!("Failed to query cookies: {}", e))?; - - Ok(cookies) -} - -/// Find SESSDATA cookie from all supported browsers -pub fn find_bilibili_cookies_as_string() -> Option { - let browsers = Browser::get_all_supported(); - let mut all_cookies = vec![]; - - for browser in browsers { - info!("Checking browser: {:?}", browser); - - if let Ok(cookies) = read_cookies_from_browser(&browser, Some("bilibili.com")) { - all_cookies.extend(cookies); - } - } - - let mut valid_cookies = all_cookies - .into_iter() - .filter(|cookie| { - if let Some(expires) = cookie.expires { - if Utc::now() > expires { - warn!( - "Found expired {} cookie, expires: {:?}", - cookie.name, expires - ); - return false; - } - } - true - }) - .collect::>(); - - // Deduplicate cookies, keeping the one with the latest expiry - valid_cookies.sort_by(|a, b| { - if a.name != b.name { - a.name.cmp(&b.name) - } else { - b.expires.cmp(&a.expires) // None is smaller - } - }); - valid_cookies.dedup_by(|a, b| a.name == b.name); - - if valid_cookies.is_empty() { - warn!("No valid bilibili cookies found in any browser"); - return None; - } - - info!("Found {} valid bilibili cookies", valid_cookies.len()); - - let cookie_string = valid_cookies - .iter() - .map(|c| format!("{}={}", c.name, c.value)) - .collect::>() - .join("; "); - - if cookie_string.contains("SESSDATA") { - Some(cookie_string) - } else { - warn!("No SESSDATA cookie found among the valid cookies"); - None - } -} - -/// Get all bilibili cookies from browsers for debugging -pub fn get_all_bilibili_cookies() -> HashMap { - let mut all_cookies = HashMap::new(); - let browsers = Browser::get_all_supported(); - - for browser in browsers { - if let Ok(cookies) = read_cookies_from_browser(&browser, Some("bilibili.com")) { - for cookie in cookies { - // Only include non-expired cookies - if let Some(expires) = cookie.expires { - if Utc::now() > expires { - continue; - } - } - - // Use the most recent cookie if duplicates exist - let key = format!("{}_{}", cookie.name, cookie.domain); - all_cookies.insert(key, cookie.value); - } - } - } - - all_cookies -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_browser_path_detection() { - for browser in Browser::get_all_supported() { - let path = browser.get_cookie_db_path(); - println!("{:?} cookie path: {:?}", browser, path); - } - } - - #[test] - fn test_find_sessdata() { - // This test will only work if you have bilibili cookies in your browser - if let Some(sessdata) = find_bilibili_cookies_as_string() { - println!("Found SESSDATA: {}", &sessdata[..20.min(sessdata.len())]); - assert!(!sessdata.is_empty()); - } else { - println!("No SESSDATA found - this is normal if you're not logged into bilibili"); - } - } -} diff --git a/vendor/blivedm/src/client/mod.rs b/vendor/blivedm/src/client/mod.rs deleted file mode 100644 index 0099f9d..0000000 --- a/vendor/blivedm/src/client/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -// src/client/lib.rs -//! Library entry for the client package - -pub mod auth; -#[cfg(feature = "browser_cookies")] -pub mod browser_cookies; -pub mod models; -pub mod scheduler; -pub mod websocket; - -// Re-export commonly used functions -pub use auth::get_cookies_or_browser; diff --git a/vendor/blivedm/src/client/models.rs b/vendor/blivedm/src/client/models.rs deleted file mode 100644 index 3bd6938..0000000 --- a/vendor/blivedm/src/client/models.rs +++ /dev/null @@ -1,97 +0,0 @@ -// src/client/models.rs -//! Data models for Bilibili live danmaku WebSocket client - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -#[derive(Debug)] -pub struct DanmuServer { - pub host: String, - pub port: i32, - pub wss_port: i32, - pub ws_port: i32, -} - -impl Default for DanmuServer { - fn default() -> Self { - Self { - host: String::from("broadcastlv.chat.bilibili.com"), - port: 2243, - wss_port: 443, - ws_port: 2244, - } - } -} - -#[derive(Copy, Clone, Debug)] -pub struct MsgHead { - pub pack_len: u32, - pub raw_header_size: u16, - pub ver: u16, - pub operation: u32, - pub seq_id: u32, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct AuthMessage { - pub uid: u64, - pub roomid: u64, - pub protover: i32, - pub platform: String, - pub type_: i32, - pub key: String, -} - -impl AuthMessage { - pub fn from(map: &HashMap) -> AuthMessage { - AuthMessage { - uid: map.get("uid").unwrap().parse::().unwrap(), - roomid: map.get("room_id").unwrap().parse::().unwrap(), - protover: 3, - platform: "web".to_string(), - type_: 2, - key: map.get("token").unwrap().to_string(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum BiliMessage { - Danmu { - user: String, - text: String, - }, - Gift { - user: String, - gift: String, - num: String, - }, - /// Online rank count message (ONLINE_RANK_COUNT) - OnlineRankCount { - /// Number of high-energy users in the live room - count: u64, - /// Number of online users in the live room - online_count: u64, - }, - // Add more variants as needed - Raw(serde_json::Value), - #[deprecated(note = "Use Raw variant instead")] - Unsupported, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_auth_message_from_map() { - let mut map = std::collections::HashMap::new(); - map.insert("uid".to_string(), "12345".to_string()); - map.insert("room_id".to_string(), "67890".to_string()); - map.insert("token".to_string(), "test_token".to_string()); - let auth = AuthMessage::from(&map); - assert_eq!(auth.uid, 12345); - assert_eq!(auth.roomid, 67890); - assert_eq!(auth.key, "test_token"); - } -} diff --git a/vendor/blivedm/src/client/scheduler.rs b/vendor/blivedm/src/client/scheduler.rs deleted file mode 100644 index 0c7e55e..0000000 --- a/vendor/blivedm/src/client/scheduler.rs +++ /dev/null @@ -1,192 +0,0 @@ -// In Cargo.toml, ensure you have: client = { path = "../client" } -use models::BiliMessage; -use std::sync::Arc; - -use crate::models; - -/// Context information passed to event handlers -#[derive(Debug, Clone)] -pub struct EventContext { - /// Bilibili cookies for authentication - pub cookies: Option, - /// Room ID where the event occurred - pub room_id: u64, -} - -impl EventContext { - /// Create a new EventContext with automatic cookie detection - pub fn new_with_auto_cookies(room_id: u64) -> Self { - let cookies = crate::auth::get_cookies_or_browser(None); - Self { cookies, room_id } - } - - /// Create a new EventContext with provided cookies - pub fn new(cookies: Option, room_id: u64) -> Self { - Self { cookies, room_id } - } -} - -/// Trait for event handlers (plugins) that process BiliMessage. -pub trait EventHandler: Send + Sync { - fn handle(&self, msg: &BiliMessage, context: &EventContext); -} - -/// Scheduling mode: Parallel or Sequential. -pub enum ScheduleMode { - Parallel, - Sequential, -} - -/// Scheduler struct: manages event handlers and dispatches messages. -pub struct Scheduler { - /// Each stage is a Vec of handlers to run in parallel; stages run sequentially. - stages: Vec>>, - /// Context information for event handlers - context: EventContext, -} - -impl Scheduler { - pub fn new(context: EventContext) -> Self { - Scheduler { - stages: Vec::new(), - context, - } - } - - /// Add a new stage (group of handlers to run in parallel) - pub fn add_stage(&mut self, handlers: Vec>) { - self.stages.push(handlers); - } - - /// Add a single handler as a new sequential stage - pub fn add_sequential_handler(&mut self, handler: Arc) { - self.stages.push(vec![handler]); - } - - /// Trigger all stages with the given BiliMessage. - pub fn trigger(&self, msg: BiliMessage) { - for stage in &self.stages { - let mut handles = vec![]; - for handler in stage { - let msg = msg.clone(); - let context = self.context.clone(); - let handler = Arc::clone(handler); - handles.push(std::thread::spawn(move || { - handler.handle(&msg, &context); - })); - } - // Wait for all handlers in this stage to finish before next stage - for handle in handles { - let _ = handle.join(); - } - } - } -} - -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use crate::models::BiliMessage; - use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; - use std::sync::{Arc, Mutex, mpsc}; - - struct AssertHandler { - called: Arc, - last_msg: Arc>>, - } - impl super::EventHandler for AssertHandler { - fn handle(&self, msg: &BiliMessage, _context: &super::EventContext) { - self.called.store(true, Ordering::SeqCst); - let mut lock = self.last_msg.lock().unwrap(); - *lock = Some(msg.clone()); - } - } - - #[test] - fn test_scheduler_with_mpsc_channel() { - let (tx, rx) = mpsc::channel(); - let called = Arc::new(AtomicBool::new(false)); - let last_msg = Arc::new(Mutex::new(None)); - let handler = AssertHandler { - called: Arc::clone(&called), - last_msg: Arc::clone(&last_msg), - }; - let context = super::EventContext { - cookies: Some("test_cookies".to_string()), - room_id: 12345, - }; - let mut scheduler = super::Scheduler::new(context); - scheduler.add_sequential_handler(Arc::new(handler)); - - // Send a test message - let test_msg = BiliMessage::Danmu { - user: "user1".to_string(), - text: "hello".to_string(), - }; - tx.send(test_msg.clone()).unwrap(); - - // Simulate receiving and triggering - if let Ok(msg) = rx.recv() { - scheduler.trigger(msg); - } - - // Assert handler was called and message matches - assert!(called.load(Ordering::SeqCst), "Handler was not called"); - let lock = last_msg.lock().unwrap(); - assert!(lock.is_some(), "No message stored in handler"); - assert_eq!(lock.as_ref().unwrap(), &test_msg, "Message does not match"); - } - - #[test] - fn test_scheduler_add_stage_and_sequential_handler() { - use crate::models::BiliMessage; - - struct CounterHandler { - counter: Arc, - } - impl super::EventHandler for CounterHandler { - fn handle(&self, _msg: &BiliMessage, _context: &super::EventContext) { - self.counter.fetch_add(1, Ordering::SeqCst); - } - } - - let counter1 = Arc::new(AtomicUsize::new(0)); - let counter2 = Arc::new(AtomicUsize::new(0)); - let counter3 = Arc::new(AtomicUsize::new(0)); - - let handler1 = Arc::new(CounterHandler { - counter: Arc::clone(&counter1), - }); - let handler2 = Arc::new(CounterHandler { - counter: Arc::clone(&counter2), - }); - let handler3 = Arc::new(CounterHandler { - counter: Arc::clone(&counter3), - }); - - let context = super::EventContext { - cookies: Some("test_cookies".to_string()), - room_id: 12345, - }; - let mut scheduler = super::Scheduler::new(context); - // Add a parallel stage (handler1 and handler2) - scheduler.add_stage(vec![handler1, handler2]); - // Add a sequential stage (handler3) - scheduler.add_sequential_handler(handler3); - - let test_msg = BiliMessage::Danmu { - user: "user2".to_string(), - text: "test".to_string(), - }; - scheduler.trigger(test_msg); - - // Both handler1 and handler2 should be called once (parallel stage) - assert_eq!(counter1.load(Ordering::SeqCst), 1, "Handler1 not called"); - assert_eq!(counter2.load(Ordering::SeqCst), 1, "Handler2 not called"); - // handler3 should be called once (sequential stage) - assert_eq!(counter3.load(Ordering::SeqCst), 1, "Handler3 not called"); - } -} diff --git a/vendor/blivedm/src/client/websocket.rs b/vendor/blivedm/src/client/websocket.rs deleted file mode 100644 index 5035fca..0000000 --- a/vendor/blivedm/src/client/websocket.rs +++ /dev/null @@ -1,544 +0,0 @@ -// src/client/websocket.rs -//! WebSocket client for Bilibili live danmaku messages (refactored from bili_live_dm) - -use native_tls::TlsStream; -use serde_json::Value; -use std::io::ErrorKind; -use std::net::TcpStream; -use std::panic; -use tungstenite::{client, Message, WebSocket}; - -use url::Url; - -use futures_channel::mpsc::Sender; -use http::Response; -use std::collections::HashMap; -use std::thread; -use std::time::Duration; - -use crate::auth::*; -use crate::models::{AuthMessage, BiliMessage, DanmuServer, MsgHead}; - -pub struct BiliLiveClient { - ws: WebSocket>, - cookies: String, - room_id: String, - auth_msg: String, - ss: Sender, -} - -impl BiliLiveClient { - pub fn new(cookies: &str, room_id: &str, r: Sender) -> Self { - let (ws, auth_msg) = Self::connect_with_auth(cookies, room_id) - .unwrap_or_else(|e| panic!("Failed to create websocket client: {}", e)); - BiliLiveClient { - ws, - cookies: cookies.to_string(), - room_id: room_id.to_string(), - auth_msg, - ss: r, - } - } - - /// Create a new client with automatic browser cookie detection - /// If cookies is None or empty, it will try to find cookies from browser - pub fn new_auto( - cookies: Option<&str>, - room_id: &str, - r: Sender, - ) -> Result { - let resolved_cookies = get_cookies_or_browser(cookies) - .ok_or_else(|| "No cookies found in provided value or browser cookies. Please log into bilibili.com in your browser or provide cookies manually.".to_string())?; - let (ws, auth_msg) = Self::connect_with_auth(&resolved_cookies, room_id)?; - Ok(BiliLiveClient { - ws, - cookies: resolved_cookies, - room_id: room_id.to_string(), - auth_msg, - ss: r, - }) - } - - /// Send authentication, including the built-in reconnect fallback. - /// - /// The boolean lets embedding applications rebuild the whole client when - /// all socket-level recovery attempts fail. Existing CLI callers may - /// safely ignore it. - pub fn send_auth(&mut self) -> bool { - match self.send_auth_internal() { - Ok(()) => true, - Err(e) => { - log::error!("failed to send auth packet: {}", e); - false - } - } - } - - /// Send a heartbeat and report whether socket-level recovery succeeded. - pub fn send_heart_beat(&mut self) -> bool { - match self.send_heart_beat_internal() { - Ok(()) => true, - Err(e) => { - log::error!("failed to send heartbeat: {}", e); - false - } - } - } - - pub fn parse_ws_message(&mut self, resv: Vec) { - let mut offset = 0; - let header = &resv[0..16]; - let mut head_1 = get_msg_header(header); - if head_1.operation == 5 || head_1.operation == 8 { - loop { - let body: &[u8] = &resv[offset + 16..offset + (head_1.pack_len as usize)]; - self.parse_business_message(head_1, body); - offset += head_1.pack_len as usize; - if offset >= resv.len() { - break; - } - let temp_head = &resv[offset..(offset + 16)]; - head_1 = get_msg_header(temp_head); - } - } else if head_1.operation == 3 { - let mut body: [u8; 4] = [0, 0, 0, 0]; - body[0] = resv[16]; - body[1] = resv[17]; - body[2] = resv[18]; - body[3] = resv[19]; - let popularity = i32::from_be_bytes(body); - log::info!("popularity:{}", popularity); - } else { - log::error!( - "unknown message operation={:?}, header={:?}}}", - head_1.operation, - head_1 - ) - } - } - - pub fn parse_business_message(&mut self, h: MsgHead, b: &[u8]) { - if h.operation == 5 { - if h.ver == 3 { - let res: Vec = decompress(b).unwrap(); - self.parse_ws_message(res); - } else if h.ver == 0 { - let s = String::from_utf8(b.to_vec()).unwrap(); - let res_json: Value = serde_json::from_str(s.as_str()).unwrap(); - if let Some(msg) = handle(res_json) { - let _ = self.ss.try_send(msg); - } - } else { - log::error!("Unknown compression format"); - } - } else if h.operation == 8 { - self.send_heart_beat(); - } else { - log::error!("Unknown message format {}", h.operation); - } - } - - pub fn receive(&mut self) -> Result<(), String> { - if self.ws.can_read() { - let msg = self.ws.read(); - match msg { - Ok(m) => { - let res = m.into_data(); - if res.len() >= 16 { - self.parse_ws_message(res); - } - Ok(()) - } - Err(tungstenite::Error::Io(error)) - if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => - { - // A short read timeout lets an embedding application check - // its cancellation token without treating idle rooms as a - // disconnected WebSocket. - Ok(()) - } - Err(e) => { - let msg = format!("read msg error: {}", e); - log::warn!("{}", msg); - self.reconnect().map_err(|reconnect_err| { - format!("{}; reconnect failed: {}", msg, reconnect_err) - }) - } - } - } else { - Ok(()) - } - } - - /// Bound a blocking read so supervisors can stop/restart one tenant's - /// listener without leaking the previous connection. - pub fn set_read_timeout(&mut self, timeout: Option) -> Result<(), String> { - self.ws - .get_mut() - .get_mut() - .set_read_timeout(timeout) - .map_err(|error| error.to_string()) - } - - pub fn close(&mut self) { - let _ = self.ws.close(None); - } - - fn connect_with_auth( - cookies: &str, - room_id: &str, - ) -> Result<(WebSocket>, String), String> { - panic::catch_unwind(|| { - let (v, auth) = init_server(cookies, room_id); - let (ws, _res) = connect_result(v["host_list"].clone())?; - let auth_msg = serde_json::to_string(&auth) - .map_err(|e| format!("serialize auth payload failed: {}", e))?; - Ok((ws, auth_msg)) - }) - .map_err(|_| format!("websocket setup panicked for room {}", room_id))? - } - - fn send_auth_internal(&mut self) -> Result<(), String> { - match self.ws.send(Message::Binary(make_packet( - self.auth_msg.as_str(), - Operation::AUTH, - ))) { - Ok(()) => Ok(()), - Err(e) => { - let msg = format!("send auth error: {}", e); - log::warn!("{}", msg); - self.reconnect()?; - self.ws - .send(Message::Binary(make_packet( - self.auth_msg.as_str(), - Operation::AUTH, - ))) - .map_err(|retry_err| format!("{}; resend auth failed: {}", msg, retry_err)) - } - } - } - - fn send_heart_beat_internal(&mut self) -> Result<(), String> { - match self - .ws - .send(Message::Binary(make_packet("{}", Operation::HEARTBEAT))) - { - Ok(()) => Ok(()), - Err(e) => { - let msg = format!("send heartbeat error: {}", e); - log::warn!("{}", msg); - self.reconnect()?; - self.ws - .send(Message::Binary(make_packet("{}", Operation::HEARTBEAT))) - .map_err(|retry_err| format!("{}; resend heartbeat failed: {}", msg, retry_err)) - } - } - } - - fn reconnect(&mut self) -> Result<(), String> { - let backoff = [1_u64, 2, 5]; - let mut last_err = None; - - for (idx, delay_secs) in backoff.iter().enumerate() { - if idx > 0 { - thread::sleep(Duration::from_secs(*delay_secs)); - } - - match Self::connect_with_auth(&self.cookies, &self.room_id) { - Ok((ws, auth_msg)) => { - self.ws = ws; - self.auth_msg = auth_msg; - let auth_resend = self.ws.send(Message::Binary(make_packet( - self.auth_msg.as_str(), - Operation::AUTH, - ))); - let heartbeat_resend = self - .ws - .send(Message::Binary(make_packet("{}", Operation::HEARTBEAT))); - - match (auth_resend, heartbeat_resend) { - (Ok(()), Ok(())) => { - log::info!( - "websocket reconnected on attempt {} for room {}", - idx + 1, - self.room_id - ); - return Ok(()); - } - (auth_result, heartbeat_result) => { - let auth_err = auth_result.err().map(|e| e.to_string()); - let heartbeat_err = heartbeat_result.err().map(|e| e.to_string()); - let reconnect_err = match (auth_err, heartbeat_err) { - (Some(auth_err), Some(heartbeat_err)) => format!( - "reconnected socket but auth resend failed: {}; heartbeat resend failed: {}", - auth_err, heartbeat_err - ), - (Some(auth_err), None) => { - format!("reconnected socket but auth resend failed: {}", auth_err) - } - (None, Some(heartbeat_err)) => format!( - "reconnected socket but heartbeat resend failed: {}", - heartbeat_err - ), - (None, None) => unreachable!(), - }; - log::warn!( - "websocket reconnect attempt {} did not fully recover for room {}: {}", - idx + 1, - self.room_id, - reconnect_err - ); - last_err = Some(reconnect_err); - } - } - } - Err(e) => { - log::warn!( - "websocket reconnect attempt {} failed for room {}: {}", - idx + 1, - self.room_id, - e - ); - last_err = Some(e); - } - } - } - - Err(last_err.unwrap_or_else(|| "unknown reconnect failure".to_string())) - } -} - -pub fn gen_damu_list(list: &Value) -> Vec { - let server_list = list.as_array().unwrap(); - let mut res: Vec = Vec::new(); - if server_list.len() == 0 { - let d = DanmuServer::default(); - res.push(d); - } - for s in server_list { - res.push(DanmuServer { - host: s["host"].as_str().unwrap().to_string(), - port: s["port"].as_u64().unwrap() as i32, - wss_port: s["wss_port"].as_u64().unwrap() as i32, - ws_port: s["ws_port"].as_u64().unwrap() as i32, - }); - } - res -} - -fn find_server(vd: Vec) -> (String, String, String) { - let (host, wss_port) = (vd.get(0).unwrap().host.clone(), vd.get(0).unwrap().wss_port); - ( - host.clone(), - format!("{}:{}", host.clone(), wss_port), - format!("wss://{}:{}/sub", host, wss_port), - ) -} - -pub fn init_server(cookies: &str, room_id: &str) -> (Value, AuthMessage) { - let mut auth_map = HashMap::new(); - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::COOKIE, - reqwest::header::HeaderValue::from_str(cookies).unwrap(), - ); - headers.insert( - reqwest::header::USER_AGENT, - reqwest::header::HeaderValue::from_static(crate::auth::USER_AGENT), - ); - log::debug!("headers: {:?}", headers); - - // Extract SESSDATA from cookies for authentication - let sessdata = cookies - .split(';') - .find_map(|kv| { - let mut parts = kv.trim().splitn(2, '='); - let key = parts.next()?.trim(); - let value = parts.next()?.trim(); - if key == "SESSDATA" { - Some(value.to_string()) - } else { - None - } - }) - .unwrap_or_else(|| "".to_string()); - - if !sessdata.is_empty() { - let (_, body1) = init_uid(headers.clone()); - let body1_v: Value = serde_json::from_str(body1.as_str()).unwrap(); - - // Check if the authentication was successful - if let Some(mid) = body1_v["data"]["mid"].as_i64() { - auth_map.insert("uid".to_string(), mid.to_string()); - log::info!("Successfully authenticated with uid: {}", mid); - } else { - log::warn!("Authentication failed - SESSDATA may be invalid or expired"); - log::debug!("Auth response: {}", body1); - auth_map.insert("uid".to_string(), "0".to_string()); - } - } else { - auth_map.insert("uid".to_string(), "0".to_string()); - } - // here the live room id is easily obtained, so we not get it by url. - auth_map.insert("room_id".to_string(), room_id.to_string()); - - let room_id_num = room_id.parse::().expect("room_id must be a valid u64"); - let (_, body4) = init_host_server(headers.clone(), room_id_num); - let body4_res: Value = serde_json::from_str(body4.as_str()).unwrap(); - let server_info = &body4_res["data"]; - let token = &body4_res["data"]["token"].as_str().unwrap(); - auth_map.insert("token".to_string(), token.to_string()); - - let auth_msg = AuthMessage::from(&auth_map); - (server_info.clone(), auth_msg) -} - -pub fn connect(v: Value) -> (WebSocket>, Response>>) { - connect_result(v).expect("Can't connect") -} - -pub fn connect_result( - v: Value, -) -> Result<(WebSocket>, Response>>), String> { - let danmu_server = gen_damu_list(&v); - let (host, url, ws_url) = find_server(danmu_server); - let connector: native_tls::TlsConnector = - native_tls::TlsConnector::new().map_err(|e| format!("tls init failed: {}", e))?; - let stream: TcpStream = TcpStream::connect(url.as_str()) - .map_err(|e| format!("tcp connect to {} failed: {}", url, e))?; - let stream: native_tls::TlsStream = connector - .connect(host.as_str(), stream) - .map_err(|e| format!("tls connect to {} failed: {}", host, e))?; - let parsed_url = - Url::parse(ws_url.as_str()).map_err(|e| format!("invalid websocket url: {}", e))?; - client(parsed_url, stream).map_err(|e| format!("websocket handshake failed: {}", e)) -} - -pub enum Operation { - AUTH, - HEARTBEAT, -} - -pub fn make_packet(body: &str, ops: Operation) -> Vec { - let json: Value = serde_json::from_str(body).unwrap(); - let temp = json.to_string(); - let body_content: &[u8] = temp.as_bytes(); - let pack_len: [u8; 4] = ((16 + body.len()) as u32).to_be_bytes(); - let raw_header_size: [u8; 2] = (16 as u16).to_be_bytes(); - let ver: [u8; 2] = (1 as u16).to_be_bytes(); - let operation: [u8; 4] = match ops { - Operation::AUTH => (7 as u32).to_be_bytes(), - Operation::HEARTBEAT => (2 as u32).to_be_bytes(), - }; - let seq_id: [u8; 4] = (1 as u32).to_be_bytes(); - let mut res = pack_len.to_vec(); - res.append(&mut raw_header_size.to_vec()); - res.append(&mut ver.to_vec()); - res.append(&mut operation.to_vec()); - res.append(&mut seq_id.to_vec()); - res.append(&mut body_content.to_vec()); - res -} - -pub fn get_msg_header(v_s: &[u8]) -> MsgHead { - let mut pack_len: [u8; 4] = [0; 4]; - let mut raw_header_size: [u8; 2] = [0; 2]; - let mut ver: [u8; 2] = [0; 2]; - let mut operation: [u8; 4] = [0; 4]; - let mut seq_id: [u8; 4] = [0; 4]; - for (i, v) in v_s.iter().enumerate() { - if i < 4 { - pack_len[i] = *v; - continue; - } - if i < 6 { - raw_header_size[i - 4] = *v; - continue; - } - if i < 8 { - ver[i - 6] = *v; - continue; - } - if i < 12 { - operation[i - 8] = *v; - continue; - } - if i < 16 { - seq_id[i - 12] = *v; - continue; - } - } - MsgHead { - pack_len: u32::from_be_bytes(pack_len), - raw_header_size: u16::from_be_bytes(raw_header_size), - ver: u16::from_be_bytes(ver), - operation: u32::from_be_bytes(operation), - seq_id: u32::from_be_bytes(seq_id), - } -} - -pub fn decompress(body: &[u8]) -> std::io::Result> { - use brotlic::DecompressorReader; - use std::io::Read; - let mut decompressed_reader: DecompressorReader<&[u8]> = DecompressorReader::new(body); - let mut decoded_input = Vec::new(); - let _ = decompressed_reader.read_to_end(&mut decoded_input)?; - Ok(decoded_input) -} - -/// here we detail [info format is online](https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/live/message_stream.md) -/// . -pub fn handle(json: Value) -> Option { - let category = json["cmd"].as_str().unwrap_or(""); - match category { - // Preserve all fields for callers that need a stable UID, gift price - // and upstream event identifier. The previous typed variants discard - // those values, which makes reliable accounting impossible. - "DANMU_MSG" | "SEND_GIFT" => Some(BiliMessage::Raw(json)), - "ONLINE_RANK_COUNT" => Some(BiliMessage::OnlineRankCount { - count: json["data"]["count"].as_u64().unwrap_or(0), - online_count: json["data"]["online_count"].as_u64().unwrap_or(0), - }), - // Add more cases for other types as needed - _ => Some(BiliMessage::Raw(json)), - } -} - -/// Enhanced init_server that can automatically detect cookies from browser -pub fn init_server_auto( - provided_cookies: Option<&str>, - room_id: &str, -) -> Result<(Value, AuthMessage), String> { - // Try to get cookies from provided value or browser cookies - let cookies = get_cookies_or_browser(provided_cookies) - .ok_or_else(|| "No cookies found in provided value or browser cookies. Please log into bilibili.com in your browser or provide cookies manually.".to_string())?; - - log::info!( - "Using cookies for authentication: {}...", - &cookies[..10.min(cookies.len())] - ); - - let result = init_server(&cookies, room_id); - Ok(result) -} - -#[cfg(test)] -mod tests { - use super::*; - use futures_channel::mpsc::channel; - - #[test] - fn test_bili_live_client_connect() { - // Always enable debug log output for test - let _ = env_logger::builder() - .is_test(true) - .filter_level(log::LevelFilter::Debug) - .try_init(); - // Get cookies from environment variable for real test - let cookies = - std::env::var("Cookie").unwrap_or_else(|_| "SESSDATA=dummy_sessdata".to_string()); - let room_id = "24779526"; - let (tx, _rx) = channel(10); - let _client = BiliLiveClient::new(&cookies, room_id, tx); - } -} diff --git a/vendor/blivedm/src/config.rs b/vendor/blivedm/src/config.rs deleted file mode 100644 index 12c220d..0000000 --- a/vendor/blivedm/src/config.rs +++ /dev/null @@ -1,276 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::fs; -use std::path::{Path, PathBuf}; - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct Config { - #[serde(default)] - pub connection: Option, - #[serde(default)] - pub tts: Option, - #[serde(default)] - pub auto_reply: Option, - #[serde(default)] - pub debug: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct ConnectionConfig { - pub cookies: Option, - pub room_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct TtsConfig { - pub server: Option, - pub voice: Option, - pub backend: Option, - pub quality: Option, - pub format: Option, - pub sample_rate: Option, - pub volume: Option, - pub command: Option, - pub args: Option, - /// Alibaba DashScope TTS configuration - pub ali_api_key: Option, - pub ali_model: Option, - pub ali_voice: Option, - pub ali_language_type: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TriggerConfig { - pub keywords: Vec, - pub response: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AutoReplyConfig { - #[serde(default)] - pub enabled: bool, - #[serde(default = "default_cooldown")] - pub cooldown_seconds: u64, - #[serde(default)] - pub triggers: Vec, -} - -impl Default for AutoReplyConfig { - fn default() -> Self { - Self { - enabled: false, - cooldown_seconds: default_cooldown(), - triggers: vec![], - } - } -} - -fn default_cooldown() -> u64 { - 5 -} - -impl AutoReplyConfig { - /// Convert to blivedm::plugins::auto_reply::AutoReplyConfig - pub fn to_plugin_config(&self) -> blivedm::plugins::auto_reply::AutoReplyConfig { - blivedm::plugins::auto_reply::AutoReplyConfig { - enabled: self.enabled, - cooldown_seconds: self.cooldown_seconds, - triggers: self - .triggers - .iter() - .map(|t| t.to_plugin_trigger()) - .collect(), - } - } -} - -impl TriggerConfig { - /// Convert to blivedm::plugins::auto_reply::TriggerConfig - pub fn to_plugin_trigger(&self) -> blivedm::plugins::auto_reply::TriggerConfig { - blivedm::plugins::auto_reply::TriggerConfig { - keywords: self.keywords.clone(), - response: self.response.clone(), - } - } -} - -impl Config { - /// Load configuration from file with fallback locations - pub fn load_from_file(config_path: Option<&Path>) -> Result> { - let config_file = if let Some(path) = config_path { - // Use provided path - path.to_path_buf() - } else { - // Try current directory first - let current_dir_config = PathBuf::from("config.toml"); - if current_dir_config.exists() { - current_dir_config - } else { - // Try XDG config directory - Self::get_default_config_path()? - } - }; - - if !config_file.exists() { - log::debug!("Config file {:?} not found", config_file); - - // Create config file if it doesn't exist and we're using default locations - if config_path.is_none() { - match Self::create_example_config(&config_file) { - Ok(()) => { - println!("Created configuration file: {:?}", config_file); - println!("You can customize it as needed."); - } - Err(e) => { - log::warn!("Failed to create config file: {}", e); - return Ok(Config::default()); - } - } - } else { - return Ok(Config::default()); - } - } - - log::info!("Loading configuration from {:?}", config_file); - let content = fs::read_to_string(&config_file) - .map_err(|e| format!("Failed to read config file {:?}: {}", config_file, e))?; - - let config: Config = toml::from_str(&content) - .map_err(|e| format!("Failed to parse config file {:?}: {}", config_file, e))?; - - Ok(config) - } - - /// Get the default configuration file path (~/.config/blivedm_rs/config.toml) - fn get_default_config_path() -> Result> { - let config_dir = dirs::config_dir() - .ok_or("Unable to determine config directory")? - .join("blivedm_rs"); - - // Create config directory if it doesn't exist - if !config_dir.exists() { - fs::create_dir_all(&config_dir).map_err(|e| { - format!("Failed to create config directory {:?}: {}", config_dir, e) - })?; - } - - Ok(config_dir.join("config.toml")) - } - - /// Create an example configuration file - pub fn create_example_config(path: &Path) -> Result<(), Box> { - let example_config = Config { - connection: None, - tts: Some(TtsConfig { - server: Some("http://localhost:8000".to_string()), - voice: None, - backend: None, - quality: None, - format: None, - sample_rate: None, - volume: None, - command: None, - args: None, - ali_api_key: None, - ali_model: None, - ali_voice: None, - ali_language_type: None, - }), - auto_reply: Some(AutoReplyConfig { - enabled: false, - cooldown_seconds: 5, - triggers: vec![ - TriggerConfig { - keywords: vec!["你好".to_string(), "hello".to_string()], - response: "欢迎来到直播间!".to_string(), - }, - TriggerConfig { - keywords: vec!["谢谢".to_string(), "thanks".to_string()], - response: "不客气~".to_string(), - }, - ], - }), - debug: None, - }; - - let toml_string = toml::to_string_pretty(&example_config) - .map_err(|e| format!("Failed to serialize example config: {}", e))?; - - fs::write(path, toml_string) - .map_err(|e| format!("Failed to write example config to {:?}: {}", path, e))?; - - Ok(()) - } - - /// Print the effective configuration (for debugging) - #[allow(clippy::too_many_arguments)] - pub fn print_effective_config( - cookies: &Option, - room_id: &str, - tts_server: &Option, - tts_voice: &Option, - tts_backend: &Option, - tts_quality: &Option, - tts_format: &Option, - tts_sample_rate: &Option, - tts_volume: &Option, - tts_command: &Option, - tts_args: &Option, - ali_api_key: &Option, - ali_model: &Option, - ali_voice: &Option, - ali_language_type: &Option, - auto_reply: &Option, - debug: bool, - ) { - println!("=== Effective Configuration ==="); - println!("Connection:"); - println!(" room_id: {}", room_id); - if let Some(cookies_val) = cookies { - println!( - " cookies: {}...", - &cookies_val.chars().take(20).collect::() - ); - } else { - println!(" cookies: None (will auto-detect)"); - } - - println!("TTS (REST API):"); - println!(" server: {:?}", tts_server); - println!(" voice: {:?}", tts_voice); - println!(" backend: {:?}", tts_backend); - println!(" quality: {:?}", tts_quality); - println!(" format: {:?}", tts_format); - println!(" sample_rate: {:?}", tts_sample_rate); - println!(" volume: {:?}", tts_volume); - println!(" command: {:?}", tts_command); - println!(" args: {:?}", tts_args); - - println!("TTS (Alibaba DashScope):"); - if let Some(key) = ali_api_key { - println!( - " api_key: {}...", - &key.chars().take(10).collect::() - ); - } else { - println!(" api_key: None"); - } - println!(" model: {:?}", ali_model); - println!(" voice: {:?}", ali_voice); - println!(" language_type: {:?}", ali_language_type); - - println!("Auto Reply:"); - if let Some(auto_reply_config) = auto_reply { - println!(" enabled: {}", auto_reply_config.enabled); - println!(" cooldown_seconds: {}", auto_reply_config.cooldown_seconds); - println!( - " triggers: {} configured", - auto_reply_config.triggers.len() - ); - } else { - println!(" enabled: false (not configured)"); - } - - println!("Debug: {}", debug); - println!("==============================="); - } -} diff --git a/vendor/blivedm/src/lib.rs b/vendor/blivedm/src/lib.rs deleted file mode 100644 index 639951e..0000000 --- a/vendor/blivedm/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -// src/lib.rs -//! Bilibili live room danmaku WebSocket client library with TTS and plugin support - -pub mod client; -pub mod plugins; -pub mod tui; - -// Re-export commonly used items from client -pub use client::{auth, get_cookies_or_browser, models, scheduler, websocket}; -#[cfg(feature = "browser_cookies")] -pub use client::browser_cookies; - -// Re-export plugin modules and helpers -pub use plugins::{ - auto_reply, auto_reply_handler, terminal_display, terminal_display_handler, tts, tts_handler, - tts_handler_command, tts_handler_default, -}; diff --git a/vendor/blivedm/src/main.rs b/vendor/blivedm/src/main.rs deleted file mode 100644 index 951102b..0000000 --- a/vendor/blivedm/src/main.rs +++ /dev/null @@ -1,481 +0,0 @@ -// src/main.rs -// Standalone binary to test integration of the terminal display plugin with the BiliLiveClient - -mod config; - -use blivedm::client::get_cookies_or_browser; -use blivedm::client::scheduler::{EventContext, Scheduler}; -use blivedm::client::websocket::BiliLiveClient; -use blivedm::plugins::terminal_display::TerminalDisplayHandler; -use blivedm::plugins::tts::TtsHandler; -use blivedm::tui::{TuiApp, TuiLogger, run_tui}; -use clap::{CommandFactory, Parser}; -use clap_complete::{Shell, generate}; -use config::Config; -use futures::channel::mpsc; -use futures::stream::StreamExt; -use std::collections::VecDeque; -use std::env; -use std::path::PathBuf; -use std::sync::atomic::AtomicU64; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration; -use tokio::runtime::Runtime; - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - /// Path to configuration file - #[arg(long, value_name = "PATH")] - config: Option, - - /// Print effective configuration and exit - #[arg(long)] - print_config: bool, - /// Cookies for Bilibili authentication (optional - will auto-detect from browser if not provided) - #[arg(long, value_name = "COOKIES")] - cookies: Option, - - /// Room ID to connect to - #[arg(long, value_name = "ROOM_ID")] - room_id: Option, - - /// TTS REST API server URL - #[arg(long, value_name = "URL")] - tts_server: Option, - - /// TTS voice ID (e.g., "zh-CN-XiaoxiaoNeural") - #[arg(long, value_name = "VOICE")] - tts_voice: Option, - - /// TTS backend ("edge", "xtts", "piper") - #[arg(long, value_name = "BACKEND")] - tts_backend: Option, - - /// TTS audio quality ("low", "medium", "high") - #[arg(long, value_name = "QUALITY")] - tts_quality: Option, - - /// TTS audio format (e.g., "wav") - #[arg(long, value_name = "FORMAT")] - tts_format: Option, - - /// TTS sample rate (e.g., 22050, 44100) - #[arg(long, value_name = "RATE")] - tts_sample_rate: Option, - - /// TTS audio volume (0.0 to 1.0) - #[arg(long, value_name = "VOLUME")] - tts_volume: Option, - - /// Local TTS command (e.g., "say", "espeak-ng") - #[arg(long, value_name = "COMMAND")] - tts_command: Option, - - /// Comma-separated arguments for TTS command - #[arg(long, value_name = "ARGS", allow_hyphen_values = true)] - tts_args: Option, - - /// Alibaba DashScope API key for ali-tts (can also use DASHSCOPE_API_KEY env) - #[arg(long, value_name = "KEY")] - ali_api_key: Option, - - /// Alibaba TTS model (e.g., "qwen3-tts-flash") - #[arg(long, value_name = "MODEL")] - ali_model: Option, - - /// Alibaba TTS voice (e.g., "Cherry", "Chelsie") - #[arg(long, value_name = "VOICE")] - ali_voice: Option, - - /// Alibaba TTS language type (e.g., "Chinese", "English") - #[arg(long, value_name = "LANG")] - ali_language_type: Option, - - /// Enable debug logging - #[arg(long)] - debug: bool, - - /// Enable auto reply plugin - #[arg(long)] - auto_reply: bool, - - /// Generate shell completion script (bash, zsh, fish, powershell, elvish) - #[arg(long, value_name = "SHELL")] - generate_completion: Option, -} - -fn main() { - let args = Args::parse(); - - // Handle shell completion generation early (before any other processing) - if let Some(shell) = args.generate_completion { - let mut cmd = Args::command(); - generate(shell, &mut cmd, "blivedm", &mut std::io::stdout()); - return; - } - - // Load configuration from file first - let config = match Config::load_from_file(args.config.as_deref()) { - Ok(config) => config, - Err(e) => { - eprintln!("Error loading configuration: {}", e); - std::process::exit(1); - } - }; - - // Initialize logging with precedence: CLI args > env vars > config file - let debug_enabled = - args.debug || env::var("DEBUG").unwrap_or_default() == "1" || config.debug.unwrap_or(false); - - // Load cookies and room_id with precedence: CLI args > env vars > config file > defaults - let cookies = args - .cookies - .or_else(|| { - env::var("Cookie") - .ok() - .filter(|s| !s.is_empty() && s != "SESSDATA=dummy_sessdata") - }) - .or_else(|| config.connection.as_ref().and_then(|c| c.cookies.clone())); - - // If no manual cookies provided, try browser auto-detection - let cookies = if cookies.is_none() { - if debug_enabled { - log::info!("No manual cookies provided, attempting browser auto-detection..."); - } - get_cookies_or_browser(None) - } else { - if debug_enabled { - log::info!("Using manually provided cookies"); - } - cookies - }; - - let room_id = args - .room_id - .or_else(|| env::var("ROOM_ID").ok()) - .or_else(|| config.connection.as_ref().and_then(|c| c.room_id.clone())) - .unwrap_or_else(|| "24779526".to_string()); - - // Configure TTS with precedence: CLI args > config file - let tts_server = args - .tts_server - .or_else(|| config.tts.as_ref().and_then(|t| t.server.clone())); - let tts_voice = args - .tts_voice - .or_else(|| config.tts.as_ref().and_then(|t| t.voice.clone())); - let tts_backend = args - .tts_backend - .or_else(|| config.tts.as_ref().and_then(|t| t.backend.clone())); - let tts_quality = args - .tts_quality - .or_else(|| config.tts.as_ref().and_then(|t| t.quality.clone())); - let tts_format = args - .tts_format - .or_else(|| config.tts.as_ref().and_then(|t| t.format.clone())); - let tts_sample_rate = args - .tts_sample_rate - .or_else(|| config.tts.as_ref().and_then(|t| t.sample_rate)); - let tts_volume = args - .tts_volume - .or_else(|| config.tts.as_ref().and_then(|t| t.volume)); - let tts_command = args - .tts_command - .or_else(|| config.tts.as_ref().and_then(|t| t.command.clone())); - let tts_args = args - .tts_args - .or_else(|| config.tts.as_ref().and_then(|t| t.args.clone())); - - // Configure Alibaba TTS with precedence: CLI args > env vars > config file - let ali_api_key = args - .ali_api_key - .or_else(|| env::var("DASHSCOPE_API_KEY").ok()) - .or_else(|| config.tts.as_ref().and_then(|t| t.ali_api_key.clone())); - let ali_model = args - .ali_model - .or_else(|| config.tts.as_ref().and_then(|t| t.ali_model.clone())); - let ali_voice = args - .ali_voice - .or_else(|| config.tts.as_ref().and_then(|t| t.ali_voice.clone())); - let ali_language_type = args.ali_language_type.or_else(|| { - config - .tts - .as_ref() - .and_then(|t| t.ali_language_type.clone()) - }); - - // Configure auto reply with precedence: CLI args > config file - let auto_reply_config = if let Some(config_auto_reply) = &config.auto_reply { - // Use config file settings, but allow CLI flag to override enabled - let mut plugin_config = config_auto_reply.to_plugin_config(); - if args.auto_reply { - plugin_config.enabled = true; - } - plugin_config - } else { - // No config file section, use defaults with CLI flag - let mut default_config = blivedm::plugins::auto_reply::AutoReplyConfig::default(); - default_config.enabled = args.auto_reply; - default_config - }; - - // If user wants to see config, print and exit - if args.print_config { - // Create a temporary config struct for display that reflects the effective settings - let effective_auto_reply = if auto_reply_config.enabled { - Some(config::AutoReplyConfig { - enabled: auto_reply_config.enabled, - cooldown_seconds: auto_reply_config.cooldown_seconds, - triggers: auto_reply_config - .triggers - .iter() - .map(|t| config::TriggerConfig { - keywords: t.keywords.clone(), - response: t.response.clone(), - }) - .collect(), - }) - } else { - None - }; - - Config::print_effective_config( - &cookies, - &room_id, - &tts_server, - &tts_voice, - &tts_backend, - &tts_quality, - &tts_format, - &tts_sample_rate, - &tts_volume, - &tts_command, - &tts_args, - &ali_api_key, - &ali_model, - &ali_voice, - &ali_language_type, - &effective_auto_reply, - debug_enabled, - ); - std::process::exit(0); - } - - // Initialize TuiLogger to capture logs into a shared buffer for the TUI logs panel. - // When debug is enabled, capture Debug level; otherwise capture Info level. - let log_level = if debug_enabled { - log::LevelFilter::Debug - } else { - log::LevelFilter::Info - }; - let log_buffer = TuiLogger::init(log_level); - - // Create client with automatic browser cookie detection - let (tx, mut rx) = mpsc::channel(64); - let mut client = match BiliLiveClient::new_auto(cookies.as_deref(), &room_id, tx) { - Ok(client) => { - log::info!("Successfully created client with automatic cookie detection"); - client - } - Err(e) => { - eprintln!("Failed to create client: {}", e); - eprintln!( - "Please ensure you are logged into bilibili.com in your browser, or provide cookies manually." - ); - std::process::exit(1); - } - }; - client.send_auth(); - client.send_heart_beat(); - let shared_client: Arc> = Arc::new(Mutex::new(client)); - let heart_beats: Arc> = Arc::clone(&shared_client); - - thread::spawn(move || { - loop { - match heart_beats.lock() { - Ok(mut heart_beats_c) => { - heart_beats_c.send_heart_beat(); - } - Err(e) => { - eprintln!("Error acquiring lock on stream: {}", e); - break; - } - } - thread::sleep(Duration::new(20, 0)); - } - }); - - let rec_msg: Arc> = Arc::clone(&shared_client); - thread::spawn(move || { - loop { - match rec_msg.lock() { - Ok(mut rec_c) => { - if let Err(e) = rec_c.receive() { - log::error!("{}", e); - } - } - Err(e) => { - eprintln!("Error acquiring lock on stream: {}", e); - break; - } - } - thread::sleep(Duration::from_millis(10)); // instead of 10 microseconds - } - }); - - // Set up the scheduler with context and add the terminal display handler - if debug_enabled { - match &cookies { - Some(cookie_str) => { - log::debug!( - "Cookies found and passed to context: {}...", - &cookie_str.chars().take(50).collect::() - ); - } - None => { - log::warn!( - "No cookies found for EventContext - auto-reply will not be able to send messages" - ); - } - } - } - - // Create shared message buffer for TUI - let message_buffer: Arc>> = Arc::new(Mutex::new(VecDeque::new())); - - // Create shared online count for TUI title display - let online_count: Arc = Arc::new(AtomicU64::new(0)); - - let context = EventContext::new(cookies.clone(), room_id.parse::().unwrap_or(0)); - let mut scheduler = Scheduler::new(context); - let terminal_handler = Arc::new(TerminalDisplayHandler::with_online_count( - Arc::clone(&message_buffer), - Arc::clone(&online_count), - )); - scheduler.add_sequential_handler(terminal_handler); - if let Some(server_url) = tts_server { - // REST API TTS configuration - let tts_handler = Arc::new(TtsHandler::new_rest_api_with_volume( - server_url, - tts_voice, - tts_backend, - tts_quality, - tts_format, - tts_sample_rate, - tts_volume, - )); - scheduler.add_sequential_handler(tts_handler); - println!("TTS configured with REST API server"); - } else if let Some(api_key) = ali_api_key { - // Alibaba DashScope TTS configuration - let model = ali_model.unwrap_or_else(|| "qwen3-tts-flash".to_string()); - let voice = ali_voice.unwrap_or_else(|| "Cherry".to_string()); - let tts_handler = Arc::new(TtsHandler::new_ali_tts( - api_key, - model.clone(), - voice.clone(), - ali_language_type, - tts_volume, - )); - scheduler.add_sequential_handler(tts_handler); - println!( - "TTS configured with Alibaba DashScope (model: {}, voice: {})", - model, voice - ); - } else if let Some(tts_cmd) = tts_command { - // Command-line TTS configuration - let cmd_args = tts_args - .map(|s| s.split(',').map(|s| s.to_string()).collect()) - .unwrap_or_default(); - let tts_handler = Arc::new(TtsHandler::new_command(tts_cmd, cmd_args)); - scheduler.add_sequential_handler(tts_handler); - println!("TTS configured with local command"); - } else { - println!( - "No TTS configuration provided. Use --ali-api-key, --tts-server, or --tts-command to enable TTS." - ); - } - - // Add auto reply plugin if enabled - if auto_reply_config.enabled { - let auto_reply_handler = blivedm::plugins::auto_reply_handler(auto_reply_config); - scheduler.add_sequential_handler(auto_reply_handler); - println!("Auto reply plugin enabled"); - } else { - println!( - "Auto reply plugin disabled. Use --auto-reply or configure in config file to enable." - ); - } - - // Add initial system message to buffer - TuiApp::add_message(&message_buffer, format!("[System] Bilibili Danmu Client")); - TuiApp::add_message( - &message_buffer, - format!("[System] Connected to room: {}", room_id), - ); - if let Some(cookies_val) = &cookies { - TuiApp::add_message( - &message_buffer, - format!( - "[System] Using provided cookies: {}...", - &cookies_val.chars().take(30).collect::() - ), - ); - } else { - TuiApp::add_message( - &message_buffer, - "[System] Using auto-detected cookies from browser".to_string(), - ); - } - - // create a thread to process the rx channel messages using tokio runtime and pass to scheduler - let rt = Arc::new(Runtime::new().unwrap()); - let rt_clone = Arc::clone(&rt); - rt.spawn(async move { - while let Some(msg) = rx.next().await { - scheduler.trigger(msg); - } - }); - - // Create TUI app - let mut tui_app = TuiApp::with_online_count( - Arc::clone(&message_buffer), - room_id.clone(), - Arc::clone(&online_count), - ); - tui_app.set_log_buffer(log_buffer); - - let context_for_chat = EventContext::new(cookies.clone(), room_id.parse::().unwrap_or(0)); - let message_buffer_for_feedback = Arc::clone(&message_buffer); - - // Run TUI with message sending callback - let tui_result = run_tui(tui_app, move |message| { - let context_clone = context_for_chat.clone(); - let rt_for_send = Arc::clone(&rt_clone); - let buffer_clone = Arc::clone(&message_buffer_for_feedback); - - rt_for_send.spawn(async move { - if let Err(e) = blivedm::plugins::send_danmaku_message(&message, &context_clone).await { - TuiApp::add_message( - &buffer_clone, - format!("[System] Error sending message: {}", e), - ); - } - }); - }); - - if let Err(e) = tui_result { - eprintln!("TUI error: {}", e); - } - - // close the client - match shared_client.lock() { - Ok(mut _client) => {} - Err(e) => { - eprintln!("Error acquiring lock on stream: {}", e); - } - } - // wait for the threads to finish - thread::sleep(Duration::new(1, 0)); -} diff --git a/vendor/blivedm/src/plugins/auto_reply.rs b/vendor/blivedm/src/plugins/auto_reply.rs deleted file mode 100644 index e4fa727..0000000 --- a/vendor/blivedm/src/plugins/auto_reply.rs +++ /dev/null @@ -1,478 +0,0 @@ -use crate::client::models::BiliMessage; -use crate::client::scheduler::{EventContext, EventHandler}; -use log::{debug, error, info, warn}; -use reqwest::header::{HeaderMap, HeaderValue}; -use serde::Serialize; -use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; -use tokio::runtime::Runtime; - -/// Configuration for keyword-response triggers -#[derive(Debug, Clone)] -pub struct TriggerConfig { - /// Keywords that trigger this response - pub keywords: Vec, - /// Response message to send - pub response: String, -} - -/// Configuration for the auto reply plugin -#[derive(Debug, Clone)] -pub struct AutoReplyConfig { - /// Whether the plugin is enabled - pub enabled: bool, - /// Minimum cooldown between replies in seconds - pub cooldown_seconds: u64, - /// List of trigger configurations - pub triggers: Vec, -} - -impl Default for AutoReplyConfig { - fn default() -> Self { - Self { - enabled: false, - cooldown_seconds: 5, - triggers: vec![ - TriggerConfig { - keywords: vec!["你好".to_string(), "hello".to_string()], - response: "欢迎来到直播间!".to_string(), - }, - TriggerConfig { - keywords: vec!["谢谢".to_string(), "thanks".to_string()], - response: "不客气~".to_string(), - }, - ], - } - } -} - -/// Parameters for sending a danmaku message to Bilibili API -#[derive(Serialize, Debug)] -struct SendDanmakuRequest { - csrf: String, - roomid: u64, - msg: String, - rnd: u64, - fontsize: u32, - color: u32, - mode: u32, - bubble: u32, - room_type: u32, - jumpfrom: u32, - reply_mid: u32, - reply_attr: u32, - reply_uname: String, - replay_dmid: String, - statistics: String, - csrf_token: String, -} - -/// Extract CSRF token from cookies string -pub fn extract_csrf_token(cookies: &str) -> Option { - for cookie in cookies.split(';') { - let cookie = cookie.trim(); - if cookie.starts_with("bili_jct=") { - return Some(cookie[9..].to_string()); - } - } - None -} - -/// Send a danmaku message to the Bilibili live room -/// -/// # Arguments -/// * `message` - The text message to send -/// * `context` - Event context containing cookies and room_id -/// -/// # Returns -/// Returns Ok(()) on success, or an error if the request fails -pub async fn send_danmaku_message( - message: &str, - context: &EventContext, -) -> Result<(), Box> { - let cookies = match &context.cookies { - Some(cookies) => cookies, - None => { - return Err("No cookies available for sending danmaku".into()); - } - }; - - let csrf_token = match extract_csrf_token(cookies) { - Some(token) => token, - None => { - return Err("Could not extract CSRF token from cookies".into()); - } - }; - - // Current timestamp - let rnd = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - - let request = SendDanmakuRequest { - csrf: csrf_token.clone(), - roomid: context.room_id, - msg: message.to_string(), - rnd, - fontsize: 25, - color: 16777215, // White color - mode: 1, // Scroll mode - bubble: 0, - room_type: 0, - jumpfrom: 0, - reply_mid: 0, - reply_attr: 0, - reply_uname: String::new(), - replay_dmid: String::new(), - statistics: r#"{"appId":100,"platform":5}"#.to_string(), - csrf_token, - }; - - // Set up headers - let mut headers = HeaderMap::new(); - headers.insert("Cookie", HeaderValue::from_str(cookies)?); - headers.insert( - "User-Agent", - HeaderValue::from_static( - "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0", - ), - ); - headers.insert( - "Referer", - HeaderValue::from_str(&format!("https://live.bilibili.com/{}", context.room_id))?, - ); - - debug!("Sending danmaku: {}", message); - - let http_client = reqwest::Client::builder() - .timeout(Duration::from_secs(10)) - .build()?; - - let response = http_client - .post("https://api.live.bilibili.com/msg/send") - .headers(headers) - .form(&request) - .send() - .await?; - - if response.status().is_success() { - info!("Successfully sent danmaku: {}", message); - Ok(()) - } else { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - warn!("Failed to send danmaku, status: {}", status); - debug!("Response body: {}", body); - Err(format!("Failed to send danmaku: {} - {}", status, body).into()) - } -} - -/// Auto reply handler that monitors danmaku for keywords and sends responses -pub struct AutoReplyHandler { - config: AutoReplyConfig, - last_reply: Arc>>, - http_client: reqwest::Client, - runtime: Arc, -} - -impl AutoReplyHandler { - /// Create a new auto reply handler with the given configuration - pub fn new(config: AutoReplyConfig) -> Self { - let http_client = reqwest::Client::builder() - .timeout(Duration::from_secs(10)) - .build() - .expect("Failed to create HTTP client"); - - let runtime = Arc::new(Runtime::new().expect("Failed to create tokio runtime")); - - Self { - config, - last_reply: Arc::new(Mutex::new(None)), - http_client, - runtime, - } - } - - /// Check if any keyword matches the message text - fn find_matching_trigger(&self, text: &str) -> Option<&TriggerConfig> { - let text_lower = text.to_lowercase(); - - for trigger in &self.config.triggers { - for keyword in &trigger.keywords { - if text_lower.contains(&keyword.to_lowercase()) { - return Some(trigger); - } - } - } - - None - } - - /// Get the response from the trigger - fn select_response(&self, trigger: &TriggerConfig) -> Option { - if trigger.response.is_empty() { - return None; - } - Some(trigger.response.clone()) - } - - /// Check if enough time has passed since the last reply - fn check_cooldown(&self) -> bool { - let last_reply = self.last_reply.lock().unwrap(); - - match *last_reply { - Some(last_time) => { - let elapsed = last_time.elapsed(); - elapsed >= Duration::from_secs(self.config.cooldown_seconds) - } - None => true, - } - } - - /// Update the last reply timestamp - fn update_last_reply(&self) { - let mut last_reply = self.last_reply.lock().unwrap(); - *last_reply = Some(Instant::now()); - } - - /// Extract CSRF token from cookies - fn extract_csrf_token(&self, cookies: &str) -> Option { - extract_csrf_token(cookies) - } - - /// Send a danmaku message to the Bilibili API - async fn send_danmaku( - &self, - message: &str, - context: &EventContext, - ) -> Result<(), reqwest::Error> { - let cookies = match &context.cookies { - Some(cookies) => cookies, - None => { - warn!("No cookies available for sending danmaku"); - return Ok(()); - } - }; - - let csrf_token = match self.extract_csrf_token(cookies) { - Some(token) => token, - None => { - error!("Could not extract CSRF token from cookies"); - return Ok(()); - } - }; - - // Current timestamp - let rnd = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - - let request = SendDanmakuRequest { - csrf: csrf_token.clone(), - roomid: context.room_id, - msg: message.to_string(), - rnd, - fontsize: 25, - color: 16777215, // White color - mode: 1, // Scroll mode - bubble: 0, - room_type: 0, - jumpfrom: 0, - reply_mid: 0, - reply_attr: 0, - reply_uname: String::new(), - replay_dmid: String::new(), - statistics: r#"{"appId":100,"platform":5}"#.to_string(), - csrf_token, - }; - - // Set up headers - let mut headers = HeaderMap::new(); - headers.insert("Cookie", HeaderValue::from_str(cookies).unwrap()); - headers.insert( - "User-Agent", - HeaderValue::from_static( - "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0", - ), - ); - headers.insert( - "Referer", - HeaderValue::from_str(&format!("https://live.bilibili.com/{}", context.room_id)) - .unwrap(), - ); - - debug!("Sending danmaku: {}", message); - - let response = self - .http_client - .post("https://api.live.bilibili.com/msg/send") - .headers(headers) - .form(&request) - .send() - .await?; - - if response.status().is_success() { - info!("Successfully sent danmaku: {}", message); - } else { - warn!("Failed to send danmaku, status: {}", response.status()); - let body = response.text().await.unwrap_or_default(); - debug!("Response body: {}", body); - } - - Ok(()) - } -} - -impl EventHandler for AutoReplyHandler { - fn handle(&self, msg: &BiliMessage, context: &EventContext) { - if !self.config.enabled { - return; - } - - // Only process danmaku messages - if let BiliMessage::Danmu { user: _, text } = msg { - // Check for keyword match - if let Some(trigger) = self.find_matching_trigger(text) { - // Check cooldown - if !self.check_cooldown() { - debug!("Auto reply on cooldown, skipping"); - return; - } - - // Select response - if let Some(response) = self.select_response(trigger) { - debug!( - "Auto reply triggered by '{}', responding with '{}'", - text, response - ); - - // Update cooldown - self.update_last_reply(); - - // Send the reply asynchronously - let runtime = Arc::clone(&self.runtime); - let _http_client = self.http_client.clone(); - let response_msg = response.clone(); - let context_clone = context.clone(); - let handler = self.clone(); - - runtime.spawn(async move { - if let Err(e) = handler.send_danmaku(&response_msg, &context_clone).await { - error!("Failed to send auto reply: {}", e); - } - }); - } - } - } - } -} - -// Implement Clone for AutoReplyHandler -impl Clone for AutoReplyHandler { - fn clone(&self) -> Self { - Self { - config: self.config.clone(), - last_reply: Arc::clone(&self.last_reply), - http_client: self.http_client.clone(), - runtime: Arc::clone(&self.runtime), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::client::models::BiliMessage; - use crate::client::scheduler::{EventContext, EventHandler}; - - #[test] - fn test_keyword_matching() { - let config = AutoReplyConfig::default(); - let handler = AutoReplyHandler::new(config); - - // Test keyword matching - assert!(handler.find_matching_trigger("你好世界").is_some()); - assert!(handler.find_matching_trigger("Hello world").is_some()); - assert!(handler.find_matching_trigger("谢谢大家").is_some()); - assert!(handler.find_matching_trigger("Thanks everyone").is_some()); - assert!(handler.find_matching_trigger("random text").is_none()); - } - - #[test] - fn test_response_selection() { - let config = AutoReplyConfig::default(); - let handler = AutoReplyHandler::new(config); - - let trigger = &handler.config.triggers[0]; - let response = handler.select_response(trigger); - assert!(response.is_some()); - assert_eq!(response.unwrap(), trigger.response); - } - - #[test] - fn test_cooldown() { - let config = AutoReplyConfig { - enabled: true, - cooldown_seconds: 1, - triggers: vec![], - }; - let handler = AutoReplyHandler::new(config); - - // Initial check should pass - assert!(handler.check_cooldown()); - - // Update timestamp - handler.update_last_reply(); - - // Should be on cooldown now - assert!(!handler.check_cooldown()); - - // Wait for cooldown - std::thread::sleep(Duration::from_secs(2)); - - // Should be off cooldown now - assert!(handler.check_cooldown()); - } - - #[test] - fn test_csrf_extraction() { - let config = AutoReplyConfig::default(); - let handler = AutoReplyHandler::new(config); - - let cookies = "SESSDATA=abc123; bili_jct=csrf_token_here; other=value"; - let csrf = handler.extract_csrf_token(cookies); - assert_eq!(csrf, Some("csrf_token_here".to_string())); - - let cookies_no_csrf = "SESSDATA=abc123; other=value"; - let csrf = handler.extract_csrf_token(cookies_no_csrf); - assert_eq!(csrf, None); - } - - #[test] - fn test_event_handler() { - let config = AutoReplyConfig { - enabled: true, - cooldown_seconds: 0, // No cooldown for testing - triggers: vec![TriggerConfig { - keywords: vec!["test".to_string()], - response: "test response".to_string(), - }], - }; - let handler = AutoReplyHandler::new(config); - - let context = EventContext { - cookies: Some("bili_jct=test_csrf; SESSDATA=test".to_string()), - room_id: 12345, - }; - - let msg = BiliMessage::Danmu { - user: "test_user".to_string(), - text: "this is a test message".to_string(), - }; - - // This should trigger the auto reply (but won't actually send due to test environment) - handler.handle(&msg, &context); - } -} diff --git a/vendor/blivedm/src/plugins/mod.rs b/vendor/blivedm/src/plugins/mod.rs deleted file mode 100644 index a7c67ad..0000000 --- a/vendor/blivedm/src/plugins/mod.rs +++ /dev/null @@ -1,58 +0,0 @@ -pub mod auto_reply; -pub mod terminal_display; -pub mod tts; - -use crate::client::scheduler::EventHandler; -use std::collections::VecDeque; -use std::sync::{Arc, Mutex}; - -// Re-export danmaku sending utility for easy access -pub use auto_reply::send_danmaku_message; - -/// Helper to create the handler as Arc -pub fn terminal_display_handler( - message_buffer: Arc>>, -) -> Arc { - Arc::new(terminal_display::TerminalDisplayHandler::new( - message_buffer, - )) -} - -/// Helper to create the TTS handler as Arc -/// Uses default Chinese voice settings with REST API -pub fn tts_handler_default(server_url: String) -> Arc { - Arc::new(tts::TtsHandler::new_rest_api_default(server_url)) -} - -/// Helper to create the TTS handler with REST API and custom configuration as Arc -pub fn tts_handler( - server_url: String, - voice: Option, - backend: Option, - quality: Option, - format: Option, - sample_rate: Option, -) -> Arc { - Arc::new(tts::TtsHandler::new_rest_api( - server_url, - voice, - backend, - quality, - format, - sample_rate, - )) -} - -/// Helper to create the command-based TTS handler as Arc -/// For local TTS commands like `say` on macOS or `espeak-ng` on Linux -pub fn tts_handler_command(tts_command: String, tts_args: Vec) -> Arc { - Arc::new(tts::TtsHandler::new_command(tts_command, tts_args)) -} - -/// Helper to create the auto reply handler as Arc -pub fn auto_reply_handler(config: auto_reply::AutoReplyConfig) -> Arc { - Arc::new(auto_reply::AutoReplyHandler::new(config)) -} - -#[cfg(test)] -mod tests {} diff --git a/vendor/blivedm/src/plugins/terminal_display.rs b/vendor/blivedm/src/plugins/terminal_display.rs deleted file mode 100644 index d555084..0000000 --- a/vendor/blivedm/src/plugins/terminal_display.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::client::models::BiliMessage; -use crate::client::scheduler::{EventContext, EventHandler}; -use std::collections::VecDeque; -use std::sync::atomic::AtomicU64; -use std::sync::{Arc, Mutex}; - -/// A plugin that adds BiliMessages to a shared message buffer for TUI display. -pub struct TerminalDisplayHandler { - /// Shared message buffer for TUI - message_buffer: Arc>>, - /// Shared online count for TUI title display - online_count: Arc, -} - -impl TerminalDisplayHandler { - /// Create a new TerminalDisplayHandler with a shared message buffer - pub fn new(message_buffer: Arc>>) -> Self { - Self { - message_buffer, - online_count: Arc::new(AtomicU64::new(0)), - } - } - - /// Create a new TerminalDisplayHandler with shared message buffer and online count - pub fn with_online_count( - message_buffer: Arc>>, - online_count: Arc, - ) -> Self { - Self { - message_buffer, - online_count, - } - } -} - -impl EventHandler for TerminalDisplayHandler { - fn handle(&self, msg: &BiliMessage, _context: &EventContext) { - let formatted_msg = match msg { - BiliMessage::Danmu { user, text } => { - format!("[Danmu] {}: {}", user, text) - } - BiliMessage::Gift { user, gift , num} => { - format!("[Gift] {} sent a gift: {} X {}", user, gift, num) - } - BiliMessage::OnlineRankCount { online_count, .. } => { - // Update the shared online count for TUI title display - crate::tui::app::TuiApp::set_online_count(&self.online_count, *online_count); - // Don't add to message buffer - just update the title counter - return; - } - BiliMessage::Raw(json) => { - format!("[Raw] {}", json["cmd"].as_str().unwrap_or("Unknown")) - } - #[allow(deprecated)] - BiliMessage::Unsupported => "[Unsupported message type]".to_string(), - }; - - // Add message to buffer using the TuiApp helper method - crate::tui::app::TuiApp::add_message(&self.message_buffer, formatted_msg); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::client::models::BiliMessage; - use crate::client::scheduler::EventHandler; - use std::collections::VecDeque; - use std::sync::{Arc, Mutex}; - - #[test] - fn test_terminal_display_handler_adds_danmu() { - let buffer = Arc::new(Mutex::new(VecDeque::new())); - let handler = TerminalDisplayHandler::new(Arc::clone(&buffer)); - let msg = BiliMessage::Danmu { - user: "test_user".to_string(), - text: "hello world".to_string(), - }; - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler.handle(&msg, &context); - - let messages = buffer.lock().unwrap(); - assert_eq!(messages.len(), 1); - assert_eq!(messages[0], "[Danmu] test_user: hello world"); - } - - #[test] - fn test_terminal_display_handler_adds_gift() { - let buffer = Arc::new(Mutex::new(VecDeque::new())); - let handler = TerminalDisplayHandler::new(Arc::clone(&buffer)); - let msg = BiliMessage::Gift { - user: "gift_user".to_string(), - gift: "rocket".to_string(), - num: "count".to_string(), - }; - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler.handle(&msg, &context); - - let messages = buffer.lock().unwrap(); - assert_eq!(messages.len(), 1); - assert_eq!(messages[0], "[Gift] gift_user sent a gift: rocket"); - } - - #[test] - fn test_terminal_display_handler_adds_unsupported() { - let buffer = Arc::new(Mutex::new(VecDeque::new())); - let handler = TerminalDisplayHandler::new(Arc::clone(&buffer)); - let msg = BiliMessage::Unsupported; - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler.handle(&msg, &context); - - let messages = buffer.lock().unwrap(); - assert_eq!(messages.len(), 1); - assert_eq!(messages[0], "[Unsupported message type]"); - } -} diff --git a/vendor/blivedm/src/plugins/tts.rs b/vendor/blivedm/src/plugins/tts.rs deleted file mode 100644 index c21c3a2..0000000 --- a/vendor/blivedm/src/plugins/tts.rs +++ /dev/null @@ -1,856 +0,0 @@ -use crate::client::models::BiliMessage; -use crate::client::scheduler::{EventContext, EventHandler}; -use base64::{Engine as _, engine::general_purpose}; -use log::{debug, error, info, warn}; -use rodio::{Decoder, OutputStream, Sink}; -use serde::{Deserialize, Serialize}; -use std::io::Cursor; -use std::process::Command; -use std::sync::mpsc::{self, Sender}; -use std::thread; -use std::thread::JoinHandle; - -#[derive(Serialize, Debug)] -struct TtsRequest { - text: String, - #[serde(skip_serializing_if = "Option::is_none")] - voice: Option, - #[serde(skip_serializing_if = "Option::is_none")] - backend: Option, - #[serde(skip_serializing_if = "Option::is_none")] - quality: Option, - #[serde(skip_serializing_if = "Option::is_none")] - format: Option, - #[serde(skip_serializing_if = "Option::is_none")] - sample_rate: Option, -} - -#[derive(Deserialize, Debug)] -struct TtsResponse { - audio_data: String, - metadata: TtsMetadata, - #[allow(dead_code)] - cached: bool, -} - -#[derive(Deserialize, Debug)] -struct TtsMetadata { - #[allow(dead_code)] - backend: String, - #[allow(dead_code)] - #[serde(skip_serializing_if = "Option::is_none")] - voice: Option, - #[serde(skip_serializing_if = "Option::is_none")] - duration: Option, - #[allow(dead_code)] - #[serde(skip_serializing_if = "Option::is_none")] - sample_rate: Option, - #[allow(dead_code)] - #[serde(skip_serializing_if = "Option::is_none")] - format: Option, - #[allow(dead_code)] - #[serde(skip_serializing_if = "Option::is_none")] - size_bytes: Option, -} - -/// Alibaba DashScope TTS request structure -#[derive(Serialize, Debug)] -struct AliTtsRequest { - model: String, - input: AliTtsInput, -} - -#[derive(Serialize, Debug)] -struct AliTtsInput { - text: String, - voice: String, - #[serde(skip_serializing_if = "Option::is_none")] - language_type: Option, -} - -/// Alibaba DashScope TTS SSE response structure -#[derive(Deserialize, Debug)] -struct AliTtsResponse { - output: Option, - #[allow(dead_code)] - request_id: Option, -} - -#[derive(Deserialize, Debug)] -struct AliTtsOutput { - #[serde(default)] - audio: Option, - /// Finish reason: "null" for intermediate, "stop" for final - #[serde(default)] - finish_reason: Option, -} - -#[derive(Deserialize, Debug)] -struct AliTtsAudio { - /// Base64 encoded audio data chunk (may be empty) - #[serde(default)] - data: Option, - /// Audio URL (only in the final response when finish_reason is "stop") - #[serde(default)] - url: Option, - #[allow(dead_code)] - #[serde(default)] - id: Option, - #[allow(dead_code)] - #[serde(default)] - expires_at: Option, -} - -/// TTS backend configuration -#[derive(Debug, Clone)] -pub enum TtsMode { - /// Use REST API for TTS with advanced neural voices - RestApi { - /// The base URL of the TTS server (e.g., "http://localhost:8000") - server_url: String, - /// Voice ID to use for TTS (e.g., "zh-CN-XiaoxiaoNeural") - voice: Option, - /// TTS backend to use (e.g., "edge", "xtts", "piper") - backend: Option, - /// Audio quality ("low", "medium", "high") - quality: Option, - /// Audio format (e.g., "wav") - format: Option, - /// Sample rate for audio - sample_rate: Option, - /// Audio volume (0.0 to 1.0, default is 1.0) - volume: Option, - }, - /// Use Alibaba DashScope TTS API (qwen3-tts) - AliTts { - /// DashScope API key (from DASHSCOPE_API_KEY env or config) - api_key: String, - /// Model to use (e.g., "qwen3-tts-flash") - model: String, - /// Voice ID to use (e.g., "Cherry", "Chelsie", etc.) - voice: String, - /// Language type (e.g., "Chinese", "English") - language_type: Option, - /// Audio volume (0.0 to 1.0, default is 1.0) - volume: Option, - }, - /// Use local command-line TTS programs - Command { - /// The TTS command to use (e.g., "say" on macOS, "espeak-ng" on Linux) - tts_command: String, - /// Optional extra arguments for the TTS command (e.g., ["-v", "SinJi"]) - tts_args: Vec, - }, -} - -/// A plugin that sends Danmaku text to a TTS service and plays the audio sequentially. -/// -/// This handler supports two modes: -/// 1. REST API mode: Sends text to a TTS REST API server, receives base64-encoded audio data, -/// decodes it and plays through the system's audio output -/// 2. Command mode: Uses local command-line TTS programs (like `say` on macOS or `espeak-ng` on Linux) -/// -/// Messages are processed sequentially to avoid overlapping audio. -pub struct TtsHandler { - /// TTS configuration (either REST API or command-based) - #[allow(dead_code)] - mode: TtsMode, - /// Channel sender for queuing TTS messages - sender: Sender, - /// Background thread handle for TTS processing - _worker_handle: JoinHandle<()>, -} - -impl TtsHandler { - /// Create a new TTS handler with the specified mode - pub fn new(mode: TtsMode) -> Self { - let (sender, receiver) = mpsc::channel::(); - - // Clone the mode for the worker thread - let mode_clone = mode.clone(); - - // Spawn worker thread to process TTS queue sequentially - let worker_handle = thread::spawn(move || match &mode_clone { - TtsMode::RestApi { .. } => { - Self::run_rest_api_worker(receiver, mode_clone); - } - TtsMode::AliTts { .. } => { - Self::run_ali_tts_worker(receiver, mode_clone); - } - TtsMode::Command { .. } => { - Self::run_command_worker(receiver, mode_clone); - } - }); - - TtsHandler { - mode, - sender, - _worker_handle: worker_handle, - } - } - - /// Create a new TTS handler with REST API using default Chinese voice settings - pub fn new_rest_api_default(server_url: String) -> Self { - Self::new_rest_api_default_with_volume(server_url, 1.0) - } - - /// Create a new TTS handler with REST API using default Chinese voice settings and custom volume - pub fn new_rest_api_default_with_volume(server_url: String, volume: f32) -> Self { - let mode = TtsMode::RestApi { - server_url, - voice: Some("zh-CN-XiaoxiaoNeural".to_string()), - backend: Some("edge".to_string()), - quality: Some("medium".to_string()), - format: Some("wav".to_string()), - sample_rate: Some(22050), - volume: Some(volume), - }; - Self::new(mode) - } - - /// Create a new TTS handler with REST API and custom configuration - pub fn new_rest_api( - server_url: String, - voice: Option, - backend: Option, - quality: Option, - format: Option, - sample_rate: Option, - ) -> Self { - Self::new_rest_api_with_volume( - server_url, - voice, - backend, - quality, - format, - sample_rate, - None, - ) - } - - /// Create a new TTS handler with REST API and custom configuration including volume - pub fn new_rest_api_with_volume( - server_url: String, - voice: Option, - backend: Option, - quality: Option, - format: Option, - sample_rate: Option, - volume: Option, - ) -> Self { - let mode = TtsMode::RestApi { - server_url, - voice, - backend, - quality, - format, - sample_rate, - volume, - }; - Self::new(mode) - } - - /// Create a new TTS handler with command-line TTS - pub fn new_command(tts_command: String, tts_args: Vec) -> Self { - let mode = TtsMode::Command { - tts_command, - tts_args, - }; - Self::new(mode) - } - - /// Create a new TTS handler with Alibaba DashScope TTS using default settings - pub fn new_ali_tts_default(api_key: String) -> Self { - Self::new_ali_tts( - api_key, - "qwen3-tts-flash".to_string(), - "Cherry".to_string(), - Some("Chinese".to_string()), - None, - ) - } - - /// Create a new TTS handler with Alibaba DashScope TTS and custom configuration - pub fn new_ali_tts( - api_key: String, - model: String, - voice: String, - language_type: Option, - volume: Option, - ) -> Self { - let mode = TtsMode::AliTts { - api_key, - model, - voice, - language_type, - volume, - }; - Self::new(mode) - } - - /// Worker thread for REST API TTS processing - fn run_rest_api_worker(receiver: std::sync::mpsc::Receiver, mode: TtsMode) { - if let TtsMode::RestApi { - server_url, - voice, - backend, - quality, - format, - sample_rate, - volume, - } = mode - { - // Create a tokio runtime for HTTP requests - let rt = tokio::runtime::Runtime::new().unwrap(); - let client = reqwest::Client::new(); - - // Initialize audio output stream (this will be reused for all audio playback) - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); - - while let Ok(message) = receiver.recv() { - let request = TtsRequest { - text: message, - voice: voice.clone(), - backend: backend.clone(), - quality: quality.clone(), - format: format.clone(), - sample_rate, - }; - - // Make HTTP request to TTS service - rt.block_on(async { - match client - .post(&format!("{}/tts", server_url)) - .header("Content-Type", "application/json") - .json(&request) - .send() - .await - { - Ok(response) => { - if response.status().is_success() { - match response.json::().await { - Ok(tts_response) => { - info!("TTS generated successfully"); - - // Decode base64 audio data and play it - match general_purpose::STANDARD - .decode(&tts_response.audio_data) - { - Ok(audio_bytes) => { - // Create a cursor from the audio bytes - let cursor = Cursor::new(audio_bytes); - - // Create a decoder for the audio format - match Decoder::new(cursor) { - Ok(source) => { - // Create a new sink for this audio - let sink = - Sink::try_new(&stream_handle).unwrap(); - - // Set volume if specified (default to 1.0 if not set) - let audio_volume = volume.unwrap_or(1.0); - sink.set_volume(audio_volume); - - // Append the audio source to the sink - sink.append(source); - - // Wait for the audio to finish playing - sink.sleep_until_end(); - - debug!("Audio playback completed"); - } - Err(e) => error!( - "Failed to decode audio format: {}", - e - ), - } - } - Err(e) => { - error!("Failed to decode base64 audio data: {}", e) - } - } - } - Err(e) => error!("Failed to parse TTS response: {}", e), - } - } else { - warn!("TTS request failed with status: {}", response.status()); - } - } - Err(e) => error!("Failed to send TTS request: {}", e), - } - }); - } - } - } - - /// Worker thread for Alibaba DashScope TTS processing with SSE streaming - fn run_ali_tts_worker(receiver: std::sync::mpsc::Receiver, mode: TtsMode) { - use futures::StreamExt; - - if let TtsMode::AliTts { - api_key, - model, - voice, - language_type, - volume, - } = mode - { - // Create a tokio runtime for HTTP requests - let rt = tokio::runtime::Runtime::new().unwrap(); - let client = reqwest::Client::new(); - - // Initialize audio output stream (this will be reused for all audio playback) - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); - - while let Ok(message) = receiver.recv() { - let request = AliTtsRequest { - model: model.clone(), - input: AliTtsInput { - text: message, - voice: voice.clone(), - language_type: language_type.clone(), - }, - }; - - // Make HTTP request to DashScope TTS service with SSE - rt.block_on(async { - match client - .post("https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation") - .header("Authorization", format!("Bearer {}", api_key)) - .header("Content-Type", "application/json") - .header("X-DashScope-SSE", "enable") - .json(&request) - .send() - .await - { - Ok(response) => { - if response.status().is_success() { - // Collect all audio chunks from SSE stream - let mut audio_chunks: Vec> = Vec::new(); - let mut audio_url: Option = None; - let mut stream = response.bytes_stream(); - - let mut buffer = String::new(); - - while let Some(chunk_result) = stream.next().await { - match chunk_result { - Ok(chunk) => { - // Append chunk to buffer - if let Ok(text) = std::str::from_utf8(&chunk) { - buffer.push_str(text); - - // Process complete SSE events in buffer - while let Some(event_end) = buffer.find("\n\n") { - let event = buffer[..event_end].to_string(); - buffer = buffer[event_end + 2..].to_string(); - - // Parse SSE event - look for data: lines - for line in event.lines() { - if let Some(data) = line.strip_prefix("data:") { - let data = data.trim(); - if data.is_empty() || data == "[DONE]" { - continue; - } - - match serde_json::from_str::(data) { - Ok(ali_response) => { - if let Some(output) = ali_response.output { - if let Some(audio) = output.audio { - // Check for base64 audio data (non-empty) - if let Some(ref audio_data) = audio.data { - if !audio_data.is_empty() { - match general_purpose::STANDARD.decode(audio_data) { - Ok(decoded) => { - if !decoded.is_empty() { - audio_chunks.push(decoded); - } - } - Err(e) => { - debug!("Failed to decode audio chunk: {}", e); - } - } - } - } - // Check for audio URL (final response) - if let Some(url) = audio.url { - debug!("Audio URL received: {}", url); - audio_url = Some(url); - } - } - // Check if this is the final response - if let Some(ref reason) = output.finish_reason { - if reason == "stop" { - debug!("Received final response with finish_reason: stop"); - } - } - } - } - Err(e) => { - debug!("Failed to parse SSE data: {} - data: {}", e, data); - } - } - } - } - } - } - } - Err(e) => { - error!("Error reading SSE stream: {}", e); - break; - } - } - } - - // Try to play audio - prefer URL download over streamed chunks - // Streamed MP3 chunks cannot be simply concatenated due to headers/frames - let audio_data = if let Some(url) = audio_url { - // Download complete audio from URL (preferred method) - info!("AliTTS: downloading audio from URL"); - match client.get(&url).send().await { - Ok(audio_response) => { - if audio_response.status().is_success() { - match audio_response.bytes().await { - Ok(bytes) => { - info!("AliTTS: downloaded {} bytes", bytes.len()); - Some(bytes.to_vec()) - } - Err(e) => { - error!("Failed to read audio bytes: {}", e); - None - } - } - } else { - error!("Failed to download audio: {}", audio_response.status()); - None - } - } - Err(e) => { - error!("Failed to fetch audio URL: {}", e); - None - } - } - } else if !audio_chunks.is_empty() { - // Fallback: try to use collected base64 chunks - // Note: This may not work correctly for MP3 format due to concatenation issues - warn!("AliTTS: No URL provided, attempting to use streamed chunks (may have decoding issues)"); - let combined: Vec = audio_chunks.into_iter().flatten().collect(); - info!("AliTTS: using {} bytes from streamed chunks", combined.len()); - Some(combined) - } else { - warn!("No audio data or URL received from AliTTS"); - None - }; - - // Play the audio - if let Some(audio_bytes) = audio_data { - let cursor = Cursor::new(audio_bytes); - match Decoder::new(cursor) { - Ok(source) => { - let sink = Sink::try_new(&stream_handle).unwrap(); - let audio_volume = volume.unwrap_or(1.0); - sink.set_volume(audio_volume); - sink.append(source); - sink.sleep_until_end(); - debug!("Audio playback completed"); - } - Err(e) => error!("Failed to decode audio format: {}", e), - } - } - } else { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - warn!("AliTTS request failed with status: {} - {}", status, body); - } - } - Err(e) => error!("Failed to send AliTTS request: {}", e), - } - }); - } - } - } - - /// Worker thread for command-line TTS processing - fn run_command_worker(receiver: std::sync::mpsc::Receiver, mode: TtsMode) { - if let TtsMode::Command { - tts_command, - tts_args, - } = mode - { - while let Ok(message) = receiver.recv() { - let mut command = Command::new(&tts_command); - for arg in &tts_args { - command.arg(arg); - } - - // Execute TTS command and wait for it to complete - match command.arg(&message).status() { - Ok(status) => { - if status.success() { - debug!("TTS command completed successfully"); - } else { - warn!("TTS command failed with status: {}", status); - } - } - Err(e) => error!("Failed to execute TTS command: {}", e), - } - } - } - } - - /// Legacy method - kept for backward compatibility - #[deprecated(note = "Use new_rest_api_default instead")] - pub fn new_default(server_url: String) -> Self { - Self::new_rest_api_default(server_url) - } -} - -impl EventHandler for TtsHandler { - fn handle(&self, msg: &BiliMessage, _context: &EventContext) { - if let BiliMessage::Danmu { user, text } = msg { - let message = format!("{}说:{}", user, text); - // Send message to the queue for sequential processing - let _ = self.sender.send(message); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::client::models::BiliMessage; - use crate::client::scheduler::EventHandler; - - #[test] - fn test_tts_handler_danmu() { - // Test with a mock server URL (won't actually make requests in this test) - let handler = TtsHandler::new_rest_api_default("http://localhost:8000".to_string()); - - let text = "您好,欢迎来到直播间。".to_string(); - let msg = BiliMessage::Danmu { - user: "测试用户".to_string(), - text: text.clone(), - }; - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler.handle(&msg, &context); - } - - #[test] - fn test_tts_handler_custom_config() { - let handler = TtsHandler::new_rest_api( - "http://localhost:8000".to_string(), - Some("zh-CN-XiaoxiaoNeural".to_string()), - Some("edge".to_string()), - Some("high".to_string()), - Some("wav".to_string()), - Some(44100), - ); - - let msg = BiliMessage::Danmu { - user: "test_user".to_string(), - text: "hello world".to_string(), - }; - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler.handle(&msg, &context); - } - - #[test] - fn test_tts_handler_sequential_processing() { - use std::time::Duration; - - // Use default configuration for testing - let handler = TtsHandler::new_rest_api_default("http://localhost:8000".to_string()); - - // Send multiple messages quickly - let messages = vec![ - ("User1", "First message"), - ("User2", "Second message"), - ("User3", "Third message"), - ]; - - for (user, text) in messages { - let msg = BiliMessage::Danmu { - user: user.to_string(), - text: text.to_string(), - }; - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler.handle(&msg, &context); - } - - // Give the worker thread some time to process the queue - std::thread::sleep(Duration::from_millis(100)); - - // The test passes if no panic occurs - the sequential processing - // is ensured by the worker thread design - } - - #[test] - fn test_tts_handler_command_mode() { - // Test command-based TTS (cross-platform using echo) - let handler = TtsHandler::new_command("echo".to_string(), vec![]); - - let msg = BiliMessage::Danmu { - user: "test_user".to_string(), - text: "test message".to_string(), - }; - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler.handle(&msg, &context); - - // Give the worker thread some time to process the message - std::thread::sleep(std::time::Duration::from_millis(50)); - } - - #[cfg(target_os = "macos")] - #[test] - fn test_tts_handler_macos_voice() { - let handler = TtsHandler::new_command( - "say".to_string(), - vec!["-v".to_string(), "Mei-Jia".to_string()], - ); - - let msg = BiliMessage::Danmu { - user: "用户".to_string(), - text: "你好".to_string(), - }; - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler.handle(&msg, &context); - } - - #[cfg(target_os = "linux")] - #[test] - fn test_tts_handler_linux_voice() { - let handler = TtsHandler::new_command( - "espeak-ng".to_string(), - vec!["-v".to_string(), "cmn".to_string()], - ); - - let msg = BiliMessage::Danmu { - user: "用户".to_string(), - text: "你好".to_string(), - }; - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler.handle(&msg, &context); - } - - #[test] - fn test_tts_request_serialization() { - let request = TtsRequest { - text: "Hello world".to_string(), - voice: Some("zh-CN-XiaoxiaoNeural".to_string()), - backend: Some("edge".to_string()), - quality: Some("medium".to_string()), - format: Some("wav".to_string()), - sample_rate: Some(22050), - }; - - let json = serde_json::to_string(&request).unwrap(); - assert!(json.contains("Hello world")); - assert!(json.contains("zh-CN-XiaoxiaoNeural")); - assert!(json.contains("edge")); - } - - #[test] - fn test_tts_handler_with_volume() { - // Test with custom volume setting - let handler = - TtsHandler::new_rest_api_default_with_volume("http://localhost:8000".to_string(), 0.5); - - let msg = BiliMessage::Danmu { - user: "test_user".to_string(), - text: "volume test".to_string(), - }; - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler.handle(&msg, &context); - - // Test with custom configuration including volume - let handler_custom = TtsHandler::new_rest_api_with_volume( - "http://localhost:8000".to_string(), - Some("zh-CN-XiaoxiaoNeural".to_string()), - Some("edge".to_string()), - Some("high".to_string()), - Some("wav".to_string()), - Some(44100), - Some(0.8), - ); - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler_custom.handle(&msg, &context); - } - - #[test] - fn test_ali_tts_handler_default() { - // Test with a mock API key (won't actually make requests in this test) - let handler = TtsHandler::new_ali_tts_default("test_api_key".to_string()); - - let msg = BiliMessage::Danmu { - user: "测试用户".to_string(), - text: "你好".to_string(), - }; - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler.handle(&msg, &context); - } - - #[test] - fn test_ali_tts_handler_custom_config() { - let handler = TtsHandler::new_ali_tts( - "test_api_key".to_string(), - "qwen3-tts-flash".to_string(), - "Chelsie".to_string(), - Some("English".to_string()), - Some(0.8), - ); - - let msg = BiliMessage::Danmu { - user: "test_user".to_string(), - text: "hello world".to_string(), - }; - let context = EventContext { - cookies: None, - room_id: 12345, - }; - handler.handle(&msg, &context); - } - - #[test] - fn test_ali_tts_request_serialization() { - let request = AliTtsRequest { - model: "qwen3-tts-flash".to_string(), - input: AliTtsInput { - text: "你好世界".to_string(), - voice: "Cherry".to_string(), - language_type: Some("Chinese".to_string()), - }, - }; - - let json = serde_json::to_string(&request).unwrap(); - assert!(json.contains("qwen3-tts-flash")); - assert!(json.contains("你好世界")); - assert!(json.contains("Cherry")); - assert!(json.contains("Chinese")); - } -} diff --git a/vendor/blivedm/src/tui/app.rs b/vendor/blivedm/src/tui/app.rs deleted file mode 100644 index 13fbbd8..0000000 --- a/vendor/blivedm/src/tui/app.rs +++ /dev/null @@ -1,624 +0,0 @@ -// src/tui/app.rs -//! TUI application state management - -use std::collections::VecDeque; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; - -/// Maximum number of messages to keep in buffer -const MAX_MESSAGES: usize = 1000; - -/// TUI Application state -pub struct TuiApp { - /// Shared message buffer (thread-safe) - pub message_buffer: Arc>>, - /// Current scroll offset (0 = bottom, 1 = one line up, etc.) - pub scroll_offset: usize, - /// Whether auto-scroll is enabled - pub auto_scroll: bool, - /// Current input text - pub input: String, - /// Cursor position in input - pub cursor_position: usize, - /// Room ID being monitored - pub room_id: String, - /// Whether to quit the application - pub should_quit: bool, - /// Shared online user count (thread-safe, updated from event handler) - pub online_count: Arc, - /// Whether to show raw event messages - pub show_raw: bool, - /// Shared log buffer for capturing log messages (thread-safe) - pub log_buffer: Arc>>, - /// Whether to show the logs panel - pub show_logs: bool, - /// Scroll offset for logs panel (0 = bottom) - pub log_scroll_offset: usize, - /// Whether auto-scroll is enabled for logs panel - pub log_auto_scroll: bool, - /// Whether the help overlay is visible - pub show_help: bool, - /// Whether Vim-style visual selection is active - pub visual_mode: bool, - /// Frozen message snapshot used while visual mode is active - frozen_messages: Vec, - /// Frozen log snapshot used while visual mode is active - frozen_logs: Vec, - /// Rendered wrapped lines for the active pane - rendered_lines: Vec, - /// First visible rendered line for the active pane - rendered_start_line: usize, - /// Visible height for the active pane - rendered_visible_height: usize, - /// Selection anchor in rendered line coordinates - visual_anchor: usize, - /// Current cursor in rendered line coordinates - visual_cursor: usize, - /// Current line cursor in normal pane navigation - pane_cursor: usize, - /// Whether the pane cursor has been initialized - pane_cursor_initialized: bool, - /// History of submitted input messages (oldest first) - input_history: Vec, - /// Position when browsing input history; None means editing the live draft - history_index: Option, - /// Draft saved when we start browsing input history - history_draft: String, -} - -impl TuiApp { - /// Create a new TUI application with shared message buffer - pub fn new(message_buffer: Arc>>, room_id: String) -> Self { - Self::with_online_count(message_buffer, room_id, Arc::new(AtomicU64::new(0))) - } - - /// Create a new TUI application with shared message buffer and online count - pub fn with_online_count( - message_buffer: Arc>>, - room_id: String, - online_count: Arc, - ) -> Self { - Self { - message_buffer, - scroll_offset: 0, - auto_scroll: true, - input: String::new(), - cursor_position: 0, - room_id, - should_quit: false, - online_count, - show_raw: false, - log_buffer: Arc::new(Mutex::new(VecDeque::new())), - show_logs: false, - log_scroll_offset: 0, - log_auto_scroll: true, - show_help: false, - visual_mode: false, - frozen_messages: Vec::new(), - frozen_logs: Vec::new(), - rendered_lines: Vec::new(), - rendered_start_line: 0, - rendered_visible_height: 0, - visual_anchor: 0, - visual_cursor: 0, - pane_cursor: 0, - pane_cursor_initialized: false, - input_history: Vec::new(), - history_index: None, - history_draft: String::new(), - } - } - - /// Get the current online count - pub fn get_online_count(&self) -> u64 { - self.online_count.load(Ordering::Relaxed) - } - - /// Update the online count (called from event handler) - pub fn set_online_count(online_count: &Arc, count: u64) { - online_count.store(count, Ordering::Relaxed); - } - - /// Add a message to the buffer (called from event handler) - pub fn add_message(buffer: &Arc>>, message: String) { - if let Ok(mut messages) = buffer.lock() { - messages.push_back(message); - while messages.len() > MAX_MESSAGES { - messages.pop_front(); - } - } - } - - /// Get messages for display (returns a copy of the buffer) - pub fn get_messages(&self) -> Vec { - if self.visual_mode { - return self.frozen_messages.clone(); - } - - if let Ok(messages) = self.message_buffer.lock() { - messages.iter().cloned().collect() - } else { - Vec::new() - } - } - - /// Get the number of messages in buffer - pub fn message_count(&self) -> usize { - if let Ok(messages) = self.message_buffer.lock() { - messages.len() - } else { - 0 - } - } - - /// Scroll up (increase offset) - pub fn scroll_up(&mut self, amount: usize) { - let max_offset = self.message_count().saturating_sub(1); - self.scroll_offset = (self.scroll_offset + amount).min(max_offset); - if self.scroll_offset > 0 { - self.auto_scroll = false; - } - } - - /// Scroll down (decrease offset) - pub fn scroll_down(&mut self, amount: usize) { - self.scroll_offset = self.scroll_offset.saturating_sub(amount); - if self.scroll_offset == 0 { - self.auto_scroll = true; - } - } - - /// Scroll to bottom - pub fn scroll_to_bottom(&mut self) { - self.scroll_offset = 0; - self.auto_scroll = true; - } - - /// Handle character input - pub fn enter_char(&mut self, c: char) { - let byte_pos = self.byte_index(); - self.input.insert(byte_pos, c); - self.cursor_position += 1; - } - - /// Delete character before cursor - pub fn delete_char(&mut self) { - if self.cursor_position > 0 { - let byte_pos = self.byte_index_at(self.cursor_position - 1); - self.input.remove(byte_pos); - self.cursor_position -= 1; - } - } - - /// Move cursor left - pub fn move_cursor_left(&mut self) { - if self.cursor_position > 0 { - self.cursor_position -= 1; - } - } - - /// Move cursor right - pub fn move_cursor_right(&mut self) { - let char_count = self.input.chars().count(); - if self.cursor_position < char_count { - self.cursor_position += 1; - } - } - - fn byte_index(&self) -> usize { - self.input - .char_indices() - .nth(self.cursor_position) - .map(|(idx, _)| idx) - .unwrap_or(self.input.len()) - } - - fn byte_index_at(&self, char_pos: usize) -> usize { - self.input - .char_indices() - .nth(char_pos) - .map(|(idx, _)| idx) - .unwrap_or(self.input.len()) - } - - /// Get current input and clear it - pub fn take_input(&mut self) -> String { - let input = self.input.clone(); - self.input.clear(); - self.cursor_position = 0; - if !input.is_empty() && self.input_history.last() != Some(&input) { - self.input_history.push(input.clone()); - } - self.history_index = None; - self.history_draft.clear(); - input - } - - /// Recall the previous (older) entry from input history into the input box - pub fn history_prev(&mut self) { - if self.input_history.is_empty() { - return; - } - - let new_index = match self.history_index { - None => { - self.history_draft = self.input.clone(); - self.input_history.len() - 1 - } - Some(0) => 0, - Some(i) => i - 1, - }; - - self.history_index = Some(new_index); - self.input = self.input_history[new_index].clone(); - self.cursor_position = self.input.chars().count(); - } - - /// Recall the next (newer) entry from input history, or restore the draft - pub fn history_next(&mut self) { - let Some(index) = self.history_index else { - return; - }; - - if index + 1 < self.input_history.len() { - let new_index = index + 1; - self.history_index = Some(new_index); - self.input = self.input_history[new_index].clone(); - } else { - self.history_index = None; - self.input = std::mem::take(&mut self.history_draft); - } - self.cursor_position = self.input.chars().count(); - } - - /// Quit the application - pub fn quit(&mut self) { - self.should_quit = true; - } - - /// Toggle raw message visibility - pub fn toggle_show_raw(&mut self) { - self.show_raw = !self.show_raw; - } - - /// Toggle logs panel visibility - pub fn toggle_show_logs(&mut self) { - self.show_logs = !self.show_logs; - self.show_help = false; - if self.show_logs { - self.log_scroll_offset = 0; - self.log_auto_scroll = true; - } - } - - /// Toggle help overlay visibility - pub fn toggle_help(&mut self) { - self.show_help = !self.show_help; - } - - /// Get the number of log messages in buffer - pub fn log_message_count(&self) -> usize { - if let Ok(logs) = self.log_buffer.lock() { - logs.len() - } else { - 0 - } - } - - /// Scroll logs up (increase offset) - pub fn log_scroll_up(&mut self, amount: usize) { - let max_offset = self.log_message_count().saturating_sub(1); - self.log_scroll_offset = (self.log_scroll_offset + amount).min(max_offset); - if self.log_scroll_offset > 0 { - self.log_auto_scroll = false; - } - } - - /// Scroll logs down (decrease offset) - pub fn log_scroll_down(&mut self, amount: usize) { - self.log_scroll_offset = self.log_scroll_offset.saturating_sub(amount); - if self.log_scroll_offset == 0 { - self.log_auto_scroll = true; - } - } - - /// Scroll logs to bottom - pub fn log_scroll_to_bottom(&mut self) { - self.log_scroll_offset = 0; - self.log_auto_scroll = true; - } - - /// Set the log buffer (used to share with the TuiLogger) - pub fn set_log_buffer(&mut self, log_buffer: Arc>>) { - self.log_buffer = log_buffer; - } - - /// Get log messages for display - pub fn get_log_messages(&self) -> Vec { - if self.visual_mode { - return self.frozen_logs.clone(); - } - - if let Ok(logs) = self.log_buffer.lock() { - logs.iter().cloned().collect() - } else { - Vec::new() - } - } - - /// Store the current wrapped-line model for the active pane - pub fn set_rendered_lines( - &mut self, - lines: Vec, - start_line: usize, - visible_height: usize, - ) -> usize { - let old_total_lines = self.rendered_lines.len(); - let old_start_line = self.rendered_start_line; - let old_visible_height = self.rendered_visible_height.max(1); - let old_last_visible = old_start_line - .saturating_add(old_visible_height.saturating_sub(1)) - .min(old_total_lines.saturating_sub(1)); - let was_following_bottom = self.pane_cursor_initialized - && old_total_lines > 0 - && self.pane_cursor >= old_last_visible - && self.active_auto_scroll(); - - self.rendered_lines = lines; - self.rendered_start_line = start_line; - self.rendered_visible_height = visible_height.max(1); - - if self.visual_mode { - let max_index = self.rendered_lines.len().saturating_sub(1); - self.visual_anchor = self.visual_anchor.min(max_index); - self.visual_cursor = self.visual_cursor.min(max_index); - self.sync_visual_view(); - } else if self.rendered_lines.is_empty() { - self.pane_cursor = 0; - self.pane_cursor_initialized = false; - } else { - let max_index = self.rendered_lines.len() - 1; - if !self.pane_cursor_initialized || was_following_bottom { - self.pane_cursor = self.initial_visible_cursor(); - self.pane_cursor_initialized = true; - } else { - self.pane_cursor = self.pane_cursor.min(max_index); - self.sync_pane_view(); - } - } - - self.rendered_start_line - } - - /// Enter Vim-style visual selection mode - pub fn enter_visual_mode(&mut self) { - if self.visual_mode || self.rendered_lines.is_empty() { - return; - } - - self.frozen_messages = self - .message_buffer - .lock() - .map(|messages| messages.iter().cloned().collect()) - .unwrap_or_default(); - self.frozen_logs = self - .log_buffer - .lock() - .map(|logs| logs.iter().cloned().collect()) - .unwrap_or_default(); - self.show_help = false; - self.visual_mode = true; - self.visual_cursor = self.pane_cursor; - self.visual_anchor = self.visual_cursor; - self.sync_visual_view(); - } - - /// Exit visual selection mode and resume live updates - pub fn exit_visual_mode(&mut self) { - self.visual_mode = false; - self.frozen_messages.clear(); - self.frozen_logs.clear(); - } - - /// Toggle visual selection mode - pub fn toggle_visual_mode(&mut self) { - if self.visual_mode { - self.exit_visual_mode(); - } else { - self.enter_visual_mode(); - } - } - - /// Move the normal pane cursor up - pub fn pane_up(&mut self, amount: usize) { - if self.visual_mode || self.rendered_lines.is_empty() { - return; - } - - self.pane_cursor = self.pane_cursor.saturating_sub(amount); - self.sync_pane_view(); - } - - /// Move the normal pane cursor down - pub fn pane_down(&mut self, amount: usize) { - if self.visual_mode || self.rendered_lines.is_empty() { - return; - } - - let max_index = self.rendered_lines.len().saturating_sub(1); - self.pane_cursor = (self.pane_cursor + amount).min(max_index); - self.sync_pane_view(); - } - - /// Jump the normal pane cursor to the first line - pub fn pane_top(&mut self) { - if self.visual_mode || self.rendered_lines.is_empty() { - return; - } - - self.pane_cursor = 0; - self.sync_pane_view(); - } - - /// Jump the normal pane cursor to the last line - pub fn pane_bottom(&mut self) { - if self.visual_mode || self.rendered_lines.is_empty() { - return; - } - - self.pane_cursor = self.rendered_lines.len() - 1; - self.sync_pane_view(); - } - - /// Move the visual cursor up - pub fn visual_up(&mut self, amount: usize) { - if !self.visual_mode { - return; - } - - self.visual_cursor = self.visual_cursor.saturating_sub(amount); - self.sync_visual_view(); - } - - /// Move the visual cursor down - pub fn visual_down(&mut self, amount: usize) { - if !self.visual_mode { - return; - } - - let max_index = self.rendered_lines.len().saturating_sub(1); - self.visual_cursor = (self.visual_cursor + amount).min(max_index); - self.sync_visual_view(); - } - - /// Jump the visual cursor to the first line - pub fn visual_top(&mut self) { - if !self.visual_mode || self.rendered_lines.is_empty() { - return; - } - - self.visual_cursor = 0; - self.sync_visual_view(); - } - - /// Jump the visual cursor to the last line - pub fn visual_bottom(&mut self) { - if !self.visual_mode || self.rendered_lines.is_empty() { - return; - } - - self.visual_cursor = self.rendered_lines.len() - 1; - self.sync_visual_view(); - } - - /// Get the selected rendered-line range - pub fn visual_range(&self) -> Option<(usize, usize)> { - if !self.visual_mode || self.rendered_lines.is_empty() { - return None; - } - - Some(( - self.visual_anchor.min(self.visual_cursor), - self.visual_anchor.max(self.visual_cursor), - )) - } - - /// Get the current visual cursor position - pub fn visual_cursor(&self) -> Option { - if self.visual_mode && !self.rendered_lines.is_empty() { - Some(self.visual_cursor) - } else { - None - } - } - - /// Get the current pane cursor position - pub fn pane_cursor(&self) -> Option { - if !self.visual_mode && self.pane_cursor_initialized && !self.rendered_lines.is_empty() { - Some(self.pane_cursor) - } else { - None - } - } - - /// Return the selected text from the current visual range - pub fn selected_text(&self) -> Option { - let (start, end) = self.visual_range()?; - Some(self.rendered_lines[start..=end].join("\n")) - } - - fn initial_visible_cursor(&self) -> usize { - if self.rendered_lines.is_empty() { - return 0; - } - - let last_visible = self - .rendered_start_line - .saturating_add(self.rendered_visible_height.saturating_sub(1)); - last_visible.min(self.rendered_lines.len() - 1) - } - - fn sync_pane_view(&mut self) { - if self.visual_mode || self.rendered_lines.is_empty() { - return; - } - - let total_lines = self.rendered_lines.len(); - let visible_height = self.rendered_visible_height.max(1).min(total_lines); - let max_start = total_lines.saturating_sub(visible_height); - let mut start_line = self.rendered_start_line.min(max_start); - - if self.pane_cursor < start_line { - start_line = self.pane_cursor; - } else if self.pane_cursor >= start_line + visible_height { - start_line = self.pane_cursor + 1 - visible_height; - } - - self.rendered_start_line = start_line; - - let scroll_offset = total_lines.saturating_sub(visible_height + start_line); - if self.show_logs { - self.log_scroll_offset = scroll_offset; - self.log_auto_scroll = scroll_offset == 0; - } else { - self.scroll_offset = scroll_offset; - self.auto_scroll = scroll_offset == 0; - } - } - - fn active_auto_scroll(&self) -> bool { - if self.show_logs { - self.log_auto_scroll - } else { - self.auto_scroll - } - } - - fn sync_visual_view(&mut self) { - if !self.visual_mode || self.rendered_lines.is_empty() { - return; - } - - let total_lines = self.rendered_lines.len(); - let visible_height = self.rendered_visible_height.max(1).min(total_lines); - let max_start = total_lines.saturating_sub(visible_height); - let mut start_line = self.rendered_start_line.min(max_start); - - if self.visual_cursor < start_line { - start_line = self.visual_cursor; - } else if self.visual_cursor >= start_line + visible_height { - start_line = self.visual_cursor + 1 - visible_height; - } - - self.rendered_start_line = start_line; - - let scroll_offset = total_lines.saturating_sub(visible_height + start_line); - if self.show_logs { - self.log_scroll_offset = scroll_offset; - self.log_auto_scroll = scroll_offset == 0; - } else { - self.scroll_offset = scroll_offset; - self.auto_scroll = scroll_offset == 0; - } - } -} diff --git a/vendor/blivedm/src/tui/event.rs b/vendor/blivedm/src/tui/event.rs deleted file mode 100644 index 33c2393..0000000 --- a/vendor/blivedm/src/tui/event.rs +++ /dev/null @@ -1,254 +0,0 @@ -// src/tui/event.rs -//! Event handling and main TUI loop - -use crate::tui::app::TuiApp; -use crate::tui::ui; -use arboard::Clipboard; -use crossterm::{ - event::{self, Event, KeyCode, KeyModifiers}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use ratatui::{backend::CrosstermBackend, Terminal}; -use std::io; -use std::time::Duration; - -/// Run the TUI application -pub fn run_tui(mut app: TuiApp, mut on_message: F) -> io::Result<()> -where - F: FnMut(String), -{ - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let result = run_app(&mut terminal, &mut app, &mut on_message); - - disable_raw_mode()?; - execute!(terminal.backend_mut(), LeaveAlternateScreen)?; - terminal.show_cursor()?; - - result -} - -fn run_app( - terminal: &mut Terminal>, - app: &mut TuiApp, - on_message: &mut F, -) -> io::Result<()> -where - F: FnMut(String), -{ - let mut needs_redraw = true; - let mut clipboard = Clipboard::new().ok(); - let mut last_message_count = app.message_count(); - let mut last_log_count = app.log_message_count(); - let mut last_online_count = app.get_online_count(); - - loop { - let message_count = app.message_count(); - let log_count = app.log_message_count(); - let online_count = app.get_online_count(); - - if !app.visual_mode - && (message_count != last_message_count - || log_count != last_log_count - || online_count != last_online_count) - { - needs_redraw = true; - } - - last_message_count = message_count; - last_log_count = log_count; - last_online_count = online_count; - - if needs_redraw { - terminal.draw(|f| ui::render(f, app))?; - needs_redraw = false; - } - - if event::poll(Duration::from_millis(16))? { - if let Event::Key(key) = event::read()? { - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { - app.quit(); - needs_redraw = true; - } - KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => { - app.toggle_visual_mode(); - needs_redraw = true; - } - KeyCode::Char('h') if key.modifiers.contains(KeyModifiers::CONTROL) => { - if !app.visual_mode { - app.toggle_help(); - needs_redraw = true; - } - } - KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => { - if !app.visual_mode { - app.toggle_show_raw(); - needs_redraw = true; - } - } - KeyCode::Char('l') if key.modifiers.contains(KeyModifiers::CONTROL) => { - if !app.visual_mode { - app.toggle_show_logs(); - needs_redraw = true; - } - } - KeyCode::Esc => { - if app.visual_mode { - app.exit_visual_mode(); - } else if app.show_help { - app.show_help = false; - } else if app.show_logs { - app.toggle_show_logs(); - } else { - app.quit(); - } - - needs_redraw = true; - } - - _ if app.show_help => {} - - _ if app.visual_mode => { - match key.code { - KeyCode::Char('k') | KeyCode::Up => app.visual_up(1), - KeyCode::Char('j') | KeyCode::Down => app.visual_down(1), - KeyCode::PageUp => app.visual_up(10), - KeyCode::PageDown => app.visual_down(10), - KeyCode::Char('g') | KeyCode::Home => app.visual_top(), - KeyCode::Char('G') | KeyCode::End => app.visual_bottom(), - KeyCode::Char('y') => { - copy_selection(app, clipboard.as_mut())?; - app.exit_visual_mode(); - } - _ => {} - } - - needs_redraw = true; - } - - _ if app.show_logs => match key.code { - KeyCode::Up => { - app.pane_up(1); - needs_redraw = true; - } - KeyCode::Down => { - app.pane_down(1); - needs_redraw = true; - } - KeyCode::PageUp => { - app.pane_up(10); - needs_redraw = true; - } - KeyCode::PageDown => { - app.pane_down(10); - needs_redraw = true; - } - KeyCode::Home if key.modifiers.contains(KeyModifiers::CONTROL) => { - app.pane_top(); - needs_redraw = true; - } - KeyCode::End if key.modifiers.contains(KeyModifiers::CONTROL) => { - app.pane_bottom(); - needs_redraw = true; - } - KeyCode::Home => { - app.pane_top(); - needs_redraw = true; - } - KeyCode::End => { - app.pane_bottom(); - needs_redraw = true; - } - _ => {} - }, - - KeyCode::Char(c) => { - app.enter_char(c); - needs_redraw = true; - } - KeyCode::Backspace => { - app.delete_char(); - needs_redraw = true; - } - KeyCode::Enter => { - let input = app.take_input(); - if !input.is_empty() { - if input == "/quit" || input == "/exit" { - app.quit(); - } else { - on_message(input); - } - } - - needs_redraw = true; - } - KeyCode::Up => { - app.history_prev(); - needs_redraw = true; - } - KeyCode::Down => { - app.history_next(); - needs_redraw = true; - } - KeyCode::Left => { - app.move_cursor_left(); - needs_redraw = true; - } - KeyCode::Right => { - app.move_cursor_right(); - needs_redraw = true; - } - KeyCode::PageUp => { - app.pane_up(10); - needs_redraw = true; - } - KeyCode::PageDown => { - app.pane_down(10); - needs_redraw = true; - } - KeyCode::Home if key.modifiers.contains(KeyModifiers::CONTROL) => { - app.pane_top(); - needs_redraw = true; - } - KeyCode::End if key.modifiers.contains(KeyModifiers::CONTROL) => { - app.pane_bottom(); - needs_redraw = true; - } - KeyCode::Home => { - app.cursor_position = 0; - needs_redraw = true; - } - KeyCode::End => { - app.cursor_position = app.input.chars().count(); - needs_redraw = true; - } - _ => {} - } - } - } - - if app.should_quit { - break; - } - } - - Ok(()) -} - -fn copy_selection(app: &TuiApp, clipboard: Option<&mut Clipboard>) -> io::Result<()> { - let Some(text) = app.selected_text() else { - return Ok(()); - }; - - let Some(clipboard) = clipboard else { - return Err(io::Error::other("clipboard is unavailable")); - }; - - clipboard.set_text(text).map_err(io::Error::other) -} diff --git a/vendor/blivedm/src/tui/logger.rs b/vendor/blivedm/src/tui/logger.rs deleted file mode 100644 index 78ecbcc..0000000 --- a/vendor/blivedm/src/tui/logger.rs +++ /dev/null @@ -1,73 +0,0 @@ -// src/tui/logger.rs -//! Custom logger that captures log messages into a shared buffer for TUI display - -use log::{Log, Metadata, Record}; -use std::collections::VecDeque; -use std::sync::{Arc, Mutex}; -use std::time::Instant; - -/// Maximum number of log messages to keep in buffer -const MAX_LOG_MESSAGES: usize = 1000; - -/// A logger that writes log messages to a shared buffer for TUI display. -/// It also optionally forwards to env_logger for file/stderr output. -pub struct TuiLogger { - buffer: Arc>>, - level: log::LevelFilter, - start_time: Instant, -} - -impl TuiLogger { - /// Create a new TuiLogger with the given shared buffer and level filter. - pub fn new(buffer: Arc>>, level: log::LevelFilter) -> Self { - Self { - buffer, - level, - start_time: Instant::now(), - } - } - - /// Initialize this logger as the global logger. - /// Returns the shared buffer so it can be passed to TuiApp. - pub fn init(level: log::LevelFilter) -> Arc>> { - let buffer = Arc::new(Mutex::new(VecDeque::new())); - let logger = TuiLogger::new(Arc::clone(&buffer), level); - log::set_boxed_logger(Box::new(logger)).expect("Failed to set TuiLogger"); - log::set_max_level(level); - buffer - } -} - -impl Log for TuiLogger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= self.level - } - - fn log(&self, record: &Record) { - if !self.enabled(record.metadata()) { - return; - } - - let elapsed = self.start_time.elapsed(); - let secs = elapsed.as_secs(); - let mins = secs / 60; - let hours = mins / 60; - let timestamp = format!("{:02}:{:02}:{:02}", hours, mins % 60, secs % 60); - let msg = format!( - "[{}] [{}] [{}] {}", - timestamp, - record.level(), - record.target(), - record.args() - ); - - if let Ok(mut buf) = self.buffer.lock() { - buf.push_back(msg); - while buf.len() > MAX_LOG_MESSAGES { - buf.pop_front(); - } - } - } - - fn flush(&self) {} -} diff --git a/vendor/blivedm/src/tui/mod.rs b/vendor/blivedm/src/tui/mod.rs deleted file mode 100644 index 4e332dc..0000000 --- a/vendor/blivedm/src/tui/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -// src/tui/mod.rs -//! TUI module for displaying messages and handling user input - -pub mod app; -pub mod event; -pub mod logger; -pub mod ui; - -pub use app::TuiApp; -pub use event::run_tui; -pub use logger::TuiLogger; diff --git a/vendor/blivedm/src/tui/ui.rs b/vendor/blivedm/src/tui/ui.rs deleted file mode 100644 index f528765..0000000 --- a/vendor/blivedm/src/tui/ui.rs +++ /dev/null @@ -1,355 +0,0 @@ -// src/tui/ui.rs -//! UI rendering logic for the TUI - -use crate::tui::app::TuiApp; -use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Style}, - text::{Line, Span}, - widgets::{Block, Borders, Clear, Paragraph, Wrap}, - Frame, -}; -use unicode_width::UnicodeWidthStr; - -pub fn render(f: &mut Frame, app: &mut TuiApp) { - if app.show_logs { - render_logs_panel(f, app, f.area()); - } else { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(90), Constraint::Percentage(10)]) - .split(f.area()); - - render_message_list(f, app, chunks[0]); - render_input_box(f, app, chunks[1]); - } - - if app.show_help { - render_help_overlay(f, app); - } -} - -fn render_message_list(f: &mut Frame, app: &mut TuiApp, area: Rect) { - let messages = app.get_messages(); - let inner_width = area.width.saturating_sub(2) as usize; - let visible_height = area.height.saturating_sub(2) as usize; - let mut all_lines = Vec::new(); - - for msg in &messages { - if !app.show_raw && msg.starts_with("[Raw]") { - continue; - } - - let style = get_message_style(msg); - for line_text in wrap_text(msg, inner_width) { - all_lines.push((line_text, style)); - } - } - - let total_lines = all_lines.len(); - let start_line = if app.auto_scroll { - total_lines.saturating_sub(visible_height) - } else { - total_lines.saturating_sub(visible_height + app.scroll_offset) - }; - - let start_line = app.set_rendered_lines( - all_lines.iter().map(|(text, _)| text.clone()).collect(), - start_line, - visible_height, - ); - - let visible_lines = all_lines - .into_iter() - .enumerate() - .skip(start_line) - .take(visible_height) - .map(|(idx, (line_text, style))| { - Line::from(Span::styled( - line_text, - style_for_line(app, idx, style, Color::Blue), - )) - }) - .collect::>(); - - let scroll_indicator = if app.visual_mode { - "VISUAL | j/k move | g/G jump | y copy | Esc cancel" - } else if app.pane_cursor().is_some() { - "Up/Down history | PgUp/Dn scroll | Ctrl+Y select" - } else if app.auto_scroll { - "Auto-scroll" - } else { - "Paused - PgUp/Dn to scroll" - }; - - let online_count = app.get_online_count(); - let online_display = if online_count > 0 { - format!(" | Online: {}", online_count) - } else { - String::new() - }; - - let raw_indicator = if app.show_raw { "Raw:ON" } else { "Raw:OFF" }; - let title = format!( - " Room {}{} | {} | {} ", - app.room_id, online_display, scroll_indicator, raw_indicator - ); - - let paragraph = Paragraph::new(visible_lines) - .block(Block::default().borders(Borders::ALL).title(title)) - .wrap(Wrap { trim: false }); - - f.render_widget(paragraph, area); -} - -fn get_message_style(msg: &str) -> Style { - if msg.starts_with("[Danmu]") { - Style::default().fg(Color::Cyan) - } else if msg.starts_with("[Gift]") { - Style::default().fg(Color::Yellow) - } else if msg.starts_with("[Raw]") { - Style::default().fg(Color::Magenta) - } else if msg.starts_with("[Unsupported") { - Style::default().fg(Color::DarkGray) - } else if msg.starts_with("[System]") { - Style::default().fg(Color::Green) - } else { - Style::default() - } -} - -fn wrap_text(text: &str, max_width: usize) -> Vec { - if max_width == 0 { - return vec![text.to_string()]; - } - - let mut lines = Vec::new(); - let mut current_line = String::new(); - let mut current_width = 0; - - for ch in text.chars() { - let char_width = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0); - - if current_width + char_width > max_width && !current_line.is_empty() { - lines.push(current_line); - current_line = String::new(); - current_width = 0; - } - - current_line.push(ch); - current_width += char_width; - } - - if !current_line.is_empty() { - lines.push(current_line); - } - - if lines.is_empty() { - lines.push(String::new()); - } - - lines -} - -fn render_logs_panel(f: &mut Frame, app: &mut TuiApp, area: Rect) { - let logs = app.get_log_messages(); - let inner_width = area.width.saturating_sub(2) as usize; - let visible_height = area.height.saturating_sub(2) as usize; - let mut all_lines = Vec::new(); - - for log_msg in &logs { - let style = get_log_style(log_msg); - for line_text in wrap_text(log_msg, inner_width) { - all_lines.push((line_text, style)); - } - } - - let total_lines = all_lines.len(); - let start_line = if app.log_auto_scroll { - total_lines.saturating_sub(visible_height) - } else { - total_lines.saturating_sub(visible_height + app.log_scroll_offset) - }; - - let start_line = app.set_rendered_lines( - all_lines.iter().map(|(text, _)| text.clone()).collect(), - start_line, - visible_height, - ); - - let visible_lines = all_lines - .into_iter() - .enumerate() - .skip(start_line) - .take(visible_height) - .map(|(idx, (line_text, style))| { - Line::from(Span::styled( - line_text, - style_for_line(app, idx, style, Color::LightBlue), - )) - }) - .collect::>(); - - let scroll_indicator = if app.visual_mode { - "VISUAL | j/k move | g/G jump | y copy | Esc cancel" - } else if app.pane_cursor().is_some() { - "CURSOR | Up/Down move | Ctrl+Y visual from cursor" - } else if app.log_auto_scroll { - "Auto-scroll" - } else { - "Paused" - }; - - let title = format!( - " Logs ({} entries) | {} | Ctrl+Y: visual | Ctrl+H: help | Ctrl+L: close ", - logs.len(), - scroll_indicator - ); - - let paragraph = Paragraph::new(visible_lines) - .block( - Block::default() - .borders(Borders::ALL) - .title(title) - .border_style(Style::default().fg(Color::LightBlue)), - ) - .wrap(Wrap { trim: false }); - - f.render_widget(paragraph, area); -} - -fn get_log_style(msg: &str) -> Style { - if msg.contains("[ERROR]") { - Style::default().fg(Color::Red) - } else if msg.contains("[WARN]") { - Style::default().fg(Color::Yellow) - } else if msg.contains("[INFO]") { - Style::default().fg(Color::Green) - } else if msg.contains("[DEBUG]") || msg.contains("[TRACE]") { - Style::default().fg(Color::DarkGray) - } else { - Style::default() - } -} - -fn render_input_box(f: &mut Frame, app: &TuiApp, area: Rect) { - let input_text = format!("> {}", app.input); - - let paragraph = Paragraph::new(input_text.as_str()) - .block( - Block::default() - .borders(Borders::ALL) - .title(" Input (Up/Down: history | Ctrl+Y: visual | Ctrl+H: help | Ctrl+C: exit) ") - .border_style(Style::default().fg(Color::Green)), - ) - .style(Style::default()); - - f.render_widget(paragraph, area); - - let text_before_cursor: String = app.input.chars().take(app.cursor_position).collect(); - let display_width = text_before_cursor.width(); - let cursor_x = area.x + 1 + 2 + display_width as u16; - let cursor_y = area.y + 1; - - if !app.visual_mode && cursor_x < area.x + area.width.saturating_sub(1) { - f.set_cursor_position((cursor_x, cursor_y)); - } -} - -fn render_help_overlay(f: &mut Frame, app: &TuiApp) { - let area = centered_rect(72, 72, f.area()); - let lines = if app.show_logs { - vec![ - Line::from("Key Map"), - Line::from(""), - Line::from("Ctrl+H Toggle this help"), - Line::from("Up/Down Pick start line"), - Line::from("Ctrl+Y Enter visual mode from cursor"), - Line::from("j/k Move visual selection"), - Line::from("g / G Jump to top or bottom"), - Line::from("y Copy selected lines"), - Line::from("Esc Close help, cancel visual, or close logs"), - Line::from("Up/Down Scroll logs normally"), - Line::from("PgUp/Dn Scroll faster"), - Line::from("Home/End Jump to top or bottom"), - Line::from("Ctrl+C Exit app"), - ] - } else { - vec![ - Line::from("Key Map"), - Line::from(""), - Line::from("Enter Send input"), - Line::from("Up/Down Browse sent-input history"), - Line::from("Ctrl+H Toggle this help"), - Line::from("Ctrl+Y Enter visual mode (scroll/select)"), - Line::from("j/k Move visual selection"), - Line::from("g / G Jump to top or bottom"), - Line::from("y Copy selected lines"), - Line::from("Ctrl+R Toggle raw messages"), - Line::from("Ctrl+L Toggle logs panel"), - Line::from("PgUp/Dn Scroll messages"), - Line::from("Left/Right Move input cursor"), - Line::from("Home/End Move input cursor"), - Line::from("Ctrl+Home Jump to top"), - Line::from("Ctrl+End Jump to bottom"), - Line::from("Esc Close help, cancel visual, or quit"), - Line::from("Ctrl+C Exit app"), - ] - }; - - let title = if app.show_logs { - " Help - Logs " - } else { - " Help - Messages " - }; - - let paragraph = Paragraph::new(lines) - .block( - Block::default() - .borders(Borders::ALL) - .title(title) - .border_style(Style::default().fg(Color::Yellow)), - ) - .wrap(Wrap { trim: false }); - - f.render_widget(Clear, area); - f.render_widget(paragraph, area); -} - -fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect { - let vertical = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ]) - .split(area); - - Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ]) - .split(vertical[1])[1] -} - -fn style_for_line(app: &TuiApp, idx: usize, base: Style, cursor_color: Color) -> Style { - if let Some((start, end)) = app.visual_range() { - if Some(idx) == app.visual_cursor() { - return base.bg(cursor_color).fg(Color::Black); - } - - if (start..=end).contains(&idx) { - return base.bg(Color::DarkGray).fg(Color::White); - } - } - - if Some(idx) == app.pane_cursor() { - return base.bg(Color::Rgb(40, 40, 40)).fg(Color::White); - } - - base -}