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:
@@ -32,6 +32,7 @@ use crate::{
|
||||
history_info::HistoryInfo,
|
||||
log_metadata::LogMetadata,
|
||||
normal_info::NormalInfo,
|
||||
plugin_info::PluginInfo,
|
||||
qrcode_data::QrcodeData,
|
||||
qrcode_status::QrcodeStatus,
|
||||
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 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 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)
|
||||
}
|
||||
|
||||
#[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,
|
||||
extensions::AppHandleExt,
|
||||
plugin::hook_context::{
|
||||
AfterPrepareContext, BeforeVideoProcessContext, HookContext, OnCompletedContext,
|
||||
},
|
||||
types::{
|
||||
audio_quality::AudioQuality,
|
||||
bangumi_info::BangumiInfo,
|
||||
@@ -200,15 +203,23 @@ impl DownloadProgress {
|
||||
}
|
||||
|
||||
#[instrument(level = "error", skip_all)]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn process(&mut self, download_task: &Arc<DownloadTask>) -> eyre::Result<()> {
|
||||
let app = &download_task.app;
|
||||
let _ = DownloadEvent::ProgressPreparing {
|
||||
task_id: self.task_id.clone(),
|
||||
}
|
||||
.emit(&download_task.app);
|
||||
.emit(app);
|
||||
|
||||
self.prepare(&download_task.app)
|
||||
.await
|
||||
.wrap_err("准备下载失败")?;
|
||||
self.prepare(app).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; // 重置完成时间戳
|
||||
download_task.update_progress(|p| *p = self.clone());
|
||||
@@ -216,35 +227,36 @@ impl DownloadProgress {
|
||||
std::fs::create_dir_all(&self.episode_dir)
|
||||
.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 episode_info = None;
|
||||
|
||||
if !video_task.is_completed() && video_task.content_length != 0 {
|
||||
video_task
|
||||
if !self.video_task.is_completed() && self.video_task.content_length != 0 {
|
||||
self.video_task
|
||||
.process(download_task, self)
|
||||
.await
|
||||
.wrap_err("下载视频文件失败")?;
|
||||
tracing::debug!("视频下载任务完成");
|
||||
}
|
||||
|
||||
if !audio_task.is_completed() && audio_task.content_length != 0 {
|
||||
audio_task
|
||||
if !self.audio_task.is_completed() && self.audio_task.content_length != 0 {
|
||||
self.audio_task
|
||||
.process(download_task, self)
|
||||
.await
|
||||
.wrap_err("下载音频文件失败")?;
|
||||
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 {
|
||||
download_task.update_progress(|p| {
|
||||
p.video_process_task.skipped = true;
|
||||
@@ -252,47 +264,47 @@ impl DownloadProgress {
|
||||
});
|
||||
tracing::debug!("受版权保护(DRM),无法处理,已跳过视频处理任务");
|
||||
} else if !video_process_task_is_completed {
|
||||
video_process_task
|
||||
self.video_process_task
|
||||
.process(download_task, self, &mut player_info)
|
||||
.await
|
||||
.wrap_err("视频处理失败")?;
|
||||
tracing::debug!("视频处理任务完成");
|
||||
}
|
||||
|
||||
if !danmaku_task.is_completed() {
|
||||
danmaku_task
|
||||
if !self.danmaku_task.is_completed() {
|
||||
self.danmaku_task
|
||||
.process(download_task, self)
|
||||
.await
|
||||
.wrap_err("下载弹幕失败")?;
|
||||
tracing::debug!("弹幕下载任务完成");
|
||||
}
|
||||
|
||||
if !subtitle_task.is_completed() {
|
||||
subtitle_task
|
||||
if !self.subtitle_task.is_completed() {
|
||||
self.subtitle_task
|
||||
.process(download_task, self, &mut player_info)
|
||||
.await
|
||||
.wrap_err("下载字幕失败")?;
|
||||
tracing::debug!("字幕下载任务完成");
|
||||
}
|
||||
|
||||
if !cover_task.is_completed() {
|
||||
cover_task
|
||||
if !self.cover_task.is_completed() {
|
||||
self.cover_task
|
||||
.process(download_task, self)
|
||||
.await
|
||||
.wrap_err("下载封面失败")?;
|
||||
tracing::debug!("封面下载任务完成");
|
||||
}
|
||||
|
||||
if !nfo_task.is_completed() {
|
||||
nfo_task
|
||||
if !self.nfo_task.is_completed() {
|
||||
self.nfo_task
|
||||
.process(download_task, self, &mut episode_info)
|
||||
.await
|
||||
.wrap_err("下载NFO失败")?;
|
||||
tracing::debug!("NFO下载任务完成");
|
||||
}
|
||||
|
||||
if !json_task.is_completed() {
|
||||
json_task
|
||||
if !self.json_task.is_completed() {
|
||||
self.json_task
|
||||
.process(download_task, self, &mut episode_info)
|
||||
.await
|
||||
.wrap_err("下载JSON元数据失败")?;
|
||||
@@ -303,8 +315,17 @@ impl DownloadProgress {
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.ok();
|
||||
if completed_ts.is_some() {
|
||||
download_task.update_progress(|p| p.completed_ts = completed_ts);
|
||||
if let Some(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(())
|
||||
|
||||
@@ -5,6 +5,7 @@ use tauri_specta::Event;
|
||||
use crate::downloader::{
|
||||
download_progress::DownloadProgress, download_task_state::DownloadTaskState,
|
||||
};
|
||||
use crate::types::plugin_info::PluginInfo;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -46,3 +47,11 @@ pub enum DownloadEvent {
|
||||
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,
|
||||
config::Config,
|
||||
downloader::{download_manager::DownloadManager, download_progress::DownloadProgress},
|
||||
plugin::plugin_manager::PluginManager,
|
||||
types::player_info::PlayerInfo,
|
||||
};
|
||||
|
||||
@@ -24,6 +25,7 @@ pub trait AppHandleExt {
|
||||
fn get_config(&self) -> State<'_, RwLock<Config>>;
|
||||
fn get_bili_client(&self) -> State<'_, BiliClient>;
|
||||
fn get_download_manager(&self) -> State<'_, DownloadManager>;
|
||||
fn get_plugin_manager(&self) -> State<'_, PluginManager>;
|
||||
}
|
||||
|
||||
impl AppHandleExt for AppHandle {
|
||||
@@ -36,6 +38,9 @@ impl AppHandleExt for AppHandle {
|
||||
fn get_download_manager(&self) -> State<'_, DownloadManager> {
|
||||
self.state::<DownloadManager>()
|
||||
}
|
||||
fn get_plugin_manager(&self) -> State<'_, PluginManager> {
|
||||
self.state::<PluginManager>()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait GetOrInitPlayerInfo {
|
||||
|
||||
+24
-9
@@ -7,6 +7,7 @@ mod errors;
|
||||
mod events;
|
||||
mod extensions;
|
||||
mod logger;
|
||||
mod plugin;
|
||||
mod types;
|
||||
mod utils;
|
||||
mod wbi;
|
||||
@@ -16,14 +17,14 @@ mod protobuf {
|
||||
}
|
||||
|
||||
use commands::{
|
||||
create_download_tasks, delete_download_tasks, generate_qrcode, get_available_media_formats,
|
||||
get_bangumi_follow_info, get_bangumi_info, get_config, get_fav_folders, get_fav_info,
|
||||
get_history_info, get_logs_dir_size, get_normal_info, get_qrcode_status, get_skip_segments,
|
||||
get_user_info, get_user_video_info, get_watch_later_info, pause_download_tasks,
|
||||
restart_download_task, restart_download_tasks, restore_download_tasks, resume_download_tasks,
|
||||
save_config, search, show_path_in_file_manager,
|
||||
add_plugin, create_download_tasks, delete_download_tasks, generate_qrcode,
|
||||
get_available_media_formats, get_bangumi_follow_info, get_bangumi_info, get_config,
|
||||
get_fav_folders, get_fav_info, get_history_info, get_logs_dir_size, get_normal_info,
|
||||
get_plugin_infos, get_qrcode_status, get_skip_segments, get_user_info, get_user_video_info,
|
||||
get_watch_later_info, pause_download_tasks, restart_download_task, restart_download_tasks,
|
||||
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 parking_lot::RwLock;
|
||||
use tauri::{Manager, Wry};
|
||||
@@ -31,9 +32,11 @@ use tauri::{Manager, Wry};
|
||||
use crate::{
|
||||
bili_client::BiliClient,
|
||||
commands::open_log_file,
|
||||
config::Config,
|
||||
downloader::download_manager::DownloadManager,
|
||||
errors::install_custom_eyre_handler,
|
||||
events::{DownloadEvent, LogEvent},
|
||||
events::{DownloadEvent, LogEvent, PluginEvent},
|
||||
plugin::plugin_manager::PluginManager,
|
||||
};
|
||||
|
||||
fn generate_context() -> tauri::Context<Wry> {
|
||||
@@ -49,6 +52,7 @@ pub fn run() {
|
||||
.commands(tauri_specta::collect_commands![
|
||||
get_config,
|
||||
save_config,
|
||||
get_plugin_infos,
|
||||
generate_qrcode,
|
||||
get_qrcode_status,
|
||||
get_user_info,
|
||||
@@ -73,8 +77,16 @@ pub fn run() {
|
||||
get_skip_segments,
|
||||
get_available_media_formats,
|
||||
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)]
|
||||
builder
|
||||
@@ -122,6 +134,9 @@ pub fn run() {
|
||||
|
||||
logger::init(app.handle())?;
|
||||
|
||||
let plugin_manager = PluginManager::new(app.handle())?;
|
||||
app.manage(plugin_manager);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(generate_context())
|
||||
|
||||
@@ -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;
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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_media_url;
|
||||
pub mod player_info;
|
||||
pub mod plugin_info;
|
||||
pub mod qrcode_data;
|
||||
pub mod qrcode_status;
|
||||
pub mod restart_download_task_params;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user