mirror of
https://github.com/Cp0204/quark-auto-save.git
synced 2026-01-18 02:40:44 +08:00
Compare commits
36 Commits
0c917d12ad
...
a02ff884f5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a02ff884f5 | ||
|
|
668897d1df | ||
|
|
9866a9d93d | ||
|
|
9b9c5fe00a | ||
|
|
dc8362db08 | ||
|
|
bc2cd1504e | ||
|
|
b118231f58 | ||
|
|
c910a986b1 | ||
|
|
f8d10236e3 | ||
|
|
2f0c51283c | ||
|
|
8611824b9a | ||
|
|
2b16246beb | ||
|
|
62464fec17 | ||
|
|
92a86ce8e0 | ||
|
|
13f89b32c6 | ||
|
|
d9fc4659b8 | ||
|
|
8309f4a4d4 | ||
|
|
805f624b89 | ||
|
|
4b2d78a0b2 | ||
|
|
e1e0a6afc4 | ||
|
|
d5a802c218 | ||
|
|
70093a3f2c | ||
|
|
81d4098b6c | ||
|
|
6f976f242a | ||
|
|
50090db1f4 | ||
|
|
4225f1986b | ||
|
|
f398f3fa07 | ||
|
|
ffe95fcf66 | ||
|
|
83fd60f1a1 | ||
|
|
dda9ec0a01 | ||
|
|
b108d24981 | ||
|
|
90051b9aa0 | ||
|
|
dc3afeae1d | ||
|
|
c59ef3f0cf | ||
|
|
6649e14472 | ||
|
|
3394ae7400 |
@ -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] 目标目录不存在时自动新建
|
||||||
|
|||||||
269
app/run.py
269
app/run.py
@ -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
162
app/sdk/cloudsaver.py
Normal 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("&", "&").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)
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
190
app/static/js/qas.addtask.user.js
Normal file
190
app/static/js/qas.addtask.user.js
Normal 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 按钮
|
||||||
|
}
|
||||||
|
})(); // 立即执行初始化
|
||||||
|
})();
|
||||||
@ -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"> 忽略后缀
|
<input type="checkbox" v-model="task.ignore_extension"> 忽略后缀
|
||||||
</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">×</span>
|
<span aria-hidden="true">×</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 || '×'}}</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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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后可测试任务是否正常)",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user