mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-08 09:06:53 +08:00
feat: 基础示例插件
This commit is contained in:
Generated
+1508
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["basic-example"]
|
||||||
|
resolver = "3"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# 插件系统(v1,实验性)
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> 插件是进程内动态库(`dll` / `so` / `dylib`),与宿主进程同权限运行
|
||||||
|
> 没有沙箱、权限隔离、签名校验,也没有网络或文件系统限制
|
||||||
|
> 插件还可以读取完整的宿主配置(包括 `sessdata`)
|
||||||
|
|
||||||
|
## 当前示例
|
||||||
|
|
||||||
|
- 当前仓库的示例插件只有一个:`basic-example`
|
||||||
|
- 位置:`src-plugin/examples/basic-example`
|
||||||
|
- 用途:演示 Descriptor、Hook 处理、异步逻辑、读取宿主配置、返回修改后的 `payload`
|
||||||
|
|
||||||
|
## 快速构建示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src-plugin/examples
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物位于 `src-plugin/examples/target/release/`,文件名按平台分别是:
|
||||||
|
|
||||||
|
- Windows: `basic_example.dll`
|
||||||
|
- Linux: `libbasic_example.so`
|
||||||
|
- macOS: `libbasic_example.dylib`
|
||||||
|
|
||||||
|
## 如何加载插件
|
||||||
|
|
||||||
|
- 在应用设置页的插件面板点击“添加插件”,选择动态库文件
|
||||||
|
- 后端要求路径必须是绝对路径,且文件必须存在
|
||||||
|
- 成功后会写入 `app_data_dir/plugin.json` 持久化配置
|
||||||
|
- `plugin.json` 保存字段:`path`、`enabled`、`priority`、`descriptor`
|
||||||
|
|
||||||
|
## Descriptor(插件内声明)
|
||||||
|
|
||||||
|
`PluginDescriptorV1` 由插件代码返回,宿主不会直接修改:
|
||||||
|
|
||||||
|
- `sdk_api_version`:SDK API 版本,当前必须等于 `1`
|
||||||
|
- `id`:插件 ID
|
||||||
|
- `name`:展示名称
|
||||||
|
- `version`:插件版本
|
||||||
|
- `hooks`:声明插件希望被调用的 Hook 点列表
|
||||||
|
- `failure_policy`:失败策略,`FailOpen` 或 `FailClosed`
|
||||||
|
- `description`:插件描述
|
||||||
|
|
||||||
|
## 运行顺序与优先级
|
||||||
|
|
||||||
|
- Hook 点相同的插件,宿主按 `priority` 从大到小执行
|
||||||
|
- 每个 Hook 点按顺序串行执行,不是并行
|
||||||
|
- 前一个插件返回后的修改,会成为后一个插件看到的输入
|
||||||
|
|
||||||
|
## Hook 时机(以实际代码为准)
|
||||||
|
|
||||||
|
| HookPoint | 触发位置 |
|
||||||
|
|:---------------------|:----------------------------|
|
||||||
|
| `AfterPrepare` | `prepare()` 成功之后,开始下载前 |
|
||||||
|
| `BeforeVideoProcess` | 视频任务和音频任务结束后,视频处理任务前 |
|
||||||
|
| `OnCompleted` | 所有任务结束后,`completed_ts` 已写入后 |
|
||||||
|
|
||||||
|
三个 Hook 都可读写 `progress`,但修改只有在当前 Hook 返回后才会被宿主应用。
|
||||||
|
|
||||||
|
## 输入输出协议
|
||||||
|
|
||||||
|
- 输入:`HookInputV1 { hook_point, payload, readonly_meta }`
|
||||||
|
- 输出:`HookOutputV1 { payload }`
|
||||||
|
- `payload` 是枚举 `HookPayloadV1`,必须与 `hook_point` 匹配
|
||||||
|
- 不匹配会被判定为插件输出无效,再按失败策略处理
|
||||||
|
- `readonly_meta` 包含 `app_version`、`os`、`arch`、`process_id`
|
||||||
|
|
||||||
|
## 可修改范围与约束
|
||||||
|
|
||||||
|
- `payload.progress` 大多数字段都可被插件修改
|
||||||
|
- `task_id` 明确禁止修改,改动会被宿主拒绝
|
||||||
|
- 宿主没有提供修改Config的 Host API
|
||||||
|
|
||||||
|
## 失败策略
|
||||||
|
|
||||||
|
- `FailOpen`:插件出错时记录日志,继续执行后续流程
|
||||||
|
- `FailClosed`:插件出错时中断当前下载流程并返回错误
|
||||||
|
- 插件返回 `Err(...)` 时,宿主会调用 `bilibili_video_downloader_plugin_last_error_v1` 读取错误文本
|
||||||
|
|
||||||
|
## Host API(当前仅 v1)
|
||||||
|
|
||||||
|
插件可在 `on_hook` 中调用:
|
||||||
|
|
||||||
|
- `host::get_config()`:读取宿主配置快照(`HostConfigV1`)
|
||||||
|
|
||||||
|
注意:
|
||||||
|
|
||||||
|
- 该配置是只读快照
|
||||||
|
- 返回内容包含敏感信息(例如 `sessdata`)
|
||||||
|
|
||||||
|
## SDK 入口(插件侧)
|
||||||
|
|
||||||
|
- 实现 trait:`PluginV1`
|
||||||
|
- 使用宏导出:`export_plugin_v1!(YourPluginType)`
|
||||||
|
- 插件类型需要满足:`Default + Send + 'static`
|
||||||
|
|
||||||
|
常规最小结构:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use bilibili_video_downloader_plugin_sdk::{
|
||||||
|
HookInputV1, HookOutputV1, PluginDescriptorV1, PluginV1, export_plugin_v1, eyre
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MyPlugin;
|
||||||
|
|
||||||
|
impl PluginV1 for MyPlugin {
|
||||||
|
fn descriptor(&self) -> PluginDescriptorV1 {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_hook(&mut self, input: HookInputV1) -> eyre::Result<HookOutputV1> {
|
||||||
|
let _ = input;
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export_plugin_v1!(MyPlugin);
|
||||||
|
```
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "basic-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bilibili-video-downloader-plugin-sdk = { path = "../../plugin-sdk" }
|
||||||
|
|
||||||
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
|
reqwest = { version = "0.13.2", default-features = false, features = ["native-tls", "system-proxy"] }
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
use bilibili_video_downloader_plugin_sdk::{
|
||||||
|
AfterPreparePayloadV1, BeforeVideoProcessPayloadV1, HookInputV1, HookOutputV1, HookPayloadV1,
|
||||||
|
HookPointV1, OnCompletedPayloadV1, PluginDescriptorV1, PluginFailurePolicy, PluginV1,
|
||||||
|
SDK_API_VERSION, export_plugin_v1,
|
||||||
|
eyre::{self, eyre},
|
||||||
|
host,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct BasicExamplePlugin;
|
||||||
|
|
||||||
|
impl PluginV1 for BasicExamplePlugin {
|
||||||
|
fn descriptor(&self) -> PluginDescriptorV1 {
|
||||||
|
PluginDescriptorV1 {
|
||||||
|
sdk_api_version: SDK_API_VERSION,
|
||||||
|
id: "basic-example".to_string(),
|
||||||
|
name: "Basic Example".to_string(),
|
||||||
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
|
hooks: vec![HookPointV1::BeforeVideoProcess, HookPointV1::AfterPrepare],
|
||||||
|
failure_policy: PluginFailurePolicy::FailOpen,
|
||||||
|
description: "基础示例插件:演示推荐的代码结构、在 Hook 中执行异步任务、读取宿主配置、处理 HookPayload,并修改 DownloadProgress".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_hook(&mut self, input: HookInputV1) -> eyre::Result<HookOutputV1> {
|
||||||
|
// 如果你需要用到异步,可以这样在同步 Hook 入口中创建 Tokio 运行时
|
||||||
|
// 然后让代码在异步运行时里执行
|
||||||
|
let output = tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?
|
||||||
|
.block_on(async { main(input).await })?;
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这并不是一个真的main函数,叫main只是为了方便理解
|
||||||
|
// 推荐将主要逻辑集中在此函数中,减少 on_hook 内部的嵌套层级
|
||||||
|
async fn main(input: HookInputV1) -> eyre::Result<HookOutputV1> {
|
||||||
|
// 示例:读取宿主当前配置。
|
||||||
|
let host_config = host::get_config()?;
|
||||||
|
println!("{}", host_config.dir_fmt);
|
||||||
|
|
||||||
|
// 示例:发起一次 HTTP 请求。
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let body = client
|
||||||
|
.get("https://jsonplaceholder.typicode.com/todos/1")
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("HTTP 请求结果:{body}");
|
||||||
|
|
||||||
|
let payload = match input.payload {
|
||||||
|
HookPayloadV1::BeforeVideoProcess(payload) => handle_before_video_process(payload),
|
||||||
|
HookPayloadV1::AfterPrepare(payload) => handle_after_prepare(payload),
|
||||||
|
HookPayloadV1::OnCompleted(payload) => handle_on_completed(payload),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
// 插件需要返回 payload,宿主会根据该返回值回写并更新自身状态
|
||||||
|
// 所以插件内对 payload 的改动不会实时生效,只有当前 Hook 返回后,宿主才会应用这些修改
|
||||||
|
Ok(HookOutputV1 { payload })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
|
fn handle_before_video_process(
|
||||||
|
mut payload: BeforeVideoProcessPayloadV1,
|
||||||
|
) -> eyre::Result<HookPayloadV1> {
|
||||||
|
println!("===========================BeforeVideoProcess========================");
|
||||||
|
payload.progress.episode_title = "BeforeVideoProcess 修改了标题".to_string();
|
||||||
|
Ok(HookPayloadV1::BeforeVideoProcess(
|
||||||
|
BeforeVideoProcessPayloadV1 {
|
||||||
|
progress: payload.progress,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
|
fn handle_after_prepare(mut payload: AfterPreparePayloadV1) -> eyre::Result<HookPayloadV1> {
|
||||||
|
println!("===========================AfterPrepare========================");
|
||||||
|
payload.progress.episode_title = "AfterPrepare 修改了标题".to_string();
|
||||||
|
Ok(HookPayloadV1::AfterPrepare(AfterPreparePayloadV1 {
|
||||||
|
progress: payload.progress,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_on_completed(mut _payload: OnCompletedPayloadV1) -> eyre::Result<HookPayloadV1> {
|
||||||
|
Err(eyre!(
|
||||||
|
"插件未声明 OnCompleted HookPoint,按预期不应进入此分支。"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 别把这行忘了
|
||||||
|
export_plugin_v1!(BasicExamplePlugin);
|
||||||
Reference in New Issue
Block a user