From 9d2d796979e13b5fdea8c98e5a1a9d9cf96f6ee9 Mon Sep 17 00:00:00 2001 From: xiaoQQya Date: Fri, 4 Apr 2025 13:53:48 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Config.load=5Fp?= =?UTF-8?q?lugins=20=E5=8F=82=E6=95=B0=E9=BB=98=E8=AE=A4=E5=80=BC=E4=B8=BA?= =?UTF-8?q?=E5=8F=AF=E5=8F=98=E5=8F=82=E6=95=B0=E5=AF=BC=E8=87=B4=E9=9D=9E?= =?UTF-8?q?=E5=88=9D=E6=AC=A1=E8=B0=83=E7=94=A8=E6=97=B6=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=80=BC=E6=97=A0=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- quark_auto_save.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/quark_auto_save.py b/quark_auto_save.py index 78b719e..6fb7447 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 = {} @@ -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") From 7760e30c493f20a6e4c56144de83ba1bb6230b8f Mon Sep 17 00:00:00 2001 From: xiaoQQya Date: Fri, 4 Apr 2025 14:00:27 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Config.load=5Fp?= =?UTF-8?q?lugins=20=E5=8F=82=E6=95=B0=E4=B8=BA=E9=BB=98=E8=AE=A4=E5=80=BC?= =?UTF-8?q?=E6=97=B6=E6=97=A0=E6=B3=95=E5=8A=A0=E8=BD=BD=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- quark_auto_save.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quark_auto_save.py b/quark_auto_save.py index 6fb7447..8724111 100644 --- a/quark_auto_save.py +++ b/quark_auto_save.py @@ -123,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 From e7f938769b1c445f6a62a9c2105bbc48b0bc9b84 Mon Sep 17 00:00:00 2001 From: xiaoQQya Date: Fri, 4 Apr 2025 17:49:42 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat(plugin):=20=E4=B8=BA=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20webhook=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/run.py | 37 +++++++++++++++++++++++++++++++++++++ decorators.py | 28 ++++++++++++++++++++++++++++ plugins/README.md | 8 ++++++++ 3 files changed, 73 insertions(+) create mode 100644 decorators.py 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/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..4048f9a 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -29,6 +29,14 @@ * `task` 是一个字典,包含任务信息。如果需要修改任务参数,返回修改后的 `task` 字典; * 无修改则不返回或返回 `None`。 +## 插件回调 + +插件支持配置 webhook 回调事件,使用方式如下: + +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) From 7c0f4b1157ea134d58bfff0836d86fbc54670df8 Mon Sep 17 00:00:00 2001 From: xiaoQQya Date: Fri, 4 Apr 2025 17:51:59 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat(alist=5Fstrm=5Fgen):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20emby=20=E5=AA=92=E4=BD=93=E5=BA=93=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E4=BA=8B=E4=BB=B6=E5=9B=9E=E8=B0=83=EF=BC=8C=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E5=88=A0=E9=99=A4=20quark=20=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/README.md | 2 +- plugins/alist_strm_gen.py | 68 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/plugins/README.md b/plugins/README.md index 4048f9a..2d77b47 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -31,7 +31,7 @@ ## 插件回调 -插件支持配置 webhook 回调事件,使用方式如下: +插件支持配置 webhook 回调事件,可参考 [alist_strm_gen.py](alist_strm_gen.py) 类中的 `emby_library_deleted_hook` 方法,使用方式如下: 1. 使用 `@hook_action("xxx_hook")` 装饰器修饰需要接收回调事件的方法,其中装饰器参数为回调事件类型,建议使用 `插件名称_事件类型_hook` 命名,避免不同插件之间类型重复; 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 From 5f78c66a8faa49579253470d134d5d82dc42f495 Mon Sep 17 00:00:00 2001 From: xiaoQQya Date: Fri, 4 Apr 2025 23:49:41 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=AD=94=E6=B3=95?= =?UTF-8?q?=E5=8C=B9=E9=85=8D=20wiki=20=E9=93=BE=E6=8E=A5=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@

魔法匹配

- ? + ?