From 8ac6829e618adce3525a0ba3d61b2a9400f25c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=80=E1=B4=8D=E1=B4=9B=E1=B4=8F=E1=B4=80=E1=B4=87?= =?UTF-8?q?=CA=80?= Date: Fri, 7 Nov 2025 20:37:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E5=99=A8=EF=BC=8C=E5=9C=A8=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E6=BA=90=E5=A4=84=E7=90=86=E6=88=96=E6=95=B4=E4=B8=AA=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E4=BB=BB=E5=8A=A1=E5=87=BA=E7=8E=B0=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=97=B6=E4=BC=9A=E8=A7=A6=E5=8F=91=E6=B6=88=E6=81=AF=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=20(#526)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/bili_sync/src/bilibili/client.rs | 4 + crates/bili_sync/src/config/current.rs | 4 + .../bili_sync/src/config/versioned_config.rs | 2 +- crates/bili_sync/src/main.rs | 1 + crates/bili_sync/src/notifier/mod.rs | 46 ++++++ crates/bili_sync/src/task/video_downloader.rs | 15 +- web/src/lib/types.ts | 15 ++ web/src/routes/settings/+page.svelte | 146 +++++++++++++++++- web/src/routes/settings/NotifierDialog.svelte | 122 +++++++++++++++ 9 files changed, 350 insertions(+), 5 deletions(-) create mode 100644 crates/bili_sync/src/notifier/mod.rs create mode 100644 web/src/routes/settings/NotifierDialog.svelte 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} +
+ +
+ + +