Compare commits

..

4 Commits

3 changed files with 1619 additions and 435 deletions

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,324 @@ 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")
# 检查是否为顺序命名模式
if regex.get("use_sequence_naming") and regex.get("sequence_naming"):
# 顺序命名模式预览
sequence_pattern = regex.get("sequence_naming")
current_sequence = 1
# 构建顺序命名的正则表达式
regex_pattern = re.escape(sequence_pattern).replace('\\{\\}', '(\\d+)')
# 实现高级排序算法
def extract_sorting_value(file):
if file["dir"]: # 跳过文件夹
return float('inf')
filename = file["file_name"]
# 提取文件名,不含扩展名
file_name_without_ext = os.path.splitext(filename)[0]
# 1. "第X期/集/话" 格式
match_chinese = re.search(r'第(\d+)[期集话]', filename)
episode_num = int(match_chinese.group(1)) if match_chinese else 0
# 5. 文件名含"上中下"(优先处理,因为可能与其他格式同时存在)
if match_chinese:
# 如果同时存在集数和上中下,则按照集数*10+位置排序
if '' in filename:
return episode_num * 10 + 1
elif '' in filename:
return episode_num * 10 + 2
elif '' in filename:
return episode_num * 10 + 3
elif '' in filename:
return 1
elif '' in filename:
return 2
elif '' in filename:
return 3
# 如果已经匹配到"第X期/集/话"格式,直接返回
if episode_num > 0:
return episode_num * 10
# 2.1 S01E01 格式,提取季数和集数
match_s_e = re.search(r'[Ss](\d+)[Ee](\d+)', filename)
if match_s_e:
season = int(match_s_e.group(1))
episode = int(match_s_e.group(2))
return season * 1000 + episode
# 2.2 E01 格式,仅提取集数
match_e = re.search(r'[Ee][Pp]?(\d+)', filename)
if match_e:
return int(match_e.group(1))
# 2.3 1x01 格式,提取季数和集数
match_x = re.search(r'(\d+)[Xx](\d+)', filename)
if match_x:
season = int(match_x.group(1))
episode = int(match_x.group(2))
return season * 1000 + episode
# 3. 日期格式识别(支持多种格式)
# 3.1 完整的YYYYMMDD格式
match_date_compact = re.search(r'(20\d{2})(\d{2})(\d{2})', filename)
if match_date_compact:
year = int(match_date_compact.group(1))
month = int(match_date_compact.group(2))
day = int(match_date_compact.group(3))
return year * 10000 + month * 100 + day
# 3.2 YYYY-MM-DD 或 YYYY.MM.DD 或 YYYY/MM/DD 格式
match_date_full = re.search(r'(20\d{2})[-./](\d{1,2})[-./](\d{1,2})', filename)
if match_date_full:
year = int(match_date_full.group(1))
month = int(match_date_full.group(2))
day = int(match_date_full.group(3))
return year * 10000 + month * 100 + day
# 3.3 MM/DD/YYYY 或 DD/MM/YYYY 格式
match_date_alt = re.search(r'(\d{1,2})[-./](\d{1,2})[-./](20\d{2})', filename)
if match_date_alt:
# 假设第一个是月,第二个是日(美式日期)
month = int(match_date_alt.group(1))
day = int(match_date_alt.group(2))
year = int(match_date_alt.group(3))
# 检查月份值如果大于12可能是欧式日期格式DD/MM/YYYY
if month > 12:
month, day = day, month
return year * 10000 + month * 100 + day
# 3.4 MM/DD 格式(无年份),假设为当前年
match_date_short = re.search(r'(\d{1,2})[-./](\d{1,2})', filename)
if match_date_short:
# 假设第一个是月,第二个是日
month = int(match_date_short.group(1))
day = int(match_date_short.group(2))
# 检查月份值如果大于12可能是欧式日期格式DD/MM
if month > 12:
month, day = day, month
# 由于没有年份,使用一个较低的基数,确保任何有年份的日期都排在后面
return month * 100 + day
# 3.5 年期格式,如"2025年14期"
match_year_issue = re.search(r'(20\d{2})[年].*?(\d+)[期]', filename)
if match_year_issue:
year = int(match_year_issue.group(1))
issue = int(match_year_issue.group(2))
return year * 1000 + issue
# 4. 纯数字格式(文件名开头是纯数字)
match_num = re.match(r'^(\d+)', file_name_without_ext)
if match_num:
return int(match_num.group(1))
# 6. 默认使用更新时间
try:
return file.get("last_update_at", 0)
except:
return 0
# 过滤出非目录文件,并且排除已经符合命名规则的文件
files_to_process = [
f for f in share_detail["list"]
if not f["dir"] and not re.match(regex_pattern, f["file_name"])
]
# 根据提取的排序值进行排序
sorted_files = sorted(files_to_process, key=extract_sorting_value)
# 应用过滤词过滤
filterwords = regex.get("filterwords", "")
if filterwords:
# 同时支持中英文逗号分隔
filterwords = filterwords.replace("", ",")
filterwords_list = [word.strip() for word in filterwords.split(',')]
for item in sorted_files:
# 被过滤的文件不会有file_name_re与不匹配正则的文件显示一致
if any(word in item['file_name'] for word in filterwords_list):
item["filtered"] = True
# 为每个文件分配序号
for file in sorted_files:
if not file.get("filtered"):
# 获取文件扩展名
file_ext = os.path.splitext(file["file_name"])[1]
# 生成预览文件名
file["file_name_re"] = sequence_pattern.replace("{}", f"{current_sequence:02d}") + file_ext
current_sequence += 1
return share_detail
else:
# 普通正则命名预览
pattern, replace = account.magic_regex_func(
regex.get("pattern", ""),
regex.get("replace", ""),
regex.get("taskname", ""),
regex.get("magic_regex", {}),
)
# 应用过滤词过滤
filterwords = 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")
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 +547,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 +575,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,43 +583,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
def filter_files(files, filterwords):
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)
# 更新配置
Config.write_json(CONFIG_PATH, config_data)
if __name__ == "__main__":

File diff suppressed because it is too large Load Diff

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": "",
},
}
@ -59,7 +64,7 @@ def send_ql_notify(title, body):
def add_notify(text):
global NOTIFYS
NOTIFYS.append(text)
print("📢", text)
print(text)
return text
@ -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,34 +632,80 @@ 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):
# 判断资源失效记录
if task.get("shareurl_ban"):
print(f"{task['taskname']}》:{task['shareurl_ban']}")
print(f"分享资源已失效:{task['shareurl_ban']}")
add_notify(f"❗《{task['taskname']}》分享资源已失效:{task['shareurl_ban']}\n")
return
# 链接转换所需参数
pwd_id, passcode, pdir_fid = self.get_id_from_url(task["shareurl"])
# print("match: ", pwd_id, pdir_fid)
# 获取stoken同时可验证资源是否失效
# 提取链接参数
pwd_id, passcode, pdir_fid, paths = self.extract_url(task["shareurl"])
if not pwd_id:
task["shareurl_ban"] = f"提取链接参数失败,请检查分享链接是否有效"
print(f"提取链接参数失败,请检查分享链接是否有效")
return
# 获取分享详情
is_sharing, stoken = self.get_stoken(pwd_id, passcode)
if not is_sharing:
add_notify(f"❌《{task['taskname']}》:{stoken}\n")
task["shareurl_ban"] = stoken
print(f"分享详情获取失败:{stoken}")
add_notify(f"❗《{task['taskname']}》分享详情获取失败:{stoken}\n")
return
# print("stoken: ", stoken)
share_detail = self.get_detail(pwd_id, stoken, pdir_fid, _fetch_share=1)
# 获取保存路径fid
savepath = task["savepath"]
if not self.savepath_fid.get(savepath):
# 检查规范化路径是否已在字典中
norm_savepath = re.sub(r"/{2,}", "/", f"/{savepath}")
if norm_savepath != savepath and self.savepath_fid.get(norm_savepath):
self.savepath_fid[savepath] = self.savepath_fid[norm_savepath]
else:
savepath_fids = self.get_fids([savepath])
if not savepath_fids:
print(f"保存路径不存在,准备新建:{savepath}")
mkdir_result = self.mkdir(savepath)
if mkdir_result["code"] == 0:
self.savepath_fid[savepath] = mkdir_result["data"]["fid"]
print(f"保存路径新建成功:{savepath}")
else:
print(f"保存路径新建失败:{mkdir_result['message']}")
return
else:
# 路径已存在直接设置fid
self.savepath_fid[savepath] = savepath_fids[0]["fid"]
updated_tree = self.dir_check_and_save(task, pwd_id, stoken, pdir_fid)
if updated_tree.size(1) > 0:
add_notify(f"✅《{task['taskname']}》添加追更:\n{updated_tree}")
return updated_tree
# 支持顺序命名模式
if task.get("use_sequence_naming") and task.get("sequence_naming"):
# 顺序命名模式下已经在do_save中打印了顺序命名信息这里不再重复打印
# 设置正则模式为空
task["regex_pattern"] = None
# 构建顺序命名的正则表达式
sequence_pattern = task["sequence_naming"]
# 将{}替换为(\d+)用于匹配
regex_pattern = re.escape(sequence_pattern).replace('\\{\\}', '(\\d+)')
task["regex_pattern"] = regex_pattern
else:
print(f"任务结束:没有新的转存任务")
# 正则命名模式
pattern, replace = self.magic_regex_func(
task.get("pattern", ""), task.get("replace", ""), task["taskname"]
)
# 注释掉这里的正则表达式打印因为在do_save函数中已经打印了
# 只有在非魔法变量情况下才显示展开后的正则表达式
# 对于魔法变量($TV等),显示原始输入
# if pattern and task.get("pattern") and task.get("pattern") not in CONFIG_DATA.get("magic_regex", MAGIC_REGEX):
# print(f"正则匹配: {pattern}")
# print(f"正则替换: {replace}")
# 保存文件
tree = self.dir_check_and_save(task, pwd_id, stoken, pdir_fid)
# 检查是否有新文件转存
if tree and tree.size() <= 1: # 只有根节点意味着没有新文件
return False
return tree
def dir_check_and_save(self, task, pwd_id, stoken, pdir_fid="", subdir_path=""):
tree = Tree()
@ -644,25 +727,40 @@ class Quark:
share_file_list = self.get_detail(
pwd_id, stoken, share_file_list[0]["fid"]
)["list"]
# 应用过滤词过滤文件
# 应用过滤词过滤
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)]
print(f"📑 应用过滤词:{task['filterwords']},剩余{len(share_file_list)}个文件")
print(f"📑 应用过滤词: {task['filterwords']},剩余{len(share_file_list)}个文件")
print()
# 获取目标目录文件列表
savepath = re.sub(r"/{2,}", "/", f"/{task['savepath']}{subdir_path}")
if not self.savepath_fid.get(savepath):
if get_fids := self.get_fids([savepath]):
self.savepath_fid[savepath] = get_fids[0]["fid"]
# 检查规范化路径是否已在字典中
norm_savepath = re.sub(r"/{2,}", "/", f"/{savepath}")
if norm_savepath != savepath and self.savepath_fid.get(norm_savepath):
self.savepath_fid[savepath] = self.savepath_fid[norm_savepath]
else:
print(f"❌ 目录 {savepath} fid获取失败跳过转存")
return tree
savepath_fids = self.get_fids([savepath])
if not savepath_fids:
print(f"保存路径不存在,准备新建:{savepath}")
mkdir_result = self.mkdir(savepath)
if mkdir_result["code"] == 0:
self.savepath_fid[savepath] = mkdir_result["data"]["fid"]
print(f"保存路径新建成功:{savepath}")
else:
print(f"保存路径新建失败:{mkdir_result['message']}")
return
else:
# 路径已存在直接设置fid
self.savepath_fid[savepath] = savepath_fids[0]["fid"]
to_pdir_fid = self.savepath_fid[savepath]
dir_file_list = self.ls_dir(to_pdir_fid)
# print("dir_file_list: ", dir_file_list)
tree.create_node(
savepath,
pdir_fid,
@ -671,72 +769,326 @@ class Quark:
},
)
# 需保存的文件清单
need_save_list = []
# 添加符合的
for share_file in share_file_list:
if share_file["dir"] and task.get("update_subdir", False):
pattern, replace = task["update_subdir"], ""
else:
pattern, replace = self.magic_regex_func(
task["pattern"], task["replace"], task["taskname"]
)
# 正则文件名匹配
if re.search(pattern, share_file["file_name"]):
# 替换后的文件名
save_name = (
re.sub(pattern, replace, share_file["file_name"])
if replace != ""
else share_file["file_name"]
)
# 忽略后缀
if task.get("ignore_extension") and not share_file["dir"]:
compare_func = lambda a, b1, b2: (
os.path.splitext(a)[0] == os.path.splitext(b1)[0]
or os.path.splitext(a)[0] == os.path.splitext(b2)[0]
)
else:
compare_func = lambda a, b1, b2: (a == b1 or a == b2)
# 判断目标目录文件是否存在
# 处理顺序命名模式
if task.get("use_sequence_naming") and task.get("sequence_naming"):
# 顺序命名模式
current_sequence = 1
sequence_pattern = task["sequence_naming"]
regex_pattern = task.get("regex_pattern")
# 查找目录中现有的最大序号
for dir_file in dir_file_list:
if not dir_file["dir"]: # 只检查文件
if matches := re.match(regex_pattern, dir_file["file_name"]):
try:
seq_num = int(matches.group(1))
current_sequence = max(current_sequence, seq_num + 1)
except (ValueError, IndexError):
pass
# 构建目标目录中所有文件的查重索引(按大小和修改时间)
dir_files_map = {}
for dir_file in dir_file_list:
if not dir_file["dir"]: # 仅处理文件
file_size = dir_file.get("size", 0)
file_ext = os.path.splitext(dir_file["file_name"])[1].lower()
update_time = dir_file.get("updated_at", 0)
# 创建大小+扩展名的索引,用于快速查重
key = f"{file_size}_{file_ext}"
if key not in dir_files_map:
dir_files_map[key] = []
dir_files_map[key].append({
"file_name": dir_file["file_name"],
"updated_at": update_time,
})
# 预先过滤掉已经存在的文件(按大小和扩展名比对)
filtered_share_files = []
for share_file in share_file_list:
if share_file["dir"]:
# 处理子目录
if task.get("update_subdir") and re.search(task["update_subdir"], share_file["file_name"]):
filtered_share_files.append(share_file)
continue
file_size = share_file.get("size", 0)
file_ext = os.path.splitext(share_file["file_name"])[1].lower()
share_update_time = share_file.get("last_update_at", 0)
# 检查是否已存在相同大小和扩展名的文件
key = f"{file_size}_{file_ext}"
is_duplicate = False
if key in dir_files_map:
for existing_file in dir_files_map[key]:
existing_update_time = existing_file.get("updated_at", 0)
# 如果修改时间相近30天内或者差距不大10%以内),认为是同一个文件
if (abs(share_update_time - existing_update_time) < 2592000 or
abs(1 - (share_update_time / existing_update_time if existing_update_time else 1)) < 0.1):
is_duplicate = True
break
# 只有非重复文件才进行处理
if not is_duplicate:
filtered_share_files.append(share_file)
# 指定文件开始订阅/到达指定文件(含)结束历遍
if share_file["fid"] == task.get("startfid", ""):
break
# 实现高级排序算法
def extract_sorting_value(file):
if file["dir"]: # 跳过文件夹
return float('inf')
filename = file["file_name"]
# 提取文件名,不含扩展名
file_name_without_ext = os.path.splitext(filename)[0]
# 1. "第X期/集/话" 格式
match_chinese = re.search(r'第(\d+)[期集话]', filename)
episode_num = int(match_chinese.group(1)) if match_chinese else 0
# 5. 文件名含"上中下"(优先处理,因为可能与其他格式同时存在)
if match_chinese:
# 如果同时存在集数和上中下,则按照集数*10+位置排序
if '' in filename:
return episode_num * 10 + 1
elif '' in filename:
return episode_num * 10 + 2
elif '' in filename:
return episode_num * 10 + 3
elif '' in filename:
return 1
elif '' in filename:
return 2
elif '' in filename:
return 3
# 如果已经匹配到"第X期/集/话"格式,直接返回
if episode_num > 0:
return episode_num * 10
# 2.1 S01E01 格式,提取季数和集数
match_s_e = re.search(r'[Ss](\d+)[Ee](\d+)', filename)
if match_s_e:
season = int(match_s_e.group(1))
episode = int(match_s_e.group(2))
return season * 1000 + episode
# 2.2 E01 格式,仅提取集数
match_e = re.search(r'[Ee][Pp]?(\d+)', filename)
if match_e:
return int(match_e.group(1))
# 2.3 1x01 格式,提取季数和集数
match_x = re.search(r'(\d+)[Xx](\d+)', filename)
if match_x:
season = int(match_x.group(1))
episode = int(match_x.group(2))
return season * 1000 + episode
# 3. 日期格式识别(支持多种格式)
# 3.1 完整的YYYYMMDD格式
match_date_compact = re.search(r'(20\d{2})(\d{2})(\d{2})', filename)
if match_date_compact:
year = int(match_date_compact.group(1))
month = int(match_date_compact.group(2))
day = int(match_date_compact.group(3))
return year * 10000 + month * 100 + day
# 3.2 YYYY-MM-DD 或 YYYY.MM.DD 或 YYYY/MM/DD 格式
match_date_full = re.search(r'(20\d{2})[-./](\d{1,2})[-./](\d{1,2})', filename)
if match_date_full:
year = int(match_date_full.group(1))
month = int(match_date_full.group(2))
day = int(match_date_full.group(3))
return year * 10000 + month * 100 + day
# 3.3 MM/DD/YYYY 或 DD/MM/YYYY 格式
match_date_alt = re.search(r'(\d{1,2})[-./](\d{1,2})[-./](20\d{2})', filename)
if match_date_alt:
# 假设第一个是月,第二个是日(美式日期)
# 在实际应用中可能需要根据具体情况调整
month = int(match_date_alt.group(1))
day = int(match_date_alt.group(2))
year = int(match_date_alt.group(3))
# 检查月份值如果大于12可能是欧式日期格式DD/MM/YYYY
if month > 12:
month, day = day, month
return year * 10000 + month * 100 + day
# 3.4 MM/DD 格式(无年份),假设为当前年
match_date_short = re.search(r'(\d{1,2})[-./](\d{1,2})', filename)
if match_date_short:
# 假设第一个是月,第二个是日
month = int(match_date_short.group(1))
day = int(match_date_short.group(2))
# 检查月份值如果大于12可能是欧式日期格式DD/MM
if month > 12:
month, day = day, month
# 由于没有年份,使用一个较低的基数,确保任何有年份的日期都排在后面
return month * 100 + day
# 3.5 年期格式,如"2025年14期"
match_year_issue = re.search(r'(20\d{2})[年].*?(\d+)[期]', filename)
if match_year_issue:
year = int(match_year_issue.group(1))
issue = int(match_year_issue.group(2))
return year * 1000 + issue
# 4. 纯数字格式(文件名开头是纯数字)
match_num = re.match(r'^(\d+)', file_name_without_ext)
if match_num:
return int(match_num.group(1))
# 6. 默认使用更新时间
try:
return file.get("updated_at", file.get("last_update_at", 0))
except:
return 0
# 过滤出非目录文件,排除已经排除掉的重复文件,然后排序
files_to_process = [
f for f in filtered_share_files
if not f["dir"] and not re.match(regex_pattern, f["file_name"])
]
# 根据提取的排序值进行排序
sorted_files = sorted(files_to_process, key=extract_sorting_value)
# 需保存的文件清单
need_save_list = []
# 为每个文件分配序号
for share_file in sorted_files:
# 获取文件扩展名
file_ext = os.path.splitext(share_file["file_name"])[1]
# 生成新文件名
save_name = sequence_pattern.replace("{}", f"{current_sequence:02d}") + file_ext
# 检查目标目录是否已存在此文件
file_exists = any(
compare_func(
dir_file["file_name"], share_file["file_name"], save_name
)
for dir_file in dir_file_list
dir_file["file_name"] == save_name for dir_file in dir_file_list
)
if not file_exists:
# 不打印保存信息
share_file["save_name"] = save_name
share_file["original_name"] = share_file["file_name"] # 保存原文件名,用于排序
need_save_list.append(share_file)
elif share_file["dir"]:
# 存在并是一个文件夹
if task.get("update_subdir", False):
if re.search(task["update_subdir"], share_file["file_name"]):
print(f"检查子文件夹:{savepath}/{share_file['file_name']}")
subdir_tree = self.dir_check_and_save(
task,
pwd_id,
stoken,
current_sequence += 1
# 指定文件开始订阅/到达指定文件(含)结束历遍
if share_file["fid"] == task.get("startfid", ""):
break
# 处理子文件夹
for share_file in share_file_list:
if share_file["dir"] and task.get("update_subdir", False):
if re.search(task["update_subdir"], share_file["file_name"]):
print(f"检查子文件夹: {savepath}/{share_file['file_name']}")
subdir_tree = self.dir_check_and_save(
task,
pwd_id,
stoken,
share_file["fid"],
f"{subdir_path}/{share_file['file_name']}",
)
if subdir_tree.size(1) > 0:
# 合并子目录树
tree.create_node(
"📁" + share_file["file_name"],
share_file["fid"],
f"{subdir_path}/{share_file['file_name']}",
parent=pdir_fid,
data={
"is_dir": share_file["dir"],
},
)
if subdir_tree.size(1) > 0:
# 合并子目录树
tree.create_node(
"📁" + share_file["file_name"],
tree.merge(share_file["fid"], subdir_tree, deep=False)
else:
# 正则命名模式
need_save_list = []
# 添加符合的
for share_file in share_file_list:
if share_file["dir"] and task.get("update_subdir", False):
pattern, replace = task["update_subdir"], ""
else:
pattern, replace = self.magic_regex_func(
task.get("pattern", ""), task.get("replace", ""), task["taskname"]
)
# 正则文件名匹配
if re.search(pattern, share_file["file_name"]):
# 替换后的文件名
save_name = (
re.sub(pattern, replace, share_file["file_name"])
if replace != ""
else share_file["file_name"]
)
# 忽略后缀
if task.get("ignore_extension") and not share_file["dir"]:
compare_func = lambda a, b1, b2: (
os.path.splitext(a)[0] == os.path.splitext(b1)[0]
or os.path.splitext(a)[0] == os.path.splitext(b2)[0]
)
else:
compare_func = lambda a, b1, b2: (a == b1 or a == b2)
# 判断目标目录文件是否存在
file_exists = False
for dir_file in dir_file_list:
if compare_func(
dir_file["file_name"], share_file["file_name"], save_name
):
file_exists = True
# 删除对文件打印部分
break
if not file_exists:
# 不打印保存信息
share_file["save_name"] = save_name
share_file["original_name"] = share_file["file_name"] # 保存原文件名,用于排序
need_save_list.append(share_file)
elif share_file["dir"]:
# 存在并是一个文件夹
if task.get("update_subdir", False):
if re.search(task["update_subdir"], share_file["file_name"]):
print(f"检查子文件夹: {savepath}/{share_file['file_name']}")
subdir_tree = self.dir_check_and_save(
task,
pwd_id,
stoken,
share_file["fid"],
parent=pdir_fid,
data={
"is_dir": share_file["dir"],
},
f"{subdir_path}/{share_file['file_name']}",
)
tree.merge(share_file["fid"], subdir_tree, deep=False)
# 指定文件开始订阅/到达指定文件(含)结束历遍
if share_file["fid"] == task.get("startfid", ""):
break
if subdir_tree.size(1) > 0:
# 合并子目录树
tree.create_node(
"📁" + share_file["file_name"],
share_file["fid"],
parent=pdir_fid,
data={
"is_dir": share_file["dir"],
},
)
tree.merge(share_file["fid"], subdir_tree, deep=False)
# 指定文件开始订阅/到达指定文件(含)结束历遍
if share_file["fid"] == task.get("startfid", ""):
break
fid_list = [item["fid"] for item in need_save_list]
fid_token_list = [item["share_fid_token"] for item in need_save_list]
if fid_list:
# 只在有新文件需要转存时才打印目录文件列表
print(f"📂 目标目录:{savepath} ({len(dir_file_list)}个文件)")
for file in dir_file_list:
print(f" {file['file_name']}")
print()
save_file_return = self.save_file(
fid_list, fid_token_list, to_pdir_fid, pwd_id, stoken
)
@ -746,12 +1098,14 @@ class Quark:
query_task_return = self.query_task(task_id)
if query_task_return["code"] == 0:
# 建立目录树
saved_files = []
for index, item in enumerate(need_save_list):
icon = (
"📁"
if item["dir"] == True
else "🎞️" if item["obj_category"] == "video" else ""
)
saved_files.append(f"{icon}{item['save_name']}")
tree.create_node(
f"{icon}{item['save_name']}",
item["fid"],
@ -762,22 +1116,188 @@ class Quark:
"is_dir": item["dir"],
},
)
# 添加成功通知
add_notify(f"✅《{task['taskname']}》 添加追更:\n/{task['savepath']}{subdir_path}")
# 打印保存文件列表
for idx, file_name in enumerate(saved_files):
prefix = "├── " if idx < len(saved_files) - 1 else "└── "
add_notify(f"{prefix}{file_name}")
add_notify("")
else:
err_msg = query_task_return["message"]
else:
err_msg = save_file_return["message"]
if err_msg:
add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n")
else:
# 没有新文件需要转存
if not subdir_path: # 只在顶层(非子目录)打印一次消息
pass
return tree
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):
# 路径已存在直接设置fid
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))
# 如果同时存在集数和上中下,则按照集数*10+位置排序
if '' in file_name:
return (episode_num, 1, file.get("created_at", 0))
elif '' in file_name:
return (episode_num, 2, file.get("created_at", 0))
elif '' in file_name:
return (episode_num, 3, file.get("created_at", 0))
return (episode_num, 10, file.get("created_at", 0))
# 如果文件名中包含"上中下",优先处理
if '' in file_name:
return (0, 1, file.get("created_at", 0))
elif '' in file_name:
return (0, 2, file.get("created_at", 0))
elif '' in file_name:
return (0, 3, file.get("created_at", 0))
# 尝试匹配常见视频格式 S01E01, E01, 1x01 等
if 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][Pp]?(\d+)', file_name):
match = re.search(r'[Ee][Pp]?(\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
# 3. 日期格式识别(支持多种格式)
# 3.1 完整的YYYYMMDD格式
match_date_compact = re.search(r'(20\d{2})(\d{2})(\d{2})', file_name)
if match_date_compact:
year = int(match_date_compact.group(1))
month = int(match_date_compact.group(2))
day = int(match_date_compact.group(3))
return (year * 10000 + month * 100 + day, 0, file.get("created_at", 0))
# 3.2 YYYY-MM-DD 或 YYYY.MM.DD 或 YYYY/MM/DD 格式
match_date_full = re.search(r'(20\d{2})[-./](\d{1,2})[-./](\d{1,2})', file_name)
if match_date_full:
year = int(match_date_full.group(1))
month = int(match_date_full.group(2))
day = int(match_date_full.group(3))
return (year * 10000 + month * 100 + day, 0, file.get("created_at", 0))
# 3.3 MM/DD/YYYY 或 DD/MM/YYYY 格式
match_date_alt = re.search(r'(\d{1,2})[-./](\d{1,2})[-./](20\d{2})', file_name)
if match_date_alt:
# 假设第一个是月,第二个是日(美式日期)
month = int(match_date_alt.group(1))
day = int(match_date_alt.group(2))
year = int(match_date_alt.group(3))
# 检查月份值如果大于12可能是欧式日期格式DD/MM/YYYY
if month > 12:
month, day = day, month
return (year * 10000 + month * 100 + day, 0, file.get("created_at", 0))
# 3.4 MM/DD 格式(无年份)
match_date_short = re.search(r'(\d{1,2})[-./](\d{1,2})', file_name)
if match_date_short:
# 假设第一个是月,第二个是日
month = int(match_date_short.group(1))
day = int(match_date_short.group(2))
# 检查月份值如果大于12可能是欧式日期格式DD/MM
if month > 12:
month, day = day, month
return (month * 100 + day, 0, file.get("created_at", 0))
# 3.5 年期格式,如"2025年14期"
match_year_issue = re.search(r'(20\d{2})[年].*?(\d+)[期]', file_name)
if match_year_issue:
year = int(match_year_issue.group(1))
issue = int(match_year_issue.group(2))
return (year * 1000 + issue, 0, file.get("created_at", 0))
# 默认使用数字排序或创建时间
match_num = re.match(r'^(\d+)', os.path.splitext(file_name)[0])
if match_num:
return (int(match_num.group(1)), 0, file.get("created_at", 0))
# 最后按创建时间排序
return (0, 0, file.get("created_at", 0))
# 按自定义逻辑排序
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
# 非顺序命名模式,使用普通正则重命名
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
savepath = re.sub(r"/{2,}", "/", f"/{task['savepath']}{subdir_path}")
if not self.savepath_fid.get(savepath):
# 路径已存在直接设置fid
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]
@ -798,11 +1318,11 @@ class Quark:
):
rename_return = self.rename(dir_file["fid"], save_name)
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
else:
print(
f"重命名{dir_file['file_name']}{save_name} 失败,{rename_return['message']}"
f"重命名: {dir_file['file_name']}{save_name} 失败,{rename_return['message']}"
)
return is_rename_count > 0
@ -875,7 +1395,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 (
@ -883,29 +1403,38 @@ 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(f"正则匹配: {task['pattern']}")
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()
print()
print(f"#{index+1}------------------")
print(f"任务名称: {task['taskname']}")
print(f"分享链接: {task['shareurl']}")
print(f"保存路径: {task['savepath']}")
# 根据命名模式显示不同信息
if task.get("use_sequence_naming") and task.get("sequence_naming"):
print(f"顺序命名: {task['sequence_naming']}")
else:
# 正则命名模式
if task.get("pattern"):
print(f"正则匹配: {task['pattern']}")
if task.get("replace") is not None: # 显示替换规则,即使为空字符串
print(f"正则替换: {task['replace']}")
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)
@ -934,6 +1463,9 @@ def do_save(account, tasklist=[]):
task = (
plugin.run(task, account=account, tree=is_new_tree) or task
)
elif is_new_tree is False: # 明确没有新文件
print(f"任务完成:没有新的文件需要转存")
print()
print()
@ -945,7 +1477,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"):
@ -962,9 +1500,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
@ -977,7 +1514,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:
@ -988,11 +1525,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:
@ -1002,8 +1538,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