diff --git a/backend/src/module/api/notification.py b/backend/src/module/api/notification.py
index a5963228..3b58e562 100644
--- a/backend/src/module/api/notification.py
+++ b/backend/src/module/api/notification.py
@@ -48,6 +48,10 @@ class TestProviderConfigRequest(BaseModel):
api_token: Optional[str] = Field(default=None, description="API token")
template: Optional[str] = Field(default=None, description="Custom template")
url: Optional[str] = Field(default=None, description="URL for generic webhook")
+ sound: Optional[str] = Field(default=None, description="Bark sound")
+ group: Optional[str] = Field(default=None, description="Bark group")
+ level: Optional[str] = Field(default=None, description="Bark level")
+ icon: Optional[str] = Field(default=None, description="Bark icon URL")
class TestResponse(BaseModel):
@@ -108,6 +112,10 @@ async def test_provider_config(request: TestProviderConfigRequest):
api_token=request.api_token or "",
template=request.template,
url=request.url or "",
+ sound=request.sound,
+ group=request.group,
+ level=request.level,
+ icon=request.icon,
)
success, message = await NotificationManager.test_provider_config(config)
diff --git a/backend/src/module/models/config.py b/backend/src/module/models/config.py
index ec9ee49f..2465b26a 100644
--- a/backend/src/module/models/config.py
+++ b/backend/src/module/models/config.py
@@ -181,7 +181,19 @@ class NotificationProvider(BaseModel):
),
)
url_: Optional[str] = Field(
- default=None, alias="url", description="URL for generic webhook provider"
+ default=None,
+ alias="url",
+ description="URL for generic webhook provider; tap-to-open URL for bark",
+ )
+ # Bark 的可选推送参数,留空则不发送
+ sound: Optional[str] = Field(default=None, description="Bark sound name")
+ group: Optional[str] = Field(default=None, description="Bark message group")
+ level: Optional[str] = Field(
+ default=None,
+ description="Bark interruption level: active/timeSensitive/passive/critical",
+ )
+ icon: Optional[str] = Field(
+ default=None, description="Bark icon URL; defaults to the poster URL"
)
@property
diff --git a/backend/src/module/notification/providers/bark.py b/backend/src/module/notification/providers/bark.py
index d2fe5a48..70e0512e 100644
--- a/backend/src/module/notification/providers/bark.py
+++ b/backend/src/module/notification/providers/bark.py
@@ -22,30 +22,42 @@ class BarkProvider(NotificationProvider):
self.device_key = config.device_key
server_url = config.server_url or self.DEFAULT_SERVER
self.notification_url = f"{server_url.rstrip('/')}/push"
+ # 可选推送参数:只发送已设置的项,旧配置的请求体不变
+ self.options = {
+ "sound": config.sound,
+ "group": config.group,
+ "level": config.level,
+ "url": config.url,
+ }
+ self.icon = config.icon
+
+ def _payload(self, title: str, body: str, icon: str | None = None) -> dict:
+ data: dict[str, str | None] = {
+ "title": title,
+ "body": body,
+ "device_key": self.device_key,
+ }
+ data.update({k: v for k, v in self.options.items() if v})
+ # 配置的图标优先,其次是调用方给的(单集通知的海报)
+ if self.icon or icon:
+ data["icon"] = self.icon or icon
+ return data
async def send(self, notification: Notification) -> bool:
"""Send notification via Bark."""
text = self._format_message(notification)
- data = {
- "title": notification.official_title,
- "body": text,
- "device_key": self.device_key,
- }
- poster_url = self._poster_url(notification)
- if poster_url:
- data["icon"] = poster_url
-
+ data = self._payload(
+ notification.official_title, text, self._poster_url(notification)
+ )
resp = await self._post_json(self.notification_url, data)
logger.debug("Bark notification: %s", resp.status_code)
return resp.status_code == 200
async def test(self) -> tuple[bool, str]:
"""Test Bark configuration by sending a test notification."""
- data = {
- "title": "AutoBangumi",
- "body": "通知测试成功!\nNotification test successful!",
- "device_key": self.device_key,
- }
+ data = self._payload(
+ "AutoBangumi", "通知测试成功!\nNotification test successful!"
+ )
try:
resp = await self._post_json(self.notification_url, data)
if resp.status_code == 200:
@@ -57,6 +69,5 @@ class BarkProvider(NotificationProvider):
async def _deliver_text(self, title: str, body: str) -> bool:
"""Deliver a system event via Bark."""
- data = {"title": title, "body": body, "device_key": self.device_key}
- resp = await self._post_json(self.notification_url, data)
+ resp = await self._post_json(self.notification_url, self._payload(title, body))
return resp.status_code == 200
diff --git a/backend/src/test/test_notification.py b/backend/src/test/test_notification.py
index 3065e5e0..09275b1f 100644
--- a/backend/src/test/test_notification.py
+++ b/backend/src/test/test_notification.py
@@ -354,6 +354,67 @@ class TestBarkProvider:
call_args = mock_post.call_args[0]
assert "device_key" in call_args[1]
+ @pytest.mark.parametrize(
+ ("options", "expected"),
+ [
+ ({}, {}),
+ ({"sound": "", "level": ""}, {}),
+ (
+ {
+ "sound": "minuet",
+ "group": "AB",
+ "level": "timeSensitive",
+ "url": "https://ab.local",
+ },
+ {
+ "sound": "minuet",
+ "group": "AB",
+ "level": "timeSensitive",
+ "url": "https://ab.local",
+ },
+ ),
+ ],
+ )
+ async def test_deliver_text_sends_only_set_options(self, options, expected):
+ """#792:sound/group/level/url 只在设置时发送,旧配置的请求体不变。"""
+ config = ProviderConfig(type="bark", device_key="k", **options)
+ provider = BarkProvider(config)
+
+ with patch.object(provider, "_post_json", new_callable=AsyncMock) as mock_post:
+ mock_post.return_value = MagicMock(status_code=200)
+ assert await provider._deliver_text("T", "B") is True
+
+ assert mock_post.call_args[0][1] == {
+ "title": "T",
+ "body": "B",
+ "device_key": "k",
+ **expected,
+ }
+
+ @pytest.mark.parametrize(
+ ("icon", "poster", "expected"),
+ [
+ (None, "https://ab/p.jpg", "https://ab/p.jpg"),
+ ("https://ab/i.png", "https://ab/p.jpg", "https://ab/i.png"),
+ (None, None, None),
+ ],
+ )
+ async def test_send_icon_prefers_configured_icon_over_poster(
+ self, icon, poster, expected
+ ):
+ """配置的 icon 优先;未配置时沿用海报地址;都没有则不发 icon。"""
+ provider = BarkProvider(ProviderConfig(type="bark", device_key="k", icon=icon))
+ notify = Notification(official_title="Test Anime", season=1, episode=5)
+
+ with (
+ patch.object(provider, "_poster_url", return_value=poster),
+ patch.object(provider, "_post_json", new_callable=AsyncMock) as mock_post,
+ ):
+ mock_post.return_value = MagicMock(status_code=200)
+ await provider.send(notify)
+
+ assert mock_post.call_args[0][1].get("icon") == expected
+
class TestWebhookProvider:
@pytest.fixture
diff --git a/docs/config/notifier.md b/docs/config/notifier.md
index 7d0078a6..08d0f822 100644
--- a/docs/config/notifier.md
+++ b/docs/config/notifier.md
@@ -24,7 +24,7 @@
- Telegram:`Bot Token`、`Chat ID`
- Discord:Webhook URL
- WeCom:Webhook URL、Key
-- Bark:Device Key,可选 Server URL
+- Bark:Device Key,可选 Server URL、铃声、分组、中断级别、图标地址、点击打开的地址
- Server Chan:SendKey
- Gotify:Server URL、App Token
- Pushover:User Key、API Token
@@ -53,9 +53,10 @@ Webhook 模板可以使用 `{{title}}`、`{{season}}`、`{{episode}}`、`{{poste
| `token` | Telegram(Bot Token)、Server Chan(SendKey)、WeCom(Key)、Gotify(App Token)使用 |
| `chat_id` | Telegram 使用 |
| `webhook_url` | Discord、WeCom 使用 |
-| `url` | Webhook 使用 |
+| `url` | Webhook 使用;Bark 中为点击通知后打开的地址(可选) |
| `server_url` | Bark(可选)、Gotify 使用 |
| `device_key` | Bark 使用 |
+| `sound` / `group` / `level` / `icon` | Bark 可选:铃声、分组、中断级别(`active`、`timeSensitive`、`passive`、`critical`)、图标地址。留空则不发送;`icon` 留空时单集通知用海报地址 |
| `user_key` / `api_token` | Pushover 使用 |
| `template` | 自定义通知模板 |
diff --git a/docs/en/config/notifier.md b/docs/en/config/notifier.md
index 37a23587..0a167cf5 100644
--- a/docs/en/config/notifier.md
+++ b/docs/en/config/notifier.md
@@ -24,7 +24,7 @@ Provider-specific fields include:
- Telegram: `Bot Token`, `Chat ID`
- Discord: Webhook URL
- WeCom: Webhook URL and Key
-- Bark: Device Key and optional Server URL
+- Bark: Device Key. Optional: Server URL, sound, group, interruption level, icon URL, and the URL to open on tap
- Server Chan: SendKey
- Gotify: Server URL and App Token
- Pushover: User Key and API Token
@@ -53,9 +53,10 @@ Each object in `providers` usually contains:
| `token` | Used by Telegram (Bot Token), Server Chan (SendKey), WeCom (Key) and Gotify (App Token) |
| `chat_id` | Used by Telegram |
| `webhook_url` | Used by Discord and WeCom |
-| `url` | Used by Webhook |
+| `url` | Used by Webhook. For Bark, the URL to open on tap (optional) |
| `server_url` | Used by Bark (optional) and Gotify |
| `device_key` | Used by Bark |
+| `sound` / `group` / `level` / `icon` | Optional for Bark: sound, group, interruption level (`active`, `timeSensitive`, `passive`, `critical`), and icon URL. AB does not send an empty field. When `icon` is empty, episode notifications use the poster URL |
| `user_key` / `api_token` | Used by Pushover |
| `template` | Custom message template |
diff --git a/docs/ja/config/notifier.md b/docs/ja/config/notifier.md
index 7ca46ae6..f0f8ed42 100644
--- a/docs/ja/config/notifier.md
+++ b/docs/ja/config/notifier.md
@@ -42,9 +42,10 @@ Webhookテンプレートでは `{{title}}`、`{{season}}`、`{{episode}}`、`{{
| `token` | Telegram(Bot Token)、Server Chan(SendKey)、WeCom(Key)、Gotify(App Token)で使用 |
| `chat_id` | Telegram で使用 |
| `webhook_url` | Discord、WeCom で使用 |
-| `url` | Webhook で使用 |
+| `url` | Webhook で使用。Bark ではタップで開く URL(任意) |
| `server_url` | Bark(任意)、Gotify で使用 |
| `device_key` | Bark で使用 |
+| `sound` / `group` / `level` / `icon` | Bark(任意):通知音、グループ、割り込みレベル(`active`、`timeSensitive`、`passive`、`critical`)、アイコン URL。空の項目は送信しません。`icon` が空のとき、エピソード通知はポスターの URL を使います |
| `user_key` / `api_token` | Pushover で使用 |
| `template` | カスタム通知テンプレート |
diff --git a/webui/src/api/notification.ts b/webui/src/api/notification.ts
index 8ddf1a70..4dd0d617 100644
--- a/webui/src/api/notification.ts
+++ b/webui/src/api/notification.ts
@@ -17,6 +17,10 @@ export interface TestProviderConfigRequest {
api_token?: string;
template?: string;
url?: string;
+ sound?: string;
+ group?: string;
+ level?: string;
+ icon?: string;
}
export interface TestResponse {
diff --git a/webui/src/components/setting/config-notification.vue b/webui/src/components/setting/config-notification.vue
index b6464bc5..daa24852 100644
--- a/webui/src/components/setting/config-notification.vue
+++ b/webui/src/components/setting/config-notification.vue
@@ -42,8 +42,10 @@ const providerFields: Record<
string,
{
key: keyof NotificationProviderConfig;
- label: string;
+ label: string | (() => string);
placeholder: string;
+ // 有选项时渲染为可清空的下拉框
+ options?: string[];
}[]
> = {
telegram: [
@@ -64,6 +66,32 @@ const providerFields: Record<
label: 'Server URL (optional)',
placeholder: 'https://api.day.app',
},
+ {
+ key: 'sound',
+ label: () => t('config.notification_set.bark_sound'),
+ placeholder: 'minuet',
+ },
+ {
+ key: 'group',
+ label: () => t('config.notification_set.bark_group'),
+ placeholder: 'AutoBangumi',
+ },
+ {
+ key: 'level',
+ label: () => t('config.notification_set.bark_level'),
+ placeholder: 'active',
+ options: ['active', 'timeSensitive', 'passive', 'critical'],
+ },
+ {
+ key: 'icon',
+ label: () => t('config.notification_set.bark_icon'),
+ placeholder: 'https://example.com/icon.png',
+ },
+ {
+ key: 'url',
+ label: () => t('config.notification_set.bark_url'),
+ placeholder: 'https://example.com',
+ },
],
'server-chan': [{ key: 'token', label: 'SendKey', placeholder: 'sendkey' }],
wecom: [
@@ -385,8 +413,15 @@ function getFieldsForType(type: string) {
:key="field.key"
:label="field.label"
>
+
@@ -457,8 +492,15 @@ function getFieldsForType(type: string) {
:key="field.key"
:label="field.label"
>
+
diff --git a/webui/src/i18n/en.json b/webui/src/i18n/en.json
index 1771eae5..40255217 100644
--- a/webui/src/i18n/en.json
+++ b/webui/src/i18n/en.json
@@ -119,7 +119,12 @@
"test_success": "Test successful",
"test_failed": "Test failed",
"plugin_label": "{id} (plugin)",
- "plugin_hint": "This channel comes from a plugin and has no fields here. Enter its credentials in the plugin's options in the Plugins section."
+ "plugin_hint": "This channel comes from a plugin and has no fields here. Enter its credentials in the plugin's options in the Plugins section.",
+ "bark_sound": "Sound (optional)",
+ "bark_group": "Group (optional)",
+ "bark_level": "Interruption level (optional)",
+ "bark_icon": "Icon URL (optional, defaults to the poster)",
+ "bark_url": "URL to open on tap (optional)"
},
"parser_set": {
"enable": "Enable",
@@ -245,7 +250,8 @@
"permission_labels": {
"fs_write": "Write files",
"network": "Network access",
- "filesystem": "File system access"
+ "filesystem": "File system access",
+ "subprocess": "Run external commands"
},
"disable": "Disable",
"disable_confirm_title": "Disable {name}?",
diff --git a/webui/src/i18n/zh-CN.json b/webui/src/i18n/zh-CN.json
index 9a69a721..294e9970 100644
--- a/webui/src/i18n/zh-CN.json
+++ b/webui/src/i18n/zh-CN.json
@@ -119,7 +119,12 @@
"test_success": "测试成功",
"test_failed": "测试失败",
"plugin_label": "{id}(插件)",
- "plugin_hint": "此渠道由插件提供,这里没有可填字段。请在插件设置中该插件的配置里填写凭据。"
+ "plugin_hint": "此渠道由插件提供,这里没有可填字段。请在插件设置中该插件的配置里填写凭据。",
+ "bark_sound": "铃声(可选)",
+ "bark_group": "分组(可选)",
+ "bark_level": "中断级别(可选)",
+ "bark_icon": "图标地址(可选,默认用海报)",
+ "bark_url": "点击打开的地址(可选)"
},
"parser_set": {
"enable": "启用",
@@ -245,7 +250,8 @@
"permission_labels": {
"fs_write": "写入文件",
"network": "访问网络",
- "filesystem": "访问文件系统"
+ "filesystem": "访问文件系统",
+ "subprocess": "运行外部命令"
},
"disable": "停用",
"disable_confirm_title": "停用 {name}?",
diff --git a/webui/types/config.ts b/webui/types/config.ts
index f1ab7766..c4f874f5 100644
--- a/webui/types/config.ts
+++ b/webui/types/config.ts
@@ -95,6 +95,11 @@ export interface NotificationProviderConfig {
api_token?: string;
template?: string;
url?: string;
+ // Bark 的可选推送参数
+ sound?: string;
+ group?: string;
+ level?: string;
+ icon?: string;
}
export interface Notification {