Compare commits

...

36 Commits

Author SHA1 Message Date
Cp0204
a02ff884f5 🐛 修复正则预览未读取自定义魔法匹配
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-04-20 21:25:45 +08:00
Cp0204
668897d1df 🎨 优化正则处理预览UI 增强引导
Some checks are pending
Docker Publish / build-and-push (push) Waiting to run
2025-04-20 11:23:48 +08:00
Cp0204
9866a9d93d 增加正则处理预览功能
- 后端添加正则处理预览逻辑,改 POST 接收前端请求
- 前端增加正则处理按钮
- 优化文件列表展示预览结果
2025-04-20 11:11:56 +08:00
Cp0204
9b9c5fe00a 调整单个任务执行逻辑
- 单任务执行前无须保存,由前端传递参数
- 单任务执行无视设定周期,始终执行
2025-04-19 23:55:09 +08:00
Cp0204
dc8362db08 🎨 优化任务周期提示输出 2025-04-19 22:01:10 +08:00
Cp0204
bc2cd1504e 🐛 修复星期全关依然运行的BUG 2025-04-19 21:59:06 +08:00
Cp0204
b118231f58 🔧 优化默认的 $TV 正则表达式
Some checks are pending
Docker Publish / build-and-push (push) Waiting to run
- 集数前强制定位,增强准确性
- 取消集数字符E编组,只取数字,替换默认加E
2025-04-19 20:57:13 +08:00
Cp0204
c910a986b1 🔧 增强任务推送成功信息提示
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-04-18 18:00:12 +08:00
Cp0204
f8d10236e3 QAS推送助手增加设置界面 2025-04-18 17:39:38 +08:00
Cp0204
2f0c51283c 🐛 修复获取 / 路径时报错 2025-04-18 15:36:06 +08:00
Cp0204
8611824b9a 新增 QAS 一键推送助手油猴脚本
- 在夸克网盘分享页面添加推送到 QAS 的按钮
- 实现 QAS 地址和 Token 的设置提示
- 增加任务创建成功与失败的反馈提示
2025-04-18 14:00:22 +08:00
Cp0204
2b16246beb 🔧 优化任务名搜索逻辑
- 增加任务名长度检查,太短不搜
2025-04-18 13:48:52 +08:00
Cp0204
62464fec17 🐛 增强容错处理 2025-04-18 00:53:59 +08:00
Cp0204
92a86ce8e0 支持分享目录选择&重构保存路径选择
- 更新前端模板,增加文件选择模态框的功能
- 调整任务建议和分享详情的获取逻辑,简化代码结构
- 修复部分样式问题,提升界面友好性
2025-04-18 00:43:10 +08:00
Cp0204
13f89b32c6 ♻️ 重构 shareurl 解析逻辑 2025-04-18 00:34:37 +08:00
Cp0204
d9fc4659b8 优化资源搜索来源切换逻辑
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
- 优化 CloudSaver 启用判断逻辑
- 搜索增加来源信息显示
2025-04-15 03:08:44 +08:00
Cp0204
8309f4a4d4 优化任务建议搜索逻辑
- 增加搜索防抖功能,避免频繁请求
- 调整搜索建议方法参数,简化逻辑
- 移除不必要的时间限制,提升用户体验
2025-04-15 02:55:34 +08:00
Cp0204
805f624b89 优化 CloudSaver 资源搜索功能
- 修复清洗引发的 list index out of range
- 增加正则表达式清洗标题和内容的逻辑
- 实现链接去重
- 前端提供更明确的服务器地址格式
2025-04-15 02:54:51 +08:00
Cp0204
4b2d78a0b2 📝 更新 CloudSaver 相关说明
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-04-12 21:19:58 +08:00
Cp0204
e1e0a6afc4 📝 更新帮助说明链接 2025-04-12 20:31:23 +08:00
Cp0204
d5a802c218 🐛 修复 CloudSaver 登录和搜索逻辑
- 更新 login 方法,增加对未设置用户名或密码的检查
- 调整 search 方法,处理未提供 token 的情况
- 优化 auto_login_search 方法,改进错误处理和 token 刷新逻辑
- 更新前端模板,使用 message 属性替代 error 属性
2025-04-12 19:52:38 +08:00
Cp0204
70093a3f2c ♻️ 重构登录和 API 验证逻辑
- 可使用 token 访问所有接口
- 重命名 get_api_token 函数为 get_login_token,以更清晰地表示其用途
- 优化了多个接口的返回格式,统一使用 success 和 message 字段
2025-04-12 18:52:45 +08:00
Cp0204
81d4098b6c ♻️ 优化配置更新接口和前端处理逻辑
- 修改后端 update 函数返回值格式,使用 JSON 格式返回成功和失败信息
- 更新前端 saveConfig 方法,根据后端返回的成功状态进行不同处理
- 优化配置保存后的提示信息展示逻辑
2025-04-12 18:52:44 +08:00
Cp0204
6f976f242a 增加配置修改后的保存提示
- configModified 跟踪配置是否被修改
- 窗口关闭前提醒保存配置
- 在运行脚本前提醒保存配置
2025-04-12 17:09:12 +08:00
Cp0204
50090db1f4 🔧 优化任务建议数据结构和展示逻辑
- 在后端增加 success 字段以区分请求是否成功
- 前端根据 success 字段决定是否显示建议列表
- 优化错误处理和提示信息展示
- 调整搜索逻辑,增加异常捕获
2025-04-12 16:26:44 +08:00
Cp0204
4225f1986b 添加 CloudSaver 资源搜索功能
- 新增 CloudSaver 类实现云盘资源搜索
- 集成 CloudSaver 到任务建议功能中
- 添加 CloudSaver 配置界面
- 优化任务建议展示逻辑,支持搜索错误提示
2025-04-12 15:26:44 +08:00
Cp0204
f398f3fa07 🐛 修复配置数据获取逻辑
- 修改数据获取方式,使用 Config 类读取最新配置
2025-04-12 13:15:18 +08:00
Cp0204
ffe95fcf66 重构配置文件读取与写入逻辑
- 将读取和写入 JSON 文件的功能封装到 Config 类中
- 更新相关代码以使用新的读取和写入方法
- 优化配置初始化流程,确保默认值的设置
2025-04-12 09:28:48 +08:00
Cp0204
83fd60f1a1 添加添加任务API接口
- 支持第三方任务添加功能
- 前端展示API Token
- 优化任务添加的错误处理和日志记录
2025-04-12 08:58:02 +08:00
Cp0204
dda9ec0a01 🔧 正则处理改为可选参数 2025-04-11 21:01:01 +08:00
Cp0204
b108d24981 🎨 调整布局,优化列宽
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-04-05 02:58:49 +08:00
Cp0204
90051b9aa0 🐛 修复魔法匹配 Wiki 链接
(cherry picked from commit 5f78c66a8f)
2025-04-05 02:37:33 +08:00
Cp0204
dc3afeae1d 添加多个配置项的提示信息
Some checks are pending
Docker Publish / build-and-push (push) Waiting to run
- 增强用户体验
- 引导阅读Wiki
2025-04-04 01:42:42 +08:00
Cp0204
c59ef3f0cf 添加排除关键词正则式到魔法匹配示例 2025-04-04 01:21:45 +08:00
Cp0204
6649e14472 UI添加自定义魔法匹配规则功能 2025-04-04 01:18:16 +08:00
Cp0204
3394ae7400 🎨 优化UI的提示信息 2025-04-03 23:38:38 +08:00
8 changed files with 1126 additions and 328 deletions

View File

@ -41,6 +41,7 @@
- [x] 支持分享链接的子目录 - [x] 支持分享链接的子目录
- [x] 记录失效分享并跳过任务 - [x] 记录失效分享并跳过任务
- [x] 支持需提取码的分享链接 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#支持需提取码的分享链接)</sup> - [x] 支持需提取码的分享链接 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#支持需提取码的分享链接)</sup>
- [x] 智能搜索资源并自动填充 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/CloudSaver搜索源)</sup>
- 文件管理 - 文件管理
- [x] 目标目录不存在时自动新建 - [x] 目标目录不存在时自动新建

View File

@ -1,6 +1,7 @@
# !/usr/bin/env python3 # !/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from flask import ( from flask import (
json,
Flask, Flask,
url_for, url_for,
session, session,
@ -14,15 +15,16 @@ 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 sdk.cloudsaver import CloudSaver
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 base64 import base64
import json
import sys import sys
import os import os
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)
@ -46,9 +48,10 @@ PYTHON_PATH = "python3" if os.path.exists("/usr/bin/python3") else "python"
SCRIPT_PATH = os.environ.get("SCRIPT_PATH", "./quark_auto_save.py") 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) DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
task_plugins_config = {} config_data = {}
task_plugins_config_default = {}
app = Flask(__name__) app = Flask(__name__)
app.config["APP_VERSION"] = get_app_ver() app.config["APP_VERSION"] = get_app_ver()
@ -77,24 +80,15 @@ def gen_md5(string):
return md5.hexdigest() return md5.hexdigest()
# 读取 JSON 文件内容 def get_login_token():
def read_json(): username = config_data["webui"]["username"]
with open(CONFIG_PATH, "r", encoding="utf-8") as f: password = config_data["webui"]["password"]
data = json.load(f) return gen_md5(f"token{username}{password}+-*/")[8:24]
return data
# 将数据写入 JSON 文件
def write_json(data):
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, sort_keys=False, indent=2)
def is_login(): def is_login():
data = read_json() login_token = get_login_token()
username = data["webui"]["username"] if session.get("token") == login_token or request.args.get("token") == login_token:
password = data["webui"]["password"]
if session.get("login") == gen_md5(username + password):
return True return True
else: else:
return False return False
@ -114,16 +108,15 @@ def favicon():
@app.route("/login", methods=["GET", "POST"]) @app.route("/login", methods=["GET", "POST"])
def login(): def login():
if request.method == "POST": if request.method == "POST":
data = read_json() username = config_data["webui"]["username"]
username = data["webui"]["username"] password = config_data["webui"]["password"]
password = data["webui"]["password"]
# 验证用户名和密码 # 验证用户名和密码
if (username == request.form.get("username")) and ( if (username == request.form.get("username")) and (
password == request.form.get("password") password == request.form.get("password")
): ):
logging.info(f">>> 用户 {username} 登录成功") logging.info(f">>> 用户 {username} 登录成功")
session["login"] = gen_md5(username + password)
session.permanent = True session.permanent = True
session["token"] = get_login_token()
return redirect(url_for("index")) return redirect(url_for("index"))
else: else:
logging.info(f">>> 用户 {username} 登录失败") logging.info(f">>> 用户 {username} 登录失败")
@ -137,7 +130,7 @@ def login():
# 退出登录 # 退出登录
@app.route("/logout") @app.route("/logout")
def logout(): def logout():
session.pop("login", None) session.pop("token", None)
return redirect(url_for("login")) return redirect(url_for("login"))
@ -155,47 +148,51 @@ def index():
@app.route("/data") @app.route("/data")
def get_data(): def get_data():
if not is_login(): if not is_login():
return redirect(url_for("login")) return jsonify({"success": False, "message": "未登录"})
data = read_json() data = Config.read_json(CONFIG_PATH)
del data["webui"] del data["webui"]
data["task_plugins_config"] = task_plugins_config data["api_token"] = get_login_token()
return jsonify(data) data["task_plugins_config_default"] = task_plugins_config_default
return jsonify({"success": True, "data": data})
# 更新数据 # 更新数据
@app.route("/update", methods=["POST"]) @app.route("/update", methods=["POST"])
def update(): def update():
global config_data
if not is_login(): if not is_login():
return "未登录" return jsonify({"success": False, "message": "未登录"})
data = request.json dont_save_keys = ["task_plugins_config_default", "api_token"]
data["webui"] = read_json()["webui"] for key, value in request.json.items():
if "task_plugins_config" in data: if key not in dont_save_keys:
del data["task_plugins_config"] config_data.update({key: value})
write_json(data) Config.write_json(CONFIG_PATH, config_data)
# 重新加载任务 # 重新加载任务
if reload_tasks(): if reload_tasks():
logging.info(f">>> 配置更新成功") logging.info(f">>> 配置更新成功")
return "配置更新成功" return jsonify({"success": True, "message": "配置更新成功"})
else: else:
logging.info(f">>> 配置更新失败") logging.info(f">>> 配置更新失败")
return "配置更新失败" return jsonify({"success": False, "message": "配置更新失败"})
# 处理运行脚本请求 # 处理运行脚本请求
@app.route("/run_script_now", methods=["GET"]) @app.route("/run_script_now", methods=["POST"])
def run_script_now(): def run_script_now():
if not is_login(): if not is_login():
return "未登录" return jsonify({"success": False, "message": "未登录"})
task_index = request.args.get("task_index", "") tasklist = request.json.get("tasklist", [])
command = [PYTHON_PATH, "-u", SCRIPT_PATH, CONFIG_PATH, task_index] command = [PYTHON_PATH, "-u", SCRIPT_PATH, CONFIG_PATH]
logging.info( logging.info(
f">>> 手动运行任务{int(task_index)+1 if task_index.isdigit() else 'all'}" f">>> 手动运行任务 [{tasklist[0].get('taskname') if len(tasklist)>0 else 'ALL'}] 开始执行..."
) )
def generate_output(): def generate_output():
# 设置环境变量 # 设置环境变量
process_env = os.environ.copy() process_env = os.environ.copy()
process_env["PYTHONIOENCODING"] = "utf-8" process_env["PYTHONIOENCODING"] = "utf-8"
if tasklist:
process_env["TASKLIST"] = json.dumps(tasklist, ensure_ascii=False)
process = subprocess.Popen( process = subprocess.Popen(
command, command,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
@ -224,64 +221,156 @@ def run_script_now():
@app.route("/task_suggestions") @app.route("/task_suggestions")
def get_task_suggestions(): def get_task_suggestions():
if not is_login(): if not is_login():
return jsonify({"error": "未登录"}) return jsonify({"success": False, "message": "未登录"})
base_url = base64.b64decode("aHR0cHM6Ly9zLjkxNzc4OC54eXo=").decode()
query = request.args.get("q", "").lower() query = request.args.get("q", "").lower()
deep = request.args.get("d", "").lower() deep = request.args.get("d", "").lower()
url = f"{base_url}/task_suggestions?q={query}&d={deep}"
try: try:
response = requests.get(url) cs_data = config_data.get("source", {}).get("cloudsaver", {})
return jsonify(response.json()) if (
cs_data.get("server")
and cs_data.get("username")
and cs_data.get("password")
):
cs = CloudSaver(cs_data.get("server"))
cs.set_auth(
cs_data.get("username", ""),
cs_data.get("password", ""),
cs_data.get("token", ""),
)
search = cs.auto_login_search(query)
if search.get("success"):
if search.get("new_token"):
cs_data["token"] = search.get("new_token")
Config.write_json(CONFIG_PATH, config_data)
search_results = cs.clean_search_results(search.get("data"))
return jsonify(
{"success": True, "source": "CloudSaver", "data": search_results}
)
else:
return jsonify({"success": True, "message": search.get("message")})
else:
base_url = base64.b64decode("aHR0cHM6Ly9zLjkxNzc4OC54eXo=").decode()
url = f"{base_url}/task_suggestions?q={query}&d={deep}"
response = requests.get(url)
return jsonify(
{"success": True, "source": "网络公开", "data": response.json()}
)
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}) return jsonify({"success": True, "message": f"error: {str(e)}"})
@app.route("/get_share_detail") @app.route("/get_share_detail", methods=["POST"])
def get_share_files(): def get_share_detail():
if not is_login(): if not is_login():
return jsonify({"error": "未登录"}) return jsonify({"success": False, "message": "未登录"})
shareurl = request.args.get("shareurl", "") shareurl = request.json.get("shareurl", "")
stoken = request.json.get("stoken", "")
account = Quark("", 0) account = Quark("", 0)
pwd_id, passcode, pdir_fid = account.get_id_from_url(shareurl) pwd_id, passcode, pdir_fid, paths = account.extract_url(shareurl)
is_sharing, stoken = account.get_stoken(pwd_id, passcode) if not stoken:
if not is_sharing: is_sharing, stoken = account.get_stoken(pwd_id, passcode)
return jsonify({"error": stoken}) if not is_sharing:
share_detail = account.get_detail(pwd_id, stoken, pdir_fid, 1) return jsonify({"success": False, "data": {"error": stoken}})
return jsonify(share_detail) share_detail = account.get_detail(pwd_id, stoken, pdir_fid, _fetch_share=1)
share_detail["paths"] = paths
share_detail["stoken"] = stoken
# 正则处理预览
def preview_regex(share_detail):
regex = request.json.get("regex")
pattern, replace = account.magic_regex_func(
regex.get("pattern", ""),
regex.get("replace", ""),
regex.get("taskname", ""),
regex.get("magic_regex", {}),
)
for item in share_detail["list"]:
file_name = item["file_name"]
if re.search(pattern, item["file_name"]):
item["file_name_re"] = (
re.sub(pattern, replace, file_name) if replace != "" else file_name
)
return share_detail
share_detail = preview_regex(share_detail)
return jsonify({"success": True, "data": share_detail})
@app.route("/get_savepath") @app.route("/get_savepath_detail")
def get_savepath(): def get_savepath_detail():
if not is_login(): if not is_login():
return jsonify({"error": "未登录"}) return jsonify({"success": False, "message": "未登录"})
data = read_json() account = Quark(config_data["cookie"][0], 0)
account = Quark(data["cookie"][0], 0) paths = []
if path := request.args.get("path"): if path := request.args.get("path"):
if path == "/": if path == "/":
fid = 0 fid = 0
elif get_fids := account.get_fids([path]):
fid = get_fids[0]["fid"]
else: else:
return jsonify([]) dir_names = path.split("/")
if dir_names[0] == "":
dir_names.pop(0)
path_fids = []
current_path = ""
for dir_name in dir_names:
current_path += "/" + dir_name
path_fids.append(current_path)
if get_fids := account.get_fids(path_fids):
fid = get_fids[-1]["fid"]
paths = [
{"fid": get_fid["fid"], "name": dir_name}
for get_fid, dir_name in zip(get_fids, dir_names)
]
else:
return jsonify({"success": False, "data": {"error": "获取fid失败"}})
else: else:
fid = request.args.get("fid", 0) fid = request.args.get("fid", "0")
file_list = account.ls_dir(fid) file_list = {
return jsonify(file_list) "list": account.ls_dir(fid),
"paths": paths,
}
return jsonify({"success": True, "data": file_list})
@app.route("/delete_file", methods=["POST"]) @app.route("/delete_file", methods=["POST"])
def delete_file(): def delete_file():
if not is_login(): if not is_login():
return jsonify({"error": "未登录"}) return jsonify({"success": False, "message": "未登录"})
data = read_json() account = Quark(config_data["cookie"][0], 0)
account = Quark(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:
response = {"error": "fid not found"} response = {"success": False, "message": "缺失必要字段: fid"}
return jsonify(response) return jsonify(response)
# 添加任务接口
@app.route("/api/add_task", methods=["POST"])
def add_task():
global config_data
# 验证token
if not is_login():
return jsonify({"success": False, "code": 1, "message": "未登录"}), 401
# 必选字段
request_data = request.json
required_fields = ["taskname", "shareurl", "savepath"]
for field in required_fields:
if field not in request_data or not request_data[field]:
return (
jsonify(
{"success": False, "code": 2, "message": f"缺少必要字段: {field}"}
),
400,
)
# 添加任务
config_data["tasklist"].append(request_data)
Config.write_json(CONFIG_PATH, config_data)
logging.info(f">>> 通过API添加任务: {request_data['taskname']}")
return jsonify(
{"success": True, "code": 0, "message": "任务添加成功", "data": request_data}
)
# 定时任务执行的函数 # 定时任务执行的函数
def run_python(args): def run_python(args):
logging.info(f">>> 定时运行任务") logging.info(f">>> 定时运行任务")
@ -290,11 +379,8 @@ def run_python(args):
# 重新加载任务 # 重新加载任务
def reload_tasks(): def reload_tasks():
# 读取数据 # 读取定时规则
data = read_json() if crontab := config_data.get("crontab"):
# 添加新任务
crontab = data.get("crontab")
if crontab:
if scheduler.state == 1: if scheduler.state == 1:
scheduler.pause() # 暂停调度器 scheduler.pause() # 暂停调度器
trigger = CronTrigger.from_crontab(crontab) trigger = CronTrigger.from_crontab(crontab)
@ -321,7 +407,7 @@ def reload_tasks():
def init(): def init():
global task_plugins_config global config_data, task_plugins_config_default
logging.info(f">>> 初始化配置") logging.info(f">>> 初始化配置")
# 检查配置文件是否存在 # 检查配置文件是否存在
if not os.path.exists(CONFIG_PATH): if not os.path.exists(CONFIG_PATH):
@ -329,23 +415,30 @@ def init():
os.makedirs(os.path.dirname(CONFIG_PATH)) os.makedirs(os.path.dirname(CONFIG_PATH))
with open("quark_config.json", "rb") as src, open(CONFIG_PATH, "wb") as dest: with open("quark_config.json", "rb") as src, open(CONFIG_PATH, "wb") as dest:
dest.write(src.read()) dest.write(src.read())
data = read_json()
Config.breaking_change_update(data) # 读取配置
config_data = Config.read_json(CONFIG_PATH)
Config.breaking_change_update(config_data)
# 默认管理账号 # 默认管理账号
data["webui"] = { config_data["webui"] = {
"username": os.environ.get("WEBUI_USERNAME") "username": os.environ.get("WEBUI_USERNAME")
or data.get("webui", {}).get("username", "admin"), or config_data.get("webui", {}).get("username", "admin"),
"password": os.environ.get("WEBUI_PASSWORD") "password": os.environ.get("WEBUI_PASSWORD")
or data.get("webui", {}).get("password", "admin123"), or config_data.get("webui", {}).get("password", "admin123"),
} }
# 默认定时规则 # 默认定时规则
if not data.get("crontab"): if not config_data.get("crontab"):
data["crontab"] = "0 8,18,20 * * *" config_data["crontab"] = "0 8,18,20 * * *"
# 初始化插件配置 # 初始化插件配置
_, plugins_config_default, task_plugins_config = Config.load_plugins() _, plugins_config_default, task_plugins_config_default = Config.load_plugins()
plugins_config_default.update(data.get("plugins", {})) plugins_config_default.update(config_data.get("plugins", {}))
data["plugins"] = plugins_config_default config_data["plugins"] = plugins_config_default
write_json(data)
# 更新配置
Config.write_json(CONFIG_PATH, config_data)
if __name__ == "__main__": if __name__ == "__main__":

162
app/sdk/cloudsaver.py Normal file
View File

@ -0,0 +1,162 @@
import re
import requests
class CloudSaver:
"""
CloudSaver 用于获取云盘资源
"""
def __init__(self, server):
self.server = server
self.username = None
self.password = None
self.token = None
self.session = requests.Session()
self.session.headers.update({"Content-Type": "application/json"})
def set_auth(self, username, password, token=""):
self.username = username
self.password = password
self.token = token
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
def login(self):
if not self.username or not self.password:
return {"success": False, "message": "CloudSaver未设置用户名或密码"}
try:
url = f"{self.server}/api/user/login"
data = {"username": self.username, "password": self.password}
response = self.session.post(url, json=data)
result = response.json()
if result.get("success"):
self.token = result.get("data", {}).get("token")
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
return {"success": True, "token": self.token}
else:
return {
"success": False,
"message": f"CloudSaver登录{result.get('message', '未知错误')}",
}
except Exception as e:
return {"success": False, "message": str(e)}
def search(self, keyword, last_message_id=""):
"""
搜索资源
Args:
keyword (str): 搜索关键词
last_message_id (str): 上一条消息ID用于分页
Returns:
list: 搜索结果列表
"""
try:
url = f"{self.server}/api/search"
params = {"keyword": keyword, "lastMessageId": last_message_id}
response = self.session.get(url, params=params)
result = response.json()
if result.get("success"):
data = result.get("data", [])
return {"success": True, "data": data}
else:
return {"success": False, "message": result.get("message", "未知错误")}
except Exception as e:
return {"success": False, "message": str(e)}
def auto_login_search(self, keyword, last_message_id=""):
"""
自动登录并搜索资源
Args:
keyword (str): 搜索关键词
last_message_id (str): 上一条消息ID用于分页
"""
result = self.search(keyword, last_message_id)
if result.get("success"):
return result
else:
if (
result.get("message") == "无效的 token"
or result.get("message") == "未提供 token"
):
login_result = self.login()
if login_result.get("success"):
result = self.search(keyword, last_message_id)
result["new_token"] = login_result.get("token")
return result
else:
return {
"success": False,
"message": login_result.get("message", "未知错误"),
}
return {"success": False, "message": result.get("message", "未知错误")}
def clean_search_results(self, search_results):
"""
清洗搜索结果
Args:
search_results (list): 搜索结果列表
Returns:
list: 夸克网盘链接列表
"""
pattern_title = r"(名称|标题)[:]?(.*)"
pattern_content = r"(描述|简介)[:]?(.*)(链接|标签)"
clean_results = []
link_array = []
for channel in search_results:
for item in channel.get("list", []):
cloud_links = item.get("cloudLinks", [])
for link in cloud_links:
if link.get("cloudType") == "quark":
# 清洗标题
title = item.get("title", "")
if match := re.search(pattern_title, title, re.DOTALL):
title = match.group(2)
title = title.replace("&amp;", "&").strip()
# 清洗内容
content = item.get("content", "")
if match := re.search(pattern_content, content, re.DOTALL):
content = match.group(2)
content = content.replace('<mark class="highlight">', "")
content = content.replace("</mark>", "")
content = content.strip()
# 链接去重
if link.get("link") not in link_array:
link_array.append(link.get("link"))
clean_results.append(
{
"shareurl": link.get("link"),
"taskname": title,
"content": content,
"tags": item.get("tags", []),
"channel": item.get("channel", ""),
"channel_id": item.get("channelId", ""),
}
)
return clean_results
# 测试示例
if __name__ == "__main__":
# 创建CloudSaver实例
server = ""
username = ""
password = ""
token = ""
cloud_saver = CloudSaver(server)
cloud_saver.set_auth(username, password, token)
# 搜索资源
results = cloud_saver.auto_login_search("黑镜")
# 提取夸克网盘链接
clean_results = cloud_saver.clean_search_results(results.get("data", []))
# 打印结果
for item in clean_results:
print(f"标题: {item['taskname']}")
print(f"描述: {item['content']}")
print(f"链接: {item['shareurl']}")
print(f"标签: {' '.join(item['tags'])}")
print("-" * 50)

View File

@ -42,6 +42,7 @@ body {
.title { .title {
margin-top: 30px; margin-top: 30px;
margin-bottom: 10px;
} }
table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child { table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child {
@ -59,12 +60,13 @@ table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child {
.task-suggestions { .task-suggestions {
width: 100%; width: 100%;
max-height: 500px; max-height: 250px;
overflow-y: auto; overflow-y: auto;
transform: translate(0, -100%); transform: translate(0, -100%);
top: 0; top: 0;
margin-top: -5px; margin-top: -5px;
border: 1px solid #007bff; border: 1px solid #007bff;
z-index: 1021;
} }
/* /*
@ -159,4 +161,8 @@ table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child {
.form-control-dark:focus { .form-control-dark:focus {
border-color: transparent; border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25); box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
}
.cursor-pointer {
cursor: pointer
} }

View File

@ -0,0 +1,190 @@
// ==UserScript==
// @name QAS一键推送助手
// @namespace https://github.com/Cp0204/quark-auto-save
// @license AGPL
// @version 0.3
// @description 在夸克网盘分享页面添加推送到 QAS 的按钮
// @icon https://pan.quark.cn/favicon.ico
// @author Cp0204
// @match https://pan.quark.cn/s/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @downloadURL https://update.greasyfork.org/scripts/533201/QAS%E4%B8%80%E9%94%AE%E6%8E%A8%E9%80%81%E5%8A%A9%E6%89%8B.user.js
// @updateURL https://update.greasyfork.org/scripts/533201/QAS%E4%B8%80%E9%94%AE%E6%8E%A8%E9%80%81%E5%8A%A9%E6%89%8B.meta.js
// ==/UserScript==
(function() {
'use strict';
let qas_base = GM_getValue('qas_base', '');
let qas_token = GM_getValue('qas_token', '');
// QAS 设置弹窗函数
function showQASSettingDialog(callback) {
Swal.fire({
title: 'QAS 设置',
html: `
<label for="qas_base">QAS 服务器</label>
<input id="qas_base" class="swal2-input" placeholder="例如: 192.168.1.8:5005" value="${qas_base}">
<label for="qas_token">QAS Token</label>
<input id="qas_token" class="swal2-input" placeholder="v0.5+ 系统配置中查找" value="${qas_token}">
`,
focusConfirm: false,
preConfirm: () => {
qas_base = document.getElementById('qas_base').value;
qas_token = document.getElementById('qas_token').value;
if (!qas_base || !qas_token) {
Swal.showValidationMessage('请填写 QAS 服务器和 Token');
}
return { qas_base: qas_base, qas_token: qas_token }
}
}).then((result) => {
if (result.isConfirmed) {
GM_setValue('qas_base', result.value.qas_base);
GM_setValue('qas_token', result.value.qas_token);
qas_base = result.value.qas_base;
qas_token = result.value.qas_token;
if (callback) {
callback(); // 执行回调函数
}
}
});
}
// 添加 QAS 设置按钮
function addQASSettingButton() {
function waitForElement(selector, callback) {
const element = document.querySelector(selector);
if (element) {
callback(element);
} else {
setTimeout(() => waitForElement(selector, callback), 500);
}
}
waitForElement('.DetailLayout--client-download--FpyCkdW.ant-dropdown-trigger', (clientDownloadButton) => {
const qasSettingButton = document.createElement('div');
qasSettingButton.className = 'DetailLayout--client-download--FpyCkdW ant-dropdown-trigger';
qasSettingButton.innerHTML = 'QAS设置';
qasSettingButton.addEventListener('click', () => {
showQASSettingDialog();
});
clientDownloadButton.parentNode.insertBefore(qasSettingButton, clientDownloadButton.nextSibling);
});
}
// 推送到 QAS 按钮
function addQASButton() {
function waitForElement(selector, callback) {
const element = document.querySelector(selector);
if (element) {
callback(element);
} else {
setTimeout(() => waitForElement(selector, callback), 500);
}
}
waitForElement('.ant-btn.share-save', (saveButton) => {
const qasButton = document.createElement('button');
qasButton.type = 'button';
qasButton.className = 'ant-btn share-save';
qasButton.style.marginLeft = '10px';
qasButton.innerHTML = '<span class="share-save-ico"></span><span>推送到QAS</span>';
let taskname, shareurl, savepath; // 声明变量
// 获取数据函数
function getData() {
const currentUrl = window.location.href;
taskname = currentUrl.lastIndexOf('-') > 0 ? decodeURIComponent(currentUrl.match(/.*\/[^-]+-(.+)$/)[1]) : document.querySelector('.author-name').textContent;
shareurl = currentUrl;
let pathElement = document.querySelector('.path-name')
savepath = pathElement ? pathElement.title.replace('全部文件', '').trim() : "";
savepath += "/" + taskname
qasButton.title = `任务名称: ${taskname}\n分享链接: ${shareurl}\n保存路径: ${savepath}`;
}
// 添加鼠标悬停事件
qasButton.addEventListener('mouseover', () => {
getData(); // 鼠标悬停时获取数据
});
// 添加点击事件
qasButton.addEventListener('click', () => {
getData(); // 点击时重新获取数据,确保最新
const apiUrl = `http://${qas_base}/api/add_task?token=${qas_token}`;
const data = {
"taskname": taskname,
"shareurl": shareurl,
"savepath": savepath,
};
GM_xmlhttpRequest({
method: 'POST',
url: apiUrl,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(data),
onload: function(response) {
try {
const jsonResponse = JSON.parse(response.responseText);
if (jsonResponse.success) {
Swal.fire({
title: '任务创建成功',
html: `<small>
<b>任务名称:</b> ${taskname}<br><br>
<b>保存路径:</b> ${savepath}<br><br>
<a href="http://${qas_base}" target="_blank"> QAS 查看</a>
<small>`,
icon: 'success'
});
} else {
Swal.fire({
title: '任务创建失败',
text: jsonResponse.message,
icon: 'error'
});
}
} catch (e) {
Swal.fire({
title: '解析响应失败',
text: `无法解析 JSON 响应: ${response.responseText}`,
icon: 'error'
});
}
},
onerror: function(error) {
Swal.fire({
title: '任务创建失败',
text: error,
icon: 'error'
});
}
});
});
saveButton.parentNode.insertBefore(qasButton, saveButton.nextSibling);
});
}
// 初始化
(function init() {
addQASSettingButton();
if (!qas_base || !qas_token) {
showQASSettingDialog(() => {
addQASButton(); // 在设置后添加 QAS 按钮
});
} else {
addQASButton(); // 如果配置存在,则直接添加 QAS 按钮
}
})(); // 立即执行初始化
})();

View File

@ -58,15 +58,15 @@
</nav> </nav>
<main class="col-md-9 col-lg-10 ml-sm-auto"> <main class="col-md-9 col-lg-10 ml-sm-auto">
<form @submit.prevent="saveConfig"> <form @submit.prevent="saveConfig" @keydown.enter.prevent>
<div v-if="activeTab === 'config'"> <div v-if="activeTab === 'config'">
<div class="row title"> <div class="row title">
<div class="col"> <div class="col-10">
<h2><i class="bi bi-cookie"></i> Cookie</h2> <h2><i class="bi bi-cookie"></i> Cookie</h2>
</div> </div>
<div class="col text-right"> <div class="col-2 text-right">
<button type="button" class="btn btn-outline-primary mb-3" @click="addCookie()">+</button> <button type="button" class="btn btn-outline-primary" @click="addCookie()">+</button>
</div> </div>
</div> </div>
<p>1. 所有账号执行签到,纯签到只需移动端参数即可!</p> <p>1. 所有账号执行签到,纯签到只需移动端参数即可!</p>
@ -86,22 +86,22 @@
</span> </span>
</div> </div>
</div> </div>
<div class="input-group mt-2 mb-2"> <div class="input-group mb-2">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text">Crontab</span> <span class="input-group-text">Crontab</span>
</div> </div>
<input type="text" v-model="formData.crontab" class="form-control" placeholder="必填"> <input type="text" v-model="formData.crontab" class="form-control" placeholder="必填">
</div> </div>
<div class="row title"> <div class="row title" title="通知推送支持多个渠道见Wiki">
<div class="col"> <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" title="通知推送服务配置">?</a> <a href="https://github.com/Cp0204/quark-auto-save/wiki/通知推送服务配置" target="_blank">?</a>
</span> </span>
</div> </div>
<div class="col text-right"> <div class="col-2 text-right">
<button type="button" class="btn btn-outline-primary mb-3" @click="addPush()">+</button> <button type="button" class="btn btn-outline-primary" @click="addPush()">+</button>
</div> </div>
</div> </div>
<div v-for="(value, key) in formData.push_config" :key="key" class="input-group mb-2"> <div v-for="(value, key) in formData.push_config" :key="key" class="input-group mb-2">
@ -117,11 +117,11 @@
</div> </div>
</div> </div>
<div class="row title" v-if="Object.keys(getAvailablePlugins(formData.plugins)).length"> <div class="row title" v-if="Object.keys(getAvailablePlugins(formData.plugins)).length" title="各插件的配置选项具体键值由插件定义见Wiki">
<div class="col"> <div class="col">
<h2 style="display: inline-block;"><i class="bi bi-plug"></i> 插件</h2> <h2 style="display: inline-block;"><i class="bi bi-plug"></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" title="插件配置">?</a> <a href="https://github.com/Cp0204/quark-auto-save/wiki/插件配置" target="_blank">?</a>
</span> </span>
</div> </div>
</div> </div>
@ -142,6 +142,78 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row title" title="预定义的正则匹配规则,在任务列表中可直接点击使用">
<div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-magic"></i> 魔法匹配</h2>
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程#21-魔法匹配" target="_blank">?</a>
</span>
</div>
<div class="col-2 text-right">
<button type="button" class="btn btn-outline-primary" @click="addMagicRegex()">+</button>
</div>
</div>
<div v-for="(value, key) in formData.magic_regex" :key="key" class="form-group mb-2">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">魔法名</span>
</div>
<input type="text" :data-oldkey="key" v-model="key" class="form-control" @change="updateMagicRegexKey($event.target.dataset.oldkey, $event.target.value)" placeholder="自定义名称">
<div class="input-group-prepend">
<span class="input-group-text">正则处理</span>
</div>
<input type="text" v-model="value.pattern" class="form-control" placeholder="匹配表达式">
<input type="text" v-model="value.replace" class="form-control" placeholder="替换表达式">
<div class="input-group-append">
<button type="button" class="btn btn-outline-danger" @click="removeMagicRegex(key)">-</button>
</div>
</div>
</div>
<div class="row title" title="API接口用以第三方添加任务等操作见Wiki">
<div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-link-45deg"></i> API</h2>
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/API接口" target="_blank">?</a>
</span>
</div>
</div>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">Token</span>
</div>
<input type="text" v-model="formData.api_token" class="form-control" style="background-color:white;" disabled>
</div>
<div class="row title" title="资源搜索服务配置,用于任务名称智能搜索">
<div class="col-10">
<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 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>
<div v-if="activeTab === 'tasklist'"> <div v-if="activeTab === 'tasklist'">
@ -199,9 +271,9 @@
<div class="col-sm-10"> <div class="col-sm-10">
<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.length && 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="text-muted text-center" style="font-size: small;">以下资源来自第三方,网络公开搜集,请自行辨识,如有侵权请联系夸克官方</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" :key="suggestion.taskname" class="dropdown-item" @click.prevent="selectSuggestion(task, suggestion)" style="cursor: pointer;"> <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>
@ -220,12 +292,13 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row" title="支持子目录链接Web端打开分享点入目录复制浏览器的URL即可支持带提取码链接说明见Wiki">
<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"> <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.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>
@ -239,33 +312,24 @@
<div class="input-group"> <div class="input-group">
<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=" <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>
bi bi-reply"></i></button> <button class="btn btn-outline-secondary" type="button" @click="showSavepathSelect(index)">选择</button>
<button class="btn btn-outline-secondary dropdown-toggle" type="button" @click="getSavepathDirs(task.savepath)" data-toggle="dropdown" aria-expanded="false">选择</button>
<div class="dropdown-menu" style="max-height: 300px; min-width: 200px; overflow-y: auto;">
<a class="dropdown-item" @click.stop.prevent="selectSavepath(index,getParentDirectory(task.savepath),'..')" href="#">..</a>
<span v-if="!savepaths.some(item => item.dir)" class="dropdown-item disabled">无子目录</span>
<a v-for="(item, key) in savepaths" :class="{'disabled': item.fid === 0 || !item.dir}" class="dropdown-item" @click.stop.prevent="selectSavepath(index,item.fid,item.file_name)" href="#">
<i class="bi" :class="item.dir ? 'bi-folder2' : 'bi-file-earmark'"></i> {{ item.file_name }}
<i class="bi bi-trash3-fill text-danger" @click.stop.prevent="deleteFile(item.fid,item.file_name,item.dir)" style="position: absolute; right: 10px; pointer-events: auto;"></i>
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <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"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text">正则处理</span> <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"> <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"> <div class="input-group-append" title="保存时只比较文件名的部分01.mp4 和 01.mkv 视同为同一文件,不重复转存">
<div class="input-group-text"> <div class="input-group-text">
<input type="checkbox" title="忽略后缀" v-model="task.ignore_extension">&nbsp;忽略后缀 <input type="checkbox" v-model="task.ignore_extension">&nbsp;忽略后缀
</div> </div>
</div> </div>
</div> </div>
@ -274,21 +338,21 @@
</datalist> </datalist>
</div> </div>
</div> </div>
<div class="form-group row"> <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"> <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="showShareFiles(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"> <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">
<input type="text" name="update_subdir[]" class="form-control" v-model="task.update_subdir" placeholder="可选,需更新子目录的正则式,多项以|分割,如 4k|1080p ,注意!深层嵌套目录慎用 .* " title="注意!深层嵌套目录逐级索引,工作强度会非常大,慎用!"> <input type="text" name="update_subdir[]" class="form-control" v-model="task.update_subdir" placeholder="可选,匹配需更新子目录(含各级嵌套目录)的正则表达式,多项以|分割,如 4k|1080p">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -297,7 +361,7 @@
<input type="date" name="enddate[]" class="form-control" v-model="task.enddate" placeholder="可选"> <input type="date" name="enddate[]" class="form-control" v-model="task.enddate" placeholder="可选">
</div> </div>
</div> </div>
<div class="form-group row"> <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 col-form-label"> <div class="col-sm-10 col-form-label">
<div class="form-check form-check-inline" title="也可用作任务总开关"> <div class="form-check form-check-inline" title="也可用作任务总开关">
@ -310,7 +374,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row" v-if="Object.keys(getAvailablePlugins(formData.plugins)).length"> <div class="form-group row" v-if="Object.keys(getAvailablePlugins(formData.plugins)).length" title="单个任务的插件选项具体键值由插件定义见Wiki">
<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">
<v-jsoneditor v-model="task.addition" :options="{mode:'tree'}" :plus="false" height="180px"></v-jsoneditor> <v-jsoneditor v-model="task.addition" :options="{mode:'tree'}" :plus="false" height="180px"></v-jsoneditor>
@ -354,42 +418,70 @@
</div> </div>
</div> </div>
<!-- 模态框 分享文件列表 --> <!-- 模态框 文件选择 -->
<div class="modal" tabindex="-1" id="shareDetailModal"> <div class="modal" tabindex="-1" id="fileSelectModal">
<div class="modal-dialog modal-xl"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><b>分享文件列表</b> <h5 class="modal-title">
<b v-if="fileSelect.previewRegex">正则处理预览</b>
<b v-else-if="fileSelect.selectDir">选择{{fileSelect.selectShare ? '需转存的' : '保存到的'}}文件夹</b>
<b v-else>选择起始文件</b>
<div v-if="modalLoading" class="spinner-border spinner-border-sm m-1" role="status"></div> <div v-if="modalLoading" class="spinner-border spinner-border-sm m-1" role="status"></div>
</h5> </h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body small">
<div class="alert alert-warning" v-if="shareFiles.error" v-html="shareFiles.error"></div> <div class="alert alert-warning" v-if="fileSelect.error" v-html="fileSelect.error"></div>
<table class="table table-hover table-sm" v-else-if="!modalLoading" title="请选择转存起始文件,将只转存修改日期>选中文件的文件"> <div v-else>
<thead> <!-- 面包屑导航 -->
<tr> <nav aria-label="breadcrumb" v-if="fileSelect.selectDir">
<!-- <th scope="col">fid</th> --> <ol class="breadcrumb">
<th scope="col">文件名</th> <li class="breadcrumb-item cursor-pointer" @click="navigateTo('0','/')"><i class="bi bi-house-door"></i></li>
<th scope="col">大小</th> <li v-for="(item, index) in fileSelect.paths" class="breadcrumb-item">
<th scope="col">修改日期 ↓</th> <a v-if="index != fileSelect.paths.length - 1" href="#" @click="navigateTo(item.fid, item.name)">{{ item.name }}</a>
</tr> <span v-else class="text-muted">{{ item.name }}</span>
</thead> </li>
<tbody> </ol>
<tr> </nav>
<td colspan="4"><i class="bi bi-folder-plus"></i> 后续更新的文件...</td> <!-- 文件列表 -->
</tr> <div class="mb-3" v-if="fileSelect.previewRegex">
<tr v-for="(file, key) in shareFiles" :key="key" @click="selectStartFid(file.fid)" style="cursor: pointer;"> <div><b>匹配表达式:</b><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span></div>
<!-- <td>{{file.fid}}</td> --> <div><b>替换表达式:</b><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].replace"></span></div>
<td><i class="bi" :class="file.dir ? 'bi-folder2' : 'bi-file-earmark'"></i> {{file.file_name}}</td> </div>
<td>{{file.size | size}}</td> <table class="table table-hover table-sm">
<td>{{file.last_update_at | ts2date}}</td> <thead>
</td> <tr>
</tr> <th scope="col">文件名</th>
</tbody> <th scope="col" v-if="fileSelect.selectShare">正则处理</th>
</table> <template v-if="!fileSelect.previewRegex">
<th scope="col">大小</th>
<th scope="col">修改日期 ↓</th>
<th scope="col" v-if="!fileSelect.selectShare">操作</th>
</template>
</tr>
</thead>
<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}">
<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' : 'text-danger'">{{file.file_name_re || '&times;'}}</td>
<template v-if="!fileSelect.previewRegex">
<td v-if="file.dir">{{ file.include_items }}项</td>
<td v-else>{{file.size | size}}</td>
<td>{{file.updated_at | ts2date}}</td>
<td v-if="!fileSelect.selectShare"><a @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)">删除</a></td>
</template>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer" v-if="fileSelect.selectDir && !fileSelect.previewRegex">
<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" 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>
@ -410,7 +502,15 @@
push_config: {}, push_config: {},
media_servers: {}, media_servers: {},
tasklist: [], tasklist: [],
magic_regex: {} magic_regex: {},
source: {
cloudsaver: {
server: "",
username: "",
password: "",
token: ""
}
},
}, },
newTask: { newTask: {
taskname: "", taskname: "",
@ -427,20 +527,28 @@
taskDirs: [""], taskDirs: [""],
taskDirSelected: "", taskDirSelected: "",
taskNameFilter: "", taskNameFilter: "",
savepaths: [],
modalLoading: false, modalLoading: false,
shareFiles: [],
forceTaskIndex: null,
smart_param: { smart_param: {
index: null, index: null,
savepath: "", savepath: "",
origin_savepath: "", origin_savepath: "",
taskSuggestions: [], taskSuggestions: {},
showSuggestions: false, showSuggestions: false,
lastSuggestionsTime: 0,
isSearching: false, isSearching: false,
searchTimer: null,
}, },
activeTab: 'tasklist', activeTab: 'tasklist',
configModified: false,
fileSelect: {
index: null,
shareurl: "",
stoken: "",
fileList: [],
paths: [],
selectDir: true,
selectShare: true,
previewRegex: false,
},
}, },
filters: { filters: {
ts2date: function (value) { ts2date: function (value) {
@ -448,15 +556,21 @@
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`; return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`;
}, },
size: function (value) { size: function (value) {
if (!value) return "0B"; if (!value) return "";
const unitArr = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const unitArr = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const srcsize = parseFloat(value); const srcsize = parseFloat(value);
const index = srcsize ? Math.floor(Math.log(srcsize) / Math.log(1024)) : 0; const index = srcsize ? Math.floor(Math.log(srcsize) / Math.log(1024)) : 0;
const size = (srcsize / Math.pow(1024, index)).toFixed(2).replace(/\.?0+$/, ""); const size = (srcsize / Math.pow(1024, index)).toFixed(1).replace(/\.?0+$/, "");
return size + unitArr[index]; return size + unitArr[index];
} }
}, },
watch: { watch: {
formData: {
handler(newVal, oldVal) {
this.configModified = true;
},
deep: true
}
}, },
mounted() { mounted() {
this.fetchData(); this.fetchData();
@ -468,6 +582,10 @@
this.smart_param.showSuggestions = false; this.smart_param.showSuggestions = false;
} }
}); });
window.addEventListener('beforeunload', this.handleBeforeUnload);
},
beforeDestroy() {
window.removeEventListener('beforeunload', this.handleBeforeUnload);
}, },
methods: { methods: {
changeTab(tab) { changeTab(tab) {
@ -493,24 +611,40 @@
fetchData() { fetchData() {
axios.get('/data') axios.get('/data')
.then(response => { .then(response => {
config_data = response.data.data
// cookie兼容 // cookie兼容
if (typeof response.data.cookie === 'string') if (typeof config_data.cookie === 'string')
response.data.cookie = [response.data.cookie]; config_data.cookie = [config_data.cookie];
// 添加星期预设 // 添加星期预设
response.data.tasklist = response.data.tasklist.map(task => { config_data.tasklist = config_data.tasklist.map(task => {
if (!task.hasOwnProperty('runweek')) { if (!task.hasOwnProperty('runweek')) {
task.runweek = [1, 2, 3, 4, 5, 6, 7]; task.runweek = [1, 2, 3, 4, 5, 6, 7];
} }
return task; return task;
}); });
// 获取所有任务父目录 // 获取所有任务父目录
response.data.tasklist.forEach(item => { config_data.tasklist.forEach(item => {
parentDir = this.getParentDirectory(item.savepath) parentDir = this.getParentDirectory(item.savepath)
if (!this.taskDirs.includes(parentDir)) if (!this.taskDirs.includes(parentDir))
this.taskDirs.push(parentDir); this.taskDirs.push(parentDir);
}); });
this.newTask.addition = response.data.task_plugins_config; this.newTask.addition = config_data.task_plugins_config_default;
this.formData = response.data; // 确保source配置存在
if (!config_data.source) {
config_data.source = {};
}
if (!config_data.source.cloudsaver) {
config_data.source.cloudsaver = {
server: "",
username: "",
password: "",
token: ""
};
}
this.formData = config_data;
setTimeout(() => {
this.configModified = false;
}, 100);
}) })
.catch(error => { .catch(error => {
console.error('Error fetching data:', error); console.error('Error fetching data:', error);
@ -527,11 +661,23 @@
} }
} }
}, },
handleBeforeUnload(e) {
if (this.configModified) {
e.preventDefault();
e.returnValue = '配置已修改但未保存,确定要离开吗?';
return e.returnValue;
}
},
saveConfig() { saveConfig() {
axios.post('/update', this.formData) axios.post('/update', this.formData)
.then(response => { .then(response => {
alert(response.data); if (response.data.success) {
console.log('Config saved successfully:', response.data); this.configModified = false;
alert(response.data.message);
} else {
alert(response.data.message);
}
console.log('Config saved result:', response.data);
}) })
.catch(error => { .catch(error => {
console.error('Error saving config:', error); console.error('Error saving config:', error);
@ -589,7 +735,12 @@
} }
}, },
changeTaskname(index, task) { changeTaskname(index, task) {
this.searchSuggestions(index, task.taskname, 500); if (this.smart_param.searchTimer) {
clearTimeout(this.smart_param.searchTimer);
}
this.smart_param.searchTimer = setTimeout(() => {
this.searchSuggestions(index, task.taskname, 0);
}, 1000);
if (this.smart_param.savepath) if (this.smart_param.savepath)
task.savepath = this.smart_param.savepath.replace('TASKNAME', task.taskname); task.savepath = this.smart_param.savepath.replace('TASKNAME', task.taskname);
}, },
@ -614,19 +765,20 @@
// 从分享中提取任务名 // 从分享中提取任务名
axios.get('/get_share_detail', { params: { shareurl: task.shareurl } }) axios.get('/get_share_detail', { params: { shareurl: task.shareurl } })
.then(response => { .then(response => {
if (response.data.error) { share_detail = response.data.data
if (response.data.error.includes("提取码")) { if (!response.data.success) {
const passcode = prompt("检查失败[" + response.data.error + "],请输入提取码:"); if (share_detail.error.includes("提取码")) {
const passcode = prompt("检查失败[" + share_detail.error + "],请输入提取码:");
if (passcode != null) { if (passcode != null) {
task.shareurl = task.shareurl.replace(/pan.quark.cn\/s\/(\w+)(\?pwd=\w*)*/, `pan.quark.cn/s/$1?pwd=${passcode}`); task.shareurl = task.shareurl.replace(/pan.quark.cn\/s\/(\w+)(\?pwd=\w*)*/, `pan.quark.cn/s/$1?pwd=${passcode}`);
this.changeShareurl(task); this.changeShareurl(task);
return; return;
} }
} }
this.$set(task, "shareurl_ban", response.data.error); this.$set(task, "shareurl_ban", share_detail.error);
} else { } else {
task.taskname = task.taskname == "" ? response.data.share.title : task.taskname; task.taskname = task.taskname == "" ? share_detail.share.title : task.taskname;
task.savepath = task.savepath.replace(/TASKNAME/g, response.data.share.title); task.savepath = task.savepath.replace(/TASKNAME/g, share_detail.share.title);
this.$set(task, "shareurl_ban", undefined); this.$set(task, "shareurl_ban", undefined);
} }
}) })
@ -637,83 +789,74 @@
clearData(target) { clearData(target) {
this[target] = ""; this[target] = "";
}, },
selectSavepath(index, fid, name) { async runScriptNow(task_index = null) {
const savepath = name == ".." ? this.getParentDirectory(this.formData.tasklist[index].savepath) : `/${this.formData.tasklist[index].savepath}/${name}`.replace(/\/{2,}/g, '/') body = {};
Vue.set(this.formData.tasklist[index], 'savepath', savepath); if (task_index != null) {
this.getSavepathDirs(fid); task = { ...this.formData.tasklist[task_index] };
}, delete task.runweek;
getSavepathDirs(fid = 0) { delete task.enddate;
if (fid.includes('/')) { body = {
params = { path: fid } "tasklist": [task]
} else { };
params = { fid: fid } } else if (this.configModified) {
} if (!confirm('配置已修改但未保存,是否继续运行?')) {
this.savepaths = [{ fid: 0, dir: true, file_name: "加载中..." }] return;
axios.get('/get_savepath', { params: params })
.then(response => {
this.savepaths = response.data
})
.catch(error => {
console.error('Error get_savepath:', error);
});
},
deleteFile(fid, fname, isDir) {
if (fid != "" && confirm(`确认删除${isDir ? '目录' : '文件'} [${fname}] 吗?`))
axios.post('/delete_file', { fid: fid })
.then(response => {
if (response.data.code == 0) {
this.savepaths = this.savepaths.filter(item => item.fid != fid);
} else {
alert('删除失败:' + response.data.message);
}
})
.catch(error => {
console.error('Error delete_file:', error);
});
},
selectStartFid(fid) {
Vue.set(this.formData.tasklist[this.forceTaskIndex], 'startfid', fid);
$('#shareDetailModal').modal('toggle')
},
showShareFiles(index) {
this.shareFiles = []
$('#shareDetailModal').modal('toggle')
this.modalLoading = true
axios.get('/get_share_detail', { params: { shareurl: this.formData.tasklist[index].shareurl } })
.then(response => {
this.forceTaskIndex = index
this.shareFiles = response.data.list;
this.modalLoading = false
})
.catch(error => {
console.error('Error get_share_detail:', error);
});
},
runScriptNow(task_index = "") {
$('#logModal').modal('toggle')
this.modalLoading = true
this.run_log = ''
const source = new EventSource(`/run_script_now?task_index=${task_index}`);
source.onmessage = (event) => {
if (event.data == "[DONE]") {
this.modalLoading = false
source.close();
// 运行后刷新数据
this.fetchData();
} else {
this.run_log += event.data + '\n';
// 在更新 run_log 后将滚动条滚动到底部
this.$nextTick(() => {
const modalBody = document.querySelector('.modal-body');
modalBody.scrollTop = modalBody.scrollHeight;
});
} }
}; }
source.onerror = (error) => { $('#logModal').modal('toggle');
this.modalLoading = false this.modalLoading = true;
this.run_log = '';
try {
// 1. 发送 POST 请求
const response = await fetch(`/run_script_now`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// 2. 处理 SSE 流
const reader = response.body.getReader();
const decoder = new TextDecoder();
let partialData = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('Stream complete.');
this.modalLoading = false;
// 运行后刷新数据
this.fetchData();
break;
}
partialData += decoder.decode(value);
const lines = partialData.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data:')) {
const eventData = line.substring(5).trim();
if (eventData === '[DONE]') {
this.modalLoading = false;
this.fetchData();
return;
}
this.run_log += eventData + '\n';
// 在更新 run_log 后将滚动条滚动到底部
this.$nextTick(() => {
const modalBody = document.querySelector('.modal-body');
modalBody.scrollTop = modalBody.scrollHeight;
});
} else {
console.warn('Unexpected line:', line);
}
}
partialData = '';
}
} catch (error) {
this.modalLoading = false;
console.error('Error:', error); console.error('Error:', error);
source.close(); }
};
}, },
getParentDirectory(path) { getParentDirectory(path) {
parentDir = path.substring(0, path.lastIndexOf('/')) parentDir = path.substring(0, path.lastIndexOf('/'))
@ -746,37 +889,188 @@
task.runweek = [1, 2, 3, 4, 5, 6, 7]; task.runweek = [1, 2, 3, 4, 5, 6, 7];
} }
}, },
searchSuggestions(index, taskname, limit_msec = 0) { searchSuggestions(index, taskname, deep = 1) {
if (taskname.length == 0) if (taskname.length < 2) {
return console.log(`任务名[${taskname}]过短${taskname.length} 不进行搜索`);
if (limit_msec > 0) { return;
const now = Date.now();
if (now - this.smart_param.lastSuggestionsTime < limit_msec)
return;
this.smart_param.lastSuggestionsTime = now;
} }
this.smart_param.isSearching = true this.smart_param.isSearching = true;
this.smart_param.index = index; this.smart_param.index = index;
axios.get('/task_suggestions', { try {
params: { axios.get('/task_suggestions', {
q: taskname, params: {
d: limit_msec == 0 ? 1 : 0 q: taskname,
d: deep
}
}).then(response => {
this.smart_param.taskSuggestions = response.data;
this.smart_param.showSuggestions = true;
}).catch(error => {
console.error('Error fetching suggestions:', error);
}).finally(() => {
this.smart_param.isSearching = false;
});
} catch (e) {
this.smart_param.taskSuggestions = {
error: "网络异常"
};
}
},
selectSuggestion(index, suggestion) {
this.smart_param.showSuggestions = false;
this.showShareSelect(index, suggestion.shareurl);
},
addMagicRegex() {
const newKey = `$MAGIC_${Object.keys(this.formData.magic_regex).length + 1}`;
this.$set(this.formData.magic_regex, newKey, { pattern: '', replace: '' });
},
updateMagicRegexKey(oldKey, newKey) {
if (oldKey !== newKey) {
if (this.formData.magic_regex[newKey]) {
alert(`魔法名 [${newKey}] 已存在,请使用其他名称`);
return;
} }
this.$set(this.formData.magic_regex, newKey, this.formData.magic_regex[oldKey]);
this.$delete(this.formData.magic_regex, oldKey);
}
},
removeMagicRegex(key) {
if (confirm(`确认删除魔法匹配规则 [${key}] 吗?`)) {
this.$delete(this.formData.magic_regex, key);
}
},
deleteFile(fid, fname, isDir) {
if (fid != "" && confirm(`确认删除${isDir ? '目录' : '文件'} [${fname}] 吗?`))
axios.post('/delete_file', {
fid: fid
}).then(response => {
if (response.data.code == 0) {
this.fileSelect.fileList = this.fileSelect.fileList.filter(item => item.fid != fid);
} else {
alert('删除失败:' + response.data.message);
}
}).catch(error => {
console.error('Error /delete_file:', error);
});
},
getSavepathDetail(params = 0) {
if (params.includes('/')) {
params = { path: params }
} else {
params = { fid: params }
}
this.modalLoading = true;
axios.get('/get_savepath_detail', {
params: params
}).then(response => { }).then(response => {
this.smart_param.taskSuggestions = response.data; this.fileSelect.fileList = response.data.data.list
this.smart_param.showSuggestions = true; if (response.data.data.paths.length > 0) {
this.fileSelect.paths = response.data.data.paths
}
this.modalLoading = false;
}).catch(error => { }).catch(error => {
console.error('Error fetching suggestions:', error); console.error('Error /get_savepath_detail:', error);
}).finally(() => { this.fileSelect.error = "获取文件夹列表失败";
this.smart_param.isSearching = false; this.modalLoading = false;
}); });
}, },
selectSuggestion(task, suggestion) { showSavepathSelect(index) {
task.taskname = suggestion.taskname; this.fileSelect.selectShare = false;
task.shareurl = suggestion.shareurl; this.fileSelect.selectDir = true;
this.changeShareurl(task); this.fileSelect.previewRegex = false;
this.smart_param.showSuggestions = false; this.fileSelect.error = undefined;
this.fileSelect.fileList = [];
this.fileSelect.paths = [];
this.fileSelect.index = index;
$('#fileSelectModal').modal('toggle');
this.getSavepathDetail(this.formData.tasklist[index].savepath);
}, },
getShareDetail() {
this.modalLoading = true;
axios.post('/get_share_detail', {
shareurl: this.fileSelect.shareurl,
stoken: this.fileSelect.stoken,
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 => {
if (response.data.success) {
this.fileSelect.fileList = response.data.data.list;
this.fileSelect.paths = response.data.data.paths;
this.fileSelect.stoken = response.data.data.stoken;
} else {
this.fileSelect.error = response.data.data.error
}
this.modalLoading = false;
}).catch(error => {
console.error('Error getting folders:', error);
this.fileSelect.error = "获取文件夹列表失败";
this.modalLoading = false;
});
},
showShareSelect(index, shareurl = null) {
this.fileSelect.selectShare = true;
this.fileSelect.fileList = [];
this.fileSelect.paths = [];
this.fileSelect.error = undefined;
if (this.getShareurl(this.fileSelect.shareurl) != this.getShareurl(this.formData.tasklist[index].shareurl)) {
this.fileSelect.stoken = "";
}
this.fileSelect.shareurl = shareurl || this.formData.tasklist[index].shareurl;
this.fileSelect.index = index;
$('#fileSelectModal').modal('toggle');
this.getShareDetail();
},
navigateTo(fid, name) {
path = { fid: fid, name: name }
if (this.fileSelect.selectShare) {
this.fileSelect.shareurl = this.getShareurl(this.fileSelect.shareurl, path);
this.getShareDetail();
} else {
if (fid == "0") {
this.fileSelect.paths = []
} else {
index = this.fileSelect.paths.findIndex(item => item.fid === fid);
if (index !== -1) {
this.fileSelect.paths = this.fileSelect.paths.slice(0, index + 1)
} else {
this.fileSelect.paths.push({ fid: fid, name: name })
}
}
this.getSavepathDetail(fid);
}
},
selectCurrentFolder(addTaskname = false) {
if (this.fileSelect.selectShare) {
this.formData.tasklist[this.fileSelect.index].shareurl_ban = undefined;
this.formData.tasklist[this.fileSelect.index].shareurl = this.fileSelect.shareurl;
} else {
this.formData.tasklist[this.fileSelect.index].savepath = "/" + this.fileSelect.paths.map(item => item.name).join("/");
if (addTaskname) {
this.formData.tasklist[this.fileSelect.index].savepath += "/" + this.formData.tasklist[this.fileSelect.index].taskname
}
}
$('#fileSelectModal').modal('hide')
},
selectStartFid(fid) {
Vue.set(this.formData.tasklist[this.fileSelect.index], 'startfid', fid);
$('#fileSelectModal').modal('hide')
},
getShareurl(shareurl, path = {}) {
if (path == {} || path.fid == 0) {
shareurl = shareurl.match(`.*s/[a-z0-9]+`)[0]
} else if (shareurl.includes(path.fid)) {
shareurl = shareurl.match(`.*/${path.fid}[^\/]*`)[0]
} else if (shareurl.includes('#/list/share')) {
shareurl = `${shareurl}/${path.fid}-${path.name}`
} else {
shareurl = `${shareurl}#/list/share/${path.fid}-${path.name}`
}
return shareurl;
}
} }
}); });
</script> </script>

View File

@ -15,6 +15,7 @@ import time
import random import random
import requests import requests
import importlib import importlib
import urllib.parse
from datetime import datetime from datetime import datetime
# 兼容青龙 # 兼容青龙
@ -33,8 +34,12 @@ GH_PROXY = os.environ.get("GH_PROXY", "https://ghproxy.net/")
MAGIC_REGEX = { MAGIC_REGEX = {
"$TV": { "$TV": {
"pattern": r".*?(?<!\d)([Ss]\d{1,2})?([Ee]?[Pp]?[Xx]?\d{1,3})(?!\d).*?\.(mp4|mkv)", "pattern": r".*?([Ss]\d{1,2})?(?:[第EePpXx\.\-\_\( ]{1,2}|^)(\d{1,3})(?!\d).*?\.(mp4|mkv)",
"replace": r"\1\2.\3", "replace": r"\1E\2.\3",
},
"$BLACK_WORD": {
"pattern": r"^(?!.*纯享)(?!.*加更)(?!.*超前企划)(?!.*训练室)(?!.*蒸蒸日上).*",
"replace": "",
}, },
} }
@ -74,6 +79,17 @@ class Config:
else: else:
return False return False
# 读取 JSON 文件内容
def read_json(config_path):
with open(config_path, "r", encoding="utf-8") as f:
data = json.load(f)
return data
# 将数据写入 JSON 文件
def write_json(config_path, data):
with open(config_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, sort_keys=False, indent=2)
# 读取CK # 读取CK
def get_cookies(cookie_val): def get_cookies(cookie_val):
if isinstance(cookie_val, list): if isinstance(cookie_val, list):
@ -321,6 +337,8 @@ class Quark:
"_sort": "file_type:asc,updated_at:desc", "_sort": "file_type:asc,updated_at:desc",
} }
response = self._send_request("GET", url, params=querystring).json() response = self._send_request("GET", url, params=querystring).json()
if response["code"] != 0:
return {"error": response["message"]}
if response["data"]["list"]: if response["data"]["list"]:
list_merge += response["data"]["list"] list_merge += response["data"]["list"]
page += 1 page += 1
@ -368,6 +386,8 @@ class Quark:
"_fetch_full_path": kwargs.get("fetch_full_path", 0), "_fetch_full_path": kwargs.get("fetch_full_path", 0),
} }
response = self._send_request("GET", url, params=querystring).json() response = self._send_request("GET", url, params=querystring).json()
if response["code"] != 0:
return {"error": response["message"]}
if response["data"]["list"]: if response["data"]["list"]:
file_list += response["data"]["list"] file_list += response["data"]["list"]
page += 1 page += 1
@ -501,8 +521,8 @@ class Quark:
# ↓ 操作函数 # ↓ 操作函数
# 魔法正则匹配 # 魔法正则匹配
def magic_regex_func(self, pattern, replace, taskname=None): def magic_regex_func(self, pattern, replace, taskname=None, magic_regex={}):
magic_regex = CONFIG_DATA.get("magic_regex") or MAGIC_REGEX or {} magic_regex = magic_regex or CONFIG_DATA.get("magic_regex") or MAGIC_REGEX
keyword = pattern keyword = pattern
if keyword in magic_regex: if keyword in magic_regex:
pattern = magic_regex[keyword]["pattern"] pattern = magic_regex[keyword]["pattern"]
@ -512,17 +532,34 @@ class Quark:
replace = replace.replace("$TASKNAME", taskname) replace = replace.replace("$TASKNAME", taskname)
return pattern, replace return pattern, replace
def get_id_from_url(self, url): # def get_id_from_url(self, url):
url = url.replace("https://pan.quark.cn/s/", "") # url = url.replace("https://pan.quark.cn/s/", "")
pattern = r"(\w+)(\?pwd=(\w+))?(#/list/share.*/(\w+))?" # pattern = r"(\w+)(\?pwd=(\w+))?(#/list/share.*/(\w+))?"
match = re.search(pattern, url) # match = re.search(pattern, url)
if match: # if match:
pwd_id = match.group(1) # pwd_id = match.group(1)
passcode = match.group(3) if match.group(3) else "" # passcode = match.group(3) if match.group(3) else ""
pdir_fid = match.group(5) if match.group(5) else 0 # pdir_fid = match.group(5) if match.group(5) else 0
return pwd_id, passcode, pdir_fid # return pwd_id, passcode, pdir_fid
else: # else:
return None # return None
def extract_url(self, url):
# pwd_id
match_id = re.search(r"/s/(\w+)", url)
pwd_id = match_id.group(1) if match_id else None
# passcode
match_pwd = re.search(r"pwd=(\w+)", url)
passcode = match_pwd.group(1) if match_pwd else ""
# path: fid-name
paths = []
matches = re.findall(r"/(\w{32})-?([^/]+)?", url)
for match in matches:
fid = match[0]
name = urllib.parse.unquote(match[1])
paths.append({"fid": fid, "name": name})
pdir_fid = paths[-1]["fid"] if matches else 0
return pwd_id, passcode, pdir_fid, paths
def update_savepath_fid(self, tasklist): def update_savepath_fid(self, tasklist):
dir_paths = [ dir_paths = [
@ -557,8 +594,8 @@ class Quark:
def do_save_check(self, shareurl, savepath): def do_save_check(self, shareurl, savepath):
try: try:
pwd_id, passcode, pdir_fid = self.get_id_from_url(shareurl) pwd_id, passcode, pdir_fid, _ = self.extract_url(shareurl)
is_sharing, stoken = self.get_stoken(pwd_id, passcode) _, stoken = self.get_stoken(pwd_id, passcode)
share_file_list = self.get_detail(pwd_id, stoken, pdir_fid)["list"] share_file_list = self.get_detail(pwd_id, stoken, pdir_fid)["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]
@ -595,8 +632,7 @@ class Quark:
else: else:
return False return False
except Exception as e: except Exception as e:
if os.environ.get("DEBUG") == True: print(f"转存测试失败: {str(e)}")
print(f"转存测试失败: {str(e)}")
def do_save_task(self, task): def do_save_task(self, task):
# 判断资源失效记录 # 判断资源失效记录
@ -605,8 +641,7 @@ class Quark:
return return
# 链接转换所需参数 # 链接转换所需参数
pwd_id, passcode, pdir_fid = self.get_id_from_url(task["shareurl"]) pwd_id, passcode, pdir_fid, _ = self.extract_url(task["shareurl"])
# print("match: ", pwd_id, pdir_fid)
# 获取stoken同时可验证资源是否失效 # 获取stoken同时可验证资源是否失效
is_sharing, stoken = self.get_stoken(pwd_id, passcode) is_sharing, stoken = self.get_stoken(pwd_id, passcode)
@ -673,7 +708,7 @@ class Quark:
pattern, replace = task["update_subdir"], "" pattern, replace = task["update_subdir"], ""
else: else:
pattern, replace = self.magic_regex_func( pattern, replace = self.magic_regex_func(
task["pattern"], task["replace"], task["taskname"] task.get("pattern", ""), task.get("replace", ""), task["taskname"]
) )
# 正则文件名匹配 # 正则文件名匹配
if re.search(pattern, share_file["file_name"]): if re.search(pattern, share_file["file_name"]):
@ -766,7 +801,7 @@ class Quark:
def do_rename_task(self, task, subdir_path=""): def do_rename_task(self, task, subdir_path=""):
pattern, replace = self.magic_regex_func( pattern, replace = self.magic_regex_func(
task["pattern"], task["replace"], task["taskname"] task.get("pattern", ""), task.get("replace", ""), task["taskname"]
) )
if not pattern or not replace: if not pattern or not replace:
return 0 return 0
@ -869,7 +904,7 @@ def do_save(account, tasklist=[]):
# 获取全部保存目录fid # 获取全部保存目录fid
account.update_savepath_fid(tasklist) account.update_savepath_fid(tasklist)
def check_date(task): def is_time(task):
return ( return (
not task.get("enddate") not task.get("enddate")
or ( or (
@ -877,29 +912,33 @@ def do_save(account, tasklist=[]):
<= datetime.strptime(task["enddate"], "%Y-%m-%d").date() <= datetime.strptime(task["enddate"], "%Y-%m-%d").date()
) )
) and ( ) and (
not task.get("runweek") "runweek" not in task
# 星期一为0星期日为6 # 星期一为0星期日为6
or (datetime.today().weekday() + 1 in task.get("runweek")) or (datetime.today().weekday() + 1 in task.get("runweek"))
) )
# 执行任务 # 执行任务
for index, task in enumerate(tasklist): for index, task in enumerate(tasklist):
# 判断任务期限 print()
if check_date(task): print(f"#{index+1}------------------")
print() print(f"任务名称: {task['taskname']}")
print(f"#{index+1}------------------") print(f"分享链接: {task['shareurl']}")
print(f"任务名称: {task['taskname']}") print(f"保存路径: {task['savepath']}")
print(f"分享链接: {task['shareurl']}") if task.get("pattern"):
print(f"保存路径: {task['savepath']}")
print(f"正则匹配: {task['pattern']}") print(f"正则匹配: {task['pattern']}")
if task.get("replace"):
print(f"正则替换: {task['replace']}") print(f"正则替换: {task['replace']}")
if task.get("enddate"): if task.get("update_subdir"):
print(f"任务截止: {task['enddate']}") print(f"更子目录: {task['update_subdir']}")
if task.get("ignore_extension"): if task.get("runweek") or task.get("enddate"):
print(f"忽略后缀: {task['ignore_extension']}") print(
if task.get("update_subdir"): f"运行周期: WK{task.get("runweek",[])} ~ {task.get('enddate','forever')}"
print(f"更子目录: {task['update_subdir']}") )
print() print()
# 判断任务周期
if not is_time(task):
print(f"任务不在运行周期内,跳过")
else:
is_new_tree = account.do_save_task(task) is_new_tree = account.do_save_task(task)
is_rename = account.do_rename_task(task) is_rename = account.do_rename_task(task)
@ -939,7 +978,13 @@ 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"
task_index = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else "" # 从环境变量中获取 TASKLIST
tasklist_from_env = []
if tasklist_json := os.environ.get("TASKLIST"):
try:
tasklist_from_env = json.loads(tasklist_json)
except Exception as e:
print(f"从环境变量解析任务列表失败 {e}")
# 检查本地文件是否存在,如果不存在就下载 # 检查本地文件是否存在,如果不存在就下载
if not os.path.exists(config_path): if not os.path.exists(config_path):
if os.environ.get("QUARK_COOKIE"): if os.environ.get("QUARK_COOKIE"):
@ -956,9 +1001,8 @@ def main():
return return
else: else:
print(f"⚙️ 正从 {config_path} 文件中读取配置") print(f"⚙️ 正从 {config_path} 文件中读取配置")
with open(config_path, "r", encoding="utf-8") as file: CONFIG_DATA = Config.read_json(config_path)
CONFIG_DATA = json.load(file) 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"): if not CONFIG_DATA.get("magic_regex"):
CONFIG_DATA["magic_regex"] = MAGIC_REGEX CONFIG_DATA["magic_regex"] = MAGIC_REGEX
@ -971,7 +1015,7 @@ def main():
accounts = [Quark(cookie, index) for index, cookie in enumerate(cookies)] accounts = [Quark(cookie, index) for index, cookie in enumerate(cookies)]
# 签到 # 签到
print(f"===============签到任务===============") print(f"===============签到任务===============")
if type(task_index) is int: if tasklist_from_env:
verify_account(accounts[0]) verify_account(accounts[0])
else: else:
for account in accounts: for account in accounts:
@ -982,11 +1026,10 @@ def main():
if accounts[0].is_active and cookie_form_file: if accounts[0].is_active and cookie_form_file:
print(f"===============转存任务===============") print(f"===============转存任务===============")
# 任务列表 # 任务列表
tasklist = CONFIG_DATA.get("tasklist", []) if tasklist_from_env:
if type(task_index) is int: do_save(accounts[0], tasklist_from_env)
do_save(accounts[0], [tasklist[task_index]])
else: else:
do_save(accounts[0], tasklist) do_save(accounts[0], CONFIG_DATA.get("tasklist", []))
print() print()
# 通知 # 通知
if NOTIFYS: if NOTIFYS:
@ -996,8 +1039,7 @@ def main():
print() print()
if cookie_form_file: if cookie_form_file:
# 更新配置 # 更新配置
with open(config_path, "w", encoding="utf-8") as file: Config.write_json(config_path, CONFIG_DATA)
json.dump(CONFIG_DATA, file, ensure_ascii=False, sort_keys=False, indent=2)
print(f"===============程序结束===============") print(f"===============程序结束===============")
duration = datetime.now() - start_time duration = datetime.now() - start_time

View File

@ -12,6 +12,16 @@
"token": "" "token": ""
} }
}, },
"magic_regex": {
"$TV": {
"pattern": ".*?([Ss]\\d{1,2})?(?:[第EePpXx\\.\\-\\_\\( ]{1,2}|^)(\\d{1,3})(?!\\d).*?\\.(mp4|mkv)",
"replace": "\\1E\\2.\\3"
},
"$BLACK_WORD": {
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*超前企划)(?!.*训练室)(?!.*蒸蒸日上).*",
"replace": ""
}
},
"tasklist": [ "tasklist": [
{ {
"taskname": "测试-魔法匹配剧集这是一组有效分享配置CK后可测试任务是否正常", "taskname": "测试-魔法匹配剧集这是一组有效分享配置CK后可测试任务是否正常",