mirror of
https://github.com/Cp0204/quark-auto-save.git
synced 2026-01-16 09:20:43 +08:00
Merge 5f78c66a8f into dc3afeae1d
This commit is contained in:
commit
ebe70feb01
37
app/run.py
37
app/run.py
@ -282,6 +282,43 @@ def delete_file():
|
|||||||
return jsonify(response)
|
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):
|
def run_python(args):
|
||||||
logging.info(f">>> 定时运行任务")
|
logging.info(f">>> 定时运行任务")
|
||||||
|
|||||||
@ -147,7 +147,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<h2 style="display: inline-block;"><i class="bi bi-magic"></i> 魔法匹配</h2>
|
<h2 style="display: inline-block;"><i class="bi bi-magic"></i> 魔法匹配</h2>
|
||||||
<span class="badge badge-pill badge-light">
|
<span class="badge badge-pill badge-light">
|
||||||
<a href="github.com/Cp0204/quark-auto-save/wiki/正则处理教程#21-魔法匹配" target="_blank">?</a>
|
<a href="https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程#21-魔法匹配" target="_blank">?</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
|
|||||||
28
decorators.py
Normal file
28
decorators.py
Normal file
@ -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
|
||||||
@ -29,6 +29,14 @@
|
|||||||
* `task` 是一个字典,包含任务信息。如果需要修改任务参数,返回修改后的 `task` 字典;
|
* `task` 是一个字典,包含任务信息。如果需要修改任务参数,返回修改后的 `task` 字典;
|
||||||
* 无修改则不返回或返回 `None`。
|
* 无修改则不返回或返回 `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)
|
参考 [emby.py](emby.py)
|
||||||
|
|||||||
@ -11,8 +11,11 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
import shutil
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from decorators import hook_action
|
||||||
|
|
||||||
|
|
||||||
class Alist_strm_gen:
|
class Alist_strm_gen:
|
||||||
|
|
||||||
@ -205,3 +208,68 @@ class Alist_strm_gen:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Alist-Strm生成: 获取Quark路径出错 {e}")
|
print(f"Alist-Strm生成: 获取Quark路径出错 {e}")
|
||||||
return ""
|
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)
|
||||||
@ -69,6 +69,7 @@ def add_notify(text):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
# 下载配置
|
# 下载配置
|
||||||
|
@staticmethod
|
||||||
def download_file(url, save_path):
|
def download_file(url, save_path):
|
||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@ -79,6 +80,7 @@ class Config:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# 读取CK
|
# 读取CK
|
||||||
|
@staticmethod
|
||||||
def get_cookies(cookie_val):
|
def get_cookies(cookie_val):
|
||||||
if isinstance(cookie_val, list):
|
if isinstance(cookie_val, list):
|
||||||
return cookie_val
|
return cookie_val
|
||||||
@ -90,7 +92,11 @@ class Config:
|
|||||||
else:
|
else:
|
||||||
return False
|
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(",")
|
PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "").split(",")
|
||||||
plugins_available = {}
|
plugins_available = {}
|
||||||
task_plugins_config = {}
|
task_plugins_config = {}
|
||||||
@ -117,10 +123,10 @@ class Config:
|
|||||||
# 检查配置中是否存在该模块的配置
|
# 检查配置中是否存在该模块的配置
|
||||||
if module_name in plugins_config:
|
if module_name in plugins_config:
|
||||||
plugin = ServerClass(**plugins_config[module_name])
|
plugin = ServerClass(**plugins_config[module_name])
|
||||||
plugins_available[module_name] = plugin
|
|
||||||
else:
|
else:
|
||||||
plugin = ServerClass()
|
plugin = ServerClass()
|
||||||
plugins_config[module_name] = plugin.default_config
|
plugins_config[module_name] = plugin.default_config
|
||||||
|
plugins_available[module_name] = plugin
|
||||||
# 检查插件是否支持单独任务配置
|
# 检查插件是否支持单独任务配置
|
||||||
if hasattr(plugin, "default_task_config"):
|
if hasattr(plugin, "default_task_config"):
|
||||||
task_plugins_config[module_name] = plugin.default_task_config
|
task_plugins_config[module_name] = plugin.default_task_config
|
||||||
@ -129,6 +135,7 @@ class Config:
|
|||||||
print()
|
print()
|
||||||
return plugins_available, plugins_config, task_plugins_config
|
return plugins_available, plugins_config, task_plugins_config
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def breaking_change_update(config_data):
|
def breaking_change_update(config_data):
|
||||||
if config_data.get("emby"):
|
if config_data.get("emby"):
|
||||||
print("🔼 Update config v0.3.6.1 to 0.3.7")
|
print("🔼 Update config v0.3.6.1 to 0.3.7")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user