Compare commits

...

36 Commits

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

View File

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

View File

@ -1,6 +1,7 @@
# !/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import (
json,
Flask,
url_for,
session,
@ -14,15 +15,16 @@ from flask import (
)
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from sdk.cloudsaver import CloudSaver
from datetime import timedelta
import subprocess
import requests
import hashlib
import logging
import base64
import json
import sys
import os
import re
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
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")
CONFIG_PATH = os.environ.get("CONFIG_PATH", "./config/quark_config.json")
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.config["APP_VERSION"] = get_app_ver()
@ -77,24 +80,15 @@ def gen_md5(string):
return md5.hexdigest()
# 读取 JSON 文件内容
def read_json():
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
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 get_login_token():
username = config_data["webui"]["username"]
password = config_data["webui"]["password"]
return gen_md5(f"token{username}{password}+-*/")[8:24]
def is_login():
data = read_json()
username = data["webui"]["username"]
password = data["webui"]["password"]
if session.get("login") == gen_md5(username + password):
login_token = get_login_token()
if session.get("token") == login_token or request.args.get("token") == login_token:
return True
else:
return False
@ -114,16 +108,15 @@ def favicon():
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
data = read_json()
username = data["webui"]["username"]
password = data["webui"]["password"]
username = config_data["webui"]["username"]
password = config_data["webui"]["password"]
# 验证用户名和密码
if (username == request.form.get("username")) and (
password == request.form.get("password")
):
logging.info(f">>> 用户 {username} 登录成功")
session["login"] = gen_md5(username + password)
session.permanent = True
session["token"] = get_login_token()
return redirect(url_for("index"))
else:
logging.info(f">>> 用户 {username} 登录失败")
@ -137,7 +130,7 @@ def login():
# 退出登录
@app.route("/logout")
def logout():
session.pop("login", None)
session.pop("token", None)
return redirect(url_for("login"))
@ -155,47 +148,51 @@ def index():
@app.route("/data")
def get_data():
if not is_login():
return redirect(url_for("login"))
data = read_json()
return jsonify({"success": False, "message": "未登录"})
data = Config.read_json(CONFIG_PATH)
del data["webui"]
data["task_plugins_config"] = task_plugins_config
return jsonify(data)
data["api_token"] = get_login_token()
data["task_plugins_config_default"] = task_plugins_config_default
return jsonify({"success": True, "data": data})
# 更新数据
@app.route("/update", methods=["POST"])
def update():
global config_data
if not is_login():
return "未登录"
data = request.json
data["webui"] = read_json()["webui"]
if "task_plugins_config" in data:
del data["task_plugins_config"]
write_json(data)
return jsonify({"success": False, "message": "未登录"})
dont_save_keys = ["task_plugins_config_default", "api_token"]
for key, value in request.json.items():
if key not in dont_save_keys:
config_data.update({key: value})
Config.write_json(CONFIG_PATH, config_data)
# 重新加载任务
if reload_tasks():
logging.info(f">>> 配置更新成功")
return "配置更新成功"
return jsonify({"success": True, "message": "配置更新成功"})
else:
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():
if not is_login():
return "未登录"
task_index = request.args.get("task_index", "")
command = [PYTHON_PATH, "-u", SCRIPT_PATH, CONFIG_PATH, task_index]
return jsonify({"success": False, "message": "未登录"})
tasklist = request.json.get("tasklist", [])
command = [PYTHON_PATH, "-u", SCRIPT_PATH, CONFIG_PATH]
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():
# 设置环境变量
process_env = os.environ.copy()
process_env["PYTHONIOENCODING"] = "utf-8"
if tasklist:
process_env["TASKLIST"] = json.dumps(tasklist, ensure_ascii=False)
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
@ -224,64 +221,156 @@ def run_script_now():
@app.route("/task_suggestions")
def get_task_suggestions():
if not is_login():
return jsonify({"error": "未登录"})
base_url = base64.b64decode("aHR0cHM6Ly9zLjkxNzc4OC54eXo=").decode()
return jsonify({"success": False, "message": "未登录"})
query = request.args.get("q", "").lower()
deep = request.args.get("d", "").lower()
url = f"{base_url}/task_suggestions?q={query}&d={deep}"
try:
response = requests.get(url)
return jsonify(response.json())
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)
return jsonify(
{"success": True, "source": "网络公开", "data": response.json()}
)
except Exception as e:
return jsonify({"error": str(e)})
return jsonify({"success": True, "message": f"error: {str(e)}"})
@app.route("/get_share_detail")
def get_share_files():
@app.route("/get_share_detail", methods=["POST"])
def get_share_detail():
if not is_login():
return jsonify({"error": "未登录"})
shareurl = request.args.get("shareurl", "")
return jsonify({"success": False, "message": "未登录"})
shareurl = request.json.get("shareurl", "")
stoken = request.json.get("stoken", "")
account = Quark("", 0)
pwd_id, passcode, pdir_fid = account.get_id_from_url(shareurl)
is_sharing, stoken = account.get_stoken(pwd_id, passcode)
if not is_sharing:
return jsonify({"error": stoken})
share_detail = account.get_detail(pwd_id, stoken, pdir_fid, 1)
return jsonify(share_detail)
pwd_id, passcode, pdir_fid, paths = account.extract_url(shareurl)
if not stoken:
is_sharing, stoken = account.get_stoken(pwd_id, passcode)
if not is_sharing:
return jsonify({"success": False, "data": {"error": stoken}})
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")
def get_savepath():
@app.route("/get_savepath_detail")
def get_savepath_detail():
if not is_login():
return jsonify({"error": "未登录"})
data = read_json()
account = Quark(data["cookie"][0], 0)
return jsonify({"success": False, "message": "未登录"})
account = Quark(config_data["cookie"][0], 0)
paths = []
if path := request.args.get("path"):
if path == "/":
fid = 0
elif get_fids := account.get_fids([path]):
fid = get_fids[0]["fid"]
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:
fid = request.args.get("fid", 0)
file_list = account.ls_dir(fid)
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"])
def delete_file():
if not is_login():
return jsonify({"error": "未登录"})
data = read_json()
account = Quark(data["cookie"][0], 0)
return jsonify({"success": False, "message": "未登录"})
account = Quark(config_data["cookie"][0], 0)
if fid := request.json.get("fid"):
response = account.delete([fid])
else:
response = {"error": "fid not found"}
response = {"success": False, "message": "缺失必要字段: fid"}
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):
logging.info(f">>> 定时运行任务")
@ -290,11 +379,8 @@ def run_python(args):
# 重新加载任务
def reload_tasks():
# 读取数据
data = read_json()
# 添加新任务
crontab = data.get("crontab")
if crontab:
# 读取定时规则
if crontab := config_data.get("crontab"):
if scheduler.state == 1:
scheduler.pause() # 暂停调度器
trigger = CronTrigger.from_crontab(crontab)
@ -321,7 +407,7 @@ def reload_tasks():
def init():
global task_plugins_config
global config_data, task_plugins_config_default
logging.info(f">>> 初始化配置")
# 检查配置文件是否存在
if not os.path.exists(CONFIG_PATH):
@ -329,23 +415,30 @@ def init():
os.makedirs(os.path.dirname(CONFIG_PATH))
with open("quark_config.json", "rb") as src, open(CONFIG_PATH, "wb") as dest:
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")
or data.get("webui", {}).get("username", "admin"),
or config_data.get("webui", {}).get("username", "admin"),
"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"):
data["crontab"] = "0 8,18,20 * * *"
if not config_data.get("crontab"):
config_data["crontab"] = "0 8,18,20 * * *"
# 初始化插件配置
_, plugins_config_default, task_plugins_config = Config.load_plugins()
plugins_config_default.update(data.get("plugins", {}))
data["plugins"] = plugins_config_default
write_json(data)
_, plugins_config_default, task_plugins_config_default = Config.load_plugins()
plugins_config_default.update(config_data.get("plugins", {}))
config_data["plugins"] = plugins_config_default
# 更新配置
Config.write_json(CONFIG_PATH, config_data)
if __name__ == "__main__":

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

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

View File

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

View File

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

View File

@ -58,15 +58,15 @@
</nav>
<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 class="row title">
<div class="col">
<div class="col-10">
<h2><i class="bi bi-cookie"></i> Cookie</h2>
</div>
<div class="col text-right">
<button type="button" class="btn btn-outline-primary mb-3" @click="addCookie()">+</button>
<div class="col-2 text-right">
<button type="button" class="btn btn-outline-primary" @click="addCookie()">+</button>
</div>
</div>
<p>1. 所有账号执行签到,纯签到只需移动端参数即可!</p>
@ -86,22 +86,22 @@
</span>
</div>
</div>
<div class="input-group mt-2 mb-2">
<div class="input-group mb-2">
<div class="input-group-prepend">
<span class="input-group-text">Crontab</span>
</div>
<input type="text" v-model="formData.crontab" class="form-control" placeholder="必填">
</div>
<div class="row title">
<div class="col">
<div class="row title" title="通知推送支持多个渠道见Wiki">
<div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-bell"></i> 通知</h2>
<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>
</div>
<div class="col text-right">
<button type="button" class="btn btn-outline-primary mb-3" @click="addPush()">+</button>
<div class="col-2 text-right">
<button type="button" class="btn btn-outline-primary" @click="addPush()">+</button>
</div>
</div>
<div v-for="(value, key) in formData.push_config" :key="key" class="input-group mb-2">
@ -117,11 +117,11 @@
</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">
<h2 style="display: inline-block;"><i class="bi bi-plug"></i> 插件</h2>
<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>
</div>
</div>
@ -142,6 +142,78 @@
</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 v-if="activeTab === 'tasklist'">
@ -199,9 +271,9 @@
<div class="col-sm-10">
<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)">
<div class="dropdown-menu show task-suggestions" v-if="smart_param.showSuggestions && smart_param.taskSuggestions.length && smart_param.index === index">
<div class="text-muted text-center" style="font-size: small;">以下资源来自第三方,网络公开搜集,请自行辨识,如有侵权请联系夸克官方</div>
<div v-for="suggestion in smart_param.taskSuggestions" :key="suggestion.taskname" class="dropdown-item" @click.prevent="selectSuggestion(task, suggestion)" style="cursor: pointer;">
<div class="dropdown-menu show task-suggestions" v-if="smart_param.showSuggestions && smart_param.taskSuggestions.success && smart_param.index === index">
<div class="dropdown-item text-muted text-center" style="font-size:12px;">{{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data.length ? `以下资源来自 ${smart_param.taskSuggestions.source} 搜索,请自行辨识,如有侵权请联系资源方` : "未搜索到资源" }}</div>
<div v-for="suggestion in smart_param.taskSuggestions.data" :key="suggestion.taskname" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(index, suggestion)" style="font-size: 12px;" :title="suggestion.content">
<span v-html="suggestion.verify ? '✅': '❔'"></span> {{ suggestion.taskname }}
<small class="text-muted">
<a :href="suggestion.shareurl" target="_blank" @click.stop>{{ suggestion.shareurl }}</a>
@ -220,12 +292,13 @@
</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>
<div class="col-sm-10">
<div class="input-group">
<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">
<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">
<a target="_blank" :href="task.shareurl"><i class="bi bi-box-arrow-up-right"></i></a>
</div>
@ -239,33 +312,24 @@
<div class="input-group">
<input type="text" name="savepath[]" class="form-control" v-model="task.savepath" placeholder="必填" @focus="focusTaskname(index, task)">
<div class="input-group-append">
<button class="btn btn-secondary" type="button" v-if="smart_param.savepath && smart_param.index == index && task.savepath != smart_param.origin_savepath" @click="task.savepath = smart_param.origin_savepath"><i class="
bi bi-reply"></i></button>
<button class="btn btn-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>
<button class="btn btn-secondary" type="button" v-if="smart_param.savepath && smart_param.index == index && task.savepath != smart_param.origin_savepath" @click="task.savepath = smart_param.origin_savepath"><i class="bi bi-reply"></i></button>
<button class="btn btn-outline-secondary" type="button" @click="showSavepathSelect(index)">选择</button>
</div>
</div>
</div>
</div>
<div class="form-group row">
<div class="form-group row" title="可用作筛选,只转存匹配到的文件名的文件,留空则转存所有文件">
<label class="col-sm-2 col-form-label">保存规则</label>
<div class="col-sm-10">
<div class="input-group">
<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>
<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="替换表达式">
<div class="input-group-append">
<div class="input-group-append" title="保存时只比较文件名的部分01.mp4 和 01.mkv 视同为同一文件,不重复转存">
<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>
@ -274,21 +338,21 @@
</datalist>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">文件</label>
<div class="form-group row" title="只转存修改日期>选中文件的文件,在容量不够或几百集动漫的场景下非常有用">
<label class="col-sm-2 col-form-label">文件</label>
<div class="col-sm-10">
<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">
<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 class="form-group row">
<label class="col-sm-2 col-form-label">目录</label>
<div class="form-group row" title="需匹配到各级嵌套目录名才会更新,否则子目录在第一次转存后不会更新。注意:原理是逐级索引,深层嵌套目录的场景下效率非常低,慎用 .*">
<label class="col-sm-2 col-form-label">目录</label>
<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 class="form-group row">
@ -297,7 +361,7 @@
<input type="date" name="enddate[]" class="form-control" v-model="task.enddate" placeholder="可选">
</div>
</div>
<div class="form-group row">
<div class="form-group row" title="只在勾选的星期时才运行,在某些周更剧的场景下非常有用">
<label class="col-sm-2 col-form-label">运行星期</label>
<div class="col-sm-10 col-form-label">
<div class="form-check form-check-inline" title="也可用作任务总开关">
@ -310,7 +374,7 @@
</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>
<div class="col-sm-10">
<v-jsoneditor v-model="task.addition" :options="{mode:'tree'}" :plus="false" height="180px"></v-jsoneditor>
@ -354,42 +418,70 @@
</div>
</div>
<!-- 模态框 分享文件列表 -->
<div class="modal" tabindex="-1" id="shareDetailModal">
<div class="modal-dialog modal-xl">
<!-- 模态框 文件选择 -->
<div class="modal" tabindex="-1" id="fileSelectModal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<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>
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="alert alert-warning" v-if="shareFiles.error" v-html="shareFiles.error"></div>
<table class="table table-hover table-sm" v-else-if="!modalLoading" title="请选择转存起始文件,将只转存修改日期>选中文件的文件">
<thead>
<tr>
<!-- <th scope="col">fid</th> -->
<th scope="col">文件名</th>
<th scope="col">大小</th>
<th scope="col">修改日期 ↓</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="4"><i class="bi bi-folder-plus"></i> 后续更新的文件...</td>
</tr>
<tr v-for="(file, key) in shareFiles" :key="key" @click="selectStartFid(file.fid)" style="cursor: pointer;">
<!-- <td>{{file.fid}}</td> -->
<td><i class="bi" :class="file.dir ? 'bi-folder2' : 'bi-file-earmark'"></i> {{file.file_name}}</td>
<td>{{file.size | size}}</td>
<td>{{file.last_update_at | ts2date}}</td>
</td>
</tr>
</tbody>
</table>
<div class="modal-body small">
<div class="alert alert-warning" v-if="fileSelect.error" v-html="fileSelect.error"></div>
<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>
<tr>
<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" v-if="!fileSelect.selectShare">操作</th>
</template>
</tr>
</thead>
<tbody>
<tr v-for="(file, key) in fileSelect.fileList" :key="key" @click="fileSelect.selectDir ? (file.dir ? navigateTo(file.fid, file.file_name) : null) : selectStartFid(file.fid)" :class="{'cursor-pointer': fileSelect.selectDir ? file.dir : true}">
<td><i class="bi" :class="file.dir ? 'bi-folder-fill text-warning' : 'bi-file-earmark'"></i> {{file.file_name}}</td>
<td v-if="fileSelect.selectShare" :class="file.file_name_re ? 'text-success' : 'text-danger'">{{file.file_name_re || '&times;'}}</td>
<template v-if="!fileSelect.previewRegex">
<td v-if="file.dir">{{ file.include_items }}项</td>
<td v-else>{{file.size | size}}</td>
<td>{{file.updated_at | ts2date}}</td>
<td v-if="!fileSelect.selectShare"><a @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)">删除</a></td>
</template>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer" v-if="fileSelect.selectDir && !fileSelect.previewRegex">
<span v-html="fileSelect.selectShare ? '转存:' : '保存到:'"></span>
<button type="button" class="btn btn-primary btn-sm" @click="selectCurrentFolder()">当前文件夹</button>
<button type="button" class="btn btn-primary btn-sm" v-if="!fileSelect.selectShare" @click="selectCurrentFolder(true)">当前文件夹<span class="badge badge-light" v-html="'/'+formData.tasklist[fileSelect.index].taskname"></span></button>
</div>
</div>
</div>
@ -410,7 +502,15 @@
push_config: {},
media_servers: {},
tasklist: [],
magic_regex: {}
magic_regex: {},
source: {
cloudsaver: {
server: "",
username: "",
password: "",
token: ""
}
},
},
newTask: {
taskname: "",
@ -427,20 +527,28 @@
taskDirs: [""],
taskDirSelected: "",
taskNameFilter: "",
savepaths: [],
modalLoading: false,
shareFiles: [],
forceTaskIndex: null,
smart_param: {
index: null,
savepath: "",
origin_savepath: "",
taskSuggestions: [],
taskSuggestions: {},
showSuggestions: false,
lastSuggestionsTime: 0,
isSearching: false,
searchTimer: null,
},
activeTab: 'tasklist',
configModified: false,
fileSelect: {
index: null,
shareurl: "",
stoken: "",
fileList: [],
paths: [],
selectDir: true,
selectShare: true,
previewRegex: false,
},
},
filters: {
ts2date: function (value) {
@ -448,15 +556,21 @@
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`;
},
size: function (value) {
if (!value) return "0B";
if (!value) return "";
const unitArr = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const srcsize = parseFloat(value);
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];
}
},
watch: {
formData: {
handler(newVal, oldVal) {
this.configModified = true;
},
deep: true
}
},
mounted() {
this.fetchData();
@ -468,6 +582,10 @@
this.smart_param.showSuggestions = false;
}
});
window.addEventListener('beforeunload', this.handleBeforeUnload);
},
beforeDestroy() {
window.removeEventListener('beforeunload', this.handleBeforeUnload);
},
methods: {
changeTab(tab) {
@ -493,24 +611,40 @@
fetchData() {
axios.get('/data')
.then(response => {
config_data = response.data.data
// cookie兼容
if (typeof response.data.cookie === 'string')
response.data.cookie = [response.data.cookie];
if (typeof config_data.cookie === 'string')
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')) {
task.runweek = [1, 2, 3, 4, 5, 6, 7];
}
return task;
});
// 获取所有任务父目录
response.data.tasklist.forEach(item => {
config_data.tasklist.forEach(item => {
parentDir = this.getParentDirectory(item.savepath)
if (!this.taskDirs.includes(parentDir))
this.taskDirs.push(parentDir);
});
this.newTask.addition = response.data.task_plugins_config;
this.formData = response.data;
this.newTask.addition = config_data.task_plugins_config_default;
// 确保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 => {
console.error('Error fetching data:', error);
@ -527,11 +661,23 @@
}
}
},
handleBeforeUnload(e) {
if (this.configModified) {
e.preventDefault();
e.returnValue = '配置已修改但未保存,确定要离开吗?';
return e.returnValue;
}
},
saveConfig() {
axios.post('/update', this.formData)
.then(response => {
alert(response.data);
console.log('Config saved successfully:', response.data);
if (response.data.success) {
this.configModified = false;
alert(response.data.message);
} else {
alert(response.data.message);
}
console.log('Config saved result:', response.data);
})
.catch(error => {
console.error('Error saving config:', error);
@ -589,7 +735,12 @@
}
},
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)
task.savepath = this.smart_param.savepath.replace('TASKNAME', task.taskname);
},
@ -614,19 +765,20 @@
// 从分享中提取任务名
axios.get('/get_share_detail', { params: { shareurl: task.shareurl } })
.then(response => {
if (response.data.error) {
if (response.data.error.includes("提取码")) {
const passcode = prompt("检查失败[" + response.data.error + "],请输入提取码:");
share_detail = response.data.data
if (!response.data.success) {
if (share_detail.error.includes("提取码")) {
const passcode = prompt("检查失败[" + share_detail.error + "],请输入提取码:");
if (passcode != null) {
task.shareurl = task.shareurl.replace(/pan.quark.cn\/s\/(\w+)(\?pwd=\w*)*/, `pan.quark.cn/s/$1?pwd=${passcode}`);
this.changeShareurl(task);
return;
}
}
this.$set(task, "shareurl_ban", response.data.error);
this.$set(task, "shareurl_ban", share_detail.error);
} else {
task.taskname = task.taskname == "" ? response.data.share.title : task.taskname;
task.savepath = task.savepath.replace(/TASKNAME/g, response.data.share.title);
task.taskname = task.taskname == "" ? share_detail.share.title : task.taskname;
task.savepath = task.savepath.replace(/TASKNAME/g, share_detail.share.title);
this.$set(task, "shareurl_ban", undefined);
}
})
@ -637,83 +789,74 @@
clearData(target) {
this[target] = "";
},
selectSavepath(index, fid, name) {
const savepath = name == ".." ? this.getParentDirectory(this.formData.tasklist[index].savepath) : `/${this.formData.tasklist[index].savepath}/${name}`.replace(/\/{2,}/g, '/')
Vue.set(this.formData.tasklist[index], 'savepath', savepath);
this.getSavepathDirs(fid);
},
getSavepathDirs(fid = 0) {
if (fid.includes('/')) {
params = { path: fid }
} else {
params = { fid: fid }
}
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);
}
})
.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;
});
async runScriptNow(task_index = null) {
body = {};
if (task_index != null) {
task = { ...this.formData.tasklist[task_index] };
delete task.runweek;
delete task.enddate;
body = {
"tasklist": [task]
};
} else if (this.configModified) {
if (!confirm('配置已修改但未保存,是否继续运行?')) {
return;
}
};
source.onerror = (error) => {
this.modalLoading = false
}
$('#logModal').modal('toggle');
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);
source.close();
};
}
},
getParentDirectory(path) {
parentDir = path.substring(0, path.lastIndexOf('/'))
@ -746,37 +889,188 @@
task.runweek = [1, 2, 3, 4, 5, 6, 7];
}
},
searchSuggestions(index, taskname, limit_msec = 0) {
if (taskname.length == 0)
return
if (limit_msec > 0) {
const now = Date.now();
if (now - this.smart_param.lastSuggestionsTime < limit_msec)
return;
this.smart_param.lastSuggestionsTime = now;
searchSuggestions(index, taskname, deep = 1) {
if (taskname.length < 2) {
console.log(`任务名[${taskname}]过短${taskname.length} 不进行搜索`);
return;
}
this.smart_param.isSearching = true
this.smart_param.isSearching = true;
this.smart_param.index = index;
axios.get('/task_suggestions', {
params: {
q: taskname,
d: limit_msec == 0 ? 1 : 0
try {
axios.get('/task_suggestions', {
params: {
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 => {
this.smart_param.taskSuggestions = response.data;
this.smart_param.showSuggestions = true;
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 fetching suggestions:', error);
}).finally(() => {
this.smart_param.isSearching = false;
console.error('Error /get_savepath_detail:', error);
this.fileSelect.error = "获取文件夹列表失败";
this.modalLoading = false;
});
},
selectSuggestion(task, suggestion) {
task.taskname = suggestion.taskname;
task.shareurl = suggestion.shareurl;
this.changeShareurl(task);
this.smart_param.showSuggestions = 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,
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>

View File

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

View File

@ -12,6 +12,16 @@
"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": [
{
"taskname": "测试-魔法匹配剧集这是一组有效分享配置CK后可测试任务是否正常",