mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-05-06 20:02:57 +08:00
feat: 进程内动态库插件系统
This commit is contained in:
@@ -7,6 +7,7 @@ dist-ssr/
|
|||||||
|
|
||||||
# Rust/Tauri backend
|
# Rust/Tauri backend
|
||||||
src-tauri/
|
src-tauri/
|
||||||
|
src-plugin/
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
3
src-plugin/.gitignore
vendored
Normal file
3
src-plugin/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
target/
|
||||||
215
src-plugin/Cargo.lock
generated
Normal file
215
src-plugin/Cargo.lock
generated
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bilibili-video-downloader-plugin-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bilibili-video-downloader-plugin-sdk"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"bilibili-video-downloader-plugin-api",
|
||||||
|
"eyre",
|
||||||
|
"parking_lot",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "eyre"
|
||||||
|
version = "0.6.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
|
||||||
|
dependencies = [
|
||||||
|
"indenter",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indenter"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.182"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[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 = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[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.149"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
3
src-plugin/Cargo.toml
Normal file
3
src-plugin/Cargo.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["plugin-api", "plugin-sdk"]
|
||||||
|
resolver = "3"
|
||||||
10
src-plugin/plugin-api/Cargo.toml
Normal file
10
src-plugin/plugin-api/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "bilibili-video-downloader-plugin-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "bilibili_video_downloader_plugin_api"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
349
src-plugin/plugin-api/src/lib.rs
Normal file
349
src-plugin/plugin-api/src/lib.rs
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
pub const SDK_API_VERSION_V1: u32 = 1;
|
||||||
|
|
||||||
|
pub mod v1 {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum HookPointV1 {
|
||||||
|
AfterPrepare,
|
||||||
|
BeforeVideoProcess,
|
||||||
|
OnCompleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum PluginFailurePolicy {
|
||||||
|
FailOpen,
|
||||||
|
FailClosed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct PluginDescriptorV1 {
|
||||||
|
pub sdk_api_version: u32,
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub hooks: Vec<HookPointV1>,
|
||||||
|
pub failure_policy: PluginFailurePolicy,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct HookReadonlyMetaV1 {
|
||||||
|
pub app_version: String,
|
||||||
|
pub os: String,
|
||||||
|
pub arch: String,
|
||||||
|
pub process_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct BeforeVideoProcessPayloadV1 {
|
||||||
|
pub progress: DownloadProgressV1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct AfterPreparePayloadV1 {
|
||||||
|
pub progress: DownloadProgressV1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct OnCompletedPayloadV1 {
|
||||||
|
pub progress: DownloadProgressV1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum HookPayloadV1 {
|
||||||
|
BeforeVideoProcess(BeforeVideoProcessPayloadV1),
|
||||||
|
AfterPrepare(AfterPreparePayloadV1),
|
||||||
|
OnCompleted(OnCompletedPayloadV1),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HookPayloadV1 {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::BeforeVideoProcess(BeforeVideoProcessPayloadV1::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct HookInputV1 {
|
||||||
|
pub hook_point: HookPointV1,
|
||||||
|
pub payload: HookPayloadV1,
|
||||||
|
pub readonly_meta: HookReadonlyMetaV1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct HookOutputV1 {
|
||||||
|
pub payload: HookPayloadV1,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type HostApiGetConfigJsonV1 =
|
||||||
|
unsafe extern "C" fn(out_ptr: *mut *mut u8, out_len: *mut usize) -> i32;
|
||||||
|
pub type HostApiFreeBufferV1 = unsafe extern "C" fn(ptr: *mut u8, len: usize);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct HostApiV1 {
|
||||||
|
pub get_config_json: HostApiGetConfigJsonV1,
|
||||||
|
pub free_buffer: HostApiFreeBufferV1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum ProxyModeV1 {
|
||||||
|
#[default]
|
||||||
|
NoProxy,
|
||||||
|
System,
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum FileExistActionV1 {
|
||||||
|
#[default]
|
||||||
|
Overwrite,
|
||||||
|
Skip,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct CanvasConfigV1 {
|
||||||
|
pub duration: f64,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub font: String,
|
||||||
|
pub font_size: u32,
|
||||||
|
pub width_ratio: f64,
|
||||||
|
pub horizontal_gap: f64,
|
||||||
|
pub lane_size: u32,
|
||||||
|
pub float_percentage: f64,
|
||||||
|
pub alpha: f64,
|
||||||
|
pub bold: bool,
|
||||||
|
pub outline: f64,
|
||||||
|
pub time_offset: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
#[allow(clippy::struct_field_names)]
|
||||||
|
pub struct HostConfigV1 {
|
||||||
|
pub download_dir: PathBuf,
|
||||||
|
pub enable_file_logger: bool,
|
||||||
|
pub sessdata: String,
|
||||||
|
pub video_quality_priority: Vec<VideoQualityV1>,
|
||||||
|
pub codec_type_priority: Vec<CodecTypeV1>,
|
||||||
|
pub audio_quality_priority: Vec<AudioQualityV1>,
|
||||||
|
pub download_video: bool,
|
||||||
|
pub download_audio: bool,
|
||||||
|
pub auto_merge: bool,
|
||||||
|
pub embed_chapter: bool,
|
||||||
|
pub embed_skip: bool,
|
||||||
|
pub download_xml_danmaku: bool,
|
||||||
|
pub download_ass_danmaku: bool,
|
||||||
|
pub download_json_danmaku: bool,
|
||||||
|
pub download_subtitle: bool,
|
||||||
|
pub download_cover: bool,
|
||||||
|
pub download_nfo: bool,
|
||||||
|
pub download_json: bool,
|
||||||
|
pub dir_fmt: String,
|
||||||
|
pub dir_fmt_for_part: String,
|
||||||
|
pub time_fmt: String,
|
||||||
|
pub proxy_mode: ProxyModeV1,
|
||||||
|
pub proxy_host: String,
|
||||||
|
pub proxy_port: u16,
|
||||||
|
pub task_concurrency: usize,
|
||||||
|
pub task_download_interval_sec: u64,
|
||||||
|
pub chunk_concurrency: usize,
|
||||||
|
pub chunk_download_interval_sec: u64,
|
||||||
|
pub danmaku_config: CanvasConfigV1,
|
||||||
|
pub file_exist_action: FileExistActionV1,
|
||||||
|
pub auto_start_download_task: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum EpisodeTypeV1 {
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
Bangumi,
|
||||||
|
Cheese,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[repr(i64)]
|
||||||
|
pub enum VideoQualityV1 {
|
||||||
|
#[default]
|
||||||
|
Unknown = -1,
|
||||||
|
#[serde(rename = "240P")]
|
||||||
|
Video240P = 6,
|
||||||
|
#[serde(rename = "360P")]
|
||||||
|
Video360P = 16,
|
||||||
|
#[serde(rename = "480P")]
|
||||||
|
Video480P = 32,
|
||||||
|
#[serde(rename = "720P")]
|
||||||
|
Video720P = 64,
|
||||||
|
#[serde(rename = "720P60")]
|
||||||
|
Video720P60 = 74,
|
||||||
|
#[serde(rename = "1080P")]
|
||||||
|
Video1080P = 80,
|
||||||
|
#[serde(rename = "AiRepair")]
|
||||||
|
VideoAiRepair = 100,
|
||||||
|
#[serde(rename = "1080P+")]
|
||||||
|
Video1080PPlus = 112,
|
||||||
|
#[serde(rename = "1080P60")]
|
||||||
|
Video1080P60 = 116,
|
||||||
|
#[serde(rename = "4K")]
|
||||||
|
Video4K = 120,
|
||||||
|
#[serde(rename = "HDR")]
|
||||||
|
VideoHDR = 125,
|
||||||
|
#[serde(rename = "Dolby")]
|
||||||
|
VideoDolby = 126,
|
||||||
|
#[serde(rename = "8K")]
|
||||||
|
Video8K = 127,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[repr(i64)]
|
||||||
|
pub enum AudioQualityV1 {
|
||||||
|
#[default]
|
||||||
|
Unknown = -1,
|
||||||
|
#[serde(rename = "64K")]
|
||||||
|
Audio64K = 30216,
|
||||||
|
#[serde(rename = "132K")]
|
||||||
|
Audio132K = 30232,
|
||||||
|
#[serde(rename = "192K")]
|
||||||
|
Audio192K = 30280,
|
||||||
|
#[serde(rename = "Dolby")]
|
||||||
|
AudioDolby = 30250,
|
||||||
|
#[serde(rename = "HiRes")]
|
||||||
|
AudioHiRes = 30251,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[repr(i64)]
|
||||||
|
pub enum CodecTypeV1 {
|
||||||
|
#[default]
|
||||||
|
Unknown = -1,
|
||||||
|
Audio = 0,
|
||||||
|
AVC = 7,
|
||||||
|
HEVC = 12,
|
||||||
|
AV1 = 13,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct MediaChunkV1 {
|
||||||
|
pub start: u64,
|
||||||
|
pub end: u64,
|
||||||
|
pub completed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct VideoTaskV1 {
|
||||||
|
pub selected: bool,
|
||||||
|
pub url: String,
|
||||||
|
pub video_quality: VideoQualityV1,
|
||||||
|
pub codec_type: CodecTypeV1,
|
||||||
|
pub content_length: u64,
|
||||||
|
pub chunks: Vec<MediaChunkV1>,
|
||||||
|
pub completed: bool,
|
||||||
|
pub skipped: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct AudioTaskV1 {
|
||||||
|
pub selected: bool,
|
||||||
|
pub url: String,
|
||||||
|
pub audio_quality: AudioQualityV1,
|
||||||
|
pub content_length: u64,
|
||||||
|
pub chunks: Vec<MediaChunkV1>,
|
||||||
|
pub completed: bool,
|
||||||
|
pub skipped: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
pub struct VideoProcessTaskV1 {
|
||||||
|
pub merge_selected: bool,
|
||||||
|
pub embed_chapter_selected: bool,
|
||||||
|
pub embed_skip_selected: bool,
|
||||||
|
pub completed: bool,
|
||||||
|
pub skipped: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct SubtitleTaskV1 {
|
||||||
|
pub selected: bool,
|
||||||
|
pub completed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
pub struct DanmakuTaskV1 {
|
||||||
|
pub xml_selected: bool,
|
||||||
|
pub ass_selected: bool,
|
||||||
|
pub json_selected: bool,
|
||||||
|
pub completed: bool,
|
||||||
|
pub skipped: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct CoverTaskV1 {
|
||||||
|
pub selected: bool,
|
||||||
|
pub url: String,
|
||||||
|
pub completed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct NfoTaskV1 {
|
||||||
|
pub selected: bool,
|
||||||
|
pub completed: bool,
|
||||||
|
pub skipped: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct JsonTaskV1 {
|
||||||
|
pub selected: bool,
|
||||||
|
pub completed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct DownloadProgressV1 {
|
||||||
|
pub task_id: String,
|
||||||
|
pub episode_type: EpisodeTypeV1,
|
||||||
|
pub aid: i64,
|
||||||
|
pub bvid: Option<String>,
|
||||||
|
pub cid: i64,
|
||||||
|
pub ep_id: Option<i64>,
|
||||||
|
pub duration: u64,
|
||||||
|
pub pub_ts: i64,
|
||||||
|
pub collection_title: String,
|
||||||
|
pub part_title: Option<String>,
|
||||||
|
pub part_order: Option<i64>,
|
||||||
|
pub episode_title: String,
|
||||||
|
pub episode_order: i64,
|
||||||
|
pub up_name: Option<String>,
|
||||||
|
pub up_uid: Option<i64>,
|
||||||
|
pub up_avatar: Option<String>,
|
||||||
|
pub episode_dir: PathBuf,
|
||||||
|
pub filename: String,
|
||||||
|
pub video_task: VideoTaskV1,
|
||||||
|
pub audio_task: AudioTaskV1,
|
||||||
|
pub video_process_task: VideoProcessTaskV1,
|
||||||
|
pub subtitle_task: SubtitleTaskV1,
|
||||||
|
pub danmaku_task: DanmakuTaskV1,
|
||||||
|
pub cover_task: CoverTaskV1,
|
||||||
|
pub nfo_task: NfoTaskV1,
|
||||||
|
pub json_task: JsonTaskV1,
|
||||||
|
pub create_ts: u64,
|
||||||
|
pub completed_ts: Option<u64>,
|
||||||
|
pub is_drm: bool,
|
||||||
|
pub is_preview: bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src-plugin/plugin-sdk/Cargo.toml
Normal file
14
src-plugin/plugin-sdk/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "bilibili-video-downloader-plugin-sdk"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "bilibili_video_downloader_plugin_sdk"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bilibili-video-downloader-plugin-api = { path = "../plugin-api" }
|
||||||
|
|
||||||
|
eyre = { version = "0.6.12" }
|
||||||
|
serde_json = { version = "1" }
|
||||||
|
parking_lot = { version = "0.12.5" }
|
||||||
191
src-plugin/plugin-sdk/src/lib.rs
Normal file
191
src-plugin/plugin-sdk/src/lib.rs
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
pub use bilibili_video_downloader_plugin_api::SDK_API_VERSION_V1 as SDK_API_VERSION;
|
||||||
|
pub use bilibili_video_downloader_plugin_api::v1::{
|
||||||
|
AfterPreparePayloadV1, BeforeVideoProcessPayloadV1, CanvasConfigV1, DownloadProgressV1,
|
||||||
|
FileExistActionV1, HookInputV1, HookOutputV1, HookPayloadV1, HookPointV1, HostApiV1,
|
||||||
|
HostConfigV1, OnCompletedPayloadV1, PluginDescriptorV1, PluginFailurePolicy, ProxyModeV1,
|
||||||
|
};
|
||||||
|
pub use eyre;
|
||||||
|
pub use parking_lot;
|
||||||
|
pub use serde_json;
|
||||||
|
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
|
pub trait PluginV1: Default + Send + 'static {
|
||||||
|
fn descriptor(&self) -> PluginDescriptorV1;
|
||||||
|
#[allow(clippy::missing_errors_doc)]
|
||||||
|
fn on_hook(&mut self, input: HookInputV1) -> eyre::Result<HookOutputV1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
static HOST_API_V1: LazyLock<Mutex<Option<HostApiV1>>> = LazyLock::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub unsafe fn register_host_api_v1(api_ptr: *const HostApiV1) -> i32 {
|
||||||
|
if api_ptr.is_null() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let api = unsafe { *api_ptr };
|
||||||
|
*HOST_API_V1.lock() = Some(api);
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_host_api_v1() -> eyre::Result<HostApiV1> {
|
||||||
|
HOST_API_V1
|
||||||
|
.lock()
|
||||||
|
.as_ref()
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| eyre::eyre!("host api 未注册"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod host {
|
||||||
|
use crate::HostConfigV1;
|
||||||
|
|
||||||
|
#[allow(clippy::missing_errors_doc)]
|
||||||
|
pub fn get_config() -> eyre::Result<HostConfigV1> {
|
||||||
|
let host_api = crate::get_host_api_v1()?;
|
||||||
|
|
||||||
|
let mut output_ptr: *mut u8 = std::ptr::null_mut();
|
||||||
|
let mut output_len: usize = 0;
|
||||||
|
let rc = unsafe { (host_api.get_config_json)(&raw mut output_ptr, &raw mut output_len) };
|
||||||
|
if rc != 0 {
|
||||||
|
return Err(eyre::eyre!("host get_config_json 调用失败: rc={rc}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if output_ptr.is_null() {
|
||||||
|
return Err(eyre::eyre!("host get_config_json 返回的缓冲区为空指针"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_bytes = unsafe { std::slice::from_raw_parts(output_ptr, output_len) }.to_vec();
|
||||||
|
unsafe {
|
||||||
|
(host_api.free_buffer)(output_ptr, output_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
let host_config = serde_json::from_slice::<HostConfigV1>(&output_bytes)?;
|
||||||
|
|
||||||
|
Ok(host_config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! export_plugin_v1 {
|
||||||
|
($ty:ty) => {
|
||||||
|
use std::ffi::{CString, c_char};
|
||||||
|
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
use $crate::parking_lot::Mutex;
|
||||||
|
|
||||||
|
fn to_cstring_lossy(value: String) -> CString {
|
||||||
|
// Replacing NUL guarantees CString invariants and avoids fallible construction.
|
||||||
|
let sanitized = value.replace('\0', " ");
|
||||||
|
unsafe { CString::from_vec_unchecked(sanitized.into_bytes()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
static INSTANCE_V1: LazyLock<Mutex<$ty>> = LazyLock::new(|| Mutex::new(<$ty>::default()));
|
||||||
|
static DESCRIPTOR_JSON_V1: LazyLock<CString> = LazyLock::new(|| {
|
||||||
|
let instance = INSTANCE_V1.lock();
|
||||||
|
let descriptor = instance.descriptor();
|
||||||
|
let descriptor_json = match $crate::serde_json::to_string(&descriptor) {
|
||||||
|
Ok(json) => json,
|
||||||
|
Err(err) => format!("{{\"error\":\"序列化 descriptor 失败: {err}\"}}"),
|
||||||
|
};
|
||||||
|
to_cstring_lossy(descriptor_json)
|
||||||
|
});
|
||||||
|
static LAST_ERROR_V1: LazyLock<Mutex<CString>> =
|
||||||
|
LazyLock::new(|| Mutex::new(to_cstring_lossy(String::new())));
|
||||||
|
|
||||||
|
fn set_last_error_v1(message: String) {
|
||||||
|
let mut guard = LAST_ERROR_V1.lock();
|
||||||
|
*guard = to_cstring_lossy(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(export_name = "bilibili_video_downloader_plugin_descriptor_v1")]
|
||||||
|
pub extern "C" fn descriptor_v1() -> *const c_char {
|
||||||
|
DESCRIPTOR_JSON_V1.as_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(export_name = "bilibili_video_downloader_plugin_last_error_v1")]
|
||||||
|
pub extern "C" fn last_error_v1() -> *const c_char {
|
||||||
|
LAST_ERROR_V1.lock().as_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(export_name = "bilibili_video_downloader_plugin_set_host_api_v1")]
|
||||||
|
pub unsafe extern "C" fn set_host_api_v1(api: *const $crate::HostApiV1) -> i32 {
|
||||||
|
let rc = unsafe { $crate::register_host_api_v1(api) };
|
||||||
|
if rc != 0 {
|
||||||
|
set_last_error_v1("无效的 host api 指针".to_string());
|
||||||
|
}
|
||||||
|
rc
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(export_name = "bilibili_video_downloader_plugin_on_hook_v1")]
|
||||||
|
pub unsafe extern "C" fn on_hook_v1(
|
||||||
|
input_ptr: *const u8,
|
||||||
|
input_len: usize,
|
||||||
|
out_ptr: *mut *mut u8,
|
||||||
|
out_len: *mut usize,
|
||||||
|
) -> i32 {
|
||||||
|
if input_ptr.is_null() || out_ptr.is_null() || out_len.is_null() {
|
||||||
|
set_last_error_v1("参数里有空指针".to_string());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_slice = unsafe { std::slice::from_raw_parts(input_ptr, input_len) };
|
||||||
|
let hook_input: $crate::HookInputV1 = match $crate::serde_json::from_slice(input_slice)
|
||||||
|
{
|
||||||
|
Ok(input) => input,
|
||||||
|
Err(err) => {
|
||||||
|
set_last_error_v1(format!("解析 hook 输入失败: {err}"));
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let hook_output = match catch_unwind(AssertUnwindSafe(|| {
|
||||||
|
let mut plugin = INSTANCE_V1.lock();
|
||||||
|
plugin.on_hook(hook_input)
|
||||||
|
})) {
|
||||||
|
Ok(Ok(output)) => output,
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
set_last_error_v1(format!("{err:?}"));
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
set_last_error_v1("处理 on_hook 时插件内部发生 panic".to_string());
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let output_bytes = match $crate::serde_json::to_vec(&hook_output) {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(err) => {
|
||||||
|
set_last_error_v1(format!("序列化 hook 输出失败: {err}"));
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let boxed = output_bytes.into_boxed_slice();
|
||||||
|
let len = boxed.len();
|
||||||
|
let ptr = Box::into_raw(boxed) as *mut u8;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
*out_ptr = ptr;
|
||||||
|
*out_len = len;
|
||||||
|
}
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(export_name = "bilibili_video_downloader_plugin_free_buffer_v1")]
|
||||||
|
pub unsafe extern "C" fn free_buffer_v1(ptr: *mut u8, len: usize) {
|
||||||
|
if ptr.is_null() || len == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let raw_slice = std::ptr::slice_from_raw_parts_mut(ptr, len);
|
||||||
|
unsafe {
|
||||||
|
drop(Box::from_raw(raw_slice));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
54
src-plugin/plugin-sdk/tests/macro_smoke.rs
Normal file
54
src-plugin/plugin-sdk/tests/macro_smoke.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use bilibili_video_downloader_plugin_sdk::{
|
||||||
|
AfterPreparePayloadV1, BeforeVideoProcessPayloadV1, HookInputV1, HookOutputV1, HookPayloadV1,
|
||||||
|
HookPointV1, OnCompletedPayloadV1, PluginDescriptorV1, PluginFailurePolicy, PluginV1,
|
||||||
|
SDK_API_VERSION, export_plugin_v1, eyre, host,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MacroSmokePlugin;
|
||||||
|
|
||||||
|
impl PluginV1 for MacroSmokePlugin {
|
||||||
|
fn descriptor(&self) -> PluginDescriptorV1 {
|
||||||
|
PluginDescriptorV1 {
|
||||||
|
sdk_api_version: SDK_API_VERSION,
|
||||||
|
id: "macro-smoke".to_string(),
|
||||||
|
name: "Macro Smoke".to_string(),
|
||||||
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
|
hooks: vec![HookPointV1::BeforeVideoProcess],
|
||||||
|
failure_policy: PluginFailurePolicy::FailOpen,
|
||||||
|
description: "Compile-time macro smoke test".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_hook(&mut self, input: HookInputV1) -> eyre::Result<HookOutputV1> {
|
||||||
|
let payload = match input.payload {
|
||||||
|
HookPayloadV1::BeforeVideoProcess(payload) => {
|
||||||
|
HookPayloadV1::BeforeVideoProcess(BeforeVideoProcessPayloadV1 {
|
||||||
|
progress: payload.progress,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
HookPayloadV1::AfterPrepare(payload) => {
|
||||||
|
HookPayloadV1::AfterPrepare(AfterPreparePayloadV1 {
|
||||||
|
progress: payload.progress,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
HookPayloadV1::OnCompleted(payload) => {
|
||||||
|
HookPayloadV1::OnCompleted(OnCompletedPayloadV1 {
|
||||||
|
progress: payload.progress,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(HookOutputV1 { payload })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export_plugin_v1!(MacroSmokePlugin);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn macro_smoke_builds() {
|
||||||
|
assert_eq!(SDK_API_VERSION, 1);
|
||||||
|
|
||||||
|
let err = host::get_config().unwrap_err();
|
||||||
|
assert!(err.to_string().contains("host api 未注册"));
|
||||||
|
}
|
||||||
23
src-tauri/Cargo.lock
generated
23
src-tauri/Cargo.lock
generated
@@ -286,9 +286,11 @@ name = "bilibili-video-downloader"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
"bilibili-video-downloader-plugin-api",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"dlopen2 0.8.2",
|
||||||
"eyre",
|
"eyre",
|
||||||
"float-ord",
|
"float-ord",
|
||||||
"fs4",
|
"fs4",
|
||||||
@@ -323,6 +325,13 @@ dependencies = [
|
|||||||
"yaserde",
|
"yaserde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bilibili-video-downloader-plugin-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -861,6 +870,18 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dlopen2"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4"
|
||||||
|
dependencies = [
|
||||||
|
"dlopen2_derive",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dlopen2_derive"
|
name = "dlopen2_derive"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -4186,7 +4207,7 @@ dependencies = [
|
|||||||
"core-graphics",
|
"core-graphics",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dispatch",
|
"dispatch",
|
||||||
"dlopen2",
|
"dlopen2 0.7.0",
|
||||||
"dpi",
|
"dpi",
|
||||||
"gdkwayland-sys",
|
"gdkwayland-sys",
|
||||||
"gdkx11-sys",
|
"gdkx11-sys",
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ tauri = { version = "2", features = [] }
|
|||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
tauri-plugin-os = "2"
|
tauri-plugin-os = "2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
|
dlopen2 = { version = "0.8.2" }
|
||||||
|
bilibili-video-downloader-plugin-api = { path = "../src-plugin/plugin-api" }
|
||||||
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -63,4 +65,3 @@ strip = true
|
|||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ use crate::{
|
|||||||
history_info::HistoryInfo,
|
history_info::HistoryInfo,
|
||||||
log_metadata::LogMetadata,
|
log_metadata::LogMetadata,
|
||||||
normal_info::NormalInfo,
|
normal_info::NormalInfo,
|
||||||
|
plugin_info::PluginInfo,
|
||||||
qrcode_data::QrcodeData,
|
qrcode_data::QrcodeData,
|
||||||
qrcode_status::QrcodeStatus,
|
qrcode_status::QrcodeStatus,
|
||||||
restart_download_task_params::RestartDownloadTaskParams,
|
restart_download_task_params::RestartDownloadTaskParams,
|
||||||
@@ -64,15 +65,16 @@ pub fn save_config(app: AppHandle, config: Config) -> CommandResult<()> {
|
|||||||
let bili_client = app.get_bili_client();
|
let bili_client = app.get_bili_client();
|
||||||
let config_state = app.get_config();
|
let config_state = app.get_config();
|
||||||
|
|
||||||
let proxy_changed = {
|
|
||||||
let config_state = config_state.read();
|
|
||||||
config_state.proxy_mode != config.proxy_mode
|
|
||||||
|| config_state.proxy_host != config.proxy_host
|
|
||||||
|| config_state.proxy_port != config.proxy_port
|
|
||||||
};
|
|
||||||
|
|
||||||
let enable_file_logger = config.enable_file_logger;
|
let enable_file_logger = config.enable_file_logger;
|
||||||
let file_logger_changed = config_state.read().enable_file_logger != enable_file_logger;
|
let (proxy_changed, file_logger_changed) = {
|
||||||
|
let current_config = config_state.read();
|
||||||
|
(
|
||||||
|
current_config.proxy_mode != config.proxy_mode
|
||||||
|
|| current_config.proxy_host != config.proxy_host
|
||||||
|
|| current_config.proxy_port != config.proxy_port,
|
||||||
|
current_config.enable_file_logger != enable_file_logger,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
// 包裹在大括号中,以便自动释放写锁
|
// 包裹在大括号中,以便自动释放写锁
|
||||||
@@ -506,3 +508,71 @@ pub fn open_log_file(path: &str) -> CommandResult<Vec<LogMetadata>> {
|
|||||||
|
|
||||||
Ok(logs)
|
Ok(logs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
#[tauri::command(async)]
|
||||||
|
#[specta::specta]
|
||||||
|
#[instrument(level = "error", skip_all)]
|
||||||
|
pub fn get_plugin_infos(app: AppHandle) -> Vec<PluginInfo> {
|
||||||
|
app.get_plugin_manager().get_plugin_infos()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
#[tauri::command(async)]
|
||||||
|
#[specta::specta]
|
||||||
|
#[instrument(level = "error", skip_all, fields(plugin_path = plugin_path))]
|
||||||
|
pub fn add_plugin(app: AppHandle, plugin_path: String) -> CommandResult<()> {
|
||||||
|
let plugin_manager = app.get_plugin_manager();
|
||||||
|
|
||||||
|
plugin_manager
|
||||||
|
.add_plugin(&plugin_path)
|
||||||
|
.map_err(|err| CommandError::from("加载插件失败", err))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
#[tauri::command(async)]
|
||||||
|
#[specta::specta]
|
||||||
|
#[instrument(level = "error", skip_all, fields(plugin_path = plugin_path))]
|
||||||
|
pub fn uninstall_plugin(app: AppHandle, plugin_path: String) -> CommandResult<()> {
|
||||||
|
let plugin_manager = app.get_plugin_manager();
|
||||||
|
|
||||||
|
plugin_manager
|
||||||
|
.uninstall_plugin(&plugin_path)
|
||||||
|
.map_err(|err| CommandError::from("卸载插件失败", err))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
#[tauri::command(async)]
|
||||||
|
#[specta::specta]
|
||||||
|
#[instrument(level = "error", skip_all, fields(plugin_path = plugin_path, enabled = enabled))]
|
||||||
|
pub fn set_plugin_enabled(app: AppHandle, plugin_path: String, enabled: bool) -> CommandResult<()> {
|
||||||
|
let plugin_manager = app.get_plugin_manager();
|
||||||
|
|
||||||
|
plugin_manager
|
||||||
|
.set_plugin_enabled(&plugin_path, enabled)
|
||||||
|
.map_err(|err| CommandError::from("设置插件启用状态失败", err))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
#[tauri::command(async)]
|
||||||
|
#[specta::specta]
|
||||||
|
#[instrument(level = "error", skip_all, fields(plugin_path = plugin_path, priority = priority))]
|
||||||
|
pub fn set_plugin_priority(
|
||||||
|
app: AppHandle,
|
||||||
|
plugin_path: String,
|
||||||
|
priority: i32,
|
||||||
|
) -> CommandResult<()> {
|
||||||
|
let plugin_manager = app.get_plugin_manager();
|
||||||
|
|
||||||
|
plugin_manager
|
||||||
|
.set_plugin_priority(&plugin_path, priority)
|
||||||
|
.map_err(|err| CommandError::from("设置插件优先级失败", err))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ use crate::{
|
|||||||
},
|
},
|
||||||
events::DownloadEvent,
|
events::DownloadEvent,
|
||||||
extensions::AppHandleExt,
|
extensions::AppHandleExt,
|
||||||
|
plugin::hook_context::{
|
||||||
|
AfterPrepareContext, BeforeVideoProcessContext, HookContext, OnCompletedContext,
|
||||||
|
},
|
||||||
types::{
|
types::{
|
||||||
audio_quality::AudioQuality,
|
audio_quality::AudioQuality,
|
||||||
bangumi_info::BangumiInfo,
|
bangumi_info::BangumiInfo,
|
||||||
@@ -200,15 +203,23 @@ impl DownloadProgress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "error", skip_all)]
|
#[instrument(level = "error", skip_all)]
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
pub async fn process(&mut self, download_task: &Arc<DownloadTask>) -> eyre::Result<()> {
|
pub async fn process(&mut self, download_task: &Arc<DownloadTask>) -> eyre::Result<()> {
|
||||||
|
let app = &download_task.app;
|
||||||
let _ = DownloadEvent::ProgressPreparing {
|
let _ = DownloadEvent::ProgressPreparing {
|
||||||
task_id: self.task_id.clone(),
|
task_id: self.task_id.clone(),
|
||||||
}
|
}
|
||||||
.emit(&download_task.app);
|
.emit(app);
|
||||||
|
|
||||||
self.prepare(&download_task.app)
|
self.prepare(app).await.wrap_err("准备下载失败")?;
|
||||||
.await
|
|
||||||
.wrap_err("准备下载失败")?;
|
let progress_before_hook = self.clone();
|
||||||
|
app.get_plugin_manager()
|
||||||
|
.run_hook(HookContext::AfterPrepare(AfterPrepareContext::new(self)))
|
||||||
|
.await?;
|
||||||
|
if *self != progress_before_hook {
|
||||||
|
download_task.update_progress(|p| *p = self.clone());
|
||||||
|
}
|
||||||
|
|
||||||
self.completed_ts = None; // 重置完成时间戳
|
self.completed_ts = None; // 重置完成时间戳
|
||||||
download_task.update_progress(|p| *p = self.clone());
|
download_task.update_progress(|p| *p = self.clone());
|
||||||
@@ -216,35 +227,36 @@ impl DownloadProgress {
|
|||||||
std::fs::create_dir_all(&self.episode_dir)
|
std::fs::create_dir_all(&self.episode_dir)
|
||||||
.wrap_err(format!("创建目录`{}`失败", self.episode_dir.display()))?;
|
.wrap_err(format!("创建目录`{}`失败", self.episode_dir.display()))?;
|
||||||
|
|
||||||
let video_task = &self.video_task;
|
|
||||||
let audio_task = &self.audio_task;
|
|
||||||
let video_process_task = &self.video_process_task;
|
|
||||||
let danmaku_task = &self.danmaku_task;
|
|
||||||
let subtitle_task = &self.subtitle_task;
|
|
||||||
let cover_task = &self.cover_task;
|
|
||||||
let nfo_task = &self.nfo_task;
|
|
||||||
let json_task = &self.json_task;
|
|
||||||
|
|
||||||
let mut player_info = None;
|
let mut player_info = None;
|
||||||
let mut episode_info = None;
|
let mut episode_info = None;
|
||||||
|
|
||||||
if !video_task.is_completed() && video_task.content_length != 0 {
|
if !self.video_task.is_completed() && self.video_task.content_length != 0 {
|
||||||
video_task
|
self.video_task
|
||||||
.process(download_task, self)
|
.process(download_task, self)
|
||||||
.await
|
.await
|
||||||
.wrap_err("下载视频文件失败")?;
|
.wrap_err("下载视频文件失败")?;
|
||||||
tracing::debug!("视频下载任务完成");
|
tracing::debug!("视频下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !audio_task.is_completed() && audio_task.content_length != 0 {
|
if !self.audio_task.is_completed() && self.audio_task.content_length != 0 {
|
||||||
audio_task
|
self.audio_task
|
||||||
.process(download_task, self)
|
.process(download_task, self)
|
||||||
.await
|
.await
|
||||||
.wrap_err("下载音频文件失败")?;
|
.wrap_err("下载音频文件失败")?;
|
||||||
tracing::debug!("音频下载任务完成");
|
tracing::debug!("音频下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
let video_process_task_is_completed = video_process_task.is_completed();
|
let progress_before_hook = self.clone();
|
||||||
|
app.get_plugin_manager()
|
||||||
|
.run_hook(HookContext::BeforeVideoProcess(
|
||||||
|
BeforeVideoProcessContext::new(self),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
if *self != progress_before_hook {
|
||||||
|
download_task.update_progress(|p| *p = self.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let video_process_task_is_completed = self.video_process_task.is_completed();
|
||||||
if self.is_drm && !video_process_task_is_completed {
|
if self.is_drm && !video_process_task_is_completed {
|
||||||
download_task.update_progress(|p| {
|
download_task.update_progress(|p| {
|
||||||
p.video_process_task.skipped = true;
|
p.video_process_task.skipped = true;
|
||||||
@@ -252,47 +264,47 @@ impl DownloadProgress {
|
|||||||
});
|
});
|
||||||
tracing::debug!("受版权保护(DRM),无法处理,已跳过视频处理任务");
|
tracing::debug!("受版权保护(DRM),无法处理,已跳过视频处理任务");
|
||||||
} else if !video_process_task_is_completed {
|
} else if !video_process_task_is_completed {
|
||||||
video_process_task
|
self.video_process_task
|
||||||
.process(download_task, self, &mut player_info)
|
.process(download_task, self, &mut player_info)
|
||||||
.await
|
.await
|
||||||
.wrap_err("视频处理失败")?;
|
.wrap_err("视频处理失败")?;
|
||||||
tracing::debug!("视频处理任务完成");
|
tracing::debug!("视频处理任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !danmaku_task.is_completed() {
|
if !self.danmaku_task.is_completed() {
|
||||||
danmaku_task
|
self.danmaku_task
|
||||||
.process(download_task, self)
|
.process(download_task, self)
|
||||||
.await
|
.await
|
||||||
.wrap_err("下载弹幕失败")?;
|
.wrap_err("下载弹幕失败")?;
|
||||||
tracing::debug!("弹幕下载任务完成");
|
tracing::debug!("弹幕下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !subtitle_task.is_completed() {
|
if !self.subtitle_task.is_completed() {
|
||||||
subtitle_task
|
self.subtitle_task
|
||||||
.process(download_task, self, &mut player_info)
|
.process(download_task, self, &mut player_info)
|
||||||
.await
|
.await
|
||||||
.wrap_err("下载字幕失败")?;
|
.wrap_err("下载字幕失败")?;
|
||||||
tracing::debug!("字幕下载任务完成");
|
tracing::debug!("字幕下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cover_task.is_completed() {
|
if !self.cover_task.is_completed() {
|
||||||
cover_task
|
self.cover_task
|
||||||
.process(download_task, self)
|
.process(download_task, self)
|
||||||
.await
|
.await
|
||||||
.wrap_err("下载封面失败")?;
|
.wrap_err("下载封面失败")?;
|
||||||
tracing::debug!("封面下载任务完成");
|
tracing::debug!("封面下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !nfo_task.is_completed() {
|
if !self.nfo_task.is_completed() {
|
||||||
nfo_task
|
self.nfo_task
|
||||||
.process(download_task, self, &mut episode_info)
|
.process(download_task, self, &mut episode_info)
|
||||||
.await
|
.await
|
||||||
.wrap_err("下载NFO失败")?;
|
.wrap_err("下载NFO失败")?;
|
||||||
tracing::debug!("NFO下载任务完成");
|
tracing::debug!("NFO下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !json_task.is_completed() {
|
if !self.json_task.is_completed() {
|
||||||
json_task
|
self.json_task
|
||||||
.process(download_task, self, &mut episode_info)
|
.process(download_task, self, &mut episode_info)
|
||||||
.await
|
.await
|
||||||
.wrap_err("下载JSON元数据失败")?;
|
.wrap_err("下载JSON元数据失败")?;
|
||||||
@@ -303,8 +315,17 @@ impl DownloadProgress {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
.ok();
|
.ok();
|
||||||
if completed_ts.is_some() {
|
if let Some(completed_ts) = completed_ts {
|
||||||
download_task.update_progress(|p| p.completed_ts = completed_ts);
|
self.completed_ts = Some(completed_ts);
|
||||||
|
download_task.update_progress(|p| p.completed_ts = Some(completed_ts));
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress_before_hook = self.clone();
|
||||||
|
app.get_plugin_manager()
|
||||||
|
.run_hook(HookContext::OnCompleted(OnCompletedContext::new(self)))
|
||||||
|
.await?;
|
||||||
|
if *self != progress_before_hook {
|
||||||
|
download_task.update_progress(|p| *p = self.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use tauri_specta::Event;
|
|||||||
use crate::downloader::{
|
use crate::downloader::{
|
||||||
download_progress::DownloadProgress, download_task_state::DownloadTaskState,
|
download_progress::DownloadProgress, download_task_state::DownloadTaskState,
|
||||||
};
|
};
|
||||||
|
use crate::types::plugin_info::PluginInfo;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -46,3 +47,11 @@ pub enum DownloadEvent {
|
|||||||
progress: DownloadProgress,
|
progress: DownloadProgress,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
|
#[serde(tag = "event", content = "data")]
|
||||||
|
pub enum PluginEvent {
|
||||||
|
Loaded { plugin_info: PluginInfo },
|
||||||
|
Update { plugin_info: PluginInfo },
|
||||||
|
Uninstall { plugin_path: String },
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use crate::{
|
|||||||
bili_client::BiliClient,
|
bili_client::BiliClient,
|
||||||
config::Config,
|
config::Config,
|
||||||
downloader::{download_manager::DownloadManager, download_progress::DownloadProgress},
|
downloader::{download_manager::DownloadManager, download_progress::DownloadProgress},
|
||||||
|
plugin::plugin_manager::PluginManager,
|
||||||
types::player_info::PlayerInfo,
|
types::player_info::PlayerInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ pub trait AppHandleExt {
|
|||||||
fn get_config(&self) -> State<'_, RwLock<Config>>;
|
fn get_config(&self) -> State<'_, RwLock<Config>>;
|
||||||
fn get_bili_client(&self) -> State<'_, BiliClient>;
|
fn get_bili_client(&self) -> State<'_, BiliClient>;
|
||||||
fn get_download_manager(&self) -> State<'_, DownloadManager>;
|
fn get_download_manager(&self) -> State<'_, DownloadManager>;
|
||||||
|
fn get_plugin_manager(&self) -> State<'_, PluginManager>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppHandleExt for AppHandle {
|
impl AppHandleExt for AppHandle {
|
||||||
@@ -36,6 +38,9 @@ impl AppHandleExt for AppHandle {
|
|||||||
fn get_download_manager(&self) -> State<'_, DownloadManager> {
|
fn get_download_manager(&self) -> State<'_, DownloadManager> {
|
||||||
self.state::<DownloadManager>()
|
self.state::<DownloadManager>()
|
||||||
}
|
}
|
||||||
|
fn get_plugin_manager(&self) -> State<'_, PluginManager> {
|
||||||
|
self.state::<PluginManager>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait GetOrInitPlayerInfo {
|
pub trait GetOrInitPlayerInfo {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod errors;
|
|||||||
mod events;
|
mod events;
|
||||||
mod extensions;
|
mod extensions;
|
||||||
mod logger;
|
mod logger;
|
||||||
|
mod plugin;
|
||||||
mod types;
|
mod types;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod wbi;
|
mod wbi;
|
||||||
@@ -16,14 +17,14 @@ mod protobuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
use commands::{
|
use commands::{
|
||||||
create_download_tasks, delete_download_tasks, generate_qrcode, get_available_media_formats,
|
add_plugin, create_download_tasks, delete_download_tasks, generate_qrcode,
|
||||||
get_bangumi_follow_info, get_bangumi_info, get_config, get_fav_folders, get_fav_info,
|
get_available_media_formats, get_bangumi_follow_info, get_bangumi_info, get_config,
|
||||||
get_history_info, get_logs_dir_size, get_normal_info, get_qrcode_status, get_skip_segments,
|
get_fav_folders, get_fav_info, get_history_info, get_logs_dir_size, get_normal_info,
|
||||||
get_user_info, get_user_video_info, get_watch_later_info, pause_download_tasks,
|
get_plugin_infos, get_qrcode_status, get_skip_segments, get_user_info, get_user_video_info,
|
||||||
restart_download_task, restart_download_tasks, restore_download_tasks, resume_download_tasks,
|
get_watch_later_info, pause_download_tasks, restart_download_task, restart_download_tasks,
|
||||||
save_config, search, show_path_in_file_manager,
|
restore_download_tasks, resume_download_tasks, save_config, search, set_plugin_enabled,
|
||||||
|
set_plugin_priority, show_path_in_file_manager, uninstall_plugin,
|
||||||
};
|
};
|
||||||
use config::Config;
|
|
||||||
use eyre::WrapErr;
|
use eyre::WrapErr;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use tauri::{Manager, Wry};
|
use tauri::{Manager, Wry};
|
||||||
@@ -31,9 +32,11 @@ use tauri::{Manager, Wry};
|
|||||||
use crate::{
|
use crate::{
|
||||||
bili_client::BiliClient,
|
bili_client::BiliClient,
|
||||||
commands::open_log_file,
|
commands::open_log_file,
|
||||||
|
config::Config,
|
||||||
downloader::download_manager::DownloadManager,
|
downloader::download_manager::DownloadManager,
|
||||||
errors::install_custom_eyre_handler,
|
errors::install_custom_eyre_handler,
|
||||||
events::{DownloadEvent, LogEvent},
|
events::{DownloadEvent, LogEvent, PluginEvent},
|
||||||
|
plugin::plugin_manager::PluginManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn generate_context() -> tauri::Context<Wry> {
|
fn generate_context() -> tauri::Context<Wry> {
|
||||||
@@ -49,6 +52,7 @@ pub fn run() {
|
|||||||
.commands(tauri_specta::collect_commands![
|
.commands(tauri_specta::collect_commands![
|
||||||
get_config,
|
get_config,
|
||||||
save_config,
|
save_config,
|
||||||
|
get_plugin_infos,
|
||||||
generate_qrcode,
|
generate_qrcode,
|
||||||
get_qrcode_status,
|
get_qrcode_status,
|
||||||
get_user_info,
|
get_user_info,
|
||||||
@@ -73,8 +77,16 @@ pub fn run() {
|
|||||||
get_skip_segments,
|
get_skip_segments,
|
||||||
get_available_media_formats,
|
get_available_media_formats,
|
||||||
open_log_file,
|
open_log_file,
|
||||||
|
add_plugin,
|
||||||
|
uninstall_plugin,
|
||||||
|
set_plugin_enabled,
|
||||||
|
set_plugin_priority,
|
||||||
])
|
])
|
||||||
.events(tauri_specta::collect_events![LogEvent, DownloadEvent]);
|
.events(tauri_specta::collect_events![
|
||||||
|
LogEvent,
|
||||||
|
DownloadEvent,
|
||||||
|
PluginEvent,
|
||||||
|
]);
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
builder
|
builder
|
||||||
@@ -122,6 +134,9 @@ pub fn run() {
|
|||||||
|
|
||||||
logger::init(app.handle())?;
|
logger::init(app.handle())?;
|
||||||
|
|
||||||
|
let plugin_manager = PluginManager::new(app.handle())?;
|
||||||
|
app.manage(plugin_manager);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.run(generate_context())
|
.run(generate_context())
|
||||||
|
|||||||
6
src-tauri/src/plugin.rs
Normal file
6
src-tauri/src/plugin.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
pub mod hook_context;
|
||||||
|
pub mod host_api;
|
||||||
|
pub mod plugin_executor;
|
||||||
|
pub mod plugin_loader;
|
||||||
|
pub mod plugin_manager;
|
||||||
|
pub mod plugin_types;
|
||||||
189
src-tauri/src/plugin/hook_context.rs
Normal file
189
src-tauri/src/plugin/hook_context.rs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
use bilibili_video_downloader_plugin_api::v1::{
|
||||||
|
AfterPreparePayloadV1, BeforeVideoProcessPayloadV1, DownloadProgressV1, HookInputV1,
|
||||||
|
HookOutputV1, HookPayloadV1, HookPointV1, HookReadonlyMetaV1, OnCompletedPayloadV1,
|
||||||
|
};
|
||||||
|
use eyre::{WrapErr, eyre};
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
|
||||||
|
use crate::downloader::download_progress::DownloadProgress;
|
||||||
|
|
||||||
|
pub struct BeforeVideoProcessContext<'a> {
|
||||||
|
progress: &'a mut DownloadProgress,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BeforeVideoProcessContext<'a> {
|
||||||
|
pub fn new(progress: &'a mut DownloadProgress) -> Self {
|
||||||
|
Self { progress }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_payload(&self) -> eyre::Result<BeforeVideoProcessPayloadV1> {
|
||||||
|
Ok(BeforeVideoProcessPayloadV1 {
|
||||||
|
progress: host_to_api_progress(self.progress)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_payload(&mut self, payload: BeforeVideoProcessPayloadV1) -> eyre::Result<()> {
|
||||||
|
validate_task_id_unchanged(self.progress, &payload.progress)?;
|
||||||
|
|
||||||
|
let next_progress = api_to_host_progress(payload.progress)?;
|
||||||
|
|
||||||
|
*self.progress = next_progress;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OnCompletedContext<'a> {
|
||||||
|
progress: &'a mut DownloadProgress,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> OnCompletedContext<'a> {
|
||||||
|
pub fn new(progress: &'a mut DownloadProgress) -> Self {
|
||||||
|
Self { progress }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_payload(&self) -> eyre::Result<OnCompletedPayloadV1> {
|
||||||
|
Ok(OnCompletedPayloadV1 {
|
||||||
|
progress: host_to_api_progress(self.progress)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_payload(&mut self, payload: OnCompletedPayloadV1) -> eyre::Result<()> {
|
||||||
|
validate_task_id_unchanged(self.progress, &payload.progress)?;
|
||||||
|
|
||||||
|
let next_progress = api_to_host_progress(payload.progress)?;
|
||||||
|
|
||||||
|
*self.progress = next_progress;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AfterPrepareContext<'a> {
|
||||||
|
progress: &'a mut DownloadProgress,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AfterPrepareContext<'a> {
|
||||||
|
pub fn new(progress: &'a mut DownloadProgress) -> Self {
|
||||||
|
Self { progress }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_payload(&self) -> eyre::Result<AfterPreparePayloadV1> {
|
||||||
|
Ok(AfterPreparePayloadV1 {
|
||||||
|
progress: host_to_api_progress(self.progress)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_payload(&mut self, payload: AfterPreparePayloadV1) -> eyre::Result<()> {
|
||||||
|
validate_task_id_unchanged(self.progress, &payload.progress)?;
|
||||||
|
|
||||||
|
let next_progress = api_to_host_progress(payload.progress)?;
|
||||||
|
|
||||||
|
*self.progress = next_progress;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum HookContext<'a> {
|
||||||
|
BeforeVideoProcess(BeforeVideoProcessContext<'a>),
|
||||||
|
AfterPrepare(AfterPrepareContext<'a>),
|
||||||
|
OnCompleted(OnCompletedContext<'a>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HookContext<'_> {
|
||||||
|
pub fn hook_point(&self) -> HookPointV1 {
|
||||||
|
match self {
|
||||||
|
HookContext::BeforeVideoProcess(_) => HookPointV1::BeforeVideoProcess,
|
||||||
|
HookContext::AfterPrepare(_) => HookPointV1::AfterPrepare,
|
||||||
|
HookContext::OnCompleted(_) => HookPointV1::OnCompleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_input(&self, app_version: &str) -> eyre::Result<HookInputV1> {
|
||||||
|
let hook_point = self.hook_point();
|
||||||
|
let payload = match self {
|
||||||
|
HookContext::BeforeVideoProcess(context) => {
|
||||||
|
HookPayloadV1::BeforeVideoProcess(context.to_payload()?)
|
||||||
|
}
|
||||||
|
HookContext::AfterPrepare(context) => {
|
||||||
|
HookPayloadV1::AfterPrepare(context.to_payload()?)
|
||||||
|
}
|
||||||
|
HookContext::OnCompleted(context) => HookPayloadV1::OnCompleted(context.to_payload()?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let input = HookInputV1 {
|
||||||
|
hook_point,
|
||||||
|
payload,
|
||||||
|
readonly_meta: HookReadonlyMetaV1 {
|
||||||
|
app_version: app_version.to_string(),
|
||||||
|
os: std::env::consts::OS.to_string(),
|
||||||
|
arch: std::env::consts::ARCH.to_string(),
|
||||||
|
process_id: std::process::id(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_output(&mut self, output: HookOutputV1) -> eyre::Result<()> {
|
||||||
|
let context_hook_point = self.hook_point();
|
||||||
|
match (self, output.payload) {
|
||||||
|
(
|
||||||
|
HookContext::BeforeVideoProcess(context),
|
||||||
|
HookPayloadV1::BeforeVideoProcess(payload),
|
||||||
|
) => context.apply_payload(payload),
|
||||||
|
|
||||||
|
(HookContext::AfterPrepare(context), HookPayloadV1::AfterPrepare(payload)) => {
|
||||||
|
context.apply_payload(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
(HookContext::OnCompleted(context), HookPayloadV1::OnCompleted(payload)) => {
|
||||||
|
context.apply_payload(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
(_, payload) => Err(eyre!(
|
||||||
|
"hook_point 与 payload 不匹配: hook_point={context_hook_point:?}, payload={payload:?}"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_task_id_unchanged(
|
||||||
|
current_progress: &DownloadProgress,
|
||||||
|
next_progress: &DownloadProgressV1,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
if current_progress.task_id != next_progress.task_id {
|
||||||
|
return Err(eyre!("task_id 不可修改"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_to_api_progress(progress: &DownloadProgress) -> eyre::Result<DownloadProgressV1> {
|
||||||
|
convert_via_json(
|
||||||
|
progress,
|
||||||
|
"序列化宿主 DownloadProgress 失败",
|
||||||
|
"反序列化为插件 DownloadProgressV1 失败",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn api_to_host_progress(progress: DownloadProgressV1) -> eyre::Result<DownloadProgress> {
|
||||||
|
convert_via_json(
|
||||||
|
progress,
|
||||||
|
"序列化插件 DownloadProgressV1 失败",
|
||||||
|
"反序列化为宿主 DownloadProgress 失败",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_via_json<TSrc, TDst>(
|
||||||
|
source: TSrc,
|
||||||
|
serialize_err: &str,
|
||||||
|
deserialize_err: &str,
|
||||||
|
) -> eyre::Result<TDst>
|
||||||
|
where
|
||||||
|
TSrc: Serialize,
|
||||||
|
TDst: DeserializeOwned,
|
||||||
|
{
|
||||||
|
let value = serde_json::to_value(source).wrap_err_with(|| serialize_err.to_string())?;
|
||||||
|
serde_json::from_value(value).wrap_err_with(|| deserialize_err.to_string())
|
||||||
|
}
|
||||||
67
src-tauri/src/plugin/host_api.rs
Normal file
67
src-tauri/src/plugin/host_api.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use bilibili_video_downloader_plugin_api::v1::{HostApiV1, HostConfigV1};
|
||||||
|
use eyre::WrapErr;
|
||||||
|
use tauri::AppHandle;
|
||||||
|
|
||||||
|
use crate::{config::Config, extensions::AppHandleExt};
|
||||||
|
|
||||||
|
static HOST_APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
|
||||||
|
|
||||||
|
pub fn init(app: &AppHandle) {
|
||||||
|
HOST_APP_HANDLE.get_or_init(|| app.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_host_api_v1() -> HostApiV1 {
|
||||||
|
HostApiV1 {
|
||||||
|
get_config_json: host_get_config_json_v1,
|
||||||
|
free_buffer: host_free_buffer_v1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn host_get_config_json_v1(out_ptr: *mut *mut u8, out_len: *mut usize) -> i32 {
|
||||||
|
if out_ptr.is_null() || out_len.is_null() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(app) = HOST_APP_HANDLE.get() else {
|
||||||
|
return 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
let host_config = app.get_config().read().clone();
|
||||||
|
let Ok(host_config_v1) = to_host_config_v1(&host_config) else {
|
||||||
|
return 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(output_bytes) = serde_json::to_vec(&host_config_v1) else {
|
||||||
|
return 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
let boxed = output_bytes.into_boxed_slice();
|
||||||
|
let len = boxed.len();
|
||||||
|
let ptr = Box::into_raw(boxed).cast::<u8>();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
*out_ptr = ptr;
|
||||||
|
*out_len = len;
|
||||||
|
}
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn host_free_buffer_v1(ptr: *mut u8, len: usize) {
|
||||||
|
if ptr.is_null() || len == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw_slice = std::ptr::slice_from_raw_parts_mut(ptr, len);
|
||||||
|
unsafe {
|
||||||
|
drop(Box::from_raw(raw_slice));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_host_config_v1(config: &Config) -> eyre::Result<HostConfigV1> {
|
||||||
|
let value = serde_json::to_value(config).wrap_err("序列化宿主 Config 失败")?;
|
||||||
|
let host_config = serde_json::from_value(value).wrap_err("反序列化为插件 HostConfigV1 失败")?;
|
||||||
|
Ok(host_config)
|
||||||
|
}
|
||||||
66
src-tauri/src/plugin/plugin_executor.rs
Normal file
66
src-tauri/src/plugin/plugin_executor.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use std::{ffi::CStr, sync::Arc};
|
||||||
|
|
||||||
|
use bilibili_video_downloader_plugin_api::v1::{HookInputV1, HookOutputV1};
|
||||||
|
use dlopen2::wrapper::Container;
|
||||||
|
use eyre::eyre;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::plugin::plugin_types::{PluginDylibApi, PluginRuntime};
|
||||||
|
|
||||||
|
#[instrument(level = "error", skip_all, fields(plugin_name = plugin.display_name(), hook_point = ?input.hook_point))]
|
||||||
|
pub async fn execute_hook(
|
||||||
|
plugin: &PluginRuntime,
|
||||||
|
input: &HookInputV1,
|
||||||
|
) -> eyre::Result<HookOutputV1> {
|
||||||
|
let input_bytes = serde_json::to_vec(input)?;
|
||||||
|
let api = plugin.api.clone();
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel::<eyre::Result<Vec<u8>>>();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
let result = call_on_hook_blocking(api, &input_bytes);
|
||||||
|
let _ = tx.send(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
let output_bytes = rx.await??;
|
||||||
|
let output: HookOutputV1 = serde_json::from_slice(&output_bytes)?;
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "error", skip_all)]
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
fn call_on_hook_blocking(
|
||||||
|
api: Arc<Container<PluginDylibApi>>,
|
||||||
|
input_bytes: &[u8],
|
||||||
|
) -> eyre::Result<Vec<u8>> {
|
||||||
|
let mut output_ptr: *mut u8 = std::ptr::null_mut();
|
||||||
|
let mut output_len: usize = 0;
|
||||||
|
let rc = unsafe {
|
||||||
|
api.on_hook(
|
||||||
|
input_bytes.as_ptr(),
|
||||||
|
input_bytes.len(),
|
||||||
|
&raw mut output_ptr,
|
||||||
|
&raw mut output_len,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if rc != 0 {
|
||||||
|
let detail = get_last_error(&api);
|
||||||
|
return Err(eyre!("插件返回错误码: code={rc}, detail={detail}"));
|
||||||
|
}
|
||||||
|
if output_ptr.is_null() {
|
||||||
|
return Err(eyre!("插件返回空输出缓冲区"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_bytes = unsafe { std::slice::from_raw_parts(output_ptr, output_len) }.to_vec();
|
||||||
|
unsafe { api.free_buffer(output_ptr, output_len) };
|
||||||
|
Ok(output_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_last_error(api: &Arc<Container<PluginDylibApi>>) -> String {
|
||||||
|
let error_ptr = unsafe { api.last_error() };
|
||||||
|
if error_ptr.is_null() {
|
||||||
|
return "获取错误信息失败,error_ptr为null".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let error_cstr = unsafe { CStr::from_ptr(error_ptr) };
|
||||||
|
error_cstr.to_string_lossy().to_string()
|
||||||
|
}
|
||||||
78
src-tauri/src/plugin/plugin_loader.rs
Normal file
78
src-tauri/src/plugin/plugin_loader.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use std::{ffi::CStr, path::Path, sync::Arc};
|
||||||
|
|
||||||
|
use bilibili_video_downloader_plugin_api::{SDK_API_VERSION_V1, v1::PluginDescriptorV1};
|
||||||
|
use dlopen2::wrapper::Container;
|
||||||
|
use eyre::{WrapErr, eyre};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::plugin::{
|
||||||
|
host_api,
|
||||||
|
plugin_types::{PluginDylibApi, PluginRuntime},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[instrument(level = "error", skip_all, fields(plugin_path = %plugin_path.display(), priority = priority, enabled = enabled))]
|
||||||
|
pub fn load_plugin_from_path(
|
||||||
|
plugin_path: &Path,
|
||||||
|
priority: i32,
|
||||||
|
enabled: bool,
|
||||||
|
) -> eyre::Result<PluginRuntime> {
|
||||||
|
if !plugin_path.is_absolute() {
|
||||||
|
return Err(eyre!("插件路径必须是绝对路径: `{}`", plugin_path.display()));
|
||||||
|
}
|
||||||
|
if !plugin_path.exists() {
|
||||||
|
return Err(eyre!("插件动态库文件`{}`不存在", plugin_path.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let api = unsafe { Container::<PluginDylibApi>::load(plugin_path) }
|
||||||
|
.wrap_err(format!("加载插件动态库文件`{}`失败", plugin_path.display()))?;
|
||||||
|
|
||||||
|
let descriptor_json = get_descriptor_json(&api).wrap_err("读取插件描述失败")?;
|
||||||
|
let descriptor: PluginDescriptorV1 = serde_json::from_str(&descriptor_json)
|
||||||
|
.wrap_err(format!("解析插件描述失败: {descriptor_json}"))?;
|
||||||
|
|
||||||
|
if descriptor.sdk_api_version != SDK_API_VERSION_V1 {
|
||||||
|
return Err(eyre!(
|
||||||
|
"插件SDK版本不匹配: 期望版本={}, 实际版本={}",
|
||||||
|
SDK_API_VERSION_V1,
|
||||||
|
descriptor.sdk_api_version
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if descriptor.id.trim().is_empty() {
|
||||||
|
return Err(eyre!("descriptor.id 为空"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if descriptor.hooks.is_empty() {
|
||||||
|
return Err(eyre!("插件未声明任何可执行 Hook"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let host_api = host_api::build_host_api_v1();
|
||||||
|
let rc = unsafe { api.set_host_api(&raw const host_api) };
|
||||||
|
if rc != 0 {
|
||||||
|
return Err(eyre!(
|
||||||
|
"注册宿主 Host API 失败: plugin_id={}, rc={rc}",
|
||||||
|
descriptor.id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PluginRuntime {
|
||||||
|
descriptor,
|
||||||
|
plugin_path: plugin_path.to_path_buf(),
|
||||||
|
enabled,
|
||||||
|
priority,
|
||||||
|
api: Arc::new(api),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "error", skip_all)]
|
||||||
|
fn get_descriptor_json(api: &Container<PluginDylibApi>) -> eyre::Result<String> {
|
||||||
|
let descriptor_ptr = unsafe { api.descriptor() };
|
||||||
|
if descriptor_ptr.is_null() {
|
||||||
|
return Err(eyre!("descriptor 指针为空"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let descriptor_cstr = unsafe { CStr::from_ptr(descriptor_ptr).to_str() }
|
||||||
|
.wrap_err("descriptor 非 UTF-8 字符串")?;
|
||||||
|
|
||||||
|
Ok(descriptor_cstr.to_string())
|
||||||
|
}
|
||||||
351
src-tauri/src/plugin/plugin_manager.rs
Normal file
351
src-tauri/src/plugin/plugin_manager.rs
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use eyre::eyre;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
use tauri_specta::Event;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
events::PluginEvent,
|
||||||
|
extensions::EyreReportToMessage,
|
||||||
|
types::plugin_info::{PluginDescriptorInfo, PluginInfo, PluginMetadata, PluginRuntimeStatus},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
hook_context::HookContext, host_api, plugin_executor, plugin_loader,
|
||||||
|
plugin_types::PluginRuntime,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct PluginManager {
|
||||||
|
app: AppHandle,
|
||||||
|
infos: RwLock<HashMap<String, PluginInfo>>,
|
||||||
|
runtimes: RwLock<Vec<PluginRuntime>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginManager {
|
||||||
|
#[instrument(level = "error", skip_all)]
|
||||||
|
pub fn new(app: &AppHandle) -> eyre::Result<PluginManager> {
|
||||||
|
host_api::init(app);
|
||||||
|
|
||||||
|
let app_data_dir = app.path().app_data_dir()?;
|
||||||
|
let plugin_json_path = app_data_dir.join("plugin.json");
|
||||||
|
|
||||||
|
let mut infos = HashMap::new();
|
||||||
|
if plugin_json_path.exists() {
|
||||||
|
let json_string = std::fs::read_to_string(&plugin_json_path)?;
|
||||||
|
|
||||||
|
let metadata_map: HashMap<String, PluginMetadata> =
|
||||||
|
serde_json::from_str(&json_string).unwrap_or_default();
|
||||||
|
|
||||||
|
for (plugin_path, metadata) in metadata_map {
|
||||||
|
let status = PluginRuntimeStatus::Unknown;
|
||||||
|
let info = PluginInfo::from_metadata(metadata, status);
|
||||||
|
infos.insert(plugin_path, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut runtimes = Vec::new();
|
||||||
|
for info in infos.values_mut() {
|
||||||
|
if !info.enabled {
|
||||||
|
info.runtime_status = PluginRuntimeStatus::Disabled;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match plugin_loader::load_plugin_from_path(&info.path, info.priority, true) {
|
||||||
|
Ok(runtime) => {
|
||||||
|
tracing::info!(
|
||||||
|
"插件加载成功: plugin_name={}, plugin_path={}",
|
||||||
|
runtime.display_name(),
|
||||||
|
runtime.plugin_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
info.runtime_status = PluginRuntimeStatus::Loaded;
|
||||||
|
info.descriptor = PluginDescriptorInfo::from_descriptor(&runtime.descriptor);
|
||||||
|
|
||||||
|
insert_runtime_by_priority(&mut runtimes, runtime);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let err_title = "某个插件加载失败,已跳过";
|
||||||
|
let message = err.to_message();
|
||||||
|
tracing::error!(err_title, message);
|
||||||
|
|
||||||
|
info.runtime_status = PluginRuntimeStatus::LoadFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let plugin_manager = Self {
|
||||||
|
app: app.clone(),
|
||||||
|
infos: RwLock::new(infos),
|
||||||
|
runtimes: RwLock::new(runtimes),
|
||||||
|
};
|
||||||
|
|
||||||
|
plugin_manager.save_metadata()?;
|
||||||
|
|
||||||
|
Ok(plugin_manager)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "error", skip_all, fields(plugin_path = plugin_path))]
|
||||||
|
pub fn add_plugin(&self, plugin_path: &str) -> eyre::Result<()> {
|
||||||
|
let runtime = plugin_loader::load_plugin_from_path(&PathBuf::from(plugin_path), 0, true)?;
|
||||||
|
|
||||||
|
let plugin_info = {
|
||||||
|
let mut infos = self.infos.write();
|
||||||
|
if infos.contains_key(plugin_path) {
|
||||||
|
return Err(eyre!("插件已存在: {plugin_path}"));
|
||||||
|
}
|
||||||
|
let status = PluginRuntimeStatus::Loaded;
|
||||||
|
let metadata = PluginMetadata::from_plugin_runtime(&runtime);
|
||||||
|
let info = PluginInfo::from_metadata(metadata, status);
|
||||||
|
infos.insert(plugin_path.to_string(), info.clone());
|
||||||
|
info
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut runtimes = self.runtimes.write();
|
||||||
|
insert_runtime_by_priority(&mut runtimes, runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save_metadata()?;
|
||||||
|
let _ = PluginEvent::Loaded { plugin_info }.emit(&self.app);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "error", skip_all, fields(plugin_path = plugin_path))]
|
||||||
|
pub fn uninstall_plugin(&self, plugin_path: &str) -> eyre::Result<()> {
|
||||||
|
{
|
||||||
|
let mut infos = self.infos.write();
|
||||||
|
if !infos.contains_key(plugin_path) {
|
||||||
|
return Err(eyre!("key中没有插件路径: {plugin_path}"));
|
||||||
|
}
|
||||||
|
infos.remove(plugin_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut runtimes = self.runtimes.write();
|
||||||
|
remove_runtime_by_path(&mut runtimes, Path::new(plugin_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = PluginEvent::Uninstall {
|
||||||
|
plugin_path: plugin_path.to_string(),
|
||||||
|
}
|
||||||
|
.emit(&self.app);
|
||||||
|
|
||||||
|
self.save_metadata()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "error", skip_all, fields(plugin_path = plugin_path, enabled = enabled))]
|
||||||
|
pub fn set_plugin_enabled(&self, plugin_path: &str, enabled: bool) -> eyre::Result<()> {
|
||||||
|
if !enabled {
|
||||||
|
let plugin_info = {
|
||||||
|
let mut infos = self.infos.write();
|
||||||
|
let Some(info) = infos.get_mut(plugin_path) else {
|
||||||
|
return Err(eyre!("key中没有插件路径: {plugin_path}"));
|
||||||
|
};
|
||||||
|
if info.enabled == enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
info.enabled = false;
|
||||||
|
info.runtime_status = PluginRuntimeStatus::Disabled;
|
||||||
|
info.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut runtimes = self.runtimes.write();
|
||||||
|
remove_runtime_by_path(&mut runtimes, Path::new(plugin_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = PluginEvent::Update { plugin_info }.emit(&self.app);
|
||||||
|
|
||||||
|
self.save_metadata()?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (plugin_file_path, priority) = {
|
||||||
|
let mut infos = self.infos.write();
|
||||||
|
let Some(info) = infos.get_mut(plugin_path) else {
|
||||||
|
return Err(eyre!("key中没有插件路径: {plugin_path}"));
|
||||||
|
};
|
||||||
|
if info.enabled == enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
info.enabled = true;
|
||||||
|
(info.path.clone(), info.priority)
|
||||||
|
};
|
||||||
|
|
||||||
|
let plugin_info =
|
||||||
|
match plugin_loader::load_plugin_from_path(&plugin_file_path, priority, true) {
|
||||||
|
Ok(runtime) => {
|
||||||
|
{
|
||||||
|
let mut runtimes = self.runtimes.write();
|
||||||
|
remove_runtime_by_path(&mut runtimes, &plugin_file_path);
|
||||||
|
insert_runtime_by_priority(&mut runtimes, runtime.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut infos = self.infos.write();
|
||||||
|
let Some(info) = infos.get_mut(plugin_path) else {
|
||||||
|
return Err(eyre!("key中没有插件路径: {plugin_path}"));
|
||||||
|
};
|
||||||
|
info.runtime_status = PluginRuntimeStatus::Loaded;
|
||||||
|
info.descriptor = PluginDescriptorInfo::from_descriptor(&runtime.descriptor);
|
||||||
|
info.clone()
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let err_title = "启用插件时加载失败";
|
||||||
|
let message = err.to_message();
|
||||||
|
tracing::error!(err_title, message);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut runtimes = self.runtimes.write();
|
||||||
|
remove_runtime_by_path(&mut runtimes, &plugin_file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut infos = self.infos.write();
|
||||||
|
let Some(info) = infos.get_mut(plugin_path) else {
|
||||||
|
return Err(eyre!("key中没有插件路径: {plugin_path}"));
|
||||||
|
};
|
||||||
|
info.runtime_status = PluginRuntimeStatus::LoadFailed;
|
||||||
|
info.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = PluginEvent::Update { plugin_info }.emit(&self.app);
|
||||||
|
|
||||||
|
self.save_metadata()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(
|
||||||
|
level = "error",
|
||||||
|
skip_all,
|
||||||
|
fields(plugin_path = plugin_path, priority = priority)
|
||||||
|
)]
|
||||||
|
pub fn set_plugin_priority(&self, plugin_path: &str, priority: i32) -> eyre::Result<()> {
|
||||||
|
let plugin_info = {
|
||||||
|
let mut infos = self.infos.write();
|
||||||
|
let Some(info) = infos.get_mut(plugin_path) else {
|
||||||
|
return Err(eyre!("key中没有插件路径: {plugin_path}"));
|
||||||
|
};
|
||||||
|
if info.priority == priority {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
info.priority = priority;
|
||||||
|
info.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut runtimes = self.runtimes.write();
|
||||||
|
if let Some(mut runtime) = remove_runtime_by_path(&mut runtimes, Path::new(plugin_path))
|
||||||
|
{
|
||||||
|
runtime.priority = priority;
|
||||||
|
insert_runtime_by_priority(&mut runtimes, runtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = PluginEvent::Update { plugin_info }.emit(&self.app);
|
||||||
|
|
||||||
|
self.save_metadata()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_plugin_infos(&self) -> Vec<PluginInfo> {
|
||||||
|
self.infos.read().values().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "error", skip_all)]
|
||||||
|
pub async fn run_hook(&self, mut context: HookContext<'_>) -> eyre::Result<()> {
|
||||||
|
let hook_point = context.hook_point();
|
||||||
|
let runtimes = self.runtimes.read().clone();
|
||||||
|
if runtimes.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_version = self.app.package_info().version.to_string();
|
||||||
|
|
||||||
|
for runtime in &runtimes {
|
||||||
|
if !runtime.enabled || !runtime.should_run_hook(hook_point) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = context.to_input(&app_version)?;
|
||||||
|
let output = match plugin_executor::execute_hook(runtime, &input).await {
|
||||||
|
Ok(output) => output,
|
||||||
|
Err(err) => match runtime.descriptor.failure_policy {
|
||||||
|
bilibili_video_downloader_plugin_api::v1::PluginFailurePolicy::FailOpen => {
|
||||||
|
let err_title = "插件执行出错,按照 FailOpen 继续其他任务";
|
||||||
|
let message = err.to_message();
|
||||||
|
tracing::error!(err_title, message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bilibili_video_downloader_plugin_api::v1::PluginFailurePolicy::FailClosed => {
|
||||||
|
let err = err.wrap_err("插件执行出错,按照 FailClosed 中断任务");
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = context.apply_output(output) {
|
||||||
|
match runtime.descriptor.failure_policy {
|
||||||
|
bilibili_video_downloader_plugin_api::v1::PluginFailurePolicy::FailOpen => {
|
||||||
|
let err_title = "插件输出无效,按照 FailOpen 继续其他任务";
|
||||||
|
let message = err.to_message();
|
||||||
|
tracing::error!(err_title, message);
|
||||||
|
}
|
||||||
|
bilibili_video_downloader_plugin_api::v1::PluginFailurePolicy::FailClosed => {
|
||||||
|
let err = err.wrap_err("插件输出无效,按照 FailClosed 中断任务");
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "error", skip_all)]
|
||||||
|
fn save_metadata(&self) -> eyre::Result<()> {
|
||||||
|
let app_data_dir = self.app.path().app_data_dir()?;
|
||||||
|
let plugin_json_path = app_data_dir.join("plugin.json");
|
||||||
|
|
||||||
|
let metadata_by_path: HashMap<String, PluginMetadata> = self
|
||||||
|
.infos
|
||||||
|
.read()
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(plugin_path, info)| (plugin_path, info.into_metadata()))
|
||||||
|
.collect();
|
||||||
|
let json_string = serde_json::to_string_pretty(&metadata_by_path)?;
|
||||||
|
|
||||||
|
std::fs::write(plugin_json_path, json_string)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_runtime_by_priority(runtimes: &mut Vec<PluginRuntime>, runtime: PluginRuntime) {
|
||||||
|
let insert_idx = runtimes
|
||||||
|
.iter()
|
||||||
|
.position(|existing| existing.priority < runtime.priority)
|
||||||
|
.unwrap_or(runtimes.len());
|
||||||
|
runtimes.insert(insert_idx, runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_runtime_by_path(
|
||||||
|
runtimes: &mut Vec<PluginRuntime>,
|
||||||
|
plugin_path: &Path,
|
||||||
|
) -> Option<PluginRuntime> {
|
||||||
|
let remove_idx = runtimes
|
||||||
|
.iter()
|
||||||
|
.position(|runtime| runtime.plugin_path == plugin_path)?;
|
||||||
|
Some(runtimes.remove(remove_idx))
|
||||||
|
}
|
||||||
45
src-tauri/src/plugin/plugin_types.rs
Normal file
45
src-tauri/src/plugin/plugin_types.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use std::{ffi::c_char, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
|
use bilibili_video_downloader_plugin_api::v1::{HookPointV1, HostApiV1, PluginDescriptorV1};
|
||||||
|
use dlopen2::wrapper::{Container, WrapperApi};
|
||||||
|
|
||||||
|
#[derive(WrapperApi)]
|
||||||
|
pub struct PluginDylibApi {
|
||||||
|
#[dlopen2_name = "bilibili_video_downloader_plugin_descriptor_v1"]
|
||||||
|
descriptor: unsafe extern "C" fn() -> *const c_char,
|
||||||
|
#[dlopen2_name = "bilibili_video_downloader_plugin_on_hook_v1"]
|
||||||
|
on_hook: unsafe extern "C" fn(
|
||||||
|
input_ptr: *const u8,
|
||||||
|
input_len: usize,
|
||||||
|
out_ptr: *mut *mut u8,
|
||||||
|
out_len: *mut usize,
|
||||||
|
) -> i32,
|
||||||
|
#[dlopen2_name = "bilibili_video_downloader_plugin_free_buffer_v1"]
|
||||||
|
free_buffer: unsafe extern "C" fn(ptr: *mut u8, len: usize),
|
||||||
|
#[dlopen2_name = "bilibili_video_downloader_plugin_last_error_v1"]
|
||||||
|
last_error: unsafe extern "C" fn() -> *const c_char,
|
||||||
|
#[dlopen2_name = "bilibili_video_downloader_plugin_set_host_api_v1"]
|
||||||
|
set_host_api: unsafe extern "C" fn(api: *const HostApiV1) -> i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PluginRuntime {
|
||||||
|
pub descriptor: PluginDescriptorV1,
|
||||||
|
pub plugin_path: PathBuf,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub priority: i32,
|
||||||
|
pub api: Arc<Container<PluginDylibApi>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginRuntime {
|
||||||
|
pub fn display_name(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"{} ({}, v{})",
|
||||||
|
self.descriptor.name, self.descriptor.id, self.descriptor.version
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn should_run_hook(&self, hook: HookPointV1) -> bool {
|
||||||
|
self.descriptor.hooks.contains(&hook)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ pub mod log_metadata;
|
|||||||
pub mod normal_info;
|
pub mod normal_info;
|
||||||
pub mod normal_media_url;
|
pub mod normal_media_url;
|
||||||
pub mod player_info;
|
pub mod player_info;
|
||||||
|
pub mod plugin_info;
|
||||||
pub mod qrcode_data;
|
pub mod qrcode_data;
|
||||||
pub mod qrcode_status;
|
pub mod qrcode_status;
|
||||||
pub mod restart_download_task_params;
|
pub mod restart_download_task_params;
|
||||||
|
|||||||
131
src-tauri/src/types/plugin_info.rs
Normal file
131
src-tauri/src/types/plugin_info.rs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use bilibili_video_downloader_plugin_api::v1::{
|
||||||
|
HookPointV1, PluginDescriptorV1, PluginFailurePolicy,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
|
||||||
|
use crate::plugin::plugin_types::PluginRuntime;
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||||
|
pub enum PluginHookPoint {
|
||||||
|
#[default]
|
||||||
|
BeforeVideoProcess,
|
||||||
|
AfterPrepare,
|
||||||
|
OnCompleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HookPointV1> for PluginHookPoint {
|
||||||
|
fn from(value: HookPointV1) -> Self {
|
||||||
|
match value {
|
||||||
|
HookPointV1::BeforeVideoProcess => Self::BeforeVideoProcess,
|
||||||
|
HookPointV1::AfterPrepare => Self::AfterPrepare,
|
||||||
|
HookPointV1::OnCompleted => Self::OnCompleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||||
|
pub enum PluginFailurePolicyInfo {
|
||||||
|
#[default]
|
||||||
|
FailOpen,
|
||||||
|
FailClosed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PluginFailurePolicy> for PluginFailurePolicyInfo {
|
||||||
|
fn from(value: PluginFailurePolicy) -> Self {
|
||||||
|
match value {
|
||||||
|
PluginFailurePolicy::FailOpen => Self::FailOpen,
|
||||||
|
PluginFailurePolicy::FailClosed => Self::FailClosed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||||
|
pub struct PluginDescriptorInfo {
|
||||||
|
pub sdk_api_version: u32,
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub hooks: Vec<PluginHookPoint>,
|
||||||
|
pub failure_policy: PluginFailurePolicyInfo,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginDescriptorInfo {
|
||||||
|
pub fn from_descriptor(descriptor: &PluginDescriptorV1) -> Self {
|
||||||
|
Self {
|
||||||
|
sdk_api_version: descriptor.sdk_api_version,
|
||||||
|
id: descriptor.id.clone(),
|
||||||
|
name: descriptor.name.clone(),
|
||||||
|
version: descriptor.version.clone(),
|
||||||
|
hooks: descriptor
|
||||||
|
.hooks
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(PluginHookPoint::from)
|
||||||
|
.collect(),
|
||||||
|
failure_policy: descriptor.failure_policy.into(),
|
||||||
|
description: descriptor.description.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||||
|
pub struct PluginMetadata {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub priority: i32,
|
||||||
|
pub descriptor: PluginDescriptorInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginMetadata {
|
||||||
|
pub fn from_plugin_runtime(runtime: &PluginRuntime) -> Self {
|
||||||
|
Self {
|
||||||
|
path: runtime.plugin_path.clone(),
|
||||||
|
enabled: runtime.enabled,
|
||||||
|
priority: runtime.priority,
|
||||||
|
descriptor: PluginDescriptorInfo::from_descriptor(&runtime.descriptor),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||||
|
pub enum PluginRuntimeStatus {
|
||||||
|
#[default]
|
||||||
|
Unknown,
|
||||||
|
Loaded,
|
||||||
|
Disabled,
|
||||||
|
LoadFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||||
|
pub struct PluginInfo {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub priority: i32,
|
||||||
|
pub descriptor: PluginDescriptorInfo,
|
||||||
|
pub runtime_status: PluginRuntimeStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginInfo {
|
||||||
|
pub fn from_metadata(metadata: PluginMetadata, runtime_status: PluginRuntimeStatus) -> Self {
|
||||||
|
Self {
|
||||||
|
path: metadata.path,
|
||||||
|
enabled: metadata.enabled,
|
||||||
|
priority: metadata.priority,
|
||||||
|
descriptor: metadata.descriptor,
|
||||||
|
runtime_status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_metadata(self) -> PluginMetadata {
|
||||||
|
PluginMetadata {
|
||||||
|
path: self.path,
|
||||||
|
enabled: self.enabled,
|
||||||
|
priority: self.priority,
|
||||||
|
descriptor: self.descriptor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,9 @@ async saveConfig(config: Config) : Promise<Result<null, CommandError>> {
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async getPluginInfos() : Promise<PluginInfo[]> {
|
||||||
|
return await TAURI_INVOKE("get_plugin_infos");
|
||||||
|
},
|
||||||
async generateQrcode() : Promise<Result<QrcodeData, CommandError>> {
|
async generateQrcode() : Promise<Result<QrcodeData, CommandError>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("generate_qrcode") };
|
return { status: "ok", data: await TAURI_INVOKE("generate_qrcode") };
|
||||||
@@ -177,6 +180,38 @@ async openLogFile(path: string) : Promise<Result<LogMetadata[], CommandError>> {
|
|||||||
if(e instanceof Error) throw e;
|
if(e instanceof Error) throw e;
|
||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async addPlugin(pluginPath: string) : Promise<Result<null, CommandError>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("add_plugin", { pluginPath }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async uninstallPlugin(pluginPath: string) : Promise<Result<null, CommandError>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("uninstall_plugin", { pluginPath }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setPluginEnabled(pluginPath: string, enabled: boolean) : Promise<Result<null, CommandError>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("set_plugin_enabled", { pluginPath, enabled }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setPluginPriority(pluginPath: string, priority: number) : Promise<Result<null, CommandError>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("set_plugin_priority", { pluginPath, priority }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,10 +220,12 @@ async openLogFile(path: string) : Promise<Result<LogMetadata[], CommandError>> {
|
|||||||
|
|
||||||
export const events = __makeEvents__<{
|
export const events = __makeEvents__<{
|
||||||
downloadEvent: DownloadEvent,
|
downloadEvent: DownloadEvent,
|
||||||
logEvent: LogEvent
|
logEvent: LogEvent,
|
||||||
|
pluginEvent: PluginEvent
|
||||||
}>({
|
}>({
|
||||||
downloadEvent: "download-event",
|
downloadEvent: "download-event",
|
||||||
logEvent: "log-event"
|
logEvent: "log-event",
|
||||||
|
pluginEvent: "plugin-event"
|
||||||
})
|
})
|
||||||
|
|
||||||
/** user-defined constants **/
|
/** user-defined constants **/
|
||||||
@@ -371,6 +408,12 @@ export type PaymentInBangumi = { discount: number; pay_type: PayType; price: str
|
|||||||
export type PendantInCheese = { image: string; name: string; pid: number }
|
export type PendantInCheese = { image: string; name: string; pid: number }
|
||||||
export type PendantInUserInfo = { pid: number; name: string; image: string; expire: number; image_enhance: string; image_enhance_frame: string; n_pid: number }
|
export type PendantInUserInfo = { pid: number; name: string; image: string; expire: number; image_enhance: string; image_enhance_frame: string; n_pid: number }
|
||||||
export type PlayStrategy = { strategies: string[] }
|
export type PlayStrategy = { strategies: string[] }
|
||||||
|
export type PluginDescriptorInfo = { sdk_api_version: number; id: string; name: string; version: string; hooks: PluginHookPoint[]; failure_policy: PluginFailurePolicyInfo; description: string }
|
||||||
|
export type PluginEvent = { event: "Loaded"; data: { plugin_info: PluginInfo } } | { event: "Update"; data: { plugin_info: PluginInfo } } | { event: "Uninstall"; data: { plugin_path: string } }
|
||||||
|
export type PluginFailurePolicyInfo = "FailOpen" | "FailClosed"
|
||||||
|
export type PluginHookPoint = "BeforeVideoProcess" | "AfterPrepare" | "OnCompleted"
|
||||||
|
export type PluginInfo = { path: string; enabled: boolean; priority: number; descriptor: PluginDescriptorInfo; runtime_status: PluginRuntimeStatus }
|
||||||
|
export type PluginRuntimeStatus = "Unknown" | "Loaded" | "Disabled" | "LoadFailed"
|
||||||
export type Positive = { id: number; title: string }
|
export type Positive = { id: number; title: string }
|
||||||
export type PreviewedPurchaseNote = { long_watch_text: string; pay_text: string; price_format: string; watch_text: string; watching_text: string }
|
export type PreviewedPurchaseNote = { long_watch_text: string; pay_text: string; price_format: string; watch_text: string; watching_text: string }
|
||||||
export type Producer = { mid: number; type: number; is_contribute: number | null; title: string }
|
export type Producer = { mid: number; type: number; is_contribute: number | null; title: string }
|
||||||
|
|||||||
Reference in New Issue
Block a user