diff --git a/app/run.py b/app/run.py
index ae22fe1..acea619 100644
--- a/app/run.py
+++ b/app/run.py
@@ -282,6 +282,43 @@ def delete_file():
return jsonify(response)
+def get_hook_actions(plugins):
+ hook_actions = {}
+ for _, plugin in plugins.items():
+ for name in dir(plugin):
+ method = getattr(plugin, name)
+ if callable(method) and hasattr(method, "__hook_action_name__"):
+ hook_actions[getattr(method, "__hook_action_name__")] = method
+ return hook_actions
+
+
+@app.route("/webhook", methods=["GET", "POST"])
+def webhook():
+ # 验证用户名和密码
+ data = read_json()
+ username = data["webui"]["username"]
+ password = data["webui"]["password"]
+ if (username != request.args.get("username")) or (
+ password != request.args.get("password")
+ ):
+ logging.info(f">>> 用户 {username} webhook 认证失败")
+ return jsonify({"error": "认证失败"})
+
+ try:
+ action = request.args.get("action")
+ plugins, _, _ = Config.load_plugins(data.get("plugins", {}))
+ hook_actions = get_hook_actions(plugins)
+ if action in hook_actions:
+ cookies = Config.get_cookies(data["cookie"])
+ account = Quark(cookies[0], 0) if cookies else None
+ args = request.args
+ body = request.get_json() if request.mimetype == "application/json" else {}
+ hook_actions[action](account=account, **args, **body)
+ except Exception as e:
+ logging.error(f">>> webhook 处理报错:{e}")
+ return Response(status=200)
+
+
# 定时任务执行的函数
def run_python(args):
logging.info(f">>> 定时运行任务")
diff --git a/app/templates/index.html b/app/templates/index.html
index f433745..09b1463 100644
--- a/app/templates/index.html
+++ b/app/templates/index.html
@@ -147,7 +147,7 @@
diff --git a/decorators.py b/decorators.py
new file mode 100644
index 0000000..5147124
--- /dev/null
+++ b/decorators.py
@@ -0,0 +1,28 @@
+#!/usr/bin/python3
+# -*- encoding: utf-8 -*-
+"""
+@File : decorators.py
+@Desc : 定义装饰器
+@Version : v1.0
+@Time : 2025/04/04
+@Author : xiaoQQya
+@Contact : xiaoQQya@126.com
+"""
+import functools
+
+
+def hook_action(name):
+ """回调动作装饰器
+
+ Args:
+ name (str): 动作名称
+ """
+ def decorator(func):
+ func.__hook_action_name__ = name
+
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ return func(*args, **kwargs)
+ return wrapper
+
+ return decorator
diff --git a/plugins/README.md b/plugins/README.md
index 53b13e1..2d77b47 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -29,6 +29,14 @@
* `task` 是一个字典,包含任务信息。如果需要修改任务参数,返回修改后的 `task` 字典;
* 无修改则不返回或返回 `None`。
+## 插件回调
+
+插件支持配置 webhook 回调事件,可参考 [alist_strm_gen.py](alist_strm_gen.py) 类中的 `emby_library_deleted_hook` 方法,使用方式如下:
+
+1. 使用 `@hook_action("xxx_hook")` 装饰器修饰需要接收回调事件的方法,其中装饰器参数为回调事件类型,建议使用 `插件名称_事件类型_hook` 命名,避免不同插件之间类型重复;
+
+2. 在外部通过 `http://host:port/webhook?username=xxx&password=xxx&action=xxx_hook` 触发回调事件,支持 GET 与 POST 方法,POST 方法只支持 `application/json` 类型参数;
+
## 插件示例
参考 [emby.py](emby.py)
diff --git a/plugins/alist_strm_gen.py b/plugins/alist_strm_gen.py
index cc614fe..9188efa 100644
--- a/plugins/alist_strm_gen.py
+++ b/plugins/alist_strm_gen.py
@@ -11,8 +11,11 @@
import os
import re
import json
+import shutil
import requests
+from decorators import hook_action
+
class Alist_strm_gen:
@@ -205,3 +208,68 @@ class Alist_strm_gen:
except Exception as e:
print(f"Alist-Strm生成: 获取Quark路径出错 {e}")
return ""
+
+ def get_file_fid(self, path):
+ url = f"{self.url}/api/fs/get"
+ headers = {"Authorization": self.token}
+ body = {
+ "password": "",
+ "path": path
+ }
+ try:
+ response = requests.post(url, headers=headers, json=body)
+ response.raise_for_status()
+ data = response.json()
+ if data.get("code") == 200:
+ return data.get("data", {}).get("id")
+ else:
+ print(f"Alist-Strm生成: 获取文件 fid 失败❌ {data.get('message')}")
+ except Exception as e:
+ print(f"Alist-Strm生成: 获取文件 fid 出错 {e}")
+ return None
+
+ def delete_file(self, account, fid, path):
+ account.delete([fid])
+ strm_path = os.path.join(self.strm_save_dir, path.lstrip("/"))
+ if os.path.exists(strm_path):
+ if os.path.isfile(strm_path):
+ os.remove(strm_path)
+ elif os.path.isdir(strm_path):
+ shutil.rmtree(strm_path)
+
+ pdir = os.path.dirname(path)
+ data = self.get_file_list(pdir, True)
+ if data.get("code") == 200:
+ if data.get("data").get("total") == 0:
+ pdir_fid = self.get_file_fid(pdir)
+ self.delete_file(account, pdir_fid, pdir)
+ else:
+ print(f"Alist-Strm生成: hook 刷新父目录失败❌ {data.get('message')}")
+
+ @hook_action("alist_strm_gen_emby_library_deleted_hook")
+ def emby_library_deleted_hook(self, **kwargs):
+ account = kwargs.get("account")
+ event = kwargs.get("Event")
+ path = kwargs.get("Item", {}).get("Path")
+ if not path or not account or event != "library.deleted":
+ return
+
+ path = path.removeprefix(self.strm_save_dir.rstrip("/"))
+ # 1. 如果 path 为 strm 文件,因为不知道媒体文件后缀故无法确定 quark 中具体的文件名称,因此通过 alist 查询其父目录文件列表通过名称匹配找到其 fid
+ fid = None
+ if path.endswith("strm"):
+ pdir, strm_file = os.path.split(path)
+ files = self.get_file_list(pdir).get("data", {}).get("content")
+ strm_name, _ = os.path.splitext(strm_file)
+
+ for file in files:
+ file_name, _ = os.path.splitext(file.get("name"))
+ if file_name == strm_name:
+ fid = file.get("id")
+ break
+ # 2. 如果 path 为目录,通过 alist 查询该目录的 fid
+ else:
+ fid = self.get_file_fid(path)
+ # 3. 根据 fid 通过 quark api 删除指定文件或目录,并递归删除空的父目录
+ if fid:
+ self.delete_file(account, fid, path)
\ No newline at end of file
diff --git a/quark_auto_save.py b/quark_auto_save.py
index 78b719e..8724111 100644
--- a/quark_auto_save.py
+++ b/quark_auto_save.py
@@ -69,6 +69,7 @@ def add_notify(text):
class Config:
# 下载配置
+ @staticmethod
def download_file(url, save_path):
response = requests.get(url)
if response.status_code == 200:
@@ -79,6 +80,7 @@ class Config:
return False
# 读取CK
+ @staticmethod
def get_cookies(cookie_val):
if isinstance(cookie_val, list):
return cookie_val
@@ -90,7 +92,11 @@ class Config:
else:
return False
- def load_plugins(plugins_config={}, plugins_dir="plugins"):
+ @staticmethod
+ def load_plugins(plugins_config=None, plugins_dir="plugins"):
+ if plugins_config is None:
+ plugins_config = {}
+
PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "").split(",")
plugins_available = {}
task_plugins_config = {}
@@ -117,10 +123,10 @@ class Config:
# 检查配置中是否存在该模块的配置
if module_name in plugins_config:
plugin = ServerClass(**plugins_config[module_name])
- plugins_available[module_name] = plugin
else:
plugin = ServerClass()
plugins_config[module_name] = plugin.default_config
+ plugins_available[module_name] = plugin
# 检查插件是否支持单独任务配置
if hasattr(plugin, "default_task_config"):
task_plugins_config[module_name] = plugin.default_task_config
@@ -129,6 +135,7 @@ class Config:
print()
return plugins_available, plugins_config, task_plugins_config
+ @staticmethod
def breaking_change_update(config_data):
if config_data.get("emby"):
print("🔼 Update config v0.3.6.1 to 0.3.7")