Skip to content

Commit 85fc6f6

Browse files
committed
feat: release v0.1.3 (add webhook apikey & smart refresh)
1 parent e98341c commit 85fc6f6

7 files changed

Lines changed: 149 additions & 23 deletions

File tree

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@
6262
```
6363
{
6464
"title": "${title}",
65-
"text": "${text}"
65+
"text": "${text}",
66+
"apikey": "如果配置了WEBHOOK_APIKEY则需要此字段"
6667
}
6768
```
6869
5. 消息类型 选择 `整理入库`
@@ -179,6 +180,7 @@ services:
179180
user: "1000:1001"
180181
environment:
181182
- WEBHOOK_DELAY=60 # Webhook延迟触发秒数 取决是否需要等MoviePilot刮削完成后同步,如果不需要可以设置为3
183+
# - WEBHOOK_APIKEY= # Webhook API Key;若设置,则请求必须包含 apikey 字段且值匹配;若留空则不校验
182184
# - WEBHOOK_OPENLIST_NAME=OpenList # Webhook 自动创建任务时使用的 OpenList 引擎备注名;若未配置或留空,默认使用第一个引擎
183185
- TVsource=/media/电视剧 # 电视剧源根目录
184186
- MOVsource=/media/电影 # 电影源根目录
@@ -261,9 +263,10 @@ task_timeout=72
261263

262264
| 变量 | 说明 | 示例 |
263265
|---|---|---|
264-
| `WEBHOOK_DELAY` | Webhook 延迟触发时间(秒) | `60` |
265-
| `WEBHOOK_OPENLIST_NAME` | Webhook 自动创建任务时使用的 OpenList 引擎备注名;若未配置或留空,默认使用第一个引擎 | `OpenList` |
266-
| `TVsource` | 电视剧源根 | `/media/电视剧` |
266+
264.| `WEBHOOK_DELAY` | Webhook 延迟触发时间(秒) | `60` |
267+
265.| `WEBHOOK_APIKEY` | Webhook API Key,可选。设置后,Webhook 请求必须包含 `apikey` 字段且值匹配 | `123456` |
268+
266.| `WEBHOOK_OPENLIST_NAME` | Webhook 自动创建任务时使用的 OpenList 引擎备注名;若未配置或留空,默认使用第一个引擎 | `OpenList` |
269+
267.| `TVsource` | 电视剧源根 | `/media/电视剧` |
267270
| `MOVsource` | 电影源根 | `/media/电影` |
268271
| `DST_TV_TARGETS` / `DST_MOV_TARGETS` | 优先同步根集合(原样使用),存在同名目录时仅同步到这里;仅在末尾追加“名称(年份)”;支持 `,;:` 分隔 | 例如 `/shanct/电视剧` 或 `/shanct/电影` |
269272
| `SYNC_TV_TARGETS` | 电视剧同步目标根集合,用 `,;:` 分隔,支持 `{max}`;仅在末尾追加“名称(年份)” | 例如 `/115/videos/电视剧,/ODC/tv{max}/电视剧` 或 `/115/videos/tv,tv{max}/tv` |
@@ -282,7 +285,7 @@ task_timeout=72
282285
- `GET/POST/PUT/DELETE /svr/openlist` 引擎管理(列表、子目录、增删改)
283286
- `GET/POST/PUT/DELETE /svr/job` 作业管理(列表、详情、手动执行、启用/禁用、中止、删除)
284287
- `GET/POST/PUT/DELETE /svr/notify` 通知配置(列表、增删改、测试)
285-
- `POST /webhook` Webhook 触发(标题解析、自动建作业与刷新)
288+
- `POST /webhook` Webhook 触发(标题解析、自动建作业与刷新)。支持参数 `apikey`(URL参数或Body参数),若服务端配置了 `WEBHOOK_APIKEY` 则必须提供且匹配。
286289

287290
## 排除项规则简单说明
288291

common/commonService.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,20 @@ def setLogger(cusLevel=None):
4040

4141

4242
def get_post_data(self):
43-
post_data = self.request.arguments
44-
post_data = {x: post_data.get(x)[0].decode("utf-8") for x in post_data.keys()}
45-
if not post_data:
46-
post_data = self.request.body.decode('utf-8')
47-
if post_data and post_data != '':
48-
post_data = json.loads(post_data)
49-
else:
50-
post_data = {}
43+
# 获取 URL 参数和表单参数
44+
args = self.request.arguments
45+
post_data = {x: args.get(x)[0].decode("utf-8") for x in args.keys()}
46+
47+
# 尝试解析 Body 中的 JSON 数据
48+
try:
49+
body_data = self.request.body.decode('utf-8')
50+
if body_data and body_data.strip() != '':
51+
json_data = json.loads(body_data)
52+
if isinstance(json_data, dict):
53+
post_data.update(json_data)
54+
except Exception:
55+
pass
56+
5157
return post_data
5258

5359

doc/changelog/v0.1.3.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## v0.1.3
2+
3+
### 🚀 Features
4+
5+
- **Webhook**: 支持 `WEBHOOK_APIKEY` 环境变量进行安全验证,防止未授权调用。
6+
- **Webhook**: 优化源目录匹配逻辑,支持自动尝试将半角冒号(`:`)与全角冒号(``)相互替换查找,提高匹配成功率。
7+
- **Refresh**: 新增智能刷新策略:
8+
1. 优先使用环境变量(`SYNC_REFRESH_*`)中配置的路径(支持 `{max}` 自动解析)。
9+
2. **兜底保障**:如果环境变量解析出的路径未包含任务实际的同步目标路径(例如 `{max}` 解析为 `tv2` 但任务实际同步到了 `tv1`),系统会自动将任务实际目标路径(`tv1`)加入刷新列表。
10+
3. **智能过滤**:当刷新列表中至少有一个路径成功时,自动忽略那些因“对象不存在”而失败的路径(例如忽略 `tv2` 的报错),避免误报干扰。
11+
12+
### 🐛 Fixes
13+
14+
- **Webhook**: 修复了 Webhook 触发逻辑中的 `NameError` 变量作用域问题。
15+
16+
### 📝 Documentation
17+
18+
- 更新 `README.md``docker-compose.yaml`,补充 `WEBHOOK_APIKEY` 相关说明与配置示例。

docker-compose.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ services:
1010
user: "1000:1001"
1111
environment:
1212
- WEBHOOK_DELAY=60 # Webhook延迟触发秒数 取决是否需要等MoviePilot刮削完成后同步,如果不需要可以设置为3
13+
# - WEBHOOK_APIKEY= # Webhook API Key;若设置,则请求必须包含 apikey 字段且值匹配;若留空则不校验
1314
# - WEBHOOK_OPENLIST_NAME=OpenList # Webhook 自动创建任务时使用的 OpenList 引擎备注名;若未配置或留空,默认使用第一个引擎
1415
- TVsource=/media/电视剧 # 电视剧源根目录
1516
- MOVsource=/media/电影 # 电影源根目录

service/webhook/refreshService.py

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,6 @@ def max_replacer(match):
9797

9898
base = re.sub(r"(^|.*?/)([^/]*)\{max\}", max_replacer, base)
9999

100-
# 2. 移除旧占位符支持(根据用户要求)
101-
# base = base.replace('{odc_tv}', tv_prefix_default).replace('{odc_mov}', mov_prefix_default)
102-
103100
if not base.startswith('/'):
104101
base = '/' + base
105102
base = re.sub(r"/{2,}", "/", base).rstrip('/')
@@ -167,6 +164,16 @@ def refresh_after_task(job, status):
167164
if path not in seen:
168165
dedup.append(path)
169166
seen.add(path)
167+
168+
# 即使配置了刷新变量,也自动追加任务的目标路径(SYNC模式下)
169+
# 这样可以保证:不管变量怎么配,至少任务同步过去的地方会被刷新
170+
# 如果用户只想刷新变量指定的目录,不想刷新任务目标目录,这种场景比较少见,暂不考虑
171+
if not dst_used:
172+
for d in dsts:
173+
d = d.rstrip('/')
174+
if d not in seen:
175+
dedup.append(d)
176+
seen.add(d)
170177
else:
171178
# 如果未配置刷新变量,则使用默认策略
172179
if dst_used:
@@ -188,6 +195,28 @@ def refresh_after_task(job, status):
188195
dedup.append(d)
189196
seen.add(d)
190197
base_paths = dedup
198+
if not base_paths:
199+
# 如果通过变量解析出的刷新路径为空,尝试回退到默认策略(刷新任务目标路径)
200+
# 这种情况通常发生在配置了变量但变量解析结果为空,或者变量配置有误
201+
logger.info("Refresh base_paths empty from env, fallback to task dsts")
202+
if dst_used:
203+
# DST模式:默认刷新源目录
204+
base = (tv_src if is_tv else mov_src).strip()
205+
if base:
206+
base = re.sub(r"/{2,}", "/", base).rstrip('/')
207+
path = f"{base}/{name}"
208+
if path not in seen:
209+
dedup.append(path)
210+
seen.add(path)
211+
else:
212+
# SYNC模式:默认刷新所有同步目标目录
213+
for d in dsts:
214+
d = d.rstrip('/')
215+
if d not in seen:
216+
dedup.append(d)
217+
seen.add(d)
218+
base_paths = dedup
219+
191220
if not base_paths:
192221
logger.info("Refresh skipped: no base_paths found")
193222
return
@@ -205,16 +234,32 @@ def refresh_after_task(job, status):
205234
if ok:
206235
ok_list.append(p)
207236
else:
208-
fail_list.append(f"{p}{msg}")
237+
fail_list.append({'path': p, 'msg': msg})
209238

210-
logger.info(f"Refresh result: ok={len(ok_list)}, fail={len(fail_list)}")
239+
# 智能过滤:如果至少有一个路径刷新成功,则忽略那些因为“对象不存在”而失败的路径
240+
# 场景:配置了 {max} 变量指向了新盘,但老剧集实际存在于旧盘。
241+
# 旧盘路径通常包含在 dsts 中并会刷新成功,此时新盘路径的“不存在”报错是可以忽略的。
242+
if ok_list:
243+
final_fail_list = []
244+
for item in fail_list:
245+
msg = item['msg'] or ''
246+
if 'object not found' in msg.lower():
247+
logger.info(f"Refresh failed but ignored (covered by other success): {item['path']}")
248+
continue
249+
final_fail_list.append(f"{item['path']}{msg}")
250+
fail_list_str = final_fail_list
251+
else:
252+
# 如果全部失败,则保留所有报错
253+
fail_list_str = [f"{x['path']}{x['msg']}" for x in fail_list]
254+
255+
logger.info(f"Refresh result: ok={len(ok_list)}, fail={len(fail_list_str)}")
211256

212257
notify_list = notifyService.getNotifyList(True)
213258
if not notify_list:
214259
logger.info("Refresh notify skipped: no notify config")
215260
return
216-
title = ('目录刷新完成 ✔️' if not fail_list else '目录刷新失败 ❌')
217-
content = ("全部目录刷新成功:\n" + "\n".join(ok_list)) if not fail_list else ("以下目录刷新失败:\n" + "\n".join(fail_list))
261+
title = ('目录刷新完成 ✔️' if not fail_list_str else '目录刷新失败 ❌')
262+
content = ("全部目录刷新成功:\n" + "\n".join(ok_list)) if not fail_list_str else ("以下目录刷新失败:\n" + "\n".join(fail_list_str))
218263
for notify in notify_list:
219264
try:
220265
notifyService.sendNotify(notify, title, content, False)

service/webhook/webhookService.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,35 @@ def handleWebhook(req):
1212
}
1313
title = None
1414
text = None
15+
if isinstance(req, dict):
16+
# 兼容 apikey 在第一层的情况,无论是否在 title/text 中找到
17+
if 'apikey' in req:
18+
req_apikey_val = str(req.get('apikey')).strip()
19+
# 临时存入 req 以便后续验证逻辑使用(虽然 req 本身就是 dict,但为了逻辑清晰)
20+
req['apikey'] = req_apikey_val
21+
22+
# API Key 验证逻辑(前置)
23+
# 只要配置了 API Key,无论是否解析出 remark,都要验证
24+
# 注意:必须在 parse_tv_title_to_remark 之前验证,因为如果 title/text 为 None,
25+
# 之前的逻辑会直接跳过。但现在我们需要在这里拦截。
26+
# 从 req 中获取 apikey(如果是 dict)
27+
req_key = None
28+
if isinstance(req, dict):
29+
req_key = (req.get('apikey') or '').strip()
30+
31+
api_key_env = (os.getenv('WEBHOOK_APIKEY') or '').strip()
32+
# 只有当环境变量中有值,且值不为空时,才进行验证
33+
# 如果环境变量配置了但为空(如 WEBHOOK_APIKEY=),则视为不启用验证
34+
if api_key_env:
35+
if req_key != api_key_env:
36+
try:
37+
import logging
38+
logging.getLogger().warning(f"Webhook ignored: apikey mismatch. Expecting configured key, got '{req_key}'")
39+
except Exception:
40+
pass
41+
result['job'] = 'ignored: apikey mismatch'
42+
return result
43+
1544
if isinstance(req, dict):
1645
title = req.get('title')
1746
text = req.get('text')
@@ -26,14 +55,17 @@ def parse_tv_title_to_remark(s):
2655
return None
2756
if '已入库' not in s:
2857
return None
29-
m = re.search(r"\s*(.+?\(\d{4}\))\s*(S\d{1,2}|E\d{1,3}|E\d{1,3}-E?\d{1,3})?\s*已入库", s)
58+
# 优化正则:支持 "剧名 (年份) S01 E01 已入库" 或 "剧名 (年份) S01E01 已入库" 等多种格式
59+
# 只要能提取出 "剧名 (年份)" 即可,中间的内容不做严格限制
60+
m = re.search(r"\s*(.+?\(\d{4}\)).*?已入库", s)
3061
if m:
3162
return m.group(1).strip()
3263
m2 = re.search(r"^\s*(.+?\(\d{4}\))", s)
3364
if m2:
3465
return m2.group(1).strip()
3566
return None
3667
remark = parse_tv_title_to_remark(title) or parse_tv_title_to_remark(text)
68+
3769
if remark:
3870
delay = req.get('delay', None)
3971
def _read_env_file(key):
@@ -60,6 +92,7 @@ def _read_env_file(key):
6092
except Exception:
6193
delay = 30
6294
def _trigger():
95+
nonlocal remark
6396
try:
6497
import logging
6598
lg = logging.getLogger()
@@ -136,13 +169,33 @@ def _is_tv(s):
136169
mov_src = os.getenv('MOVsource') or ''
137170
media_root = tv_src if tv_flag else mov_src
138171
has_src = False
172+
173+
# 尝试查找源目录,支持半角冒号转全角冒号
174+
final_remark = remark
139175
if client is not None:
140176
try:
141177
dirs = client.filePathList(media_root)
142178
names = [d['path'] for d in dirs]
143-
has_src = remark in names
179+
if remark in names:
180+
has_src = True
181+
elif ':' in remark:
182+
# 尝试将半角冒号替换为全角冒号
183+
remark_cn = remark.replace(':', ':')
184+
if remark_cn in names:
185+
has_src = True
186+
final_remark = remark_cn
187+
elif ':' in remark:
188+
# 尝试将全角冒号替换为半角冒号
189+
remark_en = remark.replace(':', ':')
190+
if remark_en in names:
191+
has_src = True
192+
final_remark = remark_en
144193
except Exception:
145194
has_src = False
195+
196+
# 更新 remark 为实际存在的目录名(如果有替换)
197+
remark = final_remark
198+
146199
force_create = False
147200
try:
148201
force_create = bool(req.get('force', False)) or (os.getenv('WEBHOOK_FORCE_CREATE', 'false').lower() in ['1','true','yes'])

version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
v0.1.2,latest
1+
v0.1.3,latest
22

33

44
该文件仅第一行有效,表示打包的版本。

0 commit comments

Comments
 (0)