mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-07 00:27:46 +08:00
feat: 进程内动态库插件系统
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
target/
|
||||
Generated
+215
@@ -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"
|
||||
@@ -0,0 +1,3 @@
|
||||
[workspace]
|
||||
members = ["plugin-api", "plugin-sdk"]
|
||||
resolver = "3"
|
||||
@@ -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"] }
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 未注册"));
|
||||
}
|
||||
Reference in New Issue
Block a user