在 Cp0204/quark-auto-save v0.5.3.1 的基础上增加过滤规则功能

This commit is contained in:
x1ao4 2025-04-21 02:18:39 +08:00
parent 6f969f9416
commit 997bba7047
3 changed files with 828 additions and 774 deletions

View File

@ -1,6 +1,7 @@
# !/usr/bin/env python3 # !/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from flask import ( from flask import (
json,
Flask, Flask,
url_for, url_for,
session, session,
@ -14,15 +15,16 @@ from flask import (
) )
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from sdk.cloudsaver import CloudSaver
from datetime import timedelta from datetime import timedelta
import subprocess import subprocess
import requests import requests
import hashlib import hashlib
import logging import logging
import base64 import base64
import json
import sys import sys
import os import os
import re
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, parent_dir) sys.path.insert(0, parent_dir)
@ -46,9 +48,10 @@ PYTHON_PATH = "python3" if os.path.exists("/usr/bin/python3") else "python"
SCRIPT_PATH = os.environ.get("SCRIPT_PATH", "./quark_auto_save.py") SCRIPT_PATH = os.environ.get("SCRIPT_PATH", "./quark_auto_save.py")
CONFIG_PATH = os.environ.get("CONFIG_PATH", "./config/quark_config.json") CONFIG_PATH = os.environ.get("CONFIG_PATH", "./config/quark_config.json")
PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "") PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "")
DEBUG = os.environ.get("DEBUG", False) DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
task_plugins_config = {} config_data = {}
task_plugins_config_default = {}
app = Flask(__name__) app = Flask(__name__)
app.config["APP_VERSION"] = get_app_ver() app.config["APP_VERSION"] = get_app_ver()
@ -77,24 +80,15 @@ def gen_md5(string):
return md5.hexdigest() return md5.hexdigest()
# 读取 JSON 文件内容 def get_login_token():
def read_json(): username = config_data["webui"]["username"]
with open(CONFIG_PATH, "r", encoding="utf-8") as f: password = config_data["webui"]["password"]
data = json.load(f) return gen_md5(f"token{username}{password}+-*/")[8:24]
return data
# 将数据写入 JSON 文件
def write_json(data):
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, sort_keys=False, indent=2)
def is_login(): def is_login():
data = read_json() login_token = get_login_token()
username = data["webui"]["username"] if session.get("token") == login_token or request.args.get("token") == login_token:
password = data["webui"]["password"]
if session.get("login") == gen_md5(username + password):
return True return True
else: else:
return False return False
@ -114,16 +108,15 @@ def favicon():
@app.route("/login", methods=["GET", "POST"]) @app.route("/login", methods=["GET", "POST"])
def login(): def login():
if request.method == "POST": if request.method == "POST":
data = read_json() username = config_data["webui"]["username"]
username = data["webui"]["username"] password = config_data["webui"]["password"]
password = data["webui"]["password"]
# 验证用户名和密码 # 验证用户名和密码
if (username == request.form.get("username")) and ( if (username == request.form.get("username")) and (
password == request.form.get("password") password == request.form.get("password")
): ):
logging.info(f">>> 用户 {username} 登录成功") logging.info(f">>> 用户 {username} 登录成功")
session["login"] = gen_md5(username + password)
session.permanent = True session.permanent = True
session["token"] = get_login_token()
return redirect(url_for("index")) return redirect(url_for("index"))
else: else:
logging.info(f">>> 用户 {username} 登录失败") logging.info(f">>> 用户 {username} 登录失败")
@ -137,7 +130,7 @@ def login():
# 退出登录 # 退出登录
@app.route("/logout") @app.route("/logout")
def logout(): def logout():
session.pop("login", None) session.pop("token", None)
return redirect(url_for("login")) return redirect(url_for("login"))
@ -155,47 +148,51 @@ def index():
@app.route("/data") @app.route("/data")
def get_data(): def get_data():
if not is_login(): if not is_login():
return redirect(url_for("login")) return jsonify({"success": False, "message": "未登录"})
data = read_json() data = Config.read_json(CONFIG_PATH)
del data["webui"] del data["webui"]
data["task_plugins_config"] = task_plugins_config data["api_token"] = get_login_token()
return jsonify(data) data["task_plugins_config_default"] = task_plugins_config_default
return jsonify({"success": True, "data": data})
# 更新数据 # 更新数据
@app.route("/update", methods=["POST"]) @app.route("/update", methods=["POST"])
def update(): def update():
global config_data
if not is_login(): if not is_login():
return "未登录" return jsonify({"success": False, "message": "未登录"})
data = request.json dont_save_keys = ["task_plugins_config_default", "api_token"]
data["webui"] = read_json()["webui"] for key, value in request.json.items():
if "task_plugins_config" in data: if key not in dont_save_keys:
del data["task_plugins_config"] config_data.update({key: value})
write_json(data) Config.write_json(CONFIG_PATH, config_data)
# 重新加载任务 # 重新加载任务
if reload_tasks(): if reload_tasks():
logging.info(f">>> 配置更新成功") logging.info(f">>> 配置更新成功")
return "配置更新成功" return jsonify({"success": True, "message": "配置更新成功"})
else: else:
logging.info(f">>> 配置更新失败") logging.info(f">>> 配置更新失败")
return "配置更新失败" return jsonify({"success": False, "message": "配置更新失败"})
# 处理运行脚本请求 # 处理运行脚本请求
@app.route("/run_script_now", methods=["GET"]) @app.route("/run_script_now", methods=["POST"])
def run_script_now(): def run_script_now():
if not is_login(): if not is_login():
return "未登录" return jsonify({"success": False, "message": "未登录"})
task_index = request.args.get("task_index", "") tasklist = request.json.get("tasklist", [])
command = [PYTHON_PATH, "-u", SCRIPT_PATH, CONFIG_PATH, task_index] command = [PYTHON_PATH, "-u", SCRIPT_PATH, CONFIG_PATH]
logging.info( logging.info(
f">>> 手动运行任务{int(task_index)+1 if task_index.isdigit() else 'all'}" f">>> 手动运行任务 [{tasklist[0].get('taskname') if len(tasklist)>0 else 'ALL'}] 开始执行..."
) )
def generate_output(): def generate_output():
# 设置环境变量 # 设置环境变量
process_env = os.environ.copy() process_env = os.environ.copy()
process_env["PYTHONIOENCODING"] = "utf-8" process_env["PYTHONIOENCODING"] = "utf-8"
if tasklist:
process_env["TASKLIST"] = json.dumps(tasklist, ensure_ascii=False)
process = subprocess.Popen( process = subprocess.Popen(
command, command,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
@ -224,64 +221,170 @@ 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:
cs_data = config_data.get("source", {}).get("cloudsaver", {})
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) response = requests.get(url)
return jsonify(response.json()) 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)
if not stoken:
is_sharing, stoken = account.get_stoken(pwd_id, passcode) is_sharing, stoken = account.get_stoken(pwd_id, passcode)
if not is_sharing: if not is_sharing:
return jsonify({"error": stoken}) return jsonify({"success": False, "data": {"error": stoken}})
share_detail = account.get_detail(pwd_id, stoken, pdir_fid, 1) share_detail = account.get_detail(pwd_id, stoken, pdir_fid, _fetch_share=1)
return jsonify(share_detail) 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", {}),
)
# 应用过滤词过滤
filterwords = request.json.get("regex", {}).get("filterwords", "")
if filterwords:
# 同时支持中英文逗号分隔
filterwords = filterwords.replace("", ",")
filterwords_list = [word.strip() for word in filterwords.split(',')]
for item in share_detail["list"]:
# 被过滤的文件不会有file_name_re与不匹配正则的文件显示一致
if any(word in item['file_name'] for word in filterwords_list):
item["filtered"] = True
# 应用正则命名
for item in share_detail["list"]:
# 只对未被过滤的文件应用正则命名
if not item.get("filtered") and re.search(pattern, item["file_name"]):
file_name = 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: else:
fid = request.args.get("fid", 0) return jsonify({"success": False, "data": {"error": "获取fid失败"}})
file_list = account.ls_dir(fid) else:
return jsonify(file_list) fid = request.args.get("fid", "0")
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 +393,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 +421,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,43 +429,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)
# 更新配置
def filter_files(files, filterwords): Config.write_json(CONFIG_PATH, config_data)
if not filterwords:
return files
filterwords_list = [word.strip() for word in filterwords.split(',')]
return [file for file in files if not any(word in file['file_name'] for word in filterwords_list)]
@app.route("/get_filtered_files")
def get_filtered_files():
if not is_login():
return jsonify({"error": "未登录"})
data = read_json()
filterwords = request.args.get("filterwords", "")
account = Quark(data["cookie"][0], 0)
fid = request.args.get("fid", 0)
files = account.ls_dir(fid)
filtered_files = filter_files(files, filterwords)
return jsonify(filtered_files)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -58,15 +58,15 @@
</nav> </nav>
<main class="col-md-9 col-lg-10 ml-sm-auto"> <main class="col-md-9 col-lg-10 ml-sm-auto">
<form @submit.prevent="saveConfig"> <form @submit.prevent="saveConfig" @keydown.enter.prevent>
<div v-if="activeTab === 'config'"> <div v-if="activeTab === 'config'">
<div class="row title"> <div class="row title">
<div class="col"> <div class="col-10">
<h2><i class="bi bi-cookie"></i> Cookie</h2> <h2><i class="bi bi-cookie"></i> Cookie</h2>
</div> </div>
<div class="col text-right"> <div class="col-2 text-right">
<button type="button" class="btn btn-outline-primary mb-3" @click="addCookie()">+</button> <button type="button" class="btn btn-outline-primary" @click="addCookie()">+</button>
</div> </div>
</div> </div>
<p>1. 所有账号执行签到,纯签到只需移动端参数即可!</p> <p>1. 所有账号执行签到,纯签到只需移动端参数即可!</p>
@ -86,22 +86,22 @@
</span> </span>
</div> </div>
</div> </div>
<div class="input-group mt-2 mb-2"> <div class="input-group mb-2">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text">Crontab</span> <span class="input-group-text">Crontab</span>
</div> </div>
<input type="text" v-model="formData.crontab" class="form-control" placeholder="必填"> <input type="text" v-model="formData.crontab" class="form-control" placeholder="必填">
</div> </div>
<div class="row title"> <div class="row title" title="通知推送支持多个渠道见Wiki">
<div class="col"> <div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-bell"></i> 通知</h2> <h2 style="display: inline-block;"><i class="bi bi-bell"></i> 通知</h2>
<span class="badge badge-pill badge-light"> <span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/通知推送服务配置" target="_blank" title="通知推送服务配置">?</a> <a href="https://github.com/Cp0204/quark-auto-save/wiki/通知推送服务配置" target="_blank">?</a>
</span> </span>
</div> </div>
<div class="col text-right"> <div class="col-2 text-right">
<button type="button" class="btn btn-outline-primary mb-3" @click="addPush()">+</button> <button type="button" class="btn btn-outline-primary" @click="addPush()">+</button>
</div> </div>
</div> </div>
<div v-for="(value, key) in formData.push_config" :key="key" class="input-group mb-2"> <div v-for="(value, key) in formData.push_config" :key="key" class="input-group mb-2">
@ -117,11 +117,11 @@
</div> </div>
</div> </div>
<div class="row title" v-if="Object.keys(getAvailablePlugins(formData.plugins)).length"> <div class="row title" v-if="Object.keys(getAvailablePlugins(formData.plugins)).length" title="各插件的配置选项具体键值由插件定义见Wiki">
<div class="col"> <div class="col">
<h2 style="display: inline-block;"><i class="bi bi-plug"></i> 插件</h2> <h2 style="display: inline-block;"><i class="bi bi-plug"></i> 插件</h2>
<span class="badge badge-pill badge-light"> <span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/插件配置" target="_blank" title="插件配置">?</a> <a href="https://github.com/Cp0204/quark-auto-save/wiki/插件配置" target="_blank">?</a>
</span> </span>
</div> </div>
</div> </div>
@ -142,6 +142,78 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row title" title="预定义的正则匹配规则,在任务列表中可直接点击使用">
<div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-magic"></i> 魔法匹配</h2>
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程#21-魔法匹配" target="_blank">?</a>
</span>
</div>
<div class="col-2 text-right">
<button type="button" class="btn btn-outline-primary" @click="addMagicRegex()">+</button>
</div>
</div>
<div v-for="(value, key) in formData.magic_regex" :key="key" class="form-group mb-2">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">魔法名</span>
</div>
<input type="text" :data-oldkey="key" v-model="key" class="form-control" @change="updateMagicRegexKey($event.target.dataset.oldkey, $event.target.value)" placeholder="自定义名称">
<div class="input-group-prepend">
<span class="input-group-text">正则命名</span>
</div>
<input type="text" v-model="value.pattern" class="form-control" placeholder="匹配表达式">
<input type="text" v-model="value.replace" class="form-control" placeholder="替换表达式">
<div class="input-group-append">
<button type="button" class="btn btn-outline-danger" @click="removeMagicRegex(key)">-</button>
</div>
</div>
</div>
<div class="row title" title="API接口用以第三方添加任务等操作见Wiki">
<div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-link-45deg"></i> API</h2>
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/API接口" target="_blank">?</a>
</span>
</div>
</div>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">Token</span>
</div>
<input type="text" v-model="formData.api_token" class="form-control" style="background-color:white;" disabled>
</div>
<div class="row title" title="资源搜索服务配置,用于任务名称智能搜索">
<div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-search"></i> CloudSaver</h2>
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/CloudSaver搜索源" target="_blank">?</a>
</span>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">服务器</label>
<div class="col-sm-10">
<input type="text" v-model="formData.source.cloudsaver.server" class="form-control" placeholder="资源搜索服务器地址,如 http://172.17.0.1:8008">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">用户名</label>
<div class="col-sm-10">
<input type="text" v-model="formData.source.cloudsaver.username" class="form-control" placeholder="用户名">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">密码</label>
<div class="col-sm-10">
<input type="password" v-model="formData.source.cloudsaver.password" class="form-control" placeholder="密码">
</div>
</div>
</div> </div>
<div v-if="activeTab === 'tasklist'"> <div v-if="activeTab === 'tasklist'">
@ -199,9 +271,9 @@
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<input type="text" name="taskname[]" class="form-control" v-model="task.taskname" placeholder="必填" @focus="smart_param.showSuggestions=true;focusTaskname(index, task)" @input="changeTaskname(index, task)"> <input type="text" name="taskname[]" class="form-control" v-model="task.taskname" placeholder="必填" @focus="smart_param.showSuggestions=true;focusTaskname(index, task)" @input="changeTaskname(index, task)">
<div class="dropdown-menu show task-suggestions" v-if="smart_param.showSuggestions && smart_param.taskSuggestions.length && smart_param.index === index"> <div class="dropdown-menu show task-suggestions" v-if="smart_param.showSuggestions && smart_param.taskSuggestions.success && smart_param.index === index">
<div class="text-muted text-center" style="font-size: small;">以下资源来自第三方,网络公开搜集,请自行辨识,如有侵权请联系夸克官方</div> <div class="dropdown-item text-muted text-center" style="font-size:12px;">{{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data.length ? `以下资源来自 ${smart_param.taskSuggestions.source} 搜索,请自行辨识,如有侵权请联系资源方` : "未搜索到资源" }}</div>
<div v-for="suggestion in smart_param.taskSuggestions" :key="suggestion.taskname" class="dropdown-item" @click.prevent="selectSuggestion(task, suggestion)" style="cursor: pointer;"> <div v-for="suggestion in smart_param.taskSuggestions.data" :key="suggestion.taskname" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(index, suggestion)" style="font-size: 12px;" :title="suggestion.content">
<span v-html="suggestion.verify ? '✅': '❔'"></span> {{ suggestion.taskname }} <span v-html="suggestion.verify ? '✅': '❔'"></span> {{ suggestion.taskname }}
<small class="text-muted"> <small class="text-muted">
<a :href="suggestion.shareurl" target="_blank" @click.stop>{{ suggestion.shareurl }}</a> <a :href="suggestion.shareurl" target="_blank" @click.stop>{{ suggestion.shareurl }}</a>
@ -220,12 +292,13 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row" title="支持子目录链接Web端打开分享点入目录复制浏览器的URL即可支持带提取码链接说明见Wiki">
<label class="col-sm-2 col-form-label">分享链接</label> <label class="col-sm-2 col-form-label">分享链接</label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<input type="text" name="shareurl[]" class="form-control" v-model="task.shareurl" placeholder="必填" @blur="changeShareurl(task)"> <input type="text" name="shareurl[]" class="form-control" v-model="task.shareurl" placeholder="必填" @blur="changeShareurl(task)">
<div class="input-group-append" v-if="task.shareurl"> <div class="input-group-append" v-if="task.shareurl">
<button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;fileSelect.previewRegex=false;showShareSelect(index)" title="选择文件夹"><i class="bi bi-folder"></i></button>
<div class="input-group-text"> <div class="input-group-text">
<a target="_blank" :href="task.shareurl"><i class="bi bi-box-arrow-up-right"></i></a> <a target="_blank" :href="task.shareurl"><i class="bi bi-box-arrow-up-right"></i></a>
</div> </div>
@ -239,33 +312,24 @@
<div class="input-group"> <div class="input-group">
<input type="text" name="savepath[]" class="form-control" v-model="task.savepath" placeholder="必填" @focus="focusTaskname(index, task)"> <input type="text" name="savepath[]" class="form-control" v-model="task.savepath" placeholder="必填" @focus="focusTaskname(index, task)">
<div class="input-group-append"> <div class="input-group-append">
<button class="btn btn-secondary" type="button" v-if="smart_param.savepath && smart_param.index == index && task.savepath != smart_param.origin_savepath" @click="task.savepath = smart_param.origin_savepath"><i class=" <button class="btn btn-secondary" type="button" v-if="smart_param.savepath && smart_param.index == index && task.savepath != smart_param.origin_savepath" @click="task.savepath = smart_param.origin_savepath"><i class="bi bi-reply"></i></button>
bi bi-reply"></i></button> <button class="btn btn-outline-secondary" type="button" @click="showSavepathSelect(index)">选择</button>
<button class="btn btn-outline-secondary dropdown-toggle" type="button" @click="getSavepathDirs(task.savepath)" data-toggle="dropdown" aria-expanded="false">选择</button>
<div class="dropdown-menu" style="max-height: 300px; min-width: 200px; overflow-y: auto;">
<a class="dropdown-item" @click.stop.prevent="selectSavepath(index,getParentDirectory(task.savepath),'..')" href="#">..</a>
<span v-if="!savepaths.some(item => item.dir)" class="dropdown-item disabled">无子目录</span>
<a v-for="(item, key) in savepaths" :class="{'disabled': item.fid === 0 || !item.dir}" class="dropdown-item" @click.stop.prevent="selectSavepath(index,item.fid,item.file_name)" href="#">
<i class="bi" :class="item.dir ? 'bi-folder2' : 'bi-file-earmark'"></i> {{ item.file_name }}
<i class="bi bi-trash3-fill text-danger" @click.stop.prevent="deleteFile(item.fid,item.file_name,item.dir)" style="position: absolute; right: 10px; pointer-events: auto;"></i>
</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="form-group row" title="可用作筛选,只转存匹配到的文件名的文件,留空则转存所有文件">
<div class="form-group row"> <label class="col-sm-2 col-form-label">保存规则</label>
<label class="col-sm-2 col-form-label">命名方式</label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text">{{ task.use_sequence_naming ? '顺序命名' : '正则命名' }}</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="匹配表达式 (E{} 或 S01E{} 表示顺序命名模式)" list="magicRegex" @input="detectNamingMode(task)"> <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="detectNamingMode(task)"> <input type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式">
<div class="input-group-append"> <div class="input-group-append" title="保存时只比较文件名的部分01.mp4 和 01.mkv 视同为同一文件,不重复转存">
<div class="input-group-text"> <div class="input-group-text">
<input type="checkbox" title="忽略后缀" v-model="task.ignore_extension">&nbsp;忽略后缀 <input type="checkbox" v-model="task.ignore_extension">&nbsp;忽略后缀
</div> </div>
</div> </div>
</div> </div>
@ -275,30 +339,30 @@
</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" name="filterwords[]" class="form-control" v-model="task.filterwords" placeholder="可选,输入过滤词汇,用逗号分隔,例如:纯享,加更,超前企划,名称包含过滤词汇的项目不会被转存"> <input type="text" name="filterwords[]" class="form-control" v-model="task.filterwords" placeholder="可选,输入过滤词汇,用逗号分隔,如:纯享,加更,超前企划,名称包含过滤词汇的项目不会被转存">
</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">
<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">
@ -307,7 +371,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="也可用作任务总开关">
@ -320,7 +384,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>
@ -364,44 +428,72 @@
</div> </div>
</div> </div>
<!-- 模态框 分享文件列表 --> <!-- 模态框 文件选择 -->
<div class="modal" tabindex="-1" id="shareDetailModal"> <div class="modal" tabindex="-1" id="fileSelectModal">
<div class="modal-dialog modal-xl"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><b>分享文件列表</b> <h5 class="modal-title">
<b v-if="fileSelect.previewRegex">正则命名预览</b>
<b v-else-if="fileSelect.selectDir">选择{{fileSelect.selectShare ? '需转存的' : '保存到的'}}文件夹</b>
<b v-else>选择起始文件</b>
<div v-if="modalLoading" class="spinner-border spinner-border-sm m-1" role="status"></div> <div v-if="modalLoading" class="spinner-border spinner-border-sm m-1" role="status"></div>
</h5> </h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body small">
<div class="alert alert-warning" v-if="shareFiles.error" v-html="shareFiles.error"></div> <div class="alert alert-warning" v-if="fileSelect.error" v-html="fileSelect.error"></div>
<table class="table table-hover table-sm" v-else-if="!modalLoading" title="请选择转存起始文件,将只转存修改日期>选中文件的文件"> <div v-else>
<!-- 面包屑导航 -->
<nav aria-label="breadcrumb" v-if="fileSelect.selectDir">
<ol class="breadcrumb">
<li class="breadcrumb-item cursor-pointer" @click="navigateTo('0','/')"><i class="bi bi-house-door"></i></li>
<li v-for="(item, index) in fileSelect.paths" class="breadcrumb-item">
<a v-if="index != fileSelect.paths.length - 1" href="#" @click="navigateTo(item.fid, item.name)">{{ item.name }}</a>
<span v-else class="text-muted">{{ item.name }}</span>
</li>
</ol>
</nav>
<!-- 文件列表 -->
<div class="mb-3" v-if="fileSelect.previewRegex">
<div><b>匹配表达式:</b><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span></div>
<div><b>替换表达式:</b><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].replace"></span></div>
</div>
<table class="table table-hover table-sm">
<thead> <thead>
<tr> <tr>
<!-- <th scope="col">fid</th> -->
<th scope="col">文件名</th> <th scope="col">文件名</th>
<th scope="col" v-if="fileSelect.selectShare">正则命名</th>
<template v-if="!fileSelect.previewRegex">
<th scope="col">大小</th> <th scope="col">大小</th>
<th scope="col">修改日期 ↓</th> <th scope="col">修改日期 ↓</th>
<th scope="col" v-if="!fileSelect.selectShare">操作</th>
</template>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <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 colspan="4"><i class="bi bi-folder-plus"></i> 后续更新的文件...</td> <td><i class="bi" :class="file.dir ? 'bi-folder-fill text-warning' : 'bi-file-earmark'"></i> {{file.file_name}}</td>
</tr> <td v-if="fileSelect.selectShare" :class="file.file_name_re ? 'text-success' : 'text-danger'">{{file.file_name_re || '&times;'}}</td>
<tr v-for="(file, key) in shareFiles" :key="key" @click="selectStartFid(file.fid)" style="cursor: pointer;"> <template v-if="!fileSelect.previewRegex">
<!-- <td>{{file.fid}}</td> --> <td v-if="file.dir">{{ file.include_items }}项</td>
<td><i class="bi" :class="file.dir ? 'bi-folder2' : 'bi-file-earmark'"></i> {{file.file_name}}</td> <td v-else>{{file.size | size}}</td>
<td>{{file.size | size}}</td> <td>{{file.updated_at | ts2date}}</td>
<td>{{file.last_update_at | ts2date}}</td> <td v-if="!fileSelect.selectShare"><a @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)">删除</a></td>
</td> </template>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</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>
</div> </div>
@ -420,7 +512,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: "",
@ -432,28 +532,34 @@
addition: {}, addition: {},
ignore_extension: false, ignore_extension: false,
filterwords: "", filterwords: "",
sequence_naming: "",
use_sequence_naming: false,
runweek: [1, 2, 3, 4, 5, 6, 7] runweek: [1, 2, 3, 4, 5, 6, 7]
}, },
run_log: "", run_log: "",
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) {
@ -461,14 +567,22 @@
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: {
formData: {
handler(newVal, oldVal) {
this.configModified = true;
},
deep: true
}
},
mounted() { mounted() {
this.fetchData(); this.fetchData();
this.checkNewVersion(); this.checkNewVersion();
@ -479,24 +593,10 @@
this.smart_param.showSuggestions = false; this.smart_param.showSuggestions = false;
} }
}); });
window.addEventListener('beforeunload', this.handleBeforeUnload);
// 初始化时检查所有任务的命名模式 },
setTimeout(() => { beforeDestroy() {
if (this.formData.tasklist && this.formData.tasklist.length > 0) { window.removeEventListener('beforeunload', this.handleBeforeUnload);
this.formData.tasklist.forEach(task => {
// 检查现有的顺序命名设置
if (task.use_sequence_naming && task.sequence_naming) {
// 已经设置过顺序命名的,将顺序命名模式转换为匹配表达式
if (!task.pattern || task._pattern_backup) {
task.pattern = task.sequence_naming;
}
} else {
// 检测是否包含顺序命名模式
this.detectNamingMode(task);
}
});
}
}, 500);
}, },
methods: { methods: {
changeTab(tab) { changeTab(tab) {
@ -522,24 +622,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);
@ -556,21 +672,23 @@
} }
} }
}, },
handleBeforeUnload(e) {
if (this.configModified) {
e.preventDefault();
e.returnValue = '配置已修改但未保存,确定要离开吗?';
return e.returnValue;
}
},
saveConfig() { saveConfig() {
// 保存前处理每个任务的命名模式
if (this.formData.tasklist && this.formData.tasklist.length > 0) {
this.formData.tasklist.forEach(task => {
// 如果是顺序命名模式确保sequence_naming字段已正确设置
if (task.use_sequence_naming && task.pattern && task.pattern.includes('E{}')) {
task.sequence_naming = task.pattern;
}
});
}
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);
@ -607,18 +725,7 @@
} }
} }
} }
// 初始化新任务的命名模式相关字段
if (newTask.taskname) {
// 默认使用正则命名模式
newTask.pattern = ".*"; // 默认匹配所有文件
newTask.replace = ""; // 默认保持原文件名
newTask.use_sequence_naming = false;
newTask.sequence_naming = "";
}
this.formData.tasklist.push(newTask); this.formData.tasklist.push(newTask);
// 滚到最下 // 滚到最下
setTimeout(() => { setTimeout(() => {
$('#collapse_' + (this.formData.tasklist.length - 1)).collapse('show').on('shown.bs.collapse', () => { $('#collapse_' + (this.formData.tasklist.length - 1)).collapse('show').on('shown.bs.collapse', () => {
@ -639,7 +746,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);
}, },
@ -664,19 +776,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);
} }
}) })
@ -687,83 +800,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('配置已修改但未保存,是否继续运行?')) {
return;
} }
this.savepaths = [{ fid: 0, dir: true, file_name: "加载中..." }]
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);
} }
}) $('#logModal').modal('toggle');
.catch(error => { this.modalLoading = true;
console.error('Error delete_file:', error); 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) {
selectStartFid(fid) { throw new Error(`HTTP error! Status: ${response.status}`);
Vue.set(this.formData.tasklist[this.forceTaskIndex], 'startfid', fid); }
$('#shareDetailModal').modal('toggle') // 2. 处理 SSE 流
}, const reader = response.body.getReader();
showShareFiles(index) { const decoder = new TextDecoder();
this.shareFiles = [] let partialData = '';
$('#shareDetailModal').modal('toggle') while (true) {
this.modalLoading = true const { done, value } = await reader.read();
axios.get('/get_share_detail', { params: { shareurl: this.formData.tasklist[index].shareurl } }) if (done) {
.then(response => { console.log('Stream complete.');
this.forceTaskIndex = index this.modalLoading = false;
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(); this.fetchData();
} else { break;
this.run_log += event.data + '\n'; }
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 后将滚动条滚动到底部 // 在更新 run_log 后将滚动条滚动到底部
this.$nextTick(() => { this.$nextTick(() => {
const modalBody = document.querySelector('.modal-body'); const modalBody = document.querySelector('.modal-body');
modalBody.scrollTop = modalBody.scrollHeight; modalBody.scrollTop = modalBody.scrollHeight;
}); });
} else {
console.warn('Unexpected line:', line);
} }
}; }
source.onerror = (error) => { partialData = '';
this.modalLoading = false }
} 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('/'))
@ -796,21 +900,18 @@
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) {
const now = Date.now();
if (now - this.smart_param.lastSuggestionsTime < limit_msec)
return; 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;
try {
axios.get('/task_suggestions', { axios.get('/task_suggestions', {
params: { params: {
q: taskname, q: taskname,
d: limit_msec == 0 ? 1 : 0 d: deep
} }
}).then(response => { }).then(response => {
this.smart_param.taskSuggestions = response.data; this.smart_param.taskSuggestions = response.data;
@ -820,81 +921,168 @@
}).finally(() => { }).finally(() => {
this.smart_param.isSearching = false; this.smart_param.isSearching = false;
}); });
} catch (e) {
this.smart_param.taskSuggestions = {
error: "网络异常"
};
}
}, },
selectSuggestion(task, suggestion) { selectSuggestion(index, suggestion) {
task.taskname = suggestion.taskname;
task.shareurl = suggestion.shareurl;
this.changeShareurl(task);
this.smart_param.showSuggestions = false; this.smart_param.showSuggestions = false;
this.showShareSelect(index, suggestion.shareurl);
}, },
detectNamingMode(task) { addMagicRegex() {
// 检测是否为顺序命名模式 const newKey = `$MAGIC_${Object.keys(this.formData.magic_regex).length + 1}`;
const sequencePatterns = ['E{}', 'EP{}', 'S\\d+E{}', '第{}集', '第{}话', '第{}期']; this.$set(this.formData.magic_regex, newKey, { pattern: '', replace: '' });
let isSequenceNaming = false; },
updateMagicRegexKey(oldKey, newKey) {
// 保存当前值以支持撤销操作 if (oldKey !== newKey) {
const currentValue = task.pattern; if (this.formData.magic_regex[newKey]) {
alert(`魔法名 [${newKey}] 已存在,请使用其他名称`);
if (task.pattern) { return;
// 检查是否包含任何顺序命名模式
isSequenceNaming = sequencePatterns.some(pattern => {
const regexPattern = pattern.replace('{}', '\\{\\}');
return new RegExp(regexPattern).test(task.pattern);
});
// 或者用户直接输入包含{}的格式,且替换表达式为空
if (!isSequenceNaming && task.pattern.includes('{}') && (!task.replace || task.replace === '')) {
isSequenceNaming = true;
} }
this.$set(this.formData.magic_regex, newKey, this.formData.magic_regex[oldKey]);
this.$delete(this.formData.magic_regex, oldKey);
} }
},
// 处理模式切换 removeMagicRegex(key) {
if (isSequenceNaming) { if (confirm(`确认删除魔法匹配规则 [${key}] 吗?`)) {
// 如果当前不是顺序命名模式,则保存现有的正则表达式 this.$delete(this.formData.magic_regex, key);
if (!task.use_sequence_naming) {
task._pattern_backup = task.pattern;
task._replace_backup = task.replace;
task.use_sequence_naming = true;
} }
// 设置序列命名模式 },
task.sequence_naming = task.pattern; 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 { } else {
// 如果当前是顺序命名模式,但现在检测不到顺序命名模式 alert('删除失败:' + response.data.message);
if (task.use_sequence_naming) {
// 如果用户正在删除内容(当前值为空或比上一次更短)
if (!currentValue || (task._lastPatternValue && currentValue.length < task._lastPatternValue.length)) {
// 保持当前编辑状态,不切换模式
task.sequence_naming = currentValue;
// 只有当完全删除后才切换回正则模式
if (!currentValue) {
task.use_sequence_naming = false;
if (task._pattern_backup) {
task.pattern = "";
task.replace = task._replace_backup || "";
} }
task.sequence_naming = null; }).catch(error => {
} console.error('Error /delete_file:', error);
} else if (task._pattern_backup && !task.pattern.includes('{}')) { });
// 正常切换回正则命名模式(非删除操作)
task.use_sequence_naming = false;
task.pattern = task._pattern_backup;
task.replace = task._replace_backup;
task._sequence_backup = task.sequence_naming;
task.sequence_naming = null;
} else if (!task._pattern_backup && !task.pattern.includes('{}')) {
// 没有备份,但需要切换回正则模式
task.use_sequence_naming = false;
task.sequence_naming = null;
}
}
}
// 保存当前值,用于下次比较
task._lastPatternValue = currentValue;
// 强制Vue更新视图
this.$forceUpdate();
}, },
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 => {
this.fileSelect.fileList = response.data.data.list
if (response.data.data.paths.length > 0) {
this.fileSelect.paths = response.data.data.paths
}
this.modalLoading = false;
}).catch(error => {
console.error('Error /get_savepath_detail:', error);
this.fileSelect.error = "获取文件夹列表失败";
this.modalLoading = false;
});
},
showSavepathSelect(index) {
this.fileSelect.selectShare = false;
this.fileSelect.selectDir = true;
this.fileSelect.previewRegex = 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,
filterwords: this.formData.tasklist[this.fileSelect.index].filterwords,
magic_regex: this.formData.magic_regex,
}
}).then(response => {
if (response.data.success) {
this.fileSelect.fileList = response.data.data.list;
this.fileSelect.paths = response.data.data.paths;
this.fileSelect.stoken = response.data.data.stoken;
} else {
this.fileSelect.error = response.data.data.error
}
this.modalLoading = false;
}).catch(error => {
console.error('Error getting folders:', error);
this.fileSelect.error = "获取文件夹列表失败";
this.modalLoading = false;
});
},
showShareSelect(index, shareurl = null) {
this.fileSelect.selectShare = true;
this.fileSelect.fileList = [];
this.fileSelect.paths = [];
this.fileSelect.error = undefined;
if (this.getShareurl(this.fileSelect.shareurl) != this.getShareurl(this.formData.tasklist[index].shareurl)) {
this.fileSelect.stoken = "";
}
this.fileSelect.shareurl = shareurl || this.formData.tasklist[index].shareurl;
this.fileSelect.index = index;
$('#fileSelectModal').modal('toggle');
this.getShareDetail();
},
navigateTo(fid, name) {
path = { fid: fid, name: name }
if (this.fileSelect.selectShare) {
this.fileSelect.shareurl = this.getShareurl(this.fileSelect.shareurl, path);
this.getShareDetail();
} else {
if (fid == "0") {
this.fileSelect.paths = []
} else {
index = this.fileSelect.paths.findIndex(item => item.fid === fid);
if (index !== -1) {
this.fileSelect.paths = this.fileSelect.paths.slice(0, index + 1)
} else {
this.fileSelect.paths.push({ fid: fid, name: name })
}
}
this.getSavepathDetail(fid);
}
},
selectCurrentFolder(addTaskname = false) {
if (this.fileSelect.selectShare) {
this.formData.tasklist[this.fileSelect.index].shareurl_ban = undefined;
this.formData.tasklist[this.fileSelect.index].shareurl = this.fileSelect.shareurl;
} else {
this.formData.tasklist[this.fileSelect.index].savepath = "/" + this.fileSelect.paths.map(item => item.name).join("/");
if (addTaskname) {
this.formData.tasklist[this.fileSelect.index].savepath += "/" + this.formData.tasklist[this.fileSelect.index].taskname
}
}
$('#fileSelectModal').modal('hide')
},
selectStartFid(fid) {
Vue.set(this.formData.tasklist[this.fileSelect.index], 'startfid', fid);
$('#fileSelectModal').modal('hide')
},
getShareurl(shareurl, path = {}) {
if (path == {} || path.fid == 0) {
shareurl = shareurl.match(`.*s/[a-z0-9]+`)[0]
} else if (shareurl.includes(path.fid)) {
shareurl = shareurl.match(`.*/${path.fid}[^\/]*`)[0]
} else if (shareurl.includes('#/list/share')) {
shareurl = `${shareurl}/${path.fid}-${path.name}`
} else {
shareurl = `${shareurl}#/list/share/${path.fid}-${path.name}`
}
return shareurl;
}
} }
}); });
</script> </script>

View File

@ -15,6 +15,7 @@ import time
import random import random
import requests import requests
import importlib import importlib
import urllib.parse
from datetime import datetime from datetime import datetime
# 兼容青龙 # 兼容青龙
@ -33,8 +34,12 @@ GH_PROXY = os.environ.get("GH_PROXY", "https://ghproxy.net/")
MAGIC_REGEX = { MAGIC_REGEX = {
"$TV": { "$TV": {
"pattern": r".*?(?<!\d)([Ss]\d{1,2})?([Ee]?[Pp]?[Xx]?\d{1,3})(?!\d).*?\.(mp4|mkv)", "pattern": r".*?([Ss]\d{1,2})?(?:[第EePpXx\.\-\_\( ]{1,2}|^)(\d{1,3})(?!\d).*?\.(mp4|mkv)",
"replace": r"\1\2.\3", "replace": r"\1E\2.\3",
},
"$BLACK_WORD": {
"pattern": r"^(?!.*纯享)(?!.*加更)(?!.*超前企划)(?!.*训练室)(?!.*蒸蒸日上).*",
"replace": "",
}, },
} }
@ -74,6 +79,17 @@ class Config:
else: else:
return False return False
# 读取 JSON 文件内容
def read_json(config_path):
with open(config_path, "r", encoding="utf-8") as f:
data = json.load(f)
return data
# 将数据写入 JSON 文件
def write_json(config_path, data):
with open(config_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, sort_keys=False, indent=2)
# 读取CK # 读取CK
def get_cookies(cookie_val): def get_cookies(cookie_val):
if isinstance(cookie_val, list): if isinstance(cookie_val, list):
@ -215,40 +231,17 @@ class Quark:
} }
) )
del headers["cookie"] del headers["cookie"]
# 添加重试机制
max_retries = 3
retry_count = 0
while retry_count < max_retries:
try: try:
response = requests.request(method, url, headers=headers, timeout=30, **kwargs) response = requests.request(method, url, headers=headers, **kwargs)
# 请求成功,返回结果 # print(f"{response.text}")
# response.raise_for_status() # 检查请求是否成功但返回非200也会抛出异常
return response return response
except requests.exceptions.SSLError as e: except Exception as e:
retry_count += 1 print(f"_send_request error:\n{e}")
if retry_count >= max_retries:
print(f"SSL错误已重试{retry_count}次,放弃请求: {str(e)}")
fake_response = requests.Response() fake_response = requests.Response()
fake_response.status_code = 500 fake_response.status_code = 500
fake_response._content = b'{"status": 500, "message": "SSL error", "code": 500}' fake_response._content = b'{"status": 500, "message": "request error"}'
return fake_response return fake_response
# 等待一段时间后重试
wait_time = retry_count * 2
print(f"SSL错误{wait_time}秒后进行第{retry_count+1}次重试: {str(e)}")
time.sleep(wait_time)
except requests.exceptions.RequestException as e:
retry_count += 1
if retry_count >= max_retries:
print(f"请求错误,已重试{retry_count}次,放弃请求: {str(e)}")
fake_response = requests.Response()
fake_response.status_code = 500
fake_response._content = b'{"status": 500, "message": "request error", "code": 500}'
return fake_response
# 等待一段时间后重试
wait_time = retry_count * 2
print(f"请求错误,{wait_time}秒后进行第{retry_count+1}次重试: {str(e)}")
time.sleep(wait_time)
def init(self): def init(self):
account_info = self.get_account_info() account_info = self.get_account_info()
@ -344,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
@ -391,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
@ -524,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"]
@ -535,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 = [
@ -580,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]
@ -618,7 +632,6 @@ 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):
@ -628,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)
@ -668,9 +680,11 @@ class Quark:
pwd_id, stoken, share_file_list[0]["fid"] pwd_id, stoken, share_file_list[0]["fid"]
)["list"] )["list"]
# 应用过滤词过滤文件 # 应用过滤词过滤
if task.get("filterwords"): if task.get("filterwords"):
filterwords_list = [word.strip() for word in task["filterwords"].split(',')] # 同时支持中英文逗号分隔
filterwords = task["filterwords"].replace("", ",")
filterwords_list = [word.strip() for word in filterwords.split(',')]
share_file_list = [file for file in share_file_list if not any(word in file['file_name'] for word in filterwords_list)] share_file_list = [file for file in share_file_list if not any(word in file['file_name'] for word in filterwords_list)]
print(f"📑 应用过滤词:{task['filterwords']},剩余{len(share_file_list)}个文件") print(f"📑 应用过滤词:{task['filterwords']},剩余{len(share_file_list)}个文件")
@ -696,44 +710,13 @@ class Quark:
# 需保存的文件清单 # 需保存的文件清单
need_save_list = [] need_save_list = []
# 顺序命名模式下获取当前序号和正则表达式
regex_pattern = None
if task.get("use_sequence_naming") and task.get("sequence_naming"):
# 获取目录中符合顺序命名格式的最大序号
sequence_pattern = task["sequence_naming"]
# 替换占位符为正则表达式捕获组
regex_pattern = re.escape(sequence_pattern).replace('\\{\\}', '(\\d+)')
max_sequence = 0
for dir_file in dir_file_list:
matches = re.match(regex_pattern, dir_file["file_name"])
if matches:
try:
current_seq = int(matches.group(1))
max_sequence = max(max_sequence, current_seq)
except (IndexError, ValueError):
pass
# 从最大序号开始计数
current_sequence = max_sequence
# 添加符合的 # 添加符合的
for share_file in share_file_list: for share_file in share_file_list:
if share_file["dir"] and task.get("update_subdir", False): if share_file["dir"] and task.get("update_subdir", False):
pattern, replace = task["update_subdir"], "" pattern, replace = task["update_subdir"], ""
elif task.get("use_sequence_naming") and task.get("sequence_naming"):
# 使用顺序命名
pattern = ".*" # 匹配任何文件
# 序号暂时留空,等收集完所有文件后再按优先级排序赋值
replace = "TO_BE_REPLACED_LATER"
# 保留文件扩展名
if not share_file["dir"]:
file_ext = os.path.splitext(share_file["file_name"])[1]
replace = replace + file_ext
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"]):
@ -751,51 +734,15 @@ class Quark:
) )
else: else:
compare_func = lambda a, b1, b2: (a == b1 or a == b2) compare_func = lambda a, b1, b2: (a == b1 or a == b2)
# 判断目标目录文件是否存在 # 判断目标目录文件是否存在
file_exists = False
# 顺序命名模式下增强去重功能
if task.get("use_sequence_naming") and task.get("sequence_naming") and not share_file["dir"]:
# 根据文件大小和修改时间判断文件是否已经存在
file_ext = os.path.splitext(share_file["file_name"])[1].lower()
for dir_file in dir_file_list:
# 检查是否为相同的文件(根据大小和扩展名判断)
dir_file_ext = os.path.splitext(dir_file["file_name"])[1].lower()
if (not dir_file["dir"] and
dir_file["size"] == share_file["size"] and
dir_file_ext == file_ext):
# 文件大小相同,扩展名相同,很可能是同一个文件
# 额外检查是否已经有符合顺序命名格式的文件(防止重复转存后重命名)
if re.match(regex_pattern, dir_file["file_name"]):
print(f"📌 顺序命名去重: {share_file['file_name']} 已存在于目录 {dir_file['file_name']},大小: {format_bytes(share_file['size'])},跳过")
file_exists = True
break
# 如果文件大小相同和扩展名相同,需要进一步检查修改时间是否接近
share_time = share_file.get("last_update_at", 0)
dir_time = dir_file.get("updated_at", 0)
# 如果修改时间在30天内或者差距不大认为是同一个文件
if abs(share_time - dir_time) < 2592000 or abs(1 - (share_time / dir_time if dir_time else 1)) < 0.1:
print(f"📌 顺序命名去重: {share_file['file_name']}{dir_file['file_name']} 匹配,大小: {format_bytes(share_file['size'])},跳过")
file_exists = True
break
else:
# 原有的文件名匹配判断
file_exists = any( file_exists = any(
compare_func( compare_func(
dir_file["file_name"], share_file["file_name"], save_name dir_file["file_name"], share_file["file_name"], save_name
) )
for dir_file in dir_file_list for dir_file in dir_file_list
) )
if not file_exists: if not file_exists:
share_file["save_name"] = save_name share_file["save_name"] = save_name
share_file["original_name"] = share_file["file_name"] # 保存原文件名,用于排序
need_save_list.append(share_file) need_save_list.append(share_file)
elif share_file["dir"]: elif share_file["dir"]:
# 存在并是一个文件夹 # 存在并是一个文件夹
@ -824,71 +771,6 @@ class Quark:
if share_file["fid"] == task.get("startfid", ""): if share_file["fid"] == task.get("startfid", ""):
break break
# 如果是顺序命名模式,需要重新排序并生成文件名
if task.get("use_sequence_naming") and task.get("sequence_naming") and need_save_list:
def custom_sort(file):
file_name = file["original_name"]
# 1. 提取文件名中的数字(期数/集数等)
episode_num = 0
# 尝试匹配"第X期/集/话"格式
episode_match = re.search(r'第(\d+)[期集话]', file_name)
if episode_match:
episode_num = int(episode_match.group(1))
# 尝试匹配常见视频格式 S01E01, E01, 1x01 等
elif re.search(r'[Ss](\d+)[Ee](\d+)', file_name):
match = re.search(r'[Ss](\d+)[Ee](\d+)', file_name)
season = int(match.group(1))
episode = int(match.group(2))
episode_num = season * 1000 + episode # 确保季和集的排序正确
elif re.search(r'[Ee](\d+)', file_name):
match = re.search(r'[Ee](\d+)', file_name)
episode_num = int(match.group(1))
elif re.search(r'(\d+)[xX](\d+)', file_name):
match = re.search(r'(\d+)[xX](\d+)', file_name)
season = int(match.group(1))
episode = int(match.group(2))
episode_num = season * 1000 + episode
# 尝试匹配日期格式 YYYYMMDD
elif re.search(r'(\d{4})(\d{2})(\d{2})', file_name):
match = re.search(r'(\d{4})(\d{2})(\d{2})', file_name)
year = int(match.group(1))
month = int(match.group(2))
day = int(match.group(3))
episode_num = year * 10000 + month * 100 + day
# 尝试匹配纯数字格式(文件名开头是纯数字)
elif re.search(r'^(\d+)', file_name):
match = re.search(r'^(\d+)', file_name)
episode_num = int(match.group(1))
# 2. 检查文件名中是否包含"上中下"等排序提示
position_order = 10 # 默认顺序值
if '' in file_name:
position_order = 1
elif '' in file_name:
position_order = 2
elif '' in file_name:
position_order = 3
# 3. 返回排序元组:先按集数排序,再按上中下,最后按更新时间
return (episode_num, position_order, file["last_update_at"] if "last_update_at" in file else 0)
# 按自定义逻辑排序
need_save_list = sorted(need_save_list, key=custom_sort)
# 重新生成命名
for index, file in enumerate(need_save_list):
current_sequence += 1
if file["dir"]:
file["save_name"] = sequence_pattern.replace("{}", f"{current_sequence:02d}")
else:
file_ext = os.path.splitext(file["file_name"])[1]
file["save_name"] = sequence_pattern.replace("{}", f"{current_sequence:02d}") + file_ext
fid_list = [item["fid"] for item in need_save_list] fid_list = [item["fid"] for item in need_save_list]
fid_token_list = [item["share_fid_token"] for item in need_save_list] fid_token_list = [item["share_fid_token"] for item in need_save_list]
if fid_list: if fid_list:
@ -926,110 +808,8 @@ class Quark:
return tree return tree
def do_rename_task(self, task, subdir_path=""): def do_rename_task(self, task, subdir_path=""):
if task.get("use_sequence_naming") and task.get("sequence_naming"):
# 使用顺序命名模式
sequence_pattern = task["sequence_naming"]
# 替换占位符为正则表达式捕获组
regex_pattern = re.escape(sequence_pattern).replace('\\{\\}', '(\\d+)')
savepath = re.sub(r"/{2,}", "/", f"/{task['savepath']}{subdir_path}")
if not self.savepath_fid.get(savepath):
self.savepath_fid[savepath] = self.get_fids([savepath])[0]["fid"]
dir_file_list = self.ls_dir(self.savepath_fid[savepath])
dir_file_name_list = [item["file_name"] for item in dir_file_list]
# 找出当前最大序号
max_sequence = 0
for dir_file in dir_file_list:
matches = re.match(regex_pattern, dir_file["file_name"])
if matches:
try:
current_seq = int(matches.group(1))
max_sequence = max(max_sequence, current_seq)
except (IndexError, ValueError):
pass
# 重命名文件
current_sequence = max_sequence
is_rename_count = 0
# 定义一个排序函数,支持多种格式的排序
def custom_sort(file):
file_name = file["file_name"]
# 1. 提取文件名中的数字(期数/集数等)
episode_num = 0
# 尝试匹配"第X期/集/话"格式
episode_match = re.search(r'第(\d+)[期集话]', file_name)
if episode_match:
episode_num = int(episode_match.group(1))
# 尝试匹配常见视频格式 S01E01, E01, 1x01 等
elif re.search(r'[Ss](\d+)[Ee](\d+)', file_name):
match = re.search(r'[Ss](\d+)[Ee](\d+)', file_name)
season = int(match.group(1))
episode = int(match.group(2))
episode_num = season * 1000 + episode # 确保季和集的排序正确
elif re.search(r'[Ee](\d+)', file_name):
match = re.search(r'[Ee](\d+)', file_name)
episode_num = int(match.group(1))
elif re.search(r'(\d+)[xX](\d+)', file_name):
match = re.search(r'(\d+)[xX](\d+)', file_name)
season = int(match.group(1))
episode = int(match.group(2))
episode_num = season * 1000 + episode
# 尝试匹配日期格式 YYYYMMDD
elif re.search(r'(\d{4})(\d{2})(\d{2})', file_name):
match = re.search(r'(\d{4})(\d{2})(\d{2})', file_name)
year = int(match.group(1))
month = int(match.group(2))
day = int(match.group(3))
episode_num = year * 10000 + month * 100 + day
# 尝试匹配纯数字格式(文件名开头是纯数字)
elif re.search(r'^(\d+)', file_name):
match = re.search(r'^(\d+)', file_name)
episode_num = int(match.group(1))
# 2. 检查文件名中是否包含"上中下"等排序提示
position_order = 10 # 默认顺序值
if '' in file_name:
position_order = 1
elif '' in file_name:
position_order = 2
elif '' in file_name:
position_order = 3
# 3. 返回排序元组:先按集数排序,再按上中下,最后按创建时间
return (episode_num, position_order, file["created_at"])
# 按自定义逻辑排序
sorted_files = sorted([f for f in dir_file_list if not f["dir"] and not re.match(regex_pattern, f["file_name"])], key=custom_sort)
for dir_file in sorted_files:
current_sequence += 1
file_ext = os.path.splitext(dir_file["file_name"])[1]
save_name = sequence_pattern.replace("{}", f"{current_sequence:02d}") + file_ext
if save_name != dir_file["file_name"] and save_name not in dir_file_name_list:
try:
rename_return = self.rename(dir_file["fid"], save_name)
# 防止网络问题导致的错误
if isinstance(rename_return, dict) and rename_return.get("code") == 0:
print(f"重命名:{dir_file['file_name']}{save_name}")
is_rename_count += 1
dir_file_name_list.append(save_name)
else:
error_msg = rename_return.get("message", "未知错误")
print(f"重命名:{dir_file['file_name']}{save_name} 失败,{error_msg}")
except Exception as e:
print(f"重命名出错:{dir_file['file_name']}{save_name},错误:{str(e)}")
return is_rename_count > 0
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 not pattern or not replace: if not pattern or not replace:
return 0 return 0
@ -1053,16 +833,14 @@ class Quark:
if save_name != dir_file["file_name"] and ( if save_name != dir_file["file_name"] and (
save_name not in dir_file_name_list save_name not in dir_file_name_list
): ):
try:
rename_return = self.rename(dir_file["fid"], save_name) rename_return = self.rename(dir_file["fid"], save_name)
if isinstance(rename_return, dict) and rename_return.get("code") == 0: if rename_return["code"] == 0:
print(f"重命名:{dir_file['file_name']}{save_name}") print(f"重命名:{dir_file['file_name']}{save_name}")
is_rename_count += 1 is_rename_count += 1
else: else:
error_msg = rename_return.get("message", "未知错误") print(
print(f"重命名:{dir_file['file_name']}{save_name} 失败,{error_msg}") f"重命名:{dir_file['file_name']}{save_name} 失败,{rename_return['message']}"
except Exception as e: )
print(f"重命名出错:{dir_file['file_name']}{save_name},错误:{str(e)}")
return is_rename_count > 0 return is_rename_count > 0
@ -1134,7 +912,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 (
@ -1142,35 +920,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):
# 判断任务期限
if check_date(task):
print() print()
print(f"#{index+1}------------------") print(f"#{index+1}------------------")
print(f"任务名称: {task['taskname']}") print(f"任务名称: {task['taskname']}")
print(f"分享链接: {task['shareurl']}") print(f"分享链接: {task['shareurl']}")
print(f"保存路径: {task['savepath']}") print(f"保存路径: {task['savepath']}")
if task.get("pattern"):
# 打印重命名规则信息
if task.get("use_sequence_naming") and task.get("sequence_naming"):
print(f"顺序命名: {task['sequence_naming']}")
else:
print(f"正则匹配: {task['pattern']}") print(f"正则匹配: {task['pattern']}")
if task.get("replace"):
print(f"正则替换: {task['replace']}") print(f"正则替换: {task['replace']}")
if task.get("enddate"):
print(f"任务截止: {task['enddate']}")
if task.get("ignore_extension"):
print(f"忽略后缀: {task['ignore_extension']}")
if task.get("update_subdir"): if task.get("update_subdir"):
print(f"更子目录: {task['update_subdir']}") print(f"更子目录: {task['update_subdir']}")
if task.get("runweek") or task.get("enddate"):
print(
f"运行周期: WK{task.get("runweek",[])} ~ {task.get('enddate','forever')}"
)
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)
@ -1210,7 +986,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"):
@ -1227,8 +1009,7 @@ 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"):
@ -1242,7 +1023,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:
@ -1253,11 +1034,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:
@ -1267,8 +1047,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