Compare commits

..

No commits in common. "main" and "v0.5.4" have entirely different histories.
main ... v0.5.4

21 changed files with 418 additions and 2176 deletions

View File

@ -1,12 +1,6 @@
# 使用官方 Python 镜像作为基础镜像 # 使用官方 Python 镜像作为基础镜像
FROM python:3.13-alpine FROM python:3.13-alpine
#构建版本
ARG BUILD_SHA
ARG BUILD_TAG
ENV BUILD_SHA=$BUILD_SHA
ENV BUILD_TAG=$BUILD_TAG
# 设置工作目录 # 设置工作目录
WORKDIR /app WORKDIR /app
@ -14,12 +8,17 @@ WORKDIR /app
COPY . /app COPY . /app
# 安装依赖 # 安装依赖
RUN pip install --no-cache-dir -r requirements.txt && \ RUN pip install --no-cache-dir -r requirements.txt
echo "{\"BUILD_SHA\":\"$BUILD_SHA\", \"BUILD_TAG\":\"$BUILD_TAG\"}" > build.json
# 时区 # 时区
ENV TZ="Asia/Shanghai" ENV TZ="Asia/Shanghai"
#构建版本
ARG BUILD_SHA
ARG BUILD_TAG
ENV BUILD_SHA=$BUILD_SHA
ENV BUILD_TAG=$BUILD_TAG
# 端口 # 端口
EXPOSE 5005 EXPOSE 5005

View File

@ -8,7 +8,7 @@
对于一些持续更新的资源,隔段时间去转存十分麻烦。 对于一些持续更新的资源,隔段时间去转存十分麻烦。
定期执行本脚本自动转存、文件名整理,配合 [SmartStrm](https://github.com/Cp0204/SmartStrm) / [OpenList](https://github.com/OpenListTeam/OpenList) , Emby 可达到自动追更的效果。🥳 定期执行本脚本自动转存、文件名整理,配合 Alist, rclone, Emby 可达到自动追更的效果。🥳
[![wiki][wiki-image]][wiki-url] [![github releases][gitHub-releases-image]][github-url] [![docker pulls][docker-pulls-image]][docker-url] [![docker image size][docker-image-size-image]][docker-url] [![wiki][wiki-image]][wiki-url] [![github releases][gitHub-releases-image]][github-url] [![docker pulls][docker-pulls-image]][docker-url] [![docker image size][docker-image-size-image]][docker-url]
@ -29,7 +29,7 @@
> ⛔️⛔️⛔️ 注意!资源不会每时每刻更新,**严禁设定过高的定时运行频率!** 以免账号风控和给夸克服务器造成不必要的压力。雪山崩塌,每一片雪花都有责任! > ⛔️⛔️⛔️ 注意!资源不会每时每刻更新,**严禁设定过高的定时运行频率!** 以免账号风控和给夸克服务器造成不必要的压力。雪山崩塌,每一片雪花都有责任!
> [!NOTE] > [!NOTE]
> 开发者≠客服,开源免费≠帮你解决使用问题;本项目 Wiki 已经相对完善,遇到问题请先翻阅 Issues 和 Wiki ,请勿盲目发问。 > 开发者≠客服,开源免费≠帮你解决使用问题;本项目Wiki和已经相对完善,遇到问题请先翻阅 Issues 和 Wiki ,请勿盲目发问。
## 功能 ## 功能
@ -58,7 +58,7 @@
- 媒体库整合 - 媒体库整合
- [x] 根据任务名搜索 Emby 媒体库 - [x] 根据任务名搜索 Emby 媒体库
- [x] 追更或整理后自动刷新 Emby 媒体库 - [x] 追更或整理后自动刷新 Emby 媒体库
- [x] 插件模块化,允许自行开发和挂载[插件](./plugins) - [x] 媒体库模块化,用户可很方便地[开发自己的媒体库hook模块](./plugins)
- 其它 - 其它
- [x] 每日签到领空间 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#每日签到领空间)</sup> - [x] 每日签到领空间 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#每日签到领空间)</sup>
@ -69,7 +69,7 @@
### Docker 部署 ### Docker 部署
Docker 部署提供 WebUI 进行管理配置,部署命令: Docker 部署提供 WebUI 管理配置,图形化配置已能满足绝大多数需求。部署命令:
```shell ```shell
docker run -d \ docker run -d \
@ -107,13 +107,11 @@ services:
管理地址http://yourhost:5005 管理地址http://yourhost:5005
| 环境变量 | 默认 | 备注 | | 环境变量 | 默认 | 备注 |
| ---------------- | ---------- | ---------------------------------------- | | ---------------- | ---------- | -------- |
| `WEBUI_USERNAME` | `admin` | 管理账号 | | `WEBUI_USERNAME` | `admin` | 管理账号 |
| `WEBUI_PASSWORD` | `admin123` | 管理密码 | | `WEBUI_PASSWORD` | `admin123` | 管理密码 |
| `PORT` | `5005` | 管理后台端口 |
| `PLUGIN_FLAGS` | | 插件标志,如 `-emby,-aria2` 禁用某些插件 | | `PLUGIN_FLAGS` | | 插件标志,如 `-emby,-aria2` 禁用某些插件 |
| `TASK_TIMEOUT` | `1800` | 任务执行超时时间(秒),超时则任务结束 |
#### 一键更新 #### 一键更新
@ -130,28 +128,32 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtow
</details> </details>
### 青龙部署
程序也支持以青龙定时任务的方式运行,但该方式无法使用 WebUI 管理任务,需手动修改配置文件。
青龙部署说明已转移到 Wiki [青龙部署教程](https://github.com/Cp0204/quark-auto-save/wiki/部署教程#青龙部署)
## 使用说明 ## 使用说明
### 正则处理示例 ### 正则理示例
| pattern | replace | 效果 | | pattern | replace | 效果 |
| -------------------------------------- | ----------------------- | ---------------------------------------------------------------------- | | -------------------------------------- | ------------ | ---------------------------------------------------------------------- |
| `.*` | | 无脑转存所有文件,不整理 | | `.*` | | 无脑转存所有文件,不整理 |
| `\.mp4$` | | 转存所有 `.mp4` 后缀的文件 | | `\.mp4$` | | 转存所有 `.mp4` 后缀的文件 |
| `^【电影TT】花好月圆(\d+)\.(mp4\|mkv)` | `\1.\2` | 【电影TT】花好月圆01.mp4 → 01.mp4<br>【电影TT】花好月圆02.mkv → 02.mkv | | `^【电影TT】花好月圆(\d+)\.(mp4\|mkv)` | `\1.\2` | 【电影TT】花好月圆01.mp4 → 01.mp4<br>【电影TT】花好月圆02.mkv → 02.mkv |
| `^(\d+)\.mp4` | `S02E\1.mp4` | 01.mp4 → S02E01.mp4<br>02.mp4 → S02E02.mp4 | | `^(\d+)\.mp4` | `S02E\1.mp4` | 01.mp4 → S02E01.mp4<br>02.mp4 → S02E02.mp4 |
| `$TV` | | [魔法匹配](#魔法匹配)剧集文件 | | `$TV` | | [魔法匹配](#魔法匹配)剧集文件 |
| `^(\d+)\.mp4` | `{TASKNAME}.S02E\1.mp4` | 01.mp4 → 任务名.S02E01.mp4 | | `^(\d+)\.mp4` | `$TASKNAME.S02E\1.mp4` | 01.mp4 → 任务名.S02E01.mp4 |
更多正则使用说明:[正则处理教程](https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程)
> [!TIP] > [!TIP]
> >
> **魔法匹配和魔法变量**:在正则处理中,我们定义了一些“魔法匹配”模式,如果 表达式 的值以 $ 开头且 替换式 留空,程序将自动使用预设的正则表达式进行匹配和替换 > **魔法匹配**:当任务 `pattern` 值为 `$开头``replace` 留空时,实际将调用程序预设的正则表达式
> >
> 自 v0.6.0 开始,支持更多以 {} 包裹的我称之为“魔法变量”,可以更灵活地进行重命名 > `$TV` 可适配和自动整理市面上90%分享剧集的文件名格式,具体实现见代码,欢迎贡献规则
>
> 更多说明请看[魔法匹配和魔法变量](https://github.com/Cp0204/quark-auto-save/wiki/魔法匹配和魔法变量) 更多正则使用说明:[正则处理教程](https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程)
### 刷新媒体库 ### 刷新媒体库
@ -163,40 +165,6 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtow
请参考 Wiki [使用技巧集锦](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦) 请参考 Wiki [使用技巧集锦](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦)
## 生态项目
以下展示 QAS 生态项目,包括官方项目和第三方项目。
### 官方项目
* [QAS一键推送助手](https://greasyfork.org/zh-CN/scripts/533201-qas一键推送助手)
油猴脚本,在夸克网盘分享页面添加推送到 QAS 的按钮
* [SmartStrm](https://github.com/Cp0204/SmartStrm)
STRM 文件生成工具,用于转存后处理,媒体免下载入库播放。
### 第三方开源项目
> [!TIP]
>
> 以下第三方开源项目均由社区开发并保持开源,与 QAS 作者无直接关联。在部署到生产环境前,请自行评估相关风险。
>
> 如果您有新的项目没有在此列出,可以通过 Issues 提交。
* [nonebot-plugin-quark-autosave](https://github.com/fllesser/nonebot-plugin-quark-autosave)
QAS Telegram 机器人,快速管理自动转存任务
* [Astrbot_plugin_quarksave](https://github.com/lm379/astrbot_plugin_quarksave)
AstrBot 插件,调用 quark_auto_save 实现自动转存资源到夸克网盘
* [Telegram 媒体资源管理机器人](https://github.com/2beetle/tgbot)
一个功能丰富的 Telegram 机器人专注于媒体资源管理、Emby 集成、自动下载和夸克网盘资源管理。
## 打赏 ## 打赏
如果这个项目让你受益你可以无偿赠与我1块钱让我知道开源有价值。谢谢 如果这个项目让你受益你可以无偿赠与我1块钱让我知道开源有价值。谢谢
@ -209,10 +177,4 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtow
程序没有任何破解行为只是对于夸克已有的API进行封装所有数据来自于夸克官方API本人不对网盘内容负责、不对夸克官方API未来可能的变动导致的影响负责请自行斟酌使用。 程序没有任何破解行为只是对于夸克已有的API进行封装所有数据来自于夸克官方API本人不对网盘内容负责、不对夸克官方API未来可能的变动导致的影响负责请自行斟酌使用。
开源仅供学习与交流使用,未盈利也未授权商业使用,严禁用于非法用途。 开源仅供学习与交流使用,未盈利也未授权商业使用,严禁用于非法用途。
## Sponsor
CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne.
<a href="https://edgeone.ai/?from=github" target="_blank"><img title="Best Asian CDN, Edge, and Secure Solutions - Tencent EdgeOne" src="https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png" width="300"></a>

View File

@ -15,15 +15,12 @@ from flask import (
) )
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from concurrent.futures import ThreadPoolExecutor, as_completed
from sdk.cloudsaver import CloudSaver from sdk.cloudsaver import CloudSaver
from sdk.pansou import PanSou
from datetime import timedelta from datetime import timedelta
import subprocess import subprocess
import requests import requests
import hashlib import hashlib
import logging import logging
import traceback
import base64 import base64
import sys import sys
import os import os
@ -31,32 +28,13 @@ import re
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, parent_dir) sys.path.insert(0, parent_dir)
from quark_auto_save import Quark, Config, MagicRename from quark_auto_save import Quark
from quark_auto_save import Config
print(
r"""
____ ___ _____
/ __ \ / | / ___/
/ / / / / /| | \__ \
/ /_/ / / ___ |___/ /
\___\_\/_/ |_/____/
-- Quark-Auto-Save --
"""
)
sys.stdout.flush()
def get_app_ver(): def get_app_ver():
"""获取应用版本""" BUILD_SHA = os.environ.get("BUILD_SHA", "")
try: BUILD_TAG = os.environ.get("BUILD_TAG", "")
with open("build.json", "r") as f:
build_info = json.loads(f.read())
BUILD_SHA = build_info["BUILD_SHA"]
BUILD_TAG = build_info["BUILD_TAG"]
except Exception as e:
BUILD_SHA = os.getenv("BUILD_SHA", "")
BUILD_TAG = os.getenv("BUILD_TAG", "")
if BUILD_TAG[:1] == "v": if BUILD_TAG[:1] == "v":
return BUILD_TAG return BUILD_TAG
elif BUILD_SHA: elif BUILD_SHA:
@ -71,9 +49,6 @@ SCRIPT_PATH = os.environ.get("SCRIPT_PATH", "./quark_auto_save.py")
CONFIG_PATH = os.environ.get("CONFIG_PATH", "./config/quark_config.json") CONFIG_PATH = os.environ.get("CONFIG_PATH", "./config/quark_config.json")
PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "") PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true" DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
HOST = os.environ.get("HOST", "0.0.0.0")
PORT = os.environ.get("PORT", 5005)
TASK_TIMEOUT = int(os.environ.get("TASK_TIMEOUT", 1800))
config_data = {} config_data = {}
task_plugins_config_default = {} task_plugins_config_default = {}
@ -97,8 +72,6 @@ logging.basicConfig(
# 过滤werkzeug日志输出 # 过滤werkzeug日志输出
if not DEBUG: if not DEBUG:
logging.getLogger("werkzeug").setLevel(logging.ERROR) logging.getLogger("werkzeug").setLevel(logging.ERROR)
logging.getLogger("apscheduler").setLevel(logging.ERROR)
sys.modules["flask.cli"].show_server_banner = lambda *x: None
def gen_md5(string): def gen_md5(string):
@ -218,14 +191,6 @@ def run_script_now():
# 设置环境变量 # 设置环境变量
process_env = os.environ.copy() process_env = os.environ.copy()
process_env["PYTHONIOENCODING"] = "utf-8" process_env["PYTHONIOENCODING"] = "utf-8"
if request.json.get("quark_test"):
process_env["QUARK_TEST"] = "true"
process_env["COOKIE"] = json.dumps(
request.json.get("cookie", []), ensure_ascii=False
)
process_env["PUSH_CONFIG"] = json.dumps(
request.json.get("push_config", {}), ensure_ascii=False
)
if tasklist: if tasklist:
process_env["TASKLIST"] = json.dumps(tasklist, ensure_ascii=False) process_env["TASKLIST"] = json.dumps(tasklist, ensure_ascii=False)
process = subprocess.Popen( process = subprocess.Popen(
@ -259,19 +224,8 @@ def get_task_suggestions():
return jsonify({"success": False, "message": "未登录"}) return jsonify({"success": False, "message": "未登录"})
query = request.args.get("q", "").lower() query = request.args.get("q", "").lower()
deep = request.args.get("d", "").lower() deep = request.args.get("d", "").lower()
net_data = config_data.get("source", {}).get("net", {}) try:
cs_data = config_data.get("source", {}).get("cloudsaver", {}) cs_data = config_data.get("source", {}).get("cloudsaver", {})
ps_data = config_data.get("source", {}).get("pansou", {})
def net_search():
if str(net_data.get("enable", "true")).lower() != "false":
base_url = base64.b64decode("aHR0cHM6Ly9zLjkxNzc4OC54eXo=").decode()
url = f"{base_url}/task_suggestions?q={query}&d={deep}"
response = requests.get(url)
return response.json()
return []
def cs_search():
if ( if (
cs_data.get("server") cs_data.get("server")
and cs_data.get("username") and cs_data.get("username")
@ -289,37 +243,18 @@ def get_task_suggestions():
cs_data["token"] = search.get("new_token") cs_data["token"] = search.get("new_token")
Config.write_json(CONFIG_PATH, config_data) Config.write_json(CONFIG_PATH, config_data)
search_results = cs.clean_search_results(search.get("data")) search_results = cs.clean_search_results(search.get("data"))
return search_results return jsonify(
return [] {"success": True, "source": "CloudSaver", "data": search_results}
)
def ps_search(): else:
if ps_data.get("server"): return jsonify({"success": True, "message": search.get("message")})
ps = PanSou(ps_data.get("server")) else:
return ps.search(query, deep == "1") base_url = base64.b64decode("aHR0cHM6Ly9zLjkxNzc4OC54eXo=").decode()
return [] url = f"{base_url}/task_suggestions?q={query}&d={deep}"
response = requests.get(url)
try: return jsonify(
search_results = [] {"success": True, "source": "网络公开", "data": response.json()}
with ThreadPoolExecutor(max_workers=3) as executor: )
features = []
features.append(executor.submit(net_search))
features.append(executor.submit(cs_search))
features.append(executor.submit(ps_search))
for future in as_completed(features):
result = future.result()
search_results.extend(result)
# 按时间排序并去重
results = []
link_array = []
search_results.sort(key=lambda x: x.get("datetime", ""), reverse=True)
for item in search_results:
url = item.get("shareurl", "")
if url != "" and url not in link_array:
link_array.append(url)
results.append(item)
return jsonify({"success": True, "data": results})
except Exception as e: except Exception as e:
return jsonify({"success": True, "message": f"error: {str(e)}"}) return jsonify({"success": True, "message": f"error: {str(e)}"})
@ -330,7 +265,7 @@ def get_share_detail():
return jsonify({"success": False, "message": "未登录"}) return jsonify({"success": False, "message": "未登录"})
shareurl = request.json.get("shareurl", "") shareurl = request.json.get("shareurl", "")
stoken = request.json.get("stoken", "") stoken = request.json.get("stoken", "")
account = Quark() account = Quark("", 0)
pwd_id, passcode, pdir_fid, paths = account.extract_url(shareurl) pwd_id, passcode, pdir_fid, paths = account.extract_url(shareurl)
if not stoken: if not stoken:
get_stoken = account.get_stoken(pwd_id, passcode) get_stoken = account.get_stoken(pwd_id, passcode)
@ -340,9 +275,7 @@ def get_share_detail():
return jsonify( return jsonify(
{"success": False, "data": {"error": get_stoken.get("message")}} {"success": False, "data": {"error": get_stoken.get("message")}}
) )
share_detail = account.get_detail( share_detail = account.get_detail(pwd_id, stoken, pdir_fid, _fetch_share=1)
pwd_id, stoken, pdir_fid, _fetch_share=1, fetch_share_full_path=1
)
if share_detail.get("code") != 0: if share_detail.get("code") != 0:
return jsonify( return jsonify(
@ -350,59 +283,28 @@ def get_share_detail():
) )
data = share_detail["data"] data = share_detail["data"]
data["paths"] = [ data["paths"] = paths
{"fid": i["fid"], "name": i["file_name"]}
for i in share_detail["data"].get("full_path", [])
] or paths
data["stoken"] = stoken data["stoken"] = stoken
# 正则处理预览 # 正则处理预览
def preview_regex(data): def preview_regex(data):
task = request.json.get("task", {}) regex = request.json.get("regex", {})
magic_regex = request.json.get("magic_regex", {}) pattern, replace = account.magic_regex_func(
mr = MagicRename(magic_regex) regex.get("pattern", ""),
mr.set_taskname(task.get("taskname", "")) regex.get("replace", ""),
account = Quark(config_data["cookie"][0]) regex.get("taskname", ""),
get_fids = account.get_fids([task.get("savepath", "")]) regex.get("magic_regex", {}),
if get_fids:
dir_file_list = account.ls_dir(get_fids[0]["fid"])["data"]["list"]
dir_filename_list = [dir_file["file_name"] for dir_file in dir_file_list]
else:
dir_file_list = []
dir_filename_list = []
pattern, replace = mr.magic_regex_conv(
task.get("pattern", ""), task.get("replace", "")
) )
for share_file in data["list"]: for item in data["list"]:
search_pattern = ( file_name = item["file_name"]
task["update_subdir"] if re.search(pattern, item["file_name"]):
if share_file["dir"] and task.get("update_subdir") item["file_name_re"] = (
else pattern re.sub(pattern, replace, file_name) if replace != "" else file_name
)
if re.search(search_pattern, share_file["file_name"]):
# 文件名重命名,目录不重命名
file_name_re = (
share_file["file_name"]
if share_file["dir"]
else mr.sub(pattern, replace, share_file["file_name"])
) )
if file_name_saved := mr.is_exists( return share_detail
file_name_re,
dir_filename_list,
(task.get("ignore_extension") and not share_file["dir"]),
):
share_file["file_name_saved"] = file_name_saved
else:
share_file["file_name_re"] = file_name_re
# 文件列表排序 if request.json.get("regex"):
if re.search(r"\{I+\}", replace): share_detail = preview_regex(data)
mr.set_dir_file_list(dir_file_list, replace)
mr.sort_file_list(data["list"])
if request.json.get("task"):
preview_regex(data)
return jsonify({"success": True, "data": data}) return jsonify({"success": True, "data": data})
@ -411,7 +313,7 @@ def get_share_detail():
def get_savepath_detail(): def get_savepath_detail():
if not is_login(): if not is_login():
return jsonify({"success": False, "message": "未登录"}) return jsonify({"success": False, "message": "未登录"})
account = Quark(config_data["cookie"][0]) account = Quark(config_data["cookie"][0], 0)
paths = [] paths = []
if path := request.args.get("path"): if path := request.args.get("path"):
path = re.sub(r"/+", "/", path) path = re.sub(r"/+", "/", path)
@ -447,7 +349,7 @@ def get_savepath_detail():
def delete_file(): def delete_file():
if not is_login(): if not is_login():
return jsonify({"success": False, "message": "未登录"}) return jsonify({"success": False, "message": "未登录"})
account = Quark(config_data["cookie"][0]) account = Quark(config_data["cookie"][0], 0)
if fid := request.json.get("fid"): if fid := request.json.get("fid"):
response = account.delete([fid]) response = account.delete([fid])
else: else:
@ -473,8 +375,6 @@ def add_task():
), ),
400, 400,
) )
if not request_data.get("addition"):
request_data["addition"] = task_plugins_config_default
# 添加任务 # 添加任务
config_data["tasklist"].append(request_data) config_data["tasklist"].append(request_data)
Config.write_json(CONFIG_PATH, config_data) Config.write_json(CONFIG_PATH, config_data)
@ -487,36 +387,7 @@ def add_task():
# 定时任务执行的函数 # 定时任务执行的函数
def run_python(args): def run_python(args):
logging.info(f">>> 定时运行任务") logging.info(f">>> 定时运行任务")
try: os.system(f"{PYTHON_PATH} {args}")
result = subprocess.run(
f"{PYTHON_PATH} {args}",
shell=True,
timeout=TASK_TIMEOUT,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
# 输出执行日志
if result.stdout:
for line in result.stdout.strip().split("\n"):
if line.strip():
logging.info(line)
if result.returncode == 0:
logging.info(f">>> 任务执行成功")
else:
logging.error(f">>> 任务执行失败,返回码: {result.returncode}")
if result.stderr:
logging.error(f"错误信息: {result.stderr[:500]}")
except subprocess.TimeoutExpired as e:
logging.error(f">>> 任务执行超时(>{TASK_TIMEOUT}s),强制终止")
except Exception as e:
logging.error(f">>> 任务执行异常: {str(e)}")
logging.error(traceback.format_exc())
finally:
# 确保函数能够正常返回
logging.debug(f">>> run_python 函数执行完成")
# 重新加载任务 # 重新加载任务
@ -532,10 +403,6 @@ def reload_tasks():
trigger=trigger, trigger=trigger,
args=[f"{SCRIPT_PATH} {CONFIG_PATH}"], args=[f"{SCRIPT_PATH} {CONFIG_PATH}"],
id=SCRIPT_PATH, id=SCRIPT_PATH,
max_instances=1, # 最多允许1个实例运行
coalesce=True, # 合并错过的任务,避免堆积
misfire_grace_time=300, # 错过任务的宽限期(秒),超过则跳过
replace_existing=True, # 替换已存在的同ID任务
) )
if scheduler.state == 0: if scheduler.state == 0:
scheduler.start() scheduler.start()
@ -554,7 +421,7 @@ def reload_tasks():
def init(): def init():
global config_data, task_plugins_config_default global config_data, task_plugins_config_default
logging.info(">>> 初始化配置") logging.info(f">>> 初始化配置")
# 检查配置文件是否存在 # 检查配置文件是否存在
if not os.path.exists(CONFIG_PATH): if not os.path.exists(CONFIG_PATH):
if not os.path.exists(os.path.dirname(CONFIG_PATH)): if not os.path.exists(os.path.dirname(CONFIG_PATH)):
@ -565,8 +432,6 @@ def init():
# 读取配置 # 读取配置
config_data = Config.read_json(CONFIG_PATH) config_data = Config.read_json(CONFIG_PATH)
Config.breaking_change_update(config_data) Config.breaking_change_update(config_data)
if not config_data.get("magic_regex"):
config_data["magic_regex"] = MagicRename().magic_regex
# 默认管理账号 # 默认管理账号
config_data["webui"] = { config_data["webui"] = {
@ -592,10 +457,4 @@ def init():
if __name__ == "__main__": if __name__ == "__main__":
init() init()
reload_tasks() reload_tasks()
logging.info(">>> 启动Web服务") app.run(debug=DEBUG, host="0.0.0.0", port=5005)
logging.info(f"运行在: http://{HOST}:{PORT}")
app.run(
debug=DEBUG,
host=HOST,
port=PORT,
)

View File

@ -1,6 +1,5 @@
import re import re
import requests import requests
from sdk.common import iso_to_cst
class CloudSaver: class CloudSaver:
@ -125,10 +124,6 @@ class CloudSaver:
content = content.replace('<mark class="highlight">', "") content = content.replace('<mark class="highlight">', "")
content = content.replace("</mark>", "") content = content.replace("</mark>", "")
content = content.strip() content = content.strip()
# 统一发布时间格式
pubdate = item.get("pubDate", "")
if pubdate:
pubdate = iso_to_cst(pubdate)
# 链接去重 # 链接去重
if link.get("link") not in link_array: if link.get("link") not in link_array:
link_array.append(link.get("link")) link_array.append(link.get("link"))
@ -137,10 +132,9 @@ class CloudSaver:
"shareurl": link.get("link"), "shareurl": link.get("link"),
"taskname": title, "taskname": title,
"content": content, "content": content,
"datetime": pubdate,
"tags": item.get("tags", []), "tags": item.get("tags", []),
"channel": item.get("channelId", ""), "channel": item.get("channel", ""),
"source": "CloudSaver" "channel_id": item.get("channelId", ""),
} }
) )
return clean_results return clean_results

View File

@ -1,16 +0,0 @@
from datetime import datetime, timezone, timedelta
def iso_to_cst(iso_time_str: str) -> str:
"""将 ISO 格式的时间字符串转换为 CST(China Standard Time) 时间并格式化为 %Y-%m-%d %H:%M:%S 格式
Args:
iso_time_str (str): ISO 格式时间字符串
Returns:
str: CST(China Standard Time) 时间字符串
"""
dt = datetime.fromisoformat(iso_time_str)
tz = timezone(timedelta(hours=8))
dt_cst = dt if dt.astimezone(tz) > datetime.now(tz) else dt.astimezone(tz)
return dt_cst.strftime("%Y-%m-%d %H:%M:%S") if dt_cst.year >= 1970 else ""

View File

@ -1,97 +0,0 @@
import re
import requests
from sdk.common import iso_to_cst
class PanSou:
"""
PanSou 用于获取云盘资源
"""
def __init__(self, server):
self.server = server
self.session = requests.Session()
def search(self, keyword: str, refresh: bool = False) -> list:
"""搜索资源
Args:
keyword (str): 搜索关键字
Returns:
list: 资源列表
"""
try:
url = f"{self.server.rstrip('/')}/api/search"
params = {"kw": keyword, "cloud_types": ["quark"], "res": "merge", "refresh": refresh}
response = self.session.get(url, params=params)
result = response.json()
if result.get("code") == 0:
data = result.get("data", {}).get("merged_by_type", {}).get("quark", [])
return self.format_search_results(data)
return []
except Exception as _:
return []
def format_search_results(self, search_results: list) -> list:
"""格式化搜索结果
Args:
search_results (list): 搜索结果列表
Returns:
list: 夸克网盘资源列表
"""
pattern = (
r'^(.*?)'
r'(?:'
r'[【\[]?'
r'(?:简介|介绍|描述)'
r'[】\]]?'
r'[:]?'
r')'
r'(.*)$'
)
format_results = []
link_array = []
for item in search_results:
url = item.get("url", "")
note = item.get("note", "")
tm = item.get("datetime", "")
if tm:
tm = iso_to_cst(tm)
match = re.search(pattern, note)
if match:
title = match.group(1)
content = match.group(2)
else:
title = note
content = ""
if url != "" and url not in link_array:
link_array.append(url)
format_results.append({
"shareurl": url,
"taskname": title,
"content": content,
"datetime": tm,
"channel": item.get("source", ""),
"source": "PanSou"
})
return format_results
if __name__ == "__main__":
server: str = "https://so.252035.xyz"
pansou = PanSou(server)
results = pansou.search("哪吒")
for item in results:
print(f"标题: {item['taskname']}")
print(f"描述: {item['content']}")
print(f"链接: {item['shareurl']}")
print(f"时间: {item['datetime']}")
print("-" * 50)

View File

@ -45,7 +45,7 @@ body {
margin-bottom: 10px; margin-bottom: 10px;
} }
table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child { table.jsoneditor-tree > tbody > tr.jsoneditor-expandable:first-child {
display: none; display: none;
} }
@ -196,48 +196,3 @@ table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
} }
/* Toast */
.toast-container {
position: fixed;
top: 80px;
right: 20px;
z-index: 9999;
width: 300px;
}
.toast {
background-color: rgba(255, 255, 255, 0.95);
border-radius: 8px;
border-width: 0 0 0 5px !important;
margin-bottom: 10px;
animation: slideIn 0.3s ease forwards;
}
.toast.success {
border-left-color: #28a745 !important;
}
.toast.error {
border-left-color: #dc3545 !important;
}
.toast.warning {
border-left-color: #ffc107 !important;
}
.toast.info {
border-left-color: #17a2b8 !important;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}

View File

@ -2,7 +2,7 @@
// @name QAS一键推送助手 // @name QAS一键推送助手
// @namespace https://github.com/Cp0204/quark-auto-save // @namespace https://github.com/Cp0204/quark-auto-save
// @license AGPL // @license AGPL
// @version 0.6 // @version 0.4
// @description 在夸克网盘分享页面添加推送到 QAS 的按钮 // @description 在夸克网盘分享页面添加推送到 QAS 的按钮
// @icon https://pan.quark.cn/favicon.ico // @icon https://pan.quark.cn/favicon.ico
// @author Cp0204 // @author Cp0204
@ -30,11 +30,11 @@
showCancelButton: true, showCancelButton: true,
html: ` html: `
<label for="qas_base">QAS 地址</label> <label for="qas_base">QAS 地址</label>
<input id="qas_base" class="swal2-input" placeholder="如: http://192.168.1.8:5005" value="${qas_base}"><br> <input id="qas_base" class="swal2-input" placeholder="如: http://192.168.1.8:5005" value="${qas_base}">
<label for="qas_token">QAS Token</label> <label for="qas_token">QAS Token</label>
<input id="qas_token" class="swal2-input" placeholder="v0.5+ 系统配置中查找" value="${qas_token}"><br> <input id="qas_token" class="swal2-input" placeholder="v0.5+ 系统配置中查找" value="${qas_token}">
<label for="qas_token">默认正则</label> <label for="qas_token">默认正则</label>
<input id="default_pattern" class="swal2-input" placeholder="如 $TV" value="${default_pattern}"><br> <input id="default_pattern" class="swal2-input" placeholder="如 $TV" value="${default_pattern}">
<label for="qas_token">默认替换</label><input id="default_replace" class="swal2-input" value="${default_replace}"> <label for="qas_token">默认替换</label><input id="default_replace" class="swal2-input" value="${default_replace}">
`, `,
focusConfirm: false, focusConfirm: false,
@ -76,16 +76,16 @@
} }
} }
waitForElement('.pc-member-entrance', (PcMemberButton) => { waitForElement('.DetailLayout--client-download--FpyCkdW.ant-dropdown-trigger', (clientDownloadButton) => {
const qasSettingButton = document.createElement('div'); const qasSettingButton = document.createElement('div');
qasSettingButton.className = 'pc-member-entrance'; qasSettingButton.className = 'DetailLayout--client-download--FpyCkdW ant-dropdown-trigger';
qasSettingButton.innerHTML = 'QAS设置'; qasSettingButton.innerHTML = 'QAS设置';
qasSettingButton.addEventListener('click', () => { qasSettingButton.addEventListener('click', () => {
showQASSettingDialog(); showQASSettingDialog();
}); });
PcMemberButton.parentNode.insertBefore(qasSettingButton, PcMemberButton.nextSibling); clientDownloadButton.parentNode.insertBefore(qasSettingButton, clientDownloadButton.nextSibling);
}); });
} }
@ -112,8 +112,7 @@
// 获取数据函数 // 获取数据函数
function getData() { function getData() {
const currentUrl = window.location.href; const currentUrl = window.location.href;
const lastTitle = document.querySelector('.primary .bcrumb-filename:last-child')?.getAttribute('title') || null; taskname = currentUrl.lastIndexOf('-') > 0 ? decodeURIComponent(currentUrl.match(/.*\/[^-]+-(.+)$/)[1]).replace('*101', '-') : document.querySelector('.author-name').textContent;
taskname = (lastTitle && lastTitle != "全部文件") ? lastTitle : document.querySelector('.author-name').textContent;
shareurl = currentUrl; shareurl = currentUrl;
let pathElement = document.querySelector('.path-name'); let pathElement = document.querySelector('.path-name');
savepath = pathElement ? pathElement.title.replace('全部文件', '').trim() : ""; savepath = pathElement ? pathElement.title.replace('全部文件', '').trim() : "";
@ -155,63 +154,6 @@
}, },
data: JSON.stringify(data), data: JSON.stringify(data),
onload: function (response) { onload: function (response) {
// 检查 HTTP 状态码
if (response.status === 401) {
Swal.fire({
title: '认证失败',
text: 'Token 无效或已过期,请重新配置 QAS Token',
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
});
return;
}
if (response.status === 503) {
Swal.fire({
title: '服务器不可用',
html: `服务器暂时无法处理请求 (503)<br><br>
<small>可能原因<br>
QAS 服务未运行<br>
服务器过载<br>
网络连接问题</small>`,
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
});
return;
}
// 检查响应内容类型
const contentType = response.responseHeaders.match(/content-type:\s*([^;\s]+)/i);
if (contentType && !contentType[1].includes('application/json')) {
Swal.fire({
title: '认证失败',
html: `服务器返回了非 JSON 响应,可能是 Token 错误<br><br>
<small>响应类型: ${contentType[1]}</small><br>
<small>响应状态: ${response.status}</small>`,
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
});
return;
}
try { try {
const jsonResponse = JSON.parse(response.responseText); const jsonResponse = JSON.parse(response.responseText);
if (jsonResponse.success) { if (jsonResponse.success) {
@ -234,34 +176,16 @@
} catch (e) { } catch (e) {
Swal.fire({ Swal.fire({
title: '解析响应失败', title: '解析响应失败',
html: `<small> text: `无法解析 JSON 响应: ${response.responseText}`,
响应状态: ${response.status}<br> icon: 'error'
响应内容: ${response.responseText.substring(0, 200)}...<br><br>
错误详情: ${e.message}
</small>`,
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
}); });
} }
}, },
onerror: function (error) { onerror: function (error) {
Swal.fire({ Swal.fire({
title: '网络请求失败', title: '任务创建失败',
text: '无法连接到 QAS 服务器,请检查网络连接和服务器地址', text: error,
icon: 'error', icon: 'error'
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
}); });
} }
}); });

View File

@ -77,7 +77,7 @@
<button type="button" class="btn btn-outline-primary" @click="addCookie()">+</button> <button type="button" class="btn btn-outline-primary" @click="addCookie()">+</button>
</div> </div>
</div> </div>
<p>1. 所有账号执行签到,纯<a class="" href="https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#每日签到领空间">签到</a>只需移动端参数即可!</p> <p>1. 所有账号执行签到,纯签到只需移动端参数即可!</p>
<p>2. 仅第一个账号执行转存,请自行确认顺序。<b>最好是手机验证码<a target="_blank" href="https://pan.quark.cn/">登录</a>CK比较完整</b>如需签到参数附在CK后面。</p> <p>2. 仅第一个账号执行转存,请自行确认顺序。<b>最好是手机验证码<a target="_blank" href="https://pan.quark.cn/">登录</a>CK比较完整</b>如需签到参数附在CK后面。</p>
<div v-for="(value, index) in formData.cookie" :key="index" class="input-group mb-2"> <div v-for="(value, index) in formData.cookie" :key="index" class="input-group mb-2">
<input type="text" v-model="formData.cookie[index]" class="form-control" placeholder="打开 pan.quark.com 按 F12 抓取"> <input type="text" v-model="formData.cookie[index]" class="form-control" placeholder="打开 pan.quark.com 按 F12 抓取">
@ -102,14 +102,13 @@
</div> </div>
<div class="row title" title="通知推送支持多个渠道见Wiki"> <div class="row title" title="通知推送支持多个渠道见Wiki">
<div class="col-8"> <div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-bell"></i> 通知</h2> <h2 style="display: inline-block;"><i class="bi bi-bell"></i> 通知</h2>
<span class="badge badge-pill badge-light"> <span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/通知推送服务配置" target="_blank">?</a> <a href="https://github.com/Cp0204/quark-auto-save/wiki/通知推送服务配置" target="_blank">?</a>
</span> </span>
</div> </div>
<div class="col-4 text-right"> <div class="col-2 text-right">
<button type="button" class="btn btn-success" title="通知推送测试" @click="testPush()"><i class="bi bi-lightning"></i></button>
<button type="button" class="btn btn-outline-primary" @click="addPush()">+</button> <button type="button" class="btn btn-outline-primary" @click="addPush()">+</button>
</div> </div>
</div> </div>
@ -198,70 +197,28 @@
<div class="row title" title="资源搜索服务配置,用于任务名称智能搜索"> <div class="row title" title="资源搜索服务配置,用于任务名称智能搜索">
<div class="col-10"> <div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-search"></i> 资源搜索</h2> <h2 style="display: inline-block;"><i class="bi bi-search"></i> CloudSaver</h2>
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/CloudSaver搜索源" target="_blank">?</a>
</span>
</div> </div>
</div> </div>
<div class="form-group row mb-0" style="display:flex; align-items:center;"> <div class="form-group row">
<div data-toggle="collapse" data-target="#collapse_net" aria-expanded="true" aria-controls="collapse_net"> <label class="col-sm-2 col-form-label">服务器</label>
<div class="btn btn-block text-left"> <div class="col-sm-10">
<i class="bi bi-caret-right-fill"></i> 网络公开搜索 <input type="text" v-model="formData.source.cloudsaver.server" class="form-control" placeholder="资源搜索服务器地址,如 http://172.17.0.1:8008">
</div>
</div> </div>
</div> </div>
<div class="collapse show ml-3" id="collapse_net"> <div class="form-group row">
<div class="form-group row"> <label class="col-sm-2 col-form-label">用户名</label>
<label class="col-sm-2 col-form-label">启用</label> <div class="col-sm-10">
<div class="col-sm-10 d-flex align-items-center"> <input type="text" v-model="formData.source.cloudsaver.username" class="form-control" placeholder="用户名">
<input type="checkbox" class="form-check-input" v-model="formData.source.net.enable" placeholder="是否启用网络公开搜索,默认启用">
</div>
</div> </div>
</div> </div>
<div class="form-group row mb-0" style="display:flex; align-items:center;"> <div class="form-group row">
<div data-toggle="collapse" data-target="#collapse_cloudsaver" aria-expanded="true" aria-controls="collapse_cloudsaver"> <label class="col-sm-2 col-form-label">密码</label>
<div class="btn btn-block text-left"> <div class="col-sm-10">
<i class="bi bi-caret-right-fill"></i> CloudSaver <input type="password" v-model="formData.source.cloudsaver.password" class="form-control" placeholder="密码">
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/CloudSaver搜索源" target="_blank">?</a>
</span>
</div>
</div>
</div>
<div class="collapse show ml-3" id="collapse_cloudsaver">
<div class="form-group row">
<label class="col-sm-2 col-form-label">服务器</label>
<div class="col-sm-10">
<input type="text" v-model="formData.source.cloudsaver.server" class="form-control" placeholder="资源搜索服务器地址,如 http://172.17.0.1:8008">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">用户名</label>
<div class="col-sm-10">
<input type="text" v-model="formData.source.cloudsaver.username" class="form-control" placeholder="用户名">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">密码</label>
<div class="col-sm-10">
<input type="password" v-model="formData.source.cloudsaver.password" class="form-control" placeholder="密码">
</div>
</div>
</div>
<div class="form-group row mb-0" style="display:flex; align-items:center;">
<div data-toggle="collapse" data-target="#collapse_pansou" aria-expanded="true" aria-controls="collapse_pansou">
<div class="btn btn-block text-left">
<i class="bi bi-caret-right-fill"></i> PanSou
<span class="badge badge-pill badge-light">
<a href="https://github.com/fish2018/pansou" target="_blank">?</a>
</span>
</div>
</div>
</div>
<div class="collapse show ml-3" id="collapse_pansou">
<div class="form-group row">
<label class="col-sm-2 col-form-label">服务器</label>
<div class="col-sm-10">
<input type="text" v-model="formData.source.pansou.server" class="form-control" placeholder="资源搜索服务器地址,如 https://so.252035.xyz">
</div>
</div> </div>
</div> </div>
@ -310,10 +267,9 @@
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-outline-primary btn-sm" @click="copyTaskToClipboard(index)" title="复制任务参数到粘贴板"><i class=" bi bi-clipboard-check-fill"></i></button> <button class="btn btn-warning" v-if="task.shareurl_ban" :title="task.shareurl_ban" disabled><i class="bi bi-exclamation-triangle-fill"></i></button>
<button class="btn btn-warning btn-sm" v-if="task.shareurl_ban" :title="task.shareurl_ban" disabled><i class="bi bi-exclamation-triangle-fill"></i></button> <button type="button" class="btn btn-outline-primary" @click="runScriptNow(index)" title="运行此任务" v-else><i class="bi bi-play-fill"></i></button>
<button type="button" class="btn btn-outline-primary btn-sm" @click="runScriptNow(index)" title="运行此任务" v-else><i class="bi bi-play-fill"></i></button> <button type="button" class="btn btn-outline-danger" @click="removeTask(index)" title="删除此任务"><i class="bi bi-trash3-fill"></i></button>
<button type="button" class="btn btn-outline-danger btn-sm" @click="removeTask(index)" title="删除此任务"><i class="bi bi-trash3-fill"></i></button>
</div> </div>
</div> </div>
<div class="collapse ml-3" :id="'collapse_'+index"> <div class="collapse ml-3" :id="'collapse_'+index">
@ -324,15 +280,12 @@
<div class="input-group"> <div class="input-group">
<input type="text" name="taskname[]" class="form-control" v-model="task.taskname" placeholder="必填" @focus="smart_param.showSuggestions=true;focusTaskname(index, task)" @input="changeTaskname(index, task)"> <input type="text" name="taskname[]" class="form-control" v-model="task.taskname" placeholder="必填" @focus="smart_param.showSuggestions=true;focusTaskname(index, task)" @input="changeTaskname(index, task)">
<div class="dropdown-menu show task-suggestions" v-if="smart_param.showSuggestions && smart_param.taskSuggestions.success && smart_param.index === index"> <div class="dropdown-menu show task-suggestions" v-if="smart_param.showSuggestions && smart_param.taskSuggestions.success && smart_param.index === index">
<div class="dropdown-item text-muted text-center" style="font-size:12px;">{{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data.length ? `以下资源来自网络搜索,请自行辨识,如有侵权请联系资源方` : "未搜索到资源" }}</div> <div class="dropdown-item text-muted text-center" style="font-size:12px;">{{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data.length ? `以下资源来自 ${smart_param.taskSuggestions.source} 搜索,请自行辨识,如有侵权请联系资源方` : "未搜索到资源" }}</div>
<div v-for="suggestion in smart_param.taskSuggestions.data" :key="suggestion.taskname" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(index, suggestion)" style="font-size: 12px;" :title="suggestion.content"> <div v-for="suggestion in smart_param.taskSuggestions.data" :key="suggestion.taskname" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(index, suggestion)" style="font-size: 12px;" :title="suggestion.content">
<span v-html="suggestion.verify ? '✅': '❔'"></span> {{ suggestion.taskname }} <span v-html="suggestion.verify ? '✅': '❔'"></span> {{ suggestion.taskname }}
<small class="text-muted"> <small class="text-muted">
<a :href="suggestion.shareurl" target="_blank" @click.stop>{{ suggestion.shareurl }}</a> <a :href="suggestion.shareurl" target="_blank" @click.stop>{{ suggestion.shareurl }}</a>
</small> </small>
<span class="badge bg-transparent border border-success text-success">{{ suggestion.source || "网络公开" }}</span>
<span class="badge bg-transparent border border-info text-info">{{ suggestion.channel }}</span>
<span v-if="suggestion.datetime" class="badge bg-transparent border border-dark text-dark">{{ suggestion.datetime }}</span>
</div> </div>
</div> </div>
<div class="input-group-append" title="深度搜索"> <div class="input-group-append" title="深度搜索">
@ -353,7 +306,7 @@
<div class="input-group"> <div class="input-group">
<input type="text" name="shareurl[]" class="form-control" v-model="task.shareurl" placeholder="必填" @blur="changeShareurl(task)"> <input type="text" name="shareurl[]" class="form-control" v-model="task.shareurl" placeholder="必填" @blur="changeShareurl(task)">
<div class="input-group-append" v-if="task.shareurl"> <div class="input-group-append" v-if="task.shareurl">
<button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;fileSelect.switchShare=false;fileSelect.previewRegex=false;fileSelect.sortBy='file_name';fileSelect.sortOrder='desc';showShareSelect(index)" title="选择文件夹"><i class="bi bi-folder"></i></button> <button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;fileSelect.previewRegex=false;showShareSelect(index)" title="选择文件夹"><i class="bi bi-folder"></i></button>
<div class="input-group-text"> <div class="input-group-text">
<a target="_blank" :href="task.shareurl"><i class="bi bi-box-arrow-up-right"></i></a> <a target="_blank" :href="task.shareurl"><i class="bi bi-box-arrow-up-right"></i></a>
</div> </div>
@ -368,7 +321,7 @@
<input type="text" name="savepath[]" class="form-control" v-model="task.savepath" placeholder="必填" @focus="focusTaskname(index, task)"> <input type="text" name="savepath[]" class="form-control" v-model="task.savepath" placeholder="必填" @focus="focusTaskname(index, task)">
<div class="input-group-append"> <div class="input-group-append">
<button class="btn btn-secondary" type="button" v-if="smart_param.savepath && smart_param.index == index && task.savepath != smart_param.origin_savepath" @click="task.savepath = smart_param.origin_savepath"><i class="bi bi-reply"></i></button> <button class="btn btn-secondary" type="button" v-if="smart_param.savepath && smart_param.index == index && task.savepath != smart_param.origin_savepath" @click="task.savepath = smart_param.origin_savepath"><i class="bi bi-reply"></i></button>
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.sortBy='file_name';fileSelect.sortOrder='asc';showSavepathSelect(index)">选择</button> <button class="btn btn-outline-secondary" type="button" @click="showSavepathSelect(index)">选择</button>
</div> </div>
</div> </div>
</div> </div>
@ -378,9 +331,9 @@
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=true;fileSelect.switchShare=false;fileSelect.previewRegex=true;fileSelect.sortBy='file_name';fileSelect.sortOrder='asc';showShareSelect(index)" title="预览正则处理效果">正则处理</button> <button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=true;fileSelect.previewRegex=true;showShareSelect(index)" title="预览正则处理效果">正则处理</button>
</div> </div>
<input type="text" name="pattern[]" class="form-control" v-model="task.pattern" placeholder="匹配表达式" list="magicRegex" @dblclick="inputRawMagicRegex(task)" title="双击可将魔法匹配释放为填入原始正则表达式"> <input type="text" name="pattern[]" class="form-control" v-model="task.pattern" placeholder="匹配表达式" list="magicRegex">
<input type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式"> <input type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式">
<div class="input-group-append" title="保存时只比较文件名的部分01.mp4 和 01.mkv 视同为同一文件,不重复转存"> <div class="input-group-append" title="保存时只比较文件名的部分01.mp4 和 01.mkv 视同为同一文件,不重复转存">
<div class="input-group-text"> <div class="input-group-text">
@ -399,22 +352,15 @@
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" placeholder="可选,只转存修改日期>此文件的文件" name="startfid[]" v-model="task.startfid"> <input type="text" class="form-control" placeholder="可选,只转存修改日期>此文件的文件" name="startfid[]" v-model="task.startfid">
<div class="input-group-append" v-if="task.shareurl"> <div class="input-group-append" v-if="task.shareurl">
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=false;fileSelect.switchShare=false;fileSelect.previewRegex=false;fileSelect.sortBy='updated_at';fileSelect.sortOrder='desc';showShareSelect(index)">选择</button> <button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=false;fileSelect.previewRegex=false;showShareSelect(index)">选择</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row" title="需匹配到各级嵌套目录名才会更新,否则子目录在第一次转存后不会更新。注意:递归模式原理是逐级索引,深层嵌套目录的场景下效率非常低,慎用 .*"> <div class="form-group row" title="需匹配到各级嵌套目录名才会更新,否则子目录在第一次转存后不会更新。注意:原理是逐级索引,深层嵌套目录的场景下效率非常低,慎用 .*">
<label class="col-sm-2 col-form-label">更新目录</label> <label class="col-sm-2 col-form-label">更新目录</label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group"> <input type="text" name="update_subdir[]" class="form-control" v-model="task.update_subdir" placeholder="可选,匹配需更新子目录(含各级嵌套目录)的正则表达式,多项以|分割,如 4k|1080p">
<input type="text" name="update_subdir[]" class="form-control" v-model="task.update_subdir" placeholder="可选,匹配需更新子目录(含各级嵌套目录)的正则表达式,多项以|分割,如 4k|1080p">
<div class="input-group-append" title="重存模式:删除该目录下所有文件,重新转存,大资源包时推荐使用&#x0A;不勾选为递归模式:递归检查,逐级更新嵌套目录,效率低">
<div class="input-group-text">
<input type="checkbox" v-model="task.update_subdir_resave_mode">&nbsp;重存模式
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -447,10 +393,7 @@
</div> </div>
<div class="row mt-5"> <div class="row mt-5">
<div class="col-sm-12 text-center"> <div class="col-sm-12 text-center">
<div class="btn-group" role="group" aria-label="任务操作"> <button type="button" class="btn btn-primary" @click="addTask()"><i class="bi bi-plus"></i> 增加任务</button>
<button type="button" class="btn btn-primary" @click="addTask()"><i class="bi bi-plus"></i> 增加任务</button>
<button type="button" class="btn btn-primary" @click="addTaskForClipboard()" title="从粘贴板导入"><i class="bi bi-clipboard-plus"></i></button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -499,45 +442,8 @@
</button> </button>
</div> </div>
<div class="modal-body small"> <div class="modal-body small">
<!-- 分享链接来源 -->
<div class="mb-3 row" v-if="fileSelect.switchShare">
<div class="col-sm-8">
<div>
<b>名称:</b>
<span :title="fileSelect.share.content">{{ fileSelect.share.taskname }}</span>
</div>
<div>
<b>链接:</b>
<a :href="fileSelect.share.shareurl" target="_blank" @click.stop>{{ fileSelect.share.shareurl }}</a>
</div>
<div>
<b>来源:</b>
<span class="badge bg-transparent border border-success text-success">{{ fileSelect.share.source || "网络公开" }}</span>
<span class="badge bg-transparent border border-info text-info" v-if="fileSelect.share.channel">{{ fileSelect.share.channel }}</span>
</div>
<div v-if="fileSelect.share.datetime">
<b>时间:</b>
<span>{{ fileSelect.share.datetime }}</span>
</div>
</div>
<div class="col-sm-4 text-right">
<div class="btn-group" title="资源搜索结果切换">
<button type="button" class="btn btn-sm btn-outline-primary" @click="switchShare(-1)">上一个</button>
<button type="button" class="btn btn-sm btn-outline-primary" @click="switchShare(1)">下一个</button>
</div>
</div>
</div>
<div class="alert alert-warning" v-if="fileSelect.error" v-html="fileSelect.error"></div> <div class="alert alert-warning" v-if="fileSelect.error" v-html="fileSelect.error"></div>
<div v-else> <div v-else>
<!-- 正则处理表达式 -->
<div class="mb-3" v-if="fileSelect.previewRegex && fileSelect.index<this.formData.tasklist.length">
<div><b>匹配表达式:</b><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
<span class="badge badge-info" v-if="formData.tasklist[fileSelect.index].pattern in formData.magic_regex">{{ formData.magic_regex[formData.tasklist[fileSelect.index].pattern].pattern }}</span>
</div>
<div><b>替换表达式:</b><span class="badge badge-info" v-if="formData.tasklist[fileSelect.index].replace" v-html="formData.tasklist[fileSelect.index].replace"></span>
<span class="badge badge-info" v-else-if="formData.tasklist[fileSelect.index].pattern in formData.magic_regex">{{ formData.magic_regex[formData.tasklist[fileSelect.index].pattern].replace }}</span>
</div>
</div>
<!-- 面包屑导航 --> <!-- 面包屑导航 -->
<nav aria-label="breadcrumb" v-if="fileSelect.selectDir"> <nav aria-label="breadcrumb" v-if="fileSelect.selectDir">
<ol class="breadcrumb"> <ol class="breadcrumb">
@ -549,39 +455,35 @@
</ol> </ol>
</nav> </nav>
<!-- 文件列表 --> <!-- 文件列表 -->
<div class="mb-3" v-if="fileSelect.previewRegex">
<div><b>匹配表达式:</b><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
<span class="badge badge-info" v-if="formData.tasklist[fileSelect.index].pattern in formData.magic_regex">{{ formData.magic_regex[formData.tasklist[fileSelect.index].pattern].pattern }}</span>
</div>
<div><b>替换表达式:</b><span class="badge badge-info" v-if="formData.tasklist[fileSelect.index].replace" v-html="formData.tasklist[fileSelect.index].replace"></span>
<span class="badge badge-info" v-else-if="formData.tasklist[fileSelect.index].pattern in formData.magic_regex">{{ formData.magic_regex[formData.tasklist[fileSelect.index].pattern].replace }}</span>
</div>
</div>
<table class="table table-hover table-sm"> <table class="table table-hover table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col" class="cursor-pointer" @click="sortFileList('file_name')"> <th scope="col">文件名</th>
文件名 <th scope="col" v-if="fileSelect.selectShare">正则处理</th>
<span v-if="fileSelect.sortBy === 'file_name'">{{ fileSelect.sortOrder === "asc" ? "↑" : "↓" }}</span>
</th>
<th scope="col" v-if="fileSelect.selectShare">
正则处理
</th>
<template v-if="!fileSelect.previewRegex"> <template v-if="!fileSelect.previewRegex">
<th scope="col"> <th scope="col">大小</th>
大小 <th scope="col">修改日期 ↓</th>
</th> <th scope="col" v-if="!fileSelect.selectShare">操作</th>
<th scope="col" class="cursor-pointer" @click="sortFileList('updated_at')">
修改日期
<span v-if="fileSelect.sortBy === 'updated_at'">{{ fileSelect.sortOrder === "asc" ? "↑" : "↓" }}</span>
</th>
<th scope="col" v-if="!fileSelect.selectShare">
操作
</th>
</template> </template>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(file, key) in fileSelect.fileList" :key="key" @click="fileSelect.selectDir ? (file.dir ? navigateTo(file.fid, file.file_name) : null) : selectStartFid(file.fid)" :class="{'cursor-pointer': fileSelect.selectDir ? file.dir : true}"> <tr v-for="(file, key) in fileSelect.fileList" :key="key" @click="fileSelect.selectDir ? (file.dir ? navigateTo(file.fid, file.file_name) : null) : selectStartFid(file.fid)" :class="{'cursor-pointer': fileSelect.selectDir ? file.dir : true}">
<td><i class="bi mr-1" :class="file.dir ? 'bi-folder-fill text-warning' : 'bi-file-earmark'"></i>{{file.file_name}}</td> <td><i class="bi" :class="file.dir ? 'bi-folder-fill text-warning' : 'bi-file-earmark'"></i> {{file.file_name}}</td>
<td v-if="fileSelect.selectShare" :class="file.file_name_re ? 'text-success' : file.file_name_saved ? 'text-muted' : 'text-danger'">{{file.file_name_re || file.file_name_saved || '&times;'}}</td> <td v-if="fileSelect.selectShare" :class="file.file_name_re ? 'text-success' : 'text-danger'">{{file.file_name_re || '&times;'}}</td>
<template v-if="!fileSelect.previewRegex"> <template v-if="!fileSelect.previewRegex">
<td v-if="file.dir">{{ file.include_items }}项</td> <td v-if="file.dir">{{ file.include_items }}项</td>
<td v-else>{{file.size | size}}</td> <td v-else>{{file.size | size}}</td>
<td>{{file.updated_at | ts2date}}</td> <td>{{file.updated_at | ts2date}}</td>
<td v-if="!fileSelect.selectShare"><a class="cursor-pointer text-muted" @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)">删除</a></td> <td v-if="!fileSelect.selectShare"><a @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)">删除</a></td>
</template> </template>
</tr> </tr>
</tbody> </tbody>
@ -591,24 +493,11 @@
<div class="modal-footer" v-if="fileSelect.selectDir && !fileSelect.previewRegex"> <div class="modal-footer" v-if="fileSelect.selectDir && !fileSelect.previewRegex">
<span v-html="fileSelect.selectShare ? '转存:' : '保存到:'"></span> <span v-html="fileSelect.selectShare ? '转存:' : '保存到:'"></span>
<button type="button" class="btn btn-primary btn-sm" @click="selectCurrentFolder()">当前文件夹</button> <button type="button" class="btn btn-primary btn-sm" @click="selectCurrentFolder()">当前文件夹</button>
<button type="button" class="btn btn-primary btn-sm" v-if="!fileSelect.selectShare" @click="selectCurrentFolder(true)">当前文件夹<span class="badge badge-light" v-if="fileSelect.index<this.formData.tasklist.length" v-html="'/'+formData.tasklist[fileSelect.index].taskname"></span></button> <button type="button" class="btn btn-primary btn-sm" v-if="!fileSelect.selectShare" @click="selectCurrentFolder(true)">当前文件夹<span class="badge badge-light" v-html="'/'+formData.tasklist[fileSelect.index].taskname"></span></button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Toast 提示 -->
<div class="toast-container">
<div v-for="toast in toasts" :key="toast.id" class="toast show shadow-sm" :class="toast.type" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body d-flex align-items-center">
<i class="bi mr-2" :class="getToastIcon(toast.type)"></i>
<span>{{ toast.message }}</span>
<button type="button" class="ml-auto close" @click="removeToast(toast.id)" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
</div>
</div> </div>
@ -627,21 +516,14 @@
tasklist: [], tasklist: [],
magic_regex: {}, magic_regex: {},
source: { source: {
net: {
enable: ""
},
cloudsaver: { cloudsaver: {
server: "", server: "",
username: "", username: "",
password: "", password: "",
token: "" token: ""
},
pansou: {
server: ""
} }
}, },
}, },
toasts: [],
newTask: { newTask: {
taskname: "", taskname: "",
shareurl: "", shareurl: "",
@ -671,17 +553,13 @@
configModified: false, configModified: false,
fileSelect: { fileSelect: {
index: null, index: null,
share: {},
shareurl: "", shareurl: "",
stoken: "", stoken: "",
fileList: [], fileList: [],
paths: [], paths: [],
selectDir: true, selectDir: true,
selectShare: true, selectShare: true,
switchShare: false,
previewRegex: false, previewRegex: false,
sortBy: "updated_at",
sortOrder: "desc"
}, },
}, },
filters: { filters: {
@ -775,16 +653,6 @@
token: "" token: ""
}; };
} }
if (!config_data.source.pansou) {
config_data.source.pansou = {
server: ""
};
}
if (!config_data.source.net) {
config_data.source.net = {
enable: ""
};
}
this.formData = config_data; this.formData = config_data;
setTimeout(() => { setTimeout(() => {
this.configModified = false; this.configModified = false;
@ -817,9 +685,9 @@
.then(response => { .then(response => {
if (response.data.success) { if (response.data.success) {
this.configModified = false; this.configModified = false;
this.showToast(response.data.message, 'success'); alert(response.data.message);
} else { } else {
this.showToast(response.data.message, 'error'); alert(response.data.message);
} }
console.log('Config saved result:', response.data); console.log('Config saved result:', response.data);
}) })
@ -834,9 +702,6 @@
if (this.formData.cookie[index] == "" || confirm("确认删除吗?")) if (this.formData.cookie[index] == "" || confirm("确认删除吗?"))
this.formData.cookie.splice(index, 1); this.formData.cookie.splice(index, 1);
}, },
testPush() {
this.runScriptNow(1, true);
},
addPush() { addPush() {
key = prompt("增加的键名", ""); key = prompt("增加的键名", "");
if (key != "" && key != null) if (key != "" && key != null)
@ -936,15 +801,9 @@
clearData(target) { clearData(target) {
this[target] = ""; this[target] = "";
}, },
async runScriptNow(task_index = null, test = false) { async runScriptNow(task_index = null) {
body = {}; body = {};
if (test) { if (task_index != null) {
body = {
"quark_test": true,
"cookie": this.formData.cookie,
"push_config": this.formData.push_config
};
} else if (task_index != null) {
task = { ...this.formData.tasklist[task_index] }; task = { ...this.formData.tasklist[task_index] };
delete task.runweek; delete task.runweek;
delete task.enddate; delete task.enddate;
@ -978,6 +837,10 @@
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) { if (done) {
console.log('Stream complete.');
this.modalLoading = false;
// 运行后刷新数据
this.fetchData();
break; break;
} }
partialData += decoder.decode(value); partialData += decoder.decode(value);
@ -987,12 +850,10 @@
const eventData = line.substring(5).trim(); const eventData = line.substring(5).trim();
if (eventData === '[DONE]') { if (eventData === '[DONE]') {
this.modalLoading = false; this.modalLoading = false;
if (task_index == null) { this.fetchData();
this.fetchData(); return;
}
break;
} }
this.run_log += eventData.replace('<', '<\u200B') + '\n'; this.run_log += eventData + '\n';
// 在更新 run_log 后将滚动条滚动到底部 // 在更新 run_log 后将滚动条滚动到底部
this.$nextTick(() => { this.$nextTick(() => {
const modalBody = document.querySelector('.modal-body'); const modalBody = document.querySelector('.modal-body');
@ -1041,7 +902,6 @@
} }
}, },
searchSuggestions(index, taskname, deep = 1) { searchSuggestions(index, taskname, deep = 1) {
taskname = taskname.replace(/\((19|20)\d{2}\)/g, '').trim();
if (taskname.length < 2) { if (taskname.length < 2) {
console.log(`任务名[${taskname}]过短${taskname.length} 不进行搜索`); console.log(`任务名[${taskname}]过短${taskname.length} 不进行搜索`);
return; return;
@ -1071,9 +931,7 @@
selectSuggestion(index, suggestion) { selectSuggestion(index, suggestion) {
this.smart_param.showSuggestions = false; this.smart_param.showSuggestions = false;
this.fileSelect.selectDir = true; this.fileSelect.selectDir = true;
this.fileSelect.switchShare = true;
this.fileSelect.previewRegex = false; this.fileSelect.previewRegex = false;
this.fileSelect.share = suggestion;
this.showShareSelect(index, suggestion.shareurl); this.showShareSelect(index, suggestion.shareurl);
}, },
addMagicRegex() { addMagicRegex() {
@ -1083,7 +941,7 @@
updateMagicRegexKey(oldKey, newKey) { updateMagicRegexKey(oldKey, newKey) {
if (oldKey !== newKey) { if (oldKey !== newKey) {
if (this.formData.magic_regex[newKey]) { if (this.formData.magic_regex[newKey]) {
this.showToast(`魔法名 [${newKey}] 已存在,请使用其他名称`, 'warning'); alert(`魔法名 [${newKey}] 已存在,请使用其他名称`);
return; return;
} }
this.$set(this.formData.magic_regex, newKey, this.formData.magic_regex[oldKey]); this.$set(this.formData.magic_regex, newKey, this.formData.magic_regex[oldKey]);
@ -1103,7 +961,7 @@
if (response.data.code == 0) { if (response.data.code == 0) {
this.fileSelect.fileList = this.fileSelect.fileList.filter(item => item.fid != fid); this.fileSelect.fileList = this.fileSelect.fileList.filter(item => item.fid != fid);
} else { } else {
this.showToast('删除失败:' + response.data.message, 'error'); alert('删除失败:' + response.data.message);
} }
}).catch(error => { }).catch(error => {
console.error('Error /delete_file:', error); console.error('Error /delete_file:', error);
@ -1119,8 +977,7 @@
axios.get('/get_savepath_detail', { axios.get('/get_savepath_detail', {
params: params params: params
}).then(response => { }).then(response => {
this.fileSelect.fileList = response.data.data.list; this.fileSelect.fileList = response.data.data.list
this.sortFileList(this.fileSelect.sortBy, this.fileSelect.sortOrder);
if (response.data.data.paths?.length > 0) { if (response.data.data.paths?.length > 0) {
this.fileSelect.paths = response.data.data.paths this.fileSelect.paths = response.data.data.paths
} }
@ -1134,7 +991,6 @@
showSavepathSelect(index) { showSavepathSelect(index) {
this.fileSelect.selectShare = false; this.fileSelect.selectShare = false;
this.fileSelect.selectDir = true; this.fileSelect.selectDir = true;
this.fileSelect.switchShare = false;
this.fileSelect.previewRegex = false; this.fileSelect.previewRegex = false;
this.fileSelect.error = undefined; this.fileSelect.error = undefined;
this.fileSelect.fileList = []; this.fileSelect.fileList = [];
@ -1149,12 +1005,15 @@
axios.post('/get_share_detail', { axios.post('/get_share_detail', {
shareurl: this.fileSelect.shareurl, shareurl: this.fileSelect.shareurl,
stoken: this.fileSelect.stoken, stoken: this.fileSelect.stoken,
task: this.formData.tasklist[this.fileSelect.index], regex: {
magic_regex: this.formData.magic_regex, pattern: this.formData.tasklist[this.fileSelect.index].pattern,
replace: this.formData.tasklist[this.fileSelect.index].replace,
taskname: this.formData.tasklist[this.fileSelect.index].taskname,
magic_regex: this.formData.magic_regex,
}
}).then(response => { }).then(response => {
if (response.data.success) { if (response.data.success) {
this.fileSelect.fileList = response.data.data.list; this.fileSelect.fileList = response.data.data.list;
this.sortFileList(this.fileSelect.sortBy, this.fileSelect.sortOrder);
this.fileSelect.paths = response.data.data.paths; this.fileSelect.paths = response.data.data.paths;
this.fileSelect.stoken = response.data.data.stoken; this.fileSelect.stoken = response.data.data.stoken;
} else { } else {
@ -1182,23 +1041,6 @@
$('#fileSelectModal').modal('toggle'); $('#fileSelectModal').modal('toggle');
this.getShareDetail(); this.getShareDetail();
}, },
switchShare(index) {
currentIndex = this.smart_param.taskSuggestions.data.indexOf(this.fileSelect.share);
nextIndex = currentIndex + index;
if (nextIndex < 0) {
this.showToast("没有上一个啦", "info");
} else if (nextIndex >= this.smart_param.taskSuggestions.data.length) {
this.showToast("没有下一个啦", "info");
} else {
this.fileSelect.error = "";
this.fileSelect.stoken = "";
this.fileSelect.share = this.smart_param.taskSuggestions.data[nextIndex];
this.fileSelect.shareurl = this.smart_param.taskSuggestions.data[nextIndex].shareurl;
this.fileSelect.paths = [];
this.fileSelect.fileList = [];
this.getShareDetail();
}
},
navigateTo(fid, name) { navigateTo(fid, name) {
dir = { fid: fid, name: name } dir = { fid: fid, name: name }
if (this.fileSelect.selectShare) { if (this.fileSelect.selectShare) {
@ -1240,111 +1082,12 @@
} else if (shareurl.includes(dir.fid)) { } else if (shareurl.includes(dir.fid)) {
shareurl = shareurl.match(`.*/${dir.fid}[^/]*`)[0] shareurl = shareurl.match(`.*/${dir.fid}[^/]*`)[0]
} else if (shareurl.includes('#/list/share')) { } else if (shareurl.includes('#/list/share')) {
shareurl = `${shareurl.split('#')[0]}#/list/share/${dir.fid}` shareurl = `${shareurl}/${dir.fid}-${dir.name?.replace('-', '*101')}`
} else { } else {
shareurl = `${shareurl.split('#')[0]}#/list/share/${dir.fid}` shareurl = `${shareurl}#/list/share/${dir.fid}-${dir.name?.replace('-', '*101')}`
} }
return shareurl; return shareurl;
}, }
sortFileList(column, order) {
if (this.fileSelect.sortBy === column && !order) {
this.fileSelect.sortOrder = this.fileSelect.sortOrder === "asc" ? "desc" : "asc";
} else {
this.fileSelect.sortBy = column;
this.fileSelect.sortOrder = order || "asc";
}
this.fileSelect.fileList.sort((a, b) => {
let valA = a[this.fileSelect.sortBy];
let valB = b[this.fileSelect.sortBy];
if (typeof valA === "string") valA = valA.toLowerCase();
if (typeof valB === "string") valB = valB.toLowerCase();
if (valA < valB) return this.fileSelect.sortOrder === "asc" ? -1 : 1;
if (valA > valB) return this.fileSelect.sortOrder === "asc" ? 1 : -1;
return 0;
});
},
inputRawMagicRegex(task) {
const item = this.formData.magic_regex[task.pattern];
if (item) {
task.pattern = item.pattern;
task.replace = item.replace;
}
},
copyText(text, callback = () => { }) {
if (!text) {
console.error('No text to copy');
return;
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text);
} else {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.top = '0';
textarea.style.left = '0';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
textarea.setSelectionRange(0, 99999);
document.execCommand("copy");
document.body.removeChild(textarea);
}
callback()
},
copyTaskToClipboard(index) {
const task = { ...this.formData.tasklist[index] };
delete task.addition;
const _this = this;
this.copyText(JSON.stringify(task), function () {
_this.showToast("任务参数已复制到剪贴板", "success");
});
},
async addTaskForClipboard() {
text = null
try {
text = await navigator.clipboard.readText();
} catch (error) {
text = prompt("当前环境不支持自动读取粘贴板,请手动粘贴任务参数", "");
}
if (text) {
try {
let task = JSON.parse(text);
task = { ...this.newTask, ...task };
this.formData.tasklist.push(task);
this.showToast("剪贴板参数已成功导入任务", "success");
// 滚到最下
setTimeout(() => {
$('#collapse_' + (this.formData.tasklist.length - 1)).collapse('show').on('shown.bs.collapse', () => {
this.scrollToX();
});
}, 1);
} catch (error) {
this.showToast("解析剪贴板内容失败", "error");
}
}
},
showToast(message, type = 'info', duration = 3000) {
const id = Date.now();
this.toasts.push({ id, message, type });
setTimeout(() => {
this.removeToast(id);
}, duration);
},
removeToast(id) {
this.toasts = this.toasts.filter(t => t.id !== id);
},
getToastIcon(type) {
switch (type) {
case 'success': return 'bi-check-circle-fill text-success';
case 'error': return 'bi-exclamation-circle-fill text-danger';
case 'warning': return 'bi-exclamation-triangle-fill text-warning';
default: return 'bi-info-circle-fill text-info';
}
},
} }
}); });
</script> </script>

209
notify.py
View File

@ -72,13 +72,8 @@ push_config = {
'CHAT_URL': '', # synology chat url 'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token 'CHAT_TOKEN': '', # synology chat token
'PUSH_PLUS_TOKEN': '', # pushplus 推送的用户令牌 'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # pushplus 推送的群组编码 'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
'PUSH_PLUS_TEMPLATE': 'html', # pushplus 发送模板支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay
'PUSH_PLUS_CHANNEL': 'wechat', # pushplus 发送渠道支持wechat,webhook,cp,mail,sms
'PUSH_PLUS_WEBHOOK': '', # pushplus webhook编码可在pushplus公众号上扩展配置出更多渠道
'PUSH_PLUS_CALLBACKURL': '', # pushplus 发送结果回调地址,会把推送最终结果通知到这个地址上
'PUSH_PLUS_TO': '', # pushplus 好友令牌微信公众号渠道填写好友令牌企业微信渠道填写企业微信用户id
'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌 'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌
'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者 'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者
@ -128,19 +123,6 @@ push_config = {
'NTFY_URL': '', # ntfy地址,如https://ntfy.sh 'NTFY_URL': '', # ntfy地址,如https://ntfy.sh
'NTFY_TOPIC': '', # ntfy的消息应用topic 'NTFY_TOPIC': '', # ntfy的消息应用topic
'NTFY_PRIORITY':'3', # 推送消息优先级,默认为3 'NTFY_PRIORITY':'3', # 推送消息优先级,默认为3
'NTFY_TOKEN': '', # 推送token,可选
'NTFY_USERNAME': '', # 推送用户名称,可选
'NTFY_PASSWORD': '', # 推送用户密码,可选
'NTFY_ACTIONS': '', # 推送用户动作,可选
'WXPUSHER_APP_TOKEN': '', # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/
'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
'WXPUSHER_UIDS': '', # wxpusher 的 用户ID多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
'DODO_BOTTOKEN': '', # DoDo机器人的token DoDo开发平台https://doker.imdodo.com/
'DODO_BOTID': '', # DoDo机器人的id
'DODO_LANDSOURCEID': '', # DoDo机器人所在的群ID
'DODO_SOURCEID': '', # DoDo机器人推送目标用户的ID
} }
# fmt: on # fmt: on
@ -155,6 +137,7 @@ def bark(title: str, content: str) -> None:
使用 bark 推送消息 使用 bark 推送消息
""" """
if not push_config.get("BARK_PUSH"): if not push_config.get("BARK_PUSH"):
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
return return
print("bark 服务启动") print("bark 服务启动")
@ -207,6 +190,7 @@ def dingding_bot(title: str, content: str) -> None:
使用 钉钉机器人 推送消息 使用 钉钉机器人 推送消息
""" """
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"): if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
print("钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\n取消推送")
return return
print("钉钉机器人 服务启动") print("钉钉机器人 服务启动")
@ -236,6 +220,7 @@ def feishu_bot(title: str, content: str) -> None:
使用 飞书机器人 推送消息 使用 飞书机器人 推送消息
""" """
if not push_config.get("FSKEY"): if not push_config.get("FSKEY"):
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
return return
print("飞书 服务启动") print("飞书 服务启动")
@ -254,6 +239,7 @@ def go_cqhttp(title: str, content: str) -> None:
使用 go_cqhttp 推送消息 使用 go_cqhttp 推送消息
""" """
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"): if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
return return
print("go-cqhttp 服务启动") print("go-cqhttp 服务启动")
@ -271,6 +257,7 @@ def gotify(title: str, content: str) -> None:
使用 gotify 推送消息 使用 gotify 推送消息
""" """
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"): if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
return return
print("gotify 服务启动") print("gotify 服务启动")
@ -293,6 +280,7 @@ def iGot(title: str, content: str) -> None:
使用 iGot 推送消息 使用 iGot 推送消息
""" """
if not push_config.get("IGOT_PUSH_KEY"): if not push_config.get("IGOT_PUSH_KEY"):
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
return return
print("iGot 服务启动") print("iGot 服务启动")
@ -312,12 +300,13 @@ def serverJ(title: str, content: str) -> None:
通过 serverJ 推送消息 通过 serverJ 推送消息
""" """
if not push_config.get("PUSH_KEY"): if not push_config.get("PUSH_KEY"):
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
return return
print("serverJ 服务启动") print("serverJ 服务启动")
data = {"text": title, "desp": content.replace("\n", "\n\n")} data = {"text": title, "desp": content.replace("\n", "\n\n")}
match = re.match(r"sctp(\d+)t", push_config.get("PUSH_KEY")) match = re.match(r'sctp(\d+)t', push_config.get("PUSH_KEY"))
if match: if match:
num = match.group(1) num = match.group(1)
url = f'https://{num}.push.ft07.com/send/{push_config.get("PUSH_KEY")}.send' url = f'https://{num}.push.ft07.com/send/{push_config.get("PUSH_KEY")}.send'
@ -337,6 +326,7 @@ def pushdeer(title: str, content: str) -> None:
通过PushDeer 推送消息 通过PushDeer 推送消息
""" """
if not push_config.get("DEER_KEY"): if not push_config.get("DEER_KEY"):
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
return return
print("PushDeer 服务启动") print("PushDeer 服务启动")
data = { data = {
@ -362,6 +352,7 @@ def chat(title: str, content: str) -> None:
通过Chat 推送消息 通过Chat 推送消息
""" """
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"): if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
return return
print("chat 服务启动") print("chat 服务启动")
data = "payload=" + json.dumps({"text": title + "\n" + content}) data = "payload=" + json.dumps({"text": title + "\n" + content})
@ -376,36 +367,26 @@ def chat(title: str, content: str) -> None:
def pushplus_bot(title: str, content: str) -> None: def pushplus_bot(title: str, content: str) -> None:
""" """
通过 pushplus 推送消息 通过 push+ 推送消息
""" """
if not push_config.get("PUSH_PLUS_TOKEN"): if not push_config.get("PUSH_PLUS_TOKEN"):
print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送")
return return
print("PUSHPLUS 服务启动") print("PUSHPLUS 服务启动")
url = "https://www.pushplus.plus/send" url = "http://www.pushplus.plus/send"
data = { data = {
"token": push_config.get("PUSH_PLUS_TOKEN"), "token": push_config.get("PUSH_PLUS_TOKEN"),
"title": title, "title": title,
"content": content, "content": content,
"topic": push_config.get("PUSH_PLUS_USER"), "topic": push_config.get("PUSH_PLUS_USER"),
"template": push_config.get("PUSH_PLUS_TEMPLATE"),
"channel": push_config.get("PUSH_PLUS_CHANNEL"),
"webhook": push_config.get("PUSH_PLUS_WEBHOOK"),
"callbackUrl": push_config.get("PUSH_PLUS_CALLBACKURL"),
"to": push_config.get("PUSH_PLUS_TO"),
} }
body = json.dumps(data).encode(encoding="utf-8") body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
response = requests.post(url=url, data=body, headers=headers).json() response = requests.post(url=url, data=body, headers=headers).json()
code = response["code"] if response["code"] == 200:
if code == 200: print("PUSHPLUS 推送成功!")
print("PUSHPLUS 推送请求成功,可根据流水号查询推送结果:" + response["data"])
print(
"注意请求成功并不代表推送成功如未收到消息请到pushplus官网使用流水号查询推送最终结果"
)
elif code == 900 or code == 903 or code == 905 or code == 999:
print(response["msg"])
else: else:
url_old = "http://pushplus.hxtrip.com/send" url_old = "http://pushplus.hxtrip.com/send"
@ -424,6 +405,7 @@ def weplus_bot(title: str, content: str) -> None:
通过 微加机器人 推送消息 通过 微加机器人 推送消息
""" """
if not push_config.get("WE_PLUS_BOT_TOKEN"): if not push_config.get("WE_PLUS_BOT_TOKEN"):
print("微加机器人 服务的 WE_PLUS_BOT_TOKEN 未设置!!\n取消推送")
return return
print("微加机器人 服务启动") print("微加机器人 服务启动")
@ -455,6 +437,7 @@ def qmsg_bot(title: str, content: str) -> None:
使用 qmsg 推送消息 使用 qmsg 推送消息
""" """
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"): if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送")
return return
print("qmsg 服务启动") print("qmsg 服务启动")
@ -473,10 +456,11 @@ def wecom_app(title: str, content: str) -> None:
通过 企业微信 APP 推送消息 通过 企业微信 APP 推送消息
""" """
if not push_config.get("QYWX_AM"): if not push_config.get("QYWX_AM"):
print("QYWX_AM 未设置!!\n取消推送")
return return
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM")) QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
if 4 < len(QYWX_AM_AY) > 5: if 4 < len(QYWX_AM_AY) > 5:
print("QYWX_AM 设置错误!!") print("QYWX_AM 设置错误!!\n取消推送")
return return
print("企业微信 APP 服务启动") print("企业微信 APP 服务启动")
@ -569,6 +553,7 @@ def wecom_bot(title: str, content: str) -> None:
通过 企业微信机器人 推送消息 通过 企业微信机器人 推送消息
""" """
if not push_config.get("QYWX_KEY"): if not push_config.get("QYWX_KEY"):
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
return return
print("企业微信机器人服务启动") print("企业微信机器人服务启动")
@ -594,6 +579,7 @@ def telegram_bot(title: str, content: str) -> None:
使用 telegram 机器人 推送消息 使用 telegram 机器人 推送消息
""" """
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"): if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
return return
print("tg 服务启动") print("tg 服务启动")
@ -642,6 +628,9 @@ def aibotk(title: str, content: str) -> None:
or not push_config.get("AIBOTK_TYPE") or not push_config.get("AIBOTK_TYPE")
or not push_config.get("AIBOTK_NAME") or not push_config.get("AIBOTK_NAME")
): ):
print(
"智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送"
)
return return
print("智能微秘书 服务启动") print("智能微秘书 服务启动")
@ -680,6 +669,9 @@ def smtp(title: str, content: str) -> None:
or not push_config.get("SMTP_PASSWORD") or not push_config.get("SMTP_PASSWORD")
or not push_config.get("SMTP_NAME") or not push_config.get("SMTP_NAME")
): ):
print(
"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送"
)
return return
print("SMTP 邮件 服务启动") print("SMTP 邮件 服务启动")
@ -734,6 +726,7 @@ def pushme(title: str, content: str) -> None:
使用 PushMe 推送消息 使用 PushMe 推送消息
""" """
if not push_config.get("PUSHME_KEY"): if not push_config.get("PUSHME_KEY"):
print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送")
return return
print("PushMe 服务启动") print("PushMe 服务启动")
@ -766,6 +759,7 @@ def chronocat(title: str, content: str) -> None:
or not push_config.get("CHRONOCAT_QQ") or not push_config.get("CHRONOCAT_QQ")
or not push_config.get("CHRONOCAT_TOKEN") or not push_config.get("CHRONOCAT_TOKEN")
): ):
print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送")
return return
print("CHRONOCAT 服务启动") print("CHRONOCAT 服务启动")
@ -809,17 +803,17 @@ def ntfy(title: str, content: str) -> None:
""" """
通过 Ntfy 推送消息 通过 Ntfy 推送消息
""" """
def encode_rfc2047(text: str) -> str: def encode_rfc2047(text: str) -> str:
"""将文本编码为符合 RFC 2047 标准的格式""" """将文本编码为符合 RFC 2047 标准的格式"""
encoded_bytes = base64.b64encode(text.encode("utf-8")) encoded_bytes = base64.b64encode(text.encode('utf-8'))
encoded_str = encoded_bytes.decode("utf-8") encoded_str = encoded_bytes.decode('utf-8')
return f"=?utf-8?B?{encoded_str}?=" return f'=?utf-8?B?{encoded_str}?='
if not push_config.get("NTFY_TOPIC"): if not push_config.get("NTFY_TOPIC"):
print("ntfy 服务的 NTFY_TOPIC 未设置!!\n取消推送")
return return
print("ntfy 服务启动") print("ntfy 服务启动")
priority = "3" priority = '3'
if not push_config.get("NTFY_PRIORITY"): if not push_config.get("NTFY_PRIORITY"):
print("ntfy 服务的NTFY_PRIORITY 未设置!!默认设置为3") print("ntfy 服务的NTFY_PRIORITY 未设置!!默认设置为3")
else: else:
@ -828,15 +822,11 @@ def ntfy(title: str, content: str) -> None:
# 使用 RFC 2047 编码 title # 使用 RFC 2047 编码 title
encoded_title = encode_rfc2047(title) encoded_title = encode_rfc2047(title)
data = content.encode(encoding="utf-8") data = content.encode(encoding='utf-8')
headers = {"Title": encoded_title, "Priority": priority, "Icon": "https://qn.whyour.cn/logo.png"} # 使用编码后的 title headers = {
if push_config.get("NTFY_TOKEN"): "Title": encoded_title, # 使用编码后的 title
headers['Authorization'] = "Bearer " + push_config.get("NTFY_TOKEN") "Priority": priority
elif push_config.get("NTFY_USERNAME") and push_config.get("NTFY_PASSWORD"): }
authStr = push_config.get("NTFY_USERNAME") + ":" + push_config.get("NTFY_PASSWORD")
headers['Authorization'] = "Basic " + base64.b64encode(authStr.encode('utf-8')).decode('utf-8')
if push_config.get("NTFY_ACTIONS"):
headers['Actions'] = encode_rfc2047(push_config.get("NTFY_ACTIONS"))
url = push_config.get("NTFY_URL") + "/" + push_config.get("NTFY_TOPIC") url = push_config.get("NTFY_URL") + "/" + push_config.get("NTFY_TOPIC")
response = requests.post(url, data=data, headers=headers) response = requests.post(url, data=data, headers=headers)
@ -845,111 +835,6 @@ def ntfy(title: str, content: str) -> None:
else: else:
print("Ntfy 推送失败!错误信息:", response.text) print("Ntfy 推送失败!错误信息:", response.text)
def dodo_bot(title: str, content: str) -> None:
"""
通过 DoDo机器人 推送消息
"""
required_keys = [
'DODO_BOTTOKEN',
'DODO_BOTID',
'DODO_LANDSOURCEID',
'DODO_SOURCEID'
]
if not all(push_config.get(key) for key in required_keys):
missing = [key for key in required_keys if not push_config.get(key)]
print(f"DoDo 服务配置不完整,缺少以下参数: {', '.join(missing)}\n取消推送")
return
print("DoDo 服务启动")
url="https://botopen.imdodo.com/api/v2/personal/message/send"
botID=push_config.get('DODO_BOTID')
botToken=push_config.get('DODO_BOTTOKEN')
islandSourceId=push_config.get('DODO_LANDSOURCEID')
dodoSourceId=push_config.get('DODO_SOURCEID')
headers = {
'Authorization': f'Bot {botID}.{botToken}',
'Content-Type': 'application/json',
'Host': 'botopen.imdodo.com'
}
payload = json.dumps({
"islandSourceId": islandSourceId,
"dodoSourceId": dodoSourceId,
"messageType": 1,
"messageBody": {
"content": f"{title}\n\n{content}"
}
})
try:
response = requests.post(url, headers=headers, data=payload)
if response.status_code == 200:
response = response.json()
if response.get("status") == 0 and response.get("message") == "success":
print(f'DoDo 推送成功!')
else:
print(f'DoDo 推送失败!错误信息:\n{response}')
else:
print("DoDo 推送失败!错误信息:", response.text)
except Exception as e:
print(f"DoDo 推送请求异常: {str(e)}")
def wxpusher_bot(title: str, content: str) -> None:
"""
通过 wxpusher 推送消息
支持的环境变量:
- WXPUSHER_APP_TOKEN: appToken
- WXPUSHER_TOPIC_IDS: 主题ID, 多个用英文分号;分隔
- WXPUSHER_UIDS: 用户ID, 多个用英文分号;分隔
"""
if not push_config.get("WXPUSHER_APP_TOKEN"):
return
url = "https://wxpusher.zjiecode.com/api/send/message"
# 处理topic_ids和uids将分号分隔的字符串转为数组
topic_ids = []
if push_config.get("WXPUSHER_TOPIC_IDS"):
topic_ids = [
int(id.strip())
for id in push_config.get("WXPUSHER_TOPIC_IDS").split(";")
if id.strip()
]
uids = []
if push_config.get("WXPUSHER_UIDS"):
uids = [
uid.strip()
for uid in push_config.get("WXPUSHER_UIDS").split(";")
if uid.strip()
]
# topic_ids uids 至少有一个
if not topic_ids and not uids:
print("wxpusher 服务的 WXPUSHER_TOPIC_IDS 和 WXPUSHER_UIDS 至少设置一个!!")
return
print("wxpusher 服务启动")
data = {
"appToken": push_config.get("WXPUSHER_APP_TOKEN"),
"content": f"<h1>{title}</h1><br/><div style='white-space: pre-wrap;'>{content}</div>",
"summary": title,
"contentType": 2,
"topicIds": topic_ids,
"uids": uids,
"verifyPayType": 0,
}
headers = {"Content-Type": "application/json"}
response = requests.post(url=url, json=data, headers=headers).json()
if response.get("code") == 1000:
print("wxpusher 推送成功!")
else:
print(f"wxpusher 推送失败!错误信息:{response.get('msg')}")
def parse_headers(headers): def parse_headers(headers):
if not headers: if not headers:
return {} return {}
@ -1006,6 +891,7 @@ def custom_notify(title: str, content: str) -> None:
通过 自定义通知 推送消息 通过 自定义通知 推送消息
""" """
if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"): if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"):
print("自定义通知的 WEBHOOK_URL 或 WEBHOOK_METHOD 未设置!!\n取消推送")
return return
print("自定义通知服务启动") print("自定义通知服务启动")
@ -1107,21 +993,10 @@ def add_notify_function():
and push_config.get("CHRONOCAT_TOKEN") and push_config.get("CHRONOCAT_TOKEN")
): ):
notify_function.append(chronocat) notify_function.append(chronocat)
if (
push_config.get("DODO_BOTTOKEN")
and push_config.get("DODO_BOTID")
and push_config.get("DODO_LANDSOURCEID")
and push_config.get("DODO_SOURCEID")
):
notify_function.append(dodo_bot)
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"): if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
notify_function.append(custom_notify) notify_function.append(custom_notify)
if push_config.get("NTFY_TOPIC"): if push_config.get("NTFY_TOPIC"):
notify_function.append(ntfy) notify_function.append(ntfy)
if push_config.get("WXPUSHER_APP_TOKEN") and (
push_config.get("WXPUSHER_TOPIC_IDS") or push_config.get("WXPUSHER_UIDS")
):
notify_function.append(wxpusher_bot)
if not notify_function: if not notify_function:
print(f"无推送渠道,请检查通知变量是否正确") print(f"无推送渠道,请检查通知变量是否正确")
return notify_function return notify_function

View File

@ -75,7 +75,7 @@ docker run -d \
```json ```json
{ {
"plugins": { "media_servers": {
"emby": { "emby": {
"url": "http://your-emby-server:8096", "url": "http://your-emby-server:8096",
"token": "YOUR_EMBY_TOKEN" "token": "YOUR_EMBY_TOKEN"
@ -91,5 +91,4 @@ docker run -d \
| 插件 | 说明 | 贡献者 | | 插件 | 说明 | 贡献者 |
| ------- | -------------------- | --------------------------------------- | | ------- | -------------------- | --------------------------------------- |
| plex.py | 自动刷新 Plex 媒体库 | [zhazhayu](https://github.com/zhazhayu) | | plex.py | 自动刷新 Plex 媒体库 | [zhazhayu](https://github.com/zhazhayu) |
| alist_strm_gen.py | 自动生成strm | [xiaoQQya](https://github.com/xiaoQQya) | | alist_strm_gen.py | 自动生成strm | [xiaoQQya](https://github.com/xiaoQQya) |
| alist_sync.py | 调用 alist 实现跨网盘转存 | [jenfonro](https://github.com/jenfonro) |

View File

@ -1,12 +1,8 @@
[ [
"smartstrm",
"fnv_refresh_v2",
"alist", "alist",
"alist_strm", "alist_strm",
"alist_strm_gen", "alist_strm_gen",
"alist_sync",
"aria2", "aria2",
"emby", "emby",
"plex", "plex"
"fnv"
] ]

View File

@ -137,7 +137,7 @@ class Alist_strm_gen:
if item.get("is_dir"): if item.get("is_dir"):
self.check_dir(item_path) self.check_dir(item_path)
else: else:
self.generate_strm(item_path, item) self.generate_strm(item_path)
def get_file_list(self, path, force_refresh=False): def get_file_list(self, path, force_refresh=False):
url = f"{self.url}/api/fs/list" url = f"{self.url}/api/fs/list"
@ -157,7 +157,7 @@ class Alist_strm_gen:
print(f"📺 Alist-Strm生成: 获取文件列表出错❌ {e}") print(f"📺 Alist-Strm生成: 获取文件列表出错❌ {e}")
return {} return {}
def generate_strm(self, file_path, file_info): def generate_strm(self, file_path):
ext = file_path.split(".")[-1] ext = file_path.split(".")[-1]
if ext.lower() in self.video_exts: if ext.lower() in self.video_exts:
strm_path = ( strm_path = (
@ -169,11 +169,8 @@ class Alist_strm_gen:
return return
if not os.path.exists(os.path.dirname(strm_path)): if not os.path.exists(os.path.dirname(strm_path)):
os.makedirs(os.path.dirname(strm_path)) os.makedirs(os.path.dirname(strm_path))
sign_param = (
"" if not file_info.get("sign") else f"?sign={file_info['sign']}"
)
with open(strm_path, "w", encoding="utf-8") as strm_file: with open(strm_path, "w", encoding="utf-8") as strm_file:
strm_file.write(f"{self.strm_server}{file_path}{sign_param}") strm_file.write(f"{self.strm_server}{file_path}")
print(f"📺 生成STRM文件 {strm_path} 成功✅") print(f"📺 生成STRM文件 {strm_path} 成功✅")
def get_root_folder_full_path(self, cookie, pdir_fid): def get_root_folder_full_path(self, cookie, pdir_fid):

View File

@ -1,313 +0,0 @@
# plugins: 调用 alist 实现跨网盘转存
# author: https://github.com/jenfonro
import re
import json
import requests
class Alist_sync:
default_config = {
"url": "", # Alist服务器URL
"token": "", # Alist服务器Token
"quark_storage_id": "", # Alist 服务器夸克存储 ID
"save_storage_id": "", # Alist 服务器同步的存储 ID
"tv_mode": "", # TV库模式填入非0值开启
# TV库模式说明
# 1.开启后会验证文件名是否包含S01E01的正则格式目前仅包含mp4及mkv
# 2.会对比保存目录下是否存在该名称的mp4、mkv文件如果不存在才会进行同步
# 3.夸克目录及同步目录均会提取为S01E01的正则进行匹配不受其它字符影响
}
is_active = False
# 缓存参数
default_task_config = {
"enable": False, # 当前任务开关,
"save_path": "", # 需要同步目录,默认空时路径则会与夸克的保存路径一致,不开启完整路径模式时,默认根目录为保存驱动的根目录
"verify_path": "", # 验证目录主要用于影视库避免重复文件一般配合alist的别名功能及full_path_mode使用用于多个网盘的源合并成一个目录
"full_path_mode": False, # 完整路径模式
# 完整路径模式开启后不再限制保存目录的存储驱动将根据填入的路径进行保存需要填写完整的alist目录
}
def __init__(self, **kwargs):
if kwargs:
for key, _ in self.default_config.items():
if key in kwargs:
setattr(self, key, kwargs[key])
else:
print(f"{self.__class__.__name__} 模块缺少必要参数: {key}")
if self.url and self.token:
if self.verify_server():
self.is_active = True
def _send_request(self, method, url, **kwargs):
headers = {"Authorization": self.token, "Content-Type": "application/json"}
if "headers" in kwargs:
headers = kwargs["headers"]
del kwargs["headers"]
try:
response = requests.request(method, url, headers=headers, **kwargs)
# print(f"{response.text}")
# response.raise_for_status() # 检查请求是否成功但返回非200也会抛出异常
return response
except Exception as e:
print(f"_send_request error:\n{e}")
fake_response = requests.Response()
fake_response.status_code = 500
fake_response._content = b'{"status": 500, "message": "request error"}'
return fake_response
def verify_server(self):
url = f"{self.url}/api/me"
querystring = ""
headers = {"Authorization": self.token, "Content-Type": "application/json"}
try:
response = requests.request("GET", url, headers=headers, params=querystring)
response.raise_for_status()
response = response.json()
if response.get("code") == 200:
if response.get("data").get("username") == "guest":
print(f"Alist登陆失败请检查token")
else:
print(
f"Alist登陆成功当前用户: {response.get('data').get('username')}"
)
return True
else:
print(f"Alist同步: 连接服务器失败❌ {response.get('message')}")
except requests.exceptions.RequestException as e:
print(f"获取Alist信息出错: {e}")
return False
def run(self, task, **kwargs):
if not task["addition"]["alist_sync"]["enable"]:
return 0
print(f"开始进行alist同步")
# 这一块注释的是获取任务的参数在web界面可以看
# print("所有任务参数:")
# print(task)
# print(task['addition'])
# print(task['addition']['alist_sync'])
# print(task['addition']['alist_sync']['target_path'])
# 获取夸克挂载根目录
data = self.get_storage_path(self.quark_storage_id)
if data["driver"] != "Quark":
print(
f"Alist同步: 存储{self.quark_storage_id}非夸克存储❌ {data['driver']}"
)
return 0
quark_mount_root_path = re.sub(r".*root_folder_id\":\"", "", data["addition"])
quark_mount_root_path = re.sub(r"\",.*", "", quark_mount_root_path)
if quark_mount_root_path != "0" and quark_mount_root_path != "":
print(f"Alist同步: 存储{self.quark_storage_id}挂载的目录非夸克根目录❌")
return 0
self.quark_mount_path = data["mount_path"]
# 获取保存路径的挂载根目录
if self.save_storage_id != 0:
data = self.get_storage_path(self.save_storage_id)
self.save_mount_path = data["mount_path"]
# 保存的目录初始化
if task["addition"]["alist_sync"]["save_path"] == "":
self.save_path = f"{self.save_mount_path}/{task['savepath']}"
else:
self.save_path = task["addition"]["alist_sync"]["save_path"]
if not task["addition"]["alist_sync"]["full_path_mode"]:
if self.save_path.startswith("/"):
self.save_path = self.save_path[1:]
if self.save_path.endswith("/"):
self.save_path = self.save_path[:-1]
self.save_path = f"{self.save_mount_path}/{self.save_path}"
else:
# print('完整路径模式')
if not self.save_path.startswith("/"):
self.save_path = "/" + self.save_path
if self.save_path.endswith("/"):
self.save_path = self.save_path[:-1]
# 获取保存目录是否存在
if not self.get_path(self.save_path):
dir_exists = False
# 如果目录不存在判断两边路径是否一致,一致时直接创建复制目录任务即可
else:
dir_exists = True
copy_dir = False
# 初始化验证目录
# 如果没有填验证目录,则验证目录与保存目录一致
if task["addition"]["alist_sync"]["verify_path"]:
self.verify_path = task["addition"]["alist_sync"]["verify_path"]
if not task["addition"]["alist_sync"]["full_path_mode"]:
if self.verify_path.startswith("/"):
self.verify_path = self.save_path[1:]
if self.verify_path.endswith("/"):
self.verify_path = self.save_path[:-1]
self.verify_path = f"{self.save_mount_path}/{self.verify_path}"
else:
# print('完整路径模式')
if not self.verify_path.startswith("/"):
self.verify_path = "/" + self.save_path
if self.verify_path.endswith("/"):
self.verify_path = self.save_path[:-1]
else:
self.verify_path = self.save_path
# 初始化夸克目录
self.source_path = f"{self.quark_mount_path}/{task['savepath']}"
# 初始化任务名
self.taskname = f"{task['taskname']}"
# 获取网盘已有文件
source_dir_list = self.get_path_list(self.source_path)
if not source_dir_list:
print("获取夸克文件列表失败请检查网络或手动刷新alist中的夸克目录")
return 0
if self.tv_mode == 0 or self.tv_mode == "":
self.tv_mode = False
else:
self.tv_mode = True
# 如果是新建的目录则将所有文件直接复制
if not dir_exists:
self.get_save_file([], source_dir_list)
else:
verify_dir_list = self.get_path_list(self.verify_path)
if verify_dir_list:
self.get_save_file(verify_dir_list, source_dir_list)
else:
self.get_save_file([], source_dir_list)
if self.save_file_data:
self.save_start(self.save_file_data)
print("同步的文件列表:")
for save_file in self.save_file_data:
print(f"└── 🎞️{save_file}")
else:
print("没有需要同步的文件")
def save_start(self, save_file_data):
url = f"{self.url}/api/fs/copy"
payload = json.dumps(
{
"src_dir": self.source_path,
"dst_dir": self.save_path,
"names": save_file_data,
}
)
response = self._send_request("POST", url, data=payload)
if response.status_code != 200:
print("未能进行Alist同步请手动同步")
else:
print("Alist创建任务成功")
self.copy_task = response.json()
def get_save_file(self, target_dir_list, source_dir_list):
self.save_file_data = []
if target_dir_list == []:
for source_list in source_dir_list:
if self.tv_mode:
if re.search(
self.taskname + r"\.s\d{1,3}e\d{1,3}\.(mkv|mp4)",
source_list["name"],
re.IGNORECASE,
):
self.save_file_data.append(source_list["name"])
else:
self.save_file_data.append(source_list["name"])
else:
for source_list in source_dir_list:
skip = False
source_list_filename = (
source_list["name"]
.replace(".mp4", "")
.replace(".mkv", "")
.replace(self.taskname + ".", "")
.lower()
)
for target_list in target_dir_list:
if source_list["is_dir"]:
# print(f"跳过目录同步")
skip = True
break
if self.tv_mode:
target_list_filename = (
target_list["name"]
.replace(".mp4", "")
.replace(".mkv", "")
.replace(self.taskname + ".", "")
.lower()
)
if source_list_filename == target_list_filename:
# print(f"文件存在,名称为:{target_list['name']}")
skip = True
break
else:
if source_list["name"] == target_list["name"]:
# print(f"文件存在,名称为:{target_dir['name']}")
skip = True
break
if self.tv_mode:
if re.search(
self.taskname + r"\.s\d{1,3}e\d{1,3}\.(mkv|mp4)",
source_list["name"],
re.IGNORECASE,
):
# 添加一句验证如果有MKVMP4存在时则只保存某一个格式
if re.search(
self.taskname + r"\.s\d{1,3}e\d{1,3}\.mp4",
source_list["name"],
re.IGNORECASE,
):
for all_file in source_dir_list:
if (
source_list["name"].replace(".mp4", ".mkv")
== all_file["name"]
):
print(
f"{source_list['name']}拥有相同版本的MKV文件跳过复制"
)
skip = True
if not skip:
self.save_file_data.append(source_list["name"])
def get_path_list(self, path):
url = f"{self.url}/api/fs/list"
payload = json.dumps(
{"path": path, "password": "", "page": 1, "per_page": 0, "refresh": True}
)
response = self._send_request("POST", url, data=payload)
if response.status_code != 200:
print(f"获取Alist目录出错: {response}")
return False
else:
return response.json()["data"]["content"]
def get_path(self, path):
url = f"{self.url}/api/fs/list"
payload = json.dumps({"path": path, "password": "", "force_root": False})
response = self._send_request("POST", url, data=payload)
if response.status_code != 200 or response.json()["message"] != "success":
return False
else:
return True
def get_storage_path(self, storage_id):
url = f"{self.url}/api/admin/storage/get"
headers = {"Authorization": self.token}
querystring = {"id": storage_id}
try:
response = requests.request("GET", url, headers=headers, params=querystring)
response.raise_for_status()
data = response.json()
if data.get("code") == 200:
return data.get("data", [])
else:
print(f"Alist同步: 存储{storage_id}连接失败❌ {data.get('message')}")
except Exception as e:
print(f"Alist同步: 获取Alist存储出错 {e}")
return []

View File

@ -46,9 +46,6 @@ class Aria2:
if not node.data.get("is_dir", True): if not node.data.get("is_dir", True):
file_fids.append(node.data.get("fid")) file_fids.append(node.data.get("fid"))
file_paths.append(node.data.get("path")) file_paths.append(node.data.get("path"))
if not file_fids:
print(f"Aria2下载: 没有下载任务,跳过")
return
download_return, cookie = account.download(file_fids) download_return, cookie = account.download(file_fids)
file_urls = [item["download_url"] for item in download_return["data"]] file_urls = [item["download_url"] for item in download_return["data"]]
for index, file_url in enumerate(file_urls): for index, file_url in enumerate(file_urls):

View File

@ -1,312 +0,0 @@
import requests
import json
import hashlib
import random
import time
from urllib.parse import urlencode
from typing import Any, Optional
# 飞牛影视插件
# 该插件用于与飞牛影视服务器API交互支持自动刷新媒体库
# 通过配置用户名、密码和密钥字符串进行认证,并提供媒体库扫描功能
class Fnv:
# --- 配置信息 ---
default_config = {
"base_url": "http://10.0.0.6:5666", # 飞牛影视服务器URL
"app_name": "trimemedia-web", # 飞牛影视应用名称
"username": "", # 飞牛影视用户名
"password": "", # 飞牛影视密码
"secret_string": "", # 飞牛影视密钥字符串
"api_key": "", # 飞牛影视API密钥
"token": None, # 飞牛影视认证Token (可选)
}
default_task_config = {
"auto_refresh": False, # 是否自动刷新媒体库
"mdb_name": "", # 飞牛影视目标媒体库名称
"mdb_dir_list": "", # 飞牛影视目标媒体库文件夹路径列表,多个用逗号分隔
}
# 定义一个可选键的集合
OPTIONAL_KEYS = {"token"}
# --- API 端点常量 ---
API_LOGIN = "/v/api/v1/login" # 登录端点
API_MDB_LIST = "/v/api/v1/mdb/list" # 获取媒体库列表
API_MDB_SCAN = "/v/api/v1/mdb/scan/{}" # 刷新媒体库端点 ({}为媒体库ID)
API_TASK_STOP = "/v/api/v1/task/stop" # 停止任务端点
# --- 实例状态 ---
is_active = False
session = requests.Session()
token = None
# =====================================================================
# Public Methods / Entry Points (公共方法/入口)
# =====================================================================
def __init__(self, **kwargs):
"""
初始化 Fnv 客户端
"""
self.plugin_name = self.__class__.__name__.lower()
if kwargs:
for key, _ in self.default_config.items():
if key in kwargs:
setattr(self, key, kwargs[key])
# 检查配置并尝试登录,以确定插件是否激活
if self._check_config():
if self.token is None or self.token == "":
self._login()
self.is_active = self.token is not None or self.token != ""
if self.is_active:
print(f"{self.plugin_name}: 插件已激活 ✅")
else:
print(f"{self.plugin_name}: 插件未激活 ❌")
def run(self, task, **kwargs):
"""
插件运行主入口
根据任务配置执行媒体库刷新操作
"""
if not self.is_active:
print(f"飞牛影视: 插件未激活,跳过任务。")
return
task_config = task.get("addition", {}).get(
self.plugin_name, self.default_task_config
)
if not task_config.get("auto_refresh"):
print("飞牛影视: 自动刷新未启用,跳过处理。")
return
target_library_name = task_config.get("mdb_name")
if not target_library_name:
print("飞牛影视: 未指定媒体库名称,跳过处理。")
return
target_library_mdb_dir_list = task_config.get("mdb_dir_list")
dir_list = []
if target_library_mdb_dir_list:
dir_list = [dir_path.strip() for dir_path in target_library_mdb_dir_list.split(",") if dir_path.strip()]
# 获取媒体库ID
library_id = self._get_library_id(target_library_name)
if library_id:
# 获取ID成功后刷新该媒体库
self._refresh_library(library_id, dir_list=dir_list)
# =====================================================================
# Internal Methods (内部实现方法)
# =====================================================================
def _check_config(self):
"""检查配置是否完整"""
missing_keys = [
key for key in self.default_config
if key not in self.OPTIONAL_KEYS and not getattr(self, key, None)
]
if missing_keys:
# print(f"{self.plugin_name} 模块缺少必要参数: {', '.join(missing_keys)}")
return False
return True
def _make_request(self, method: str, rel_url: str, params: dict = None, data: dict = None) -> Optional[Any]:
"""
一个统一的私有方法用于发送所有API请求
它会自动处理签名请求头错误和响应解析
当认证失败时会自动尝试重新登录并重试最多3次
"""
max_retries = 3
for attempt in range(max_retries):
url = f"{self.base_url.rstrip('/')}{rel_url}"
authx = self._cse_sign(method, rel_url, params, data)
if not authx:
print(f"飞牛影视: 为 {rel_url} 生成签名失败,请求中止。")
return None
headers = {
"Content-Type": "application/json",
"authx": authx,
}
if self.token:
headers["Authorization"] = self.token
try:
response = self.session.request(
method, url, headers=headers, params=params,
data=self._serialize_data(data if data is not None else {})
)
response.raise_for_status()
response_data = response.json()
except requests.exceptions.RequestException as e:
print(f"飞牛影视: 请求 {url} 时出错: {e}")
return None
except json.JSONDecodeError:
print(f"飞牛影视: 解析来自 {url} 的响应失败内容非JSON格式。")
return None
response_code = response_data.get("code")
if response_code is None:
print(f"飞牛影视: 响应格式错误,未找到 'code' 字段。")
return None
if response_code == 0:
return response_data
if response_code == -2:
print(f"飞牛影视: 认证失败 (尝试 {attempt + 1}/{max_retries}),尝试重新登录...")
if rel_url == self.API_LOGIN:
print("飞牛影视: 登录接口认证失败,请检查用户名和密码。")
return response_data
if not self._login():
print("飞牛影视: 重新登录失败,无法继续请求。")
return None
continue
else:
msg = response_data.get('msg', '未知错误')
print(f"飞牛影视: API调用失败 ({rel_url}): {msg}")
return response_data
print(f"飞牛影视: 请求 {rel_url} 在尝试 {max_retries} 次后仍然失败。")
return None
def _login(self) -> bool:
"""
登录到飞牛影视服务器并获取认证token
"""
app_name = self.app_name or self.default_config["app_name"]
username = self.username or self.default_config["username"]
password = self.password or self.default_config["password"]
print("飞牛影视: 正在尝试登录...")
payload = {"username": username, "password": password, "app_name": app_name}
response_json = self._make_request('post', self.API_LOGIN, data=payload)
if response_json and response_json.get("data", {}).get("token"):
self.token = response_json["data"]["token"]
print("飞牛影视: 登录成功 ✅")
return True
else:
print("飞牛影视: 登录失败 ❌")
return False
def _get_library_id(self, library_name: str) -> Optional[str]:
"""
根据媒体库的名称获取其唯一ID (guid)
"""
if not self.token:
print("飞牛影视: 必须先登录才能获取媒体库列表。")
return None
print(f"飞牛影视: 正在查找媒体库 '{library_name}'...")
response_json = self._make_request('get', self.API_MDB_LIST)
if response_json and response_json.get("data"):
for library in response_json.get("data", []):
if library.get("name") == library_name:
print(f"飞牛影视: 找到目标媒体库 ✅ID: {library.get('guid')}")
return library.get("guid")
print(f"飞牛影视: 未在媒体库列表中找到名为 '{library_name}' 的媒体库 ❌")
return None
def _refresh_library(self, library_id: str, dir_list: list[str] = None) -> bool:
"""
根据给定的媒体库ID触发一次媒体库扫描/刷新
"""
if not self.token:
print("飞牛影视: 必须先登录才能刷新媒体库。")
return False
if dir_list:
print(f"飞牛影视: 正在为媒体库 {library_id} 发送部分目录{dir_list}刷新指令...")
else:
print(f"飞牛影视: 正在为媒体库 {library_id} 发送刷新指令...")
rel_url = self.API_MDB_SCAN.format(library_id)
request_body = {"dir_list": dir_list} if dir_list else {}
response_json = self._make_request('post', rel_url, data=request_body)
if not response_json: return False
response_code = response_json.get("code")
if response_code == 0:
print(f"飞牛影视: 发送刷新指令成功 ✅")
return True
elif response_code == -14:
if self._stop_refresh_task(library_id):
print(f"飞牛影视: 发现重复任务,已停止旧任务,重新发送刷新指令...")
response_json = self._make_request('post', rel_url, data={})
if response_json and response_json.get("code") == 0:
print(f"飞牛影视: 发送刷新指令成功 ✅")
return True
else:
print(f"飞牛影视: 重新发送刷新指令失败 ❌")
else:
print(f"飞牛影视: 停止旧任务失败,无法继续刷新操作 ❌")
return False
def _stop_refresh_task(self, library_id: str) -> bool:
"""
停止指定的媒体库刷新任务
"""
if not self.token:
print("飞牛影视: 必须先登录才能停止刷新任务。")
return False
print(f"飞牛影视: 正在停止媒体库刷新任务 {library_id}...")
payload = {"guid": library_id, "type": "TaskItemScrap"}
response_json = self._make_request('post', self.API_TASK_STOP, data=payload)
if response_json and response_json.get("code") == 0:
print(f"飞牛影视: 停止刷新任务成功 ✅")
return True
else:
print(f"飞牛影视: 停止刷新任务失败 ❌")
return False
def _cse_sign(self, method: str, path: str, params: dict = None, data: dict = None) -> str:
"""
为API请求生成 cse 签名参数字符串
"""
nonce = str(random.randint(100000, 999999))
timestamp = str(int(time.time() * 1000))
serialized_str = ""
if method.lower() == 'get':
if params:
serialized_str = urlencode(sorted(params.items()))
else:
serialized_str = self._serialize_data(data)
body_hash = self._md5_hash(serialized_str)
string_to_sign_parts = [
self.secret_string, path, nonce, timestamp, body_hash, self.api_key
]
string_to_sign = "_".join(string_to_sign_parts)
final_sign = self._md5_hash(string_to_sign)
return f"nonce={nonce}&timestamp={timestamp}&sign={final_sign}"
# =====================================================================
# Static Utility Methods (静态工具方法)
# =====================================================================
@staticmethod
def _md5_hash(s: str) -> str:
"""计算并返回字符串的小写 MD5 哈希值。"""
return hashlib.md5(s.encode('utf-8')).hexdigest()
@staticmethod
def _serialize_data(data: Any) -> str:
"""
将请求体数据序列化为紧凑的JSON字符串
"""
if isinstance(data, dict):
return json.dumps(data, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
if isinstance(data, str):
return data
if not data:
return ""
return ""

Binary file not shown.

View File

@ -1,75 +0,0 @@
import requests
class Smartstrm:
default_config = {
"webhook": "", # SmartStrm Webhook 地址
"strmtask": "", # SmartStrm 任务名,支持多个如 `tv,movie`
"xlist_path_fix": "", # 路径映射, SmartStrm 任务使用 quark 驱动时无须填写;使用 openlist 驱动时需填写 `/storage_mount_path:/quark_root_dir` ,例如把夸克根目录挂载在 OpenList 的 /quark 下,则填写 `/quark:/` ;以及 SmartStrm 会使 OpenList 强制刷新目录,无需再用 alist 插件刷新。
}
is_active = False
def __init__(self, **kwargs):
self.plugin_name = self.__class__.__name__.lower()
if kwargs:
for key, _ in self.default_config.items():
if key in kwargs:
setattr(self, key, kwargs[key])
else:
print(f"{self.plugin_name} 模块缺少必要参数: {key}")
if self.webhook and self.strmtask:
if self.get_info():
self.is_active = True
def get_info(self):
"""获取 SmartStrm 信息"""
try:
response = requests.request(
"GET",
self.webhook,
timeout=5,
)
response = response.json()
if response.get("success"):
print(f"SmartStrm 触发任务: 连接成功 {response.get('version','')}")
return response
print(f"SmartStrm 触发任务:连接失败 {response.get('message','')}")
return None
except Exception as e:
print(f"SmartStrm 触发任务:连接出错 {str(e)}")
return None
def run(self, task, **kwargs):
"""
插件主入口函数
:param task: 任务配置
:param kwargs: 其他参数
"""
try:
# 准备发送的数据
headers = {"Content-Type": "application/json"}
payload = {
"event": "qas_strm",
"data": {
"strmtask": self.strmtask,
"savepath": task["savepath"],
"xlist_path_fix": self.xlist_path_fix,
},
}
# 发送 POST 请求
response = requests.request(
"POST",
self.webhook,
headers=headers,
json=payload,
timeout=5,
)
response = response.json()
if response.get("success"):
print(
f"SmartStrm 触发任务: [{response['task']['name']}] {response['task']['storage_path']} 成功✅"
)
else:
print(f"SmartStrm 触发任务: {response['message']}")
except Exception as e:
print(f"SmartStrm 触发任务:出错 {str(e)}")

View File

@ -1,6 +1,6 @@
# !/usr/bin/env python3 # !/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Modify: 2025-09-05 # Modify: 2024-11-13
# Repo: https://github.com/Cp0204/quark_auto_save # Repo: https://github.com/Cp0204/quark_auto_save
# ConfigFile: quark_config.json # ConfigFile: quark_config.json
""" """
@ -15,10 +15,8 @@ import time
import random import random
import requests import requests
import importlib import importlib
import traceback
import urllib.parse import urllib.parse
from datetime import datetime from datetime import datetime
from natsort import natsorted
# 兼容青龙 # 兼容青龙
try: try:
@ -34,6 +32,18 @@ NOTIFYS = []
GH_PROXY = os.environ.get("GH_PROXY", "https://ghproxy.net/") GH_PROXY = os.environ.get("GH_PROXY", "https://ghproxy.net/")
MAGIC_REGEX = {
"$TV": {
"pattern": r".*?([Ss]\d{1,2})?(?:[第EePpXx\.\-\_\( ]{1,2}|^)(\d{1,3})(?!\d).*?\.(mp4|mkv)",
"replace": r"\1E\2.\3",
},
"$BLACK_WORD": {
"pattern": r"^(?!.*纯享)(?!.*加更)(?!.*超前企划)(?!.*训练室)(?!.*蒸蒸日上).*",
"replace": "",
},
}
# 发送通知消息 # 发送通知消息
def send_ql_notify(title, body): def send_ql_notify(title, body):
try: try:
@ -42,7 +52,7 @@ def send_ql_notify(title, body):
# 如未配置 push_config 则使用青龙环境通知设置 # 如未配置 push_config 则使用青龙环境通知设置
if CONFIG_DATA.get("push_config"): if CONFIG_DATA.get("push_config"):
notify.push_config.update(CONFIG_DATA["push_config"]) notify.push_config = CONFIG_DATA["push_config"].copy()
notify.push_config["CONSOLE"] = notify.push_config.get("CONSOLE", True) notify.push_config["CONSOLE"] = notify.push_config.get("CONSOLE", True)
notify.send(title, body) notify.send(title, body)
except Exception as e: except Exception as e:
@ -96,26 +106,20 @@ class 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 = {}
# 获取所有模块
py_ext = [".py", ".pyd"] if sys.platform == "win32" else [".py", ".so"]
all_modules = [ all_modules = [
f.replace(ext, "") f.replace(".py", "") for f in os.listdir(plugins_dir) if f.endswith(".py")
for f in os.listdir(plugins_dir)
for ext in py_ext
if f.endswith(ext)
] ]
# 调整模块优先级 # 调整模块优先级
priority_path = os.path.join(plugins_dir, "_priority.json") priority_path = os.path.join(plugins_dir, "_priority.json")
try: try:
with open(priority_path, encoding="utf-8") as f: with open(priority_path, encoding="utf-8") as f:
priority_modules = json.load(f) priority_modules = json.load(f)
if priority_modules:
all_modules = [
module for module in priority_modules if module in all_modules
] + [module for module in all_modules if module not in priority_modules]
except (FileNotFoundError, json.JSONDecodeError): except (FileNotFoundError, json.JSONDecodeError):
priority_modules = [] priority_modules = []
if priority_modules:
all_modules = [
module for module in priority_modules if module in all_modules
] + [module for module in all_modules if module not in priority_modules]
# 加载模块
for module_name in all_modules: for module_name in all_modules:
if f"-{module_name}" in PLUGIN_FLAGS: if f"-{module_name}" in PLUGIN_FLAGS:
continue continue
@ -134,223 +138,33 @@ class Config:
task_plugins_config[module_name] = plugin.default_task_config task_plugins_config[module_name] = plugin.default_task_config
except (ImportError, AttributeError) as e: except (ImportError, AttributeError) as e:
print(f"载入模块 {module_name} 失败: {e}") print(f"载入模块 {module_name} 失败: {e}")
print()
return plugins_available, plugins_config, task_plugins_config return plugins_available, plugins_config, task_plugins_config
def breaking_change_update(config_data): def breaking_change_update(config_data):
# 🔼 Update config v0.5.x to 0.6.0 if config_data.get("emby"):
for task in config_data.get("tasklist", []): print("🔼 Update config v0.3.6.1 to 0.3.7")
if "$TASKNAME" in task.get("replace", ""): config_data.setdefault("media_servers", {})["emby"] = {
task["replace"] = task["replace"].replace("$TASKNAME", "{TASKNAME}") "url": config_data["emby"]["url"],
"token": config_data["emby"]["apikey"],
}
class MagicRename: del config_data["emby"]
for task in config_data.get("tasklist", {}):
magic_regex = { task["media_id"] = task.get("emby_id", "")
"$TV": { if task.get("emby_id"):
"pattern": r".*?([Ss]\d{1,2})?(?:[第EePpXx\.\-\_\( ]{1,2}|^)(\d{1,3})(?!\d).*?\.(mp4|mkv)", del task["emby_id"]
"replace": r"\1E\2.\3", if config_data.get("media_servers"):
}, print("🔼 Update config v0.3.8 to 0.3.9")
"$BLACK_WORD": { config_data["plugins"] = config_data.get("media_servers")
"pattern": r"^(?!.*纯享)(?!.*加更)(?!.*超前企划)(?!.*训练室)(?!.*蒸蒸日上).*", del config_data["media_servers"]
"replace": "", for task in config_data.get("tasklist", {}):
}, task["addition"] = {
} "emby": {
"media_id": task.get("media_id", ""),
magic_variable = { }
"{TASKNAME}": "", }
"{I}": 1, if task.get("media_id"):
"{EXT}": [r"(?<=\.)\w+$"], del task["media_id"]
"{CHINESE}": [r"[\u4e00-\u9fa5]{2,}"],
"{DATE}": [
r"(18|19|20)?\d{2}[\.\-/年]\d{1,2}[\.\-/月]\d{1,2}",
r"(?<!\d)[12]\d{3}[01]?\d[0123]?\d",
r"(?<!\d)[01]?\d[\.\-/月][0123]?\d",
],
"{YEAR}": [r"(?<!\d)(18|19|20)\d{2}(?!\d)"],
"{S}": [r"(?<=[Ss])\d{1,2}(?=[EeXx])", r"(?<=[Ss])\d{1,2}"],
"{SXX}": [r"[Ss]\d{1,2}(?=[EeXx])", r"[Ss]\d{1,2}"],
"{E}": [
r"(?<=[Ss]\d\d[Ee])\d{1,3}",
r"(?<=[Ee])\d{1,3}",
r"(?<=[Ee][Pp])\d{1,3}",
r"(?<=第)\d{1,3}(?=[集期话部篇])",
r"(?<!\d)\d{1,3}(?=[集期话部篇])",
r"(?!.*19)(?!.*20)(?<=[\._])\d{1,3}(?=[\._])",
r"^\d{1,3}(?=\.\w+)",
r"(?<!\d)\d{1,3}(?!\d)(?!$)",
],
"{PART}": [
r"(?<=[集期话部篇第])[上中下一二三四五六七八九十]",
r"[上中下一二三四五六七八九十]",
],
"{VER}": [r"[\u4e00-\u9fa5]+版"],
}
priority_list = [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
]
def __init__(self, magic_regex={}, magic_variable={}):
self.magic_regex.update(magic_regex)
self.magic_variable.update(magic_variable)
self.dir_filename_dict = {}
def set_taskname(self, taskname):
"""设置任务名称"""
self.magic_variable["{TASKNAME}"] = taskname
def magic_regex_conv(self, pattern, replace):
"""魔法正则匹配"""
keyword = pattern
if keyword in self.magic_regex:
pattern = self.magic_regex[keyword]["pattern"]
if replace == "":
replace = self.magic_regex[keyword]["replace"]
return pattern, replace
def sub(self, pattern, replace, file_name):
"""魔法正则、变量替换"""
if not replace:
return file_name
# 预处理替换变量
for key, p_list in self.magic_variable.items():
if key in replace:
# 正则类替换变量
if p_list and isinstance(p_list, list):
for p in p_list:
match = re.search(p, file_name)
if match:
# 匹配成功,替换为匹配到的值
value = match.group()
# 日期格式处理:补全、格式化
if key == "{DATE}":
value = "".join(
[char for char in value if char.isdigit()]
)
value = (
str(datetime.now().year)[: (8 - len(value))] + value
)
replace = replace.replace(key, value)
break
# 非正则类替换变量
if key == "{TASKNAME}":
replace = replace.replace(key, self.magic_variable["{TASKNAME}"])
elif key == "{SXX}" and not match:
replace = replace.replace(key, "S01")
elif key == "{I}":
continue
else:
# 清理未匹配的 magic_variable key
replace = replace.replace(key, "")
if pattern and replace:
file_name = re.sub(pattern, replace, file_name)
else:
file_name = replace
return file_name
def _custom_sort_key(self, name):
"""自定义排序键"""
for i, keyword in enumerate(self.priority_list):
if keyword in name:
name = name.replace(keyword, f"_{i:02d}_") # 替换为数字,方便排序
return name
def sort_file_list(self, file_list, dir_filename_dict={}):
"""文件列表统一排序,给{I+}赋值"""
filename_list = [
# 强制加入`文件修改时间`字段供排序效果1无可排序字符时则按修改时间排序2和目录已有文件重名时始终在其后
f"{f['file_name_re']}_{f['updated_at']}"
for f in file_list
if f.get("file_name_re") and not f["dir"]
]
# print(f"filename_list_before: {filename_list}")
dir_filename_dict = dir_filename_dict or self.dir_filename_dict
# print(f"dir_filename_list: {dir_filename_list}")
# 合并目录文件列表
filename_list = list(set(filename_list) | set(dir_filename_dict.values()))
filename_list = natsorted(filename_list, key=self._custom_sort_key)
filename_index = {}
for name in filename_list:
if name in dir_filename_dict.values():
continue
i = filename_list.index(name) + 1
while i in dir_filename_dict.keys():
i += 1
dir_filename_dict[i] = name
filename_index[name] = i
for file in file_list:
if file.get("file_name_re"):
if match := re.search(r"\{I+\}", file["file_name_re"]):
i = filename_index.get(
f"{file['file_name_re']}_{file['updated_at']}", 0
)
file["file_name_re"] = re.sub(
match.group(),
str(i).zfill(match.group().count("I")),
file["file_name_re"],
)
def set_dir_file_list(self, file_list, replace):
"""设置目录文件列表"""
self.dir_filename_dict = {}
filename_list = [f["file_name"] for f in file_list if not f["dir"]]
filename_list.sort()
if not filename_list:
return
if match := re.search(r"\{I+\}", replace):
# 由替换式转换匹配式
magic_i = match.group()
pattern_i = r"\d" * magic_i.count("I")
pattern = replace.replace(match.group(), "🔢")
for key, _ in self.magic_variable.items():
if key in pattern:
pattern = pattern.replace(key, "🔣")
pattern = re.sub(r"\\[0-9]+", "🔣", pattern) # \1 \2 \3
pattern = f"({re.escape(pattern).replace('🔣', '.*?').replace('🔢', f')({pattern_i})(')})"
# print(f"pattern: {pattern}")
# 获取起始编号
if match := re.match(pattern, filename_list[-1]):
self.magic_variable["{I}"] = int(match.group(2))
# 目录文件列表
for filename in filename_list:
if match := re.match(pattern, filename):
self.dir_filename_dict[int(match.group(2))] = (
match.group(1) + magic_i + match.group(3)
)
# print(f"filename_list: {self.filename_list}")
def is_exists(self, filename, filename_list, ignore_ext=False):
"""判断文件是否存在,处理忽略扩展名"""
# print(f"filename: {filename} filename_list: {filename_list}")
if ignore_ext:
filename = os.path.splitext(filename)[0]
filename_list = [os.path.splitext(f)[0] for f in filename_list]
# {I+} 模式用I通配数字序号
if match := re.search(r"\{I+\}", filename):
magic_i = match.group()
pattern_i = r"\d" * magic_i.count("I")
pattern = re.escape(filename).replace(re.escape(magic_i), pattern_i)
for filename in filename_list:
if re.match(pattern, filename):
return filename
return None
else:
return filename if filename in filename_list else None
class Quark: class Quark:
@ -358,7 +172,7 @@ class Quark:
BASE_URL_APP = "https://drive-m.quark.cn" BASE_URL_APP = "https://drive-m.quark.cn"
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/3.14.2 Chrome/112.0.5615.165 Electron/24.1.3.8 Safari/537.36 Channel/pckk_other_ch" USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/3.14.2 Chrome/112.0.5615.165 Electron/24.1.3.8 Safari/537.36 Channel/pckk_other_ch"
def __init__(self, cookie="", index=0): def __init__(self, cookie, index=None):
self.cookie = cookie.strip() self.cookie = cookie.strip()
self.index = index + 1 self.index = index + 1
self.is_active = False self.is_active = False
@ -502,9 +316,7 @@ class Quark:
).json() ).json()
return response return response
def get_detail( def get_detail(self, pwd_id, stoken, pdir_fid, _fetch_share=0):
self, pwd_id, stoken, pdir_fid, _fetch_share=0, fetch_share_full_path=0
):
list_merge = [] list_merge = []
page = 1 page = 1
while True: while True:
@ -522,8 +334,6 @@ class Quark:
"_fetch_share": _fetch_share, "_fetch_share": _fetch_share,
"_fetch_total": "1", "_fetch_total": "1",
"_sort": "file_type:asc,updated_at:desc", "_sort": "file_type:asc,updated_at:desc",
"ver": "2",
"fetch_share_full_path": fetch_share_full_path,
} }
response = self._send_request("GET", url, params=querystring).json() response = self._send_request("GET", url, params=querystring).json()
if response["code"] != 0: if response["code"] != 0:
@ -573,8 +383,6 @@ class Quark:
"_fetch_sub_dirs": "0", "_fetch_sub_dirs": "0",
"_sort": "file_type:asc,updated_at:desc", "_sort": "file_type:asc,updated_at:desc",
"_fetch_full_path": kwargs.get("fetch_full_path", 0), "_fetch_full_path": kwargs.get("fetch_full_path", 0),
"fetch_all_file": 1, # 跟随Web端作用未知
"fetch_risk_file_name": 1, # 如无此参数,违规文件名会被变 ***
} }
response = self._send_request("GET", url, params=querystring).json() response = self._send_request("GET", url, params=querystring).json()
if response["code"] != 0: if response["code"] != 0:
@ -627,9 +435,7 @@ class Quark:
"__t": datetime.now().timestamp(), "__t": datetime.now().timestamp(),
} }
response = self._send_request("GET", url, params=querystring).json() response = self._send_request("GET", url, params=querystring).json()
if response["status"] != 200: if response["data"]["status"] != 0:
return response
if response["data"]["status"] == 2:
if retry_index > 0: if retry_index > 0:
print() print()
break break
@ -714,6 +520,30 @@ class Quark:
# ↑ 请求函数 # ↑ 请求函数
# ↓ 操作函数 # ↓ 操作函数
# 魔法正则匹配
def magic_regex_func(self, pattern, replace, taskname=None, magic_regex={}):
magic_regex = magic_regex or CONFIG_DATA.get("magic_regex") or MAGIC_REGEX
keyword = pattern
if keyword in magic_regex:
pattern = magic_regex[keyword]["pattern"]
if replace == "":
replace = magic_regex[keyword]["replace"]
if taskname:
replace = replace.replace("$TASKNAME", taskname)
return pattern, replace
# def get_id_from_url(self, url):
# url = url.replace("https://pan.quark.cn/s/", "")
# pattern = r"(\w+)(\?pwd=(\w+))?(#/list/share.*/(\w+))?"
# match = re.search(pattern, url)
# if match:
# pwd_id = match.group(1)
# passcode = match.group(3) if match.group(3) else ""
# pdir_fid = match.group(5) if match.group(5) else 0
# return pwd_id, passcode, pdir_fid
# else:
# return None
def extract_url(self, url): def extract_url(self, url):
# pwd_id # pwd_id
match_id = re.search(r"/s/(\w+)", url) match_id = re.search(r"/s/(\w+)", url)
@ -722,7 +552,6 @@ class Quark:
match_pwd = re.search(r"pwd=(\w+)", url) match_pwd = re.search(r"pwd=(\w+)", url)
passcode = match_pwd.group(1) if match_pwd else "" passcode = match_pwd.group(1) if match_pwd else ""
# path: fid-name # path: fid-name
# Legacy 20250905
paths = [] paths = []
matches = re.findall(r"/(\w{32})-?([^/]+)?", url) matches = re.findall(r"/(\w{32})-?([^/]+)?", url)
for match in matches: for match in matches:
@ -768,9 +597,11 @@ class Quark:
pwd_id, passcode, pdir_fid, _ = self.extract_url(shareurl) pwd_id, passcode, pdir_fid, _ = self.extract_url(shareurl)
stoken = self.get_stoken(pwd_id, passcode)["data"]["stoken"] stoken = self.get_stoken(pwd_id, passcode)["data"]["stoken"]
share_file_list = self.get_detail(pwd_id, stoken, pdir_fid)["data"]["list"] share_file_list = self.get_detail(pwd_id, stoken, pdir_fid)["data"]["list"]
print(f"获取分享: {share_file_list}")
fid_list = [item["fid"] for item in share_file_list] fid_list = [item["fid"] for item in share_file_list]
fid_token_list = [item["share_fid_token"] for item in share_file_list] fid_token_list = [item["share_fid_token"] for item in share_file_list]
file_name_list = [item["file_name"] for item in share_file_list]
if not fid_list:
return
get_fids = self.get_fids([savepath]) get_fids = self.get_fids([savepath])
to_pdir_fid = ( to_pdir_fid = (
get_fids[0]["fid"] if get_fids else self.mkdir(savepath)["data"]["fid"] get_fids[0]["fid"] if get_fids else self.mkdir(savepath)["data"]["fid"]
@ -778,31 +609,30 @@ class Quark:
save_file = self.save_file( save_file = self.save_file(
fid_list, fid_token_list, to_pdir_fid, pwd_id, stoken fid_list, fid_token_list, to_pdir_fid, pwd_id, stoken
) )
print(f"转存文件: {save_file}") if save_file["code"] == 41017:
if save_file["code"] == 0: return
task_id = save_file["data"]["task_id"] elif save_file["code"] == 0:
query_task = self.query_task(task_id) dir_file_list = self.ls_dir(to_pdir_fid)["data"]["list"]
print(f"查询转存: {query_task}") del_list = [
if query_task["code"] == 0: item["fid"]
del_list = query_task["data"]["save_as"]["save_as_top_fids"] for item in dir_file_list
if del_list: if (item["file_name"] in file_name_list)
delete_return = self.delete(del_list) and ((datetime.now().timestamp() - item["created_at"]) < 60)
print(f"删除转存: {delete_return}") ]
recycle_list = self.recycle_list() if del_list:
record_id_list = [ self.delete(del_list)
item["record_id"] recycle_list = self.recycle_list()
for item in recycle_list record_id_list = [
if item["fid"] in del_list item["record_id"]
] for item in recycle_list
recycle_remove = self.recycle_remove(record_id_list) if item["fid"] in del_list
print(f"清理转存: {recycle_remove}") ]
print(f"✅ 转存测试成功") self.recycle_remove(record_id_list)
return True return save_file
print(f"❌ 转存测试失败: 中断") else:
return False return False
except Exception as e: except Exception as e:
print(f"❌ 转存测试失败: {str(e)}") print(f"转存测试失败: {str(e)}")
traceback.print_exc()
def do_save_task(self, task): def do_save_task(self, task):
# 判断资源失效记录 # 判断资源失效记录
@ -818,7 +648,7 @@ class Quark:
if get_stoken.get("status") == 200: if get_stoken.get("status") == 200:
stoken = get_stoken["data"]["stoken"] stoken = get_stoken["data"]["stoken"]
elif get_stoken.get("status") == 500: elif get_stoken.get("status") == 500:
print(f"跳过任务:网络异常 {get_stoken.get('message')}") print(f"跳过任务:网络异常 {get_stoken.get("message")}")
return return
else: else:
message = get_stoken.get("message") message = get_stoken.get("message")
@ -829,8 +659,6 @@ class Quark:
updated_tree = self.dir_check_and_save(task, pwd_id, stoken, pdir_fid) updated_tree = self.dir_check_and_save(task, pwd_id, stoken, pdir_fid)
if updated_tree.size(1) > 0: if updated_tree.size(1) > 0:
self.do_rename(updated_tree)
print()
add_notify(f"✅《{task['taskname']}》添加追更:\n{updated_tree}") add_notify(f"✅《{task['taskname']}》添加追更:\n{updated_tree}")
return updated_tree return updated_tree
else: else:
@ -868,7 +696,6 @@ class Quark:
return tree return tree
to_pdir_fid = self.savepath_fid[savepath] to_pdir_fid = self.savepath_fid[savepath]
dir_file_list = self.ls_dir(to_pdir_fid)["data"]["list"] dir_file_list = self.ls_dir(to_pdir_fid)["data"]["list"]
dir_filename_list = [dir_file["file_name"] for dir_file in dir_file_list]
# print("dir_file_list: ", dir_file_list) # print("dir_file_list: ", dir_file_list)
tree.create_node( tree.create_node(
@ -879,78 +706,47 @@ class Quark:
}, },
) )
# 文件命名类
mr = MagicRename(CONFIG_DATA.get("magic_regex", {}))
mr.set_taskname(task["taskname"])
# 魔法正则转换
pattern, replace = mr.magic_regex_conv(
task.get("pattern", ""), task.get("replace", "")
)
# 需保存的文件清单 # 需保存的文件清单
need_save_list = [] need_save_list = []
# 添加符合的 # 添加符合的
for share_file in share_file_list: for share_file in share_file_list:
search_pattern = ( if share_file["dir"] and task.get("update_subdir", False):
task["update_subdir"] pattern, replace = task["update_subdir"], ""
if share_file["dir"] and task.get("update_subdir") else:
else pattern pattern, replace = self.magic_regex_func(
) task.get("pattern", ""), task.get("replace", ""), task["taskname"]
)
# 正则文件名匹配 # 正则文件名匹配
if re.search(search_pattern, share_file["file_name"]): if re.search(pattern, share_file["file_name"]):
# 判断原文件名是否存在,处理忽略扩展名 # 替换后的文件名
if not mr.is_exists( save_name = (
share_file["file_name"], re.sub(pattern, replace, share_file["file_name"])
dir_filename_list, if replace != ""
(task.get("ignore_extension") and not share_file["dir"]), else share_file["file_name"]
): )
# 文件夹、子目录文件不进行重命名 # 忽略后缀
if share_file["dir"] or subdir_path: if task.get("ignore_extension") and not share_file["dir"]:
share_file["file_name_re"] = share_file["file_name"] compare_func = lambda a, b1, b2: (
need_save_list.append(share_file) os.path.splitext(a)[0] == os.path.splitext(b1)[0]
else: or os.path.splitext(a)[0] == os.path.splitext(b2)[0]
# 替换后的文件名 )
file_name_re = mr.sub(pattern, replace, share_file["file_name"]) else:
# 判断替换后的文件名是否存在 compare_func = lambda a, b1, b2: (a == b1 or a == b2)
if not mr.is_exists( # 判断目标目录文件是否存在
file_name_re, file_exists = any(
dir_filename_list, compare_func(
task.get("ignore_extension"), dir_file["file_name"], share_file["file_name"], save_name
): )
share_file["file_name_re"] = file_name_re for dir_file in dir_file_list
need_save_list.append(share_file) )
if not file_exists:
share_file["save_name"] = save_name
need_save_list.append(share_file)
elif share_file["dir"]: elif share_file["dir"]:
# 存在并是一个目录,历遍子目录 # 存在并是一个文件夹
if task.get("update_subdir", False) and re.search( if task.get("update_subdir", False):
task["update_subdir"], share_file["file_name"] if re.search(task["update_subdir"], share_file["file_name"]):
): print(f"检查子文件夹:{savepath}/{share_file['file_name']}")
if task.get("update_subdir_resave_mode", False):
# 重存模式:删除该目录下所有文件,重新转存
print(f"重存子目录:{savepath}/{share_file['file_name']}")
# 删除子目录、回收站中彻底删除
subdir = next(
(
f
for f in dir_file_list
if f["file_name"] == share_file["file_name"]
),
None,
)
delete_return = self.delete([subdir["fid"]])
self.query_task(delete_return["data"]["task_id"])
recycle_list = self.recycle_list()
record_id_list = [
item["record_id"]
for item in recycle_list
if item["fid"] == subdir["fid"]
]
self.recycle_remove(record_id_list)
# 作为新文件添加到转存列表
share_file["file_name_re"] = share_file["file_name"]
need_save_list.append(share_file)
else:
# 递归模式
print(f"检查子目录:{savepath}/{share_file['file_name']}")
subdir_tree = self.dir_check_and_save( subdir_tree = self.dir_check_and_save(
task, task,
pwd_id, pwd_id,
@ -973,82 +769,77 @@ class Quark:
if share_file["fid"] == task.get("startfid", ""): if share_file["fid"] == task.get("startfid", ""):
break break
if re.search(r"\{I+\}", replace):
mr.set_dir_file_list(dir_file_list, replace)
mr.sort_file_list(need_save_list)
# 转存文件
fid_list = [item["fid"] for item in need_save_list] fid_list = [item["fid"] for item in need_save_list]
fid_token_list = [item["share_fid_token"] for item in need_save_list] fid_token_list = [item["share_fid_token"] for item in need_save_list]
if fid_list: if fid_list:
save_file_return = self.save_file(
fid_list, fid_token_list, to_pdir_fid, pwd_id, stoken
)
err_msg = None err_msg = None
save_as_top_fids = [] if save_file_return["code"] == 0:
while fid_list: task_id = save_file_return["data"]["task_id"]
# 分次转存100个/次因query_task返回save_as_top_fids最多100 query_task_return = self.query_task(task_id)
save_file_return = self.save_file( if query_task_return["code"] == 0:
fid_list[:100], fid_token_list[:100], to_pdir_fid, pwd_id, stoken # 建立目录树
) for index, item in enumerate(need_save_list):
fid_list = fid_list[100:] icon = (
fid_token_list = fid_token_list[100:] "📁"
if save_file_return["code"] == 0: if item["dir"] == True
# 转存成功,查询转存结果 else "🎞️" if item["obj_category"] == "video" else ""
task_id = save_file_return["data"]["task_id"] )
query_task_return = self.query_task(task_id) tree.create_node(
if query_task_return["code"] == 0: f"{icon}{item['save_name']}",
save_as_top_fids.extend( item["fid"],
query_task_return["data"]["save_as"]["save_as_top_fids"] parent=pdir_fid,
data={
"fid": f"{query_task_return['data']['save_as']['save_as_top_fids'][index]}",
"path": f"{savepath}/{item['save_name']}",
"is_dir": item["dir"],
},
) )
else:
err_msg = query_task_return["message"]
else: else:
err_msg = save_file_return["message"] err_msg = query_task_return["message"]
if err_msg: else:
add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n") err_msg = save_file_return["message"]
# 建立目录树 if err_msg:
if len(need_save_list) == len(save_as_top_fids): add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n")
for index, item in enumerate(need_save_list):
icon = self._get_file_icon(item)
tree.create_node(
f"{icon}{item['file_name_re']}",
item["fid"],
parent=pdir_fid,
data={
"file_name": item["file_name"],
"file_name_re": item["file_name_re"],
"fid": f"{save_as_top_fids[index]}",
"path": f"{savepath}/{item['file_name_re']}",
"is_dir": item["dir"],
"obj_category": item.get("obj_category", ""),
},
)
return tree return tree
def do_rename(self, tree, node_id=None): def do_rename_task(self, task, subdir_path=""):
if node_id is None: pattern, replace = self.magic_regex_func(
node_id = tree.root task.get("pattern", ""), task.get("replace", ""), task["taskname"]
for child in tree.children(node_id): )
file = child.data if not pattern or not replace:
if file.get("is_dir"): return 0
# self.do_rename(tree, child.identifier) savepath = re.sub(r"/{2,}", "/", f"/{task['savepath']}{subdir_path}")
pass if not self.savepath_fid.get(savepath):
elif file.get("file_name_re") and file["file_name_re"] != file["file_name"]: self.savepath_fid[savepath] = self.get_fids([savepath])[0]["fid"]
rename_ret = self.rename(file["fid"], file["file_name_re"]) dir_file_list = self.ls_dir(self.savepath_fid[savepath])["data"]["list"]
print(f"重命名:{file['file_name']}{file['file_name_re']}") dir_file_name_list = [item["file_name"] for item in dir_file_list]
if rename_ret["code"] != 0: is_rename_count = 0
print(f" ↑ 失败,{rename_ret['message']}") for dir_file in dir_file_list:
if dir_file["dir"]:
def _get_file_icon(self, f): is_rename_count += self.do_rename_task(
if f.get("dir"): task, f"{subdir_path}/{dir_file['file_name']}"
return "📁" )
ico_maps = { if re.search(pattern, dir_file["file_name"]):
"video": "🎞️", save_name = (
"image": "🖼️", re.sub(pattern, replace, dir_file["file_name"])
"audio": "🎵", if replace != ""
"doc": "📄", else dir_file["file_name"]
"archive": "📦", )
"default": "", if save_name != dir_file["file_name"] and (
} save_name not in dir_file_name_list
return ico_maps.get(f.get("obj_category"), "") ):
rename_return = self.rename(dir_file["fid"], save_name)
if rename_return["code"] == 0:
print(f"重命名:{dir_file['file_name']}{save_name}")
is_rename_count += 1
else:
print(
f"重命名:{dir_file['file_name']}{save_name} 失败,{rename_return['message']}"
)
return is_rename_count > 0
def verify_account(account): def verify_account(account):
@ -1115,7 +906,6 @@ def do_save(account, tasklist=[]):
plugins, CONFIG_DATA["plugins"], task_plugins_config = Config.load_plugins( plugins, CONFIG_DATA["plugins"], task_plugins_config = Config.load_plugins(
CONFIG_DATA.get("plugins", {}) CONFIG_DATA.get("plugins", {})
) )
print()
print(f"转存账号: {account.nickname}") print(f"转存账号: {account.nickname}")
# 获取全部保存目录fid # 获取全部保存目录fid
account.update_savepath_fid(tasklist) account.update_savepath_fid(tasklist)
@ -1156,6 +946,7 @@ def do_save(account, tasklist=[]):
print(f"任务不在运行周期内,跳过") print(f"任务不在运行周期内,跳过")
else: else:
is_new_tree = account.do_save_task(task) is_new_tree = account.do_save_task(task)
is_rename = account.do_rename_task(task)
# 补充任务的插件配置 # 补充任务的插件配置
def merge_dicts(a, b): def merge_dicts(a, b):
@ -1175,21 +966,14 @@ def do_save(account, tasklist=[]):
task.get("addition", {}), task_plugins_config task.get("addition", {}), task_plugins_config
) )
# 调用插件 # 调用插件
if is_new_tree: if is_new_tree or is_rename:
print(f"🧩 调用插件") print(f"🧩 调用插件")
for plugin_name, plugin in plugins.items(): for plugin_name, plugin in plugins.items():
if plugin.is_active: if plugin.is_active and (is_new_tree or is_rename):
task = ( task = (
plugin.run(task, account=account, tree=is_new_tree) or task plugin.run(task, account=account, tree=is_new_tree) or task
) )
print() print()
print(f"===============插件收尾===============")
for plugin_name, plugin in plugins.items():
if plugin.is_active and hasattr(plugin, "task_after"):
data = plugin.task_after()
if data.get("config"):
CONFIG_DATA["plugins"][plugin_name] = data["config"]
print()
def main(): def main():
@ -1200,21 +984,6 @@ def main():
print() print()
# 读取启动参数 # 读取启动参数
config_path = sys.argv[1] if len(sys.argv) > 1 else "quark_config.json" config_path = sys.argv[1] if len(sys.argv) > 1 else "quark_config.json"
# 推送测试
if os.environ.get("QUARK_TEST", "").lower() == "true":
print(f"===============通知测试===============")
CONFIG_DATA["push_config"] = json.loads(os.environ.get("PUSH_CONFIG"))
send_ql_notify(
"【夸克自动转存】",
f"通知测试\n\n{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
)
print()
if cookies := json.loads(os.environ.get("COOKIE", "[]")):
print(f"===============转存测试===============")
accounts = Quark(cookies[0])
accounts.do_save_check("https://pan.quark.cn/s/1ed94d530d63", "/来自:分享")
print()
return
# 从环境变量中获取 TASKLIST # 从环境变量中获取 TASKLIST
tasklist_from_env = [] tasklist_from_env = []
if tasklist_json := os.environ.get("TASKLIST"): if tasklist_json := os.environ.get("TASKLIST"):
@ -1241,6 +1010,8 @@ def main():
CONFIG_DATA = Config.read_json(config_path) CONFIG_DATA = Config.read_json(config_path)
Config.breaking_change_update(CONFIG_DATA) Config.breaking_change_update(CONFIG_DATA)
cookie_val = CONFIG_DATA.get("cookie") cookie_val = CONFIG_DATA.get("cookie")
if not CONFIG_DATA.get("magic_regex"):
CONFIG_DATA["magic_regex"] = MAGIC_REGEX
cookie_form_file = True cookie_form_file = True
# 获取cookie # 获取cookie
cookies = Config.get_cookies(cookie_val) cookies = Config.get_cookies(cookie_val)
@ -1270,7 +1041,7 @@ def main():
if NOTIFYS: if NOTIFYS:
notify_body = "\n".join(NOTIFYS) notify_body = "\n".join(NOTIFYS)
print(f"===============推送通知===============") print(f"===============推送通知===============")
send_ql_notify("【夸克自动转存", notify_body) send_ql_notify("【夸克自动追更", notify_body)
print() print()
if cookie_form_file: if cookie_form_file:
# 更新配置 # 更新配置

View File

@ -6,51 +6,36 @@
"QUARK_SIGN_NOTIFY": true, "QUARK_SIGN_NOTIFY": true,
"其他推送渠道//此项可删": "配置方法同青龙" "其他推送渠道//此项可删": "配置方法同青龙"
}, },
"plugins": { "media_servers": {
"emby": { "emby": {
"url": "", "url": "",
"token": "" "token": ""
} }
}, },
"magic_regex": { "magic_regex": {
"$TV_REGEX": { "$TV": {
"pattern": ".*?([Ss]\\d{1,2})?(?:[第EePpXx\\.\\-\\_\\( ]{1,2}|^)(\\d{1,3})(?!\\d).*?\\.(mp4|mkv)", "pattern": ".*?([Ss]\\d{1,2})?(?:[第EePpXx\\.\\-\\_\\( ]{1,2}|^)(\\d{1,3})(?!\\d).*?\\.(mp4|mkv)",
"replace": "\\1E\\2.\\3" "replace": "\\1E\\2.\\3"
}, },
"$BLACK_WORD": { "$BLACK_WORD": {
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*超前企划)(?!.*训练室)(?!.*蒸蒸日上).*", "pattern": "^(?!.*纯享)(?!.*加更)(?!.*超前企划)(?!.*训练室)(?!.*蒸蒸日上).*",
"replace": "" "replace": ""
},
"$SHOW_MAGIC": {
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*抢先)(?!.*预告).*?第\\d+期.*",
"replace": "{TASKNAME}.{SXX}E{II}.第{E}期{PART}.{EXT}"
},
"$TV_MAGIC": {
"pattern": ".*\\.(mp4|mkv|mov|m4v|avi|mpeg|ts)$",
"replace": "{TASKNAME}.{SXX}E{E}.{EXT}"
} }
}, },
"tasklist": [ "tasklist": [
{ {
"taskname": "测试-魔法匹配剧集这是一组有效分享配置CK后可测试任务是否正常", "taskname": "测试-魔法匹配剧集这是一组有效分享配置CK后可测试任务是否正常",
"shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-夸克自动转存测试", "shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-夸克自动转存测试",
"savepath": "/夸克自动转存测试/剧集", "savepath": "/夸克自动转存测试",
"pattern": "$TV_REGEX", "pattern": "$TV",
"replace": "", "replace": "",
"enddate": "2099-01-30", "enddate": "2099-01-30",
"update_subdir": "4k|1080p" "update_subdir": "4k|1080p"
}, },
{ {
"taskname": "测试-综艺命名", "taskname": "测试-广告过滤",
"shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-%E5%A4%B8%E5%85%8B%E8%87%AA%E5%8A%A8%E8%BD%AC%E5%AD%98%E6%B5%8B%E8%AF%95/71df3902f42d4270a58c0eb12aa2b014-%E7%BB%BC%E8%89%BA%E5%91%BD%E5%90%8D",
"savepath": "/夸克自动转存测试/综艺命名",
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*抢先)(?!.*预告).*?第\\d+期.*",
"replace": "{II}.{TASKNAME}.{DATE}.第{E}期{PART}.{EXT}"
},
{
"taskname": "测试-去广告字符",
"shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-夸克自动转存测试/680d91e490814da0927c38b432f88edc-带广告文件夹", "shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-夸克自动转存测试/680d91e490814da0927c38b432f88edc-带广告文件夹",
"savepath": "/夸克自动转存测试/去广告字符", "savepath": "/夸克自动转存测试/带广告文件夹",
"pattern": "【XX电影网】(.*)\\.(mp4|mkv)", "pattern": "【XX电影网】(.*)\\.(mp4|mkv)",
"replace": "\\1.\\2", "replace": "\\1.\\2",
"enddate": "2099-01-30" "enddate": "2099-01-30"
@ -58,7 +43,7 @@
{ {
"taskname": "测试-超期任务", "taskname": "测试-超期任务",
"shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-夸克自动转存测试", "shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-夸克自动转存测试",
"savepath": "/夸克自动转存测试/不会运行", "savepath": "/夸克自动转存测试",
"pattern": "", "pattern": "",
"replace": "", "replace": "",
"enddate": "2000-01-30", "enddate": "2000-01-30",

View File

@ -2,4 +2,3 @@ flask
apscheduler apscheduler
requests requests
treelib treelib
natsort