diff --git a/crates/bili_sync/src/bilibili/client.rs b/crates/bili_sync/src/bilibili/client.rs index ffeb2a9..8e41e36 100644 --- a/crates/bili_sync/src/bilibili/client.rs +++ b/crates/bili_sync/src/bilibili/client.rs @@ -134,4 +134,8 @@ impl BiliClient { pub async fn wbi_img(&self, credential: &Credential) -> Result { credential.wbi_img(&self.client).await } + + pub fn inner_client(&self) -> &reqwest::Client { + &self.client.0 + } } diff --git a/crates/bili_sync/src/config/current.rs b/crates/bili_sync/src/config/current.rs index 9ff6ed3..8d02389 100644 --- a/crates/bili_sync/src/config/current.rs +++ b/crates/bili_sync/src/config/current.rs @@ -11,6 +11,7 @@ use crate::config::default::{default_auth_token, default_bind_address, default_t use crate::config::item::{ ConcurrentLimit, NFOTimeType, SkipOption, default_collection_path, default_favorite_path, default_submission_path, }; +use crate::notifier::Notifier; use crate::utils::model::{load_db_config, save_db_config}; pub static CONFIG_DIR: LazyLock = @@ -27,6 +28,8 @@ pub struct Config { pub skip_option: SkipOption, pub video_name: String, pub page_name: String, + #[serde(default)] + pub notifiers: Option>, #[serde(default = "default_favorite_path")] pub favorite_default_path: String, #[serde(default = "default_collection_path")] @@ -98,6 +101,7 @@ impl Default for Config { skip_option: SkipOption::default(), video_name: "{{title}}".to_owned(), page_name: "{{bvid}}".to_owned(), + notifiers: None, favorite_default_path: default_favorite_path(), collection_default_path: default_collection_path(), submission_default_path: default_submission_path(), diff --git a/crates/bili_sync/src/config/versioned_config.rs b/crates/bili_sync/src/config/versioned_config.rs index f6fc688..6eefe4e 100644 --- a/crates/bili_sync/src/config/versioned_config.rs +++ b/crates/bili_sync/src/config/versioned_config.rs @@ -8,7 +8,7 @@ use tokio::sync::{OnceCell, watch}; use crate::bilibili::Credential; use crate::config::{CONFIG_DIR, Config}; -pub static VERSIONED_CONFIG: OnceCell = OnceCell::const_new(); +static VERSIONED_CONFIG: OnceCell = OnceCell::const_new(); pub struct VersionedConfig { inner: ArcSwap, diff --git a/crates/bili_sync/src/main.rs b/crates/bili_sync/src/main.rs index fa3bc97..faac80f 100644 --- a/crates/bili_sync/src/main.rs +++ b/crates/bili_sync/src/main.rs @@ -8,6 +8,7 @@ mod config; mod database; mod downloader; mod error; +mod notifier; mod task; mod utils; mod workflow; diff --git a/crates/bili_sync/src/notifier/mod.rs b/crates/bili_sync/src/notifier/mod.rs new file mode 100644 index 0000000..0dd1578 --- /dev/null +++ b/crates/bili_sync/src/notifier/mod.rs @@ -0,0 +1,46 @@ +use anyhow::Result; +use futures::future; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", tag = "type")] +pub enum Notifier { + Telegram { bot_token: String, chat_id: String }, + Webhook { url: String }, +} + +#[derive(Serialize)] +struct WebhookPayload<'a> { + text: &'a str, +} + +pub trait NotifierAllExt { + async fn notify_all(&self, client: &reqwest::Client, message: &str) -> Result<()>; +} + +impl NotifierAllExt for Option> { + async fn notify_all(&self, client: &reqwest::Client, message: &str) -> Result<()> { + let Some(notifiers) = self else { + return Ok(()); + }; + future::join_all(notifiers.iter().map(|notifier| notifier.notify(client, message))).await; + Ok(()) + } +} + +impl Notifier { + pub async fn notify(&self, client: &reqwest::Client, message: &str) -> Result<()> { + match self { + Notifier::Telegram { bot_token, chat_id } => { + let url = format!("https://api.telegram.org/bot{}/sendMessage", bot_token); + let params = [("chat_id", chat_id.as_str()), ("text", message)]; + client.post(&url).form(¶ms).send().await?; + } + Notifier::Webhook { url } => { + let payload = WebhookPayload { text: message }; + client.post(url).json(&payload).send().await?; + } + } + Ok(()) + } +} diff --git a/crates/bili_sync/src/task/video_downloader.rs b/crates/bili_sync/src/task/video_downloader.rs index 8a818cb..9fa74d0 100644 --- a/crates/bili_sync/src/task/video_downloader.rs +++ b/crates/bili_sync/src/task/video_downloader.rs @@ -8,6 +8,7 @@ use tokio::time; use crate::adapter::VideoSource; use crate::bilibili::{self, BiliClient}; use crate::config::{Config, TEMPLATE, VersionedConfig}; +use crate::notifier::NotifierAllExt; use crate::utils::model::get_enabled_video_sources; use crate::utils::task_notifier::TASK_STATUS_NOTIFIER; use crate::workflow::process_video_source; @@ -20,7 +21,12 @@ pub async fn video_downloader(connection: DatabaseConnection, bili_client: Arc i !== index); + } + async function loadConfig() { loading = true; try { @@ -142,11 +193,12 @@ {:else if formData}
- + 基本设置 B站认证 视频处理 弹幕渲染 + 通知设置 高级设置 @@ -620,6 +672,66 @@
+ + +
+
+
+

通知器管理

+

+ 配置通知器,在下载任务出现错误时发送通知 +

+
+ +
+ + {#if !formData.notifiers || formData.notifiers.length === 0} +
+

暂无通知器配置

+ +
+ {:else} +
+ {#each formData.notifiers as notifier, index (index)} +
+
+ {#if notifier.type === 'telegram'} +
+ Telegram + + Chat ID: {notifier.chat_id} + +
+ {:else if notifier.type === 'webhook'} +
+ Webhook + + {notifier.url} + +
+ {/if} +
+
+ + +
+
+ {/each} +
+ {/if} +
+
+
@@ -756,3 +868,33 @@
{/if} + + + + + + + + {isEditing ? '编辑通知器' : '添加通知器'} + + 配置通知器类型和参数 + + + {#if showNotifierDialog} + { + if (isEditing && editingNotifierIndex !== null) { + updateNotifier(editingNotifierIndex, notifier); + } else { + addNotifier(notifier); + } + }} + onCancel={closeNotifierDialog} + /> + {/if} + + + diff --git a/web/src/routes/settings/NotifierDialog.svelte b/web/src/routes/settings/NotifierDialog.svelte new file mode 100644 index 0000000..11b7a9a --- /dev/null +++ b/web/src/routes/settings/NotifierDialog.svelte @@ -0,0 +1,122 @@ + + +
+
+ + +
+ + {#if type === 'telegram'} +
+ + +

从 @BotFather 获取的 Bot Token

+
+
+ + +

目标聊天室的 ID(个人用户、群组或频道)

+
+ {:else if type === 'webhook'} +
+ + +

+ 接收通知的 Webhook 地址
+ 格式示例:{jsonExample} +

+
+ {/if} +
+ +
+ + +