mirror of
https://github.com/Cp0204/quark-auto-save.git
synced 2026-01-12 07:10:44 +08:00
新增文件整理功能,优化命名规则切换和部分排序逻辑
This commit is contained in:
parent
ce8f0c94b9
commit
7b019ab1e0
494
app/run.py
494
app/run.py
@ -591,6 +591,22 @@ def get_share_detail():
|
||||
episode_pattern = regex.get("episode_naming")
|
||||
episode_patterns = regex.get("episode_patterns", [])
|
||||
|
||||
# 获取默认的剧集模式
|
||||
default_episode_pattern = {"regex": '第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?'}
|
||||
|
||||
# 获取配置的剧集模式,确保每个模式都是字典格式
|
||||
episode_patterns = []
|
||||
raw_patterns = config_data.get("episode_patterns", [default_episode_pattern])
|
||||
for p in raw_patterns:
|
||||
if isinstance(p, dict) and p.get("regex"):
|
||||
episode_patterns.append(p)
|
||||
elif isinstance(p, str):
|
||||
episode_patterns.append({"regex": p})
|
||||
|
||||
# 如果没有有效的模式,使用默认模式
|
||||
if not episode_patterns:
|
||||
episode_patterns = [default_episode_pattern]
|
||||
|
||||
# 添加中文数字匹配模式
|
||||
chinese_patterns = [
|
||||
{"regex": r'第([一二三四五六七八九十百千万零两]+)集'},
|
||||
@ -600,74 +616,7 @@ def get_share_detail():
|
||||
{"regex": r'([一二三四五六七八九十百千万零两]+)期'},
|
||||
{"regex": r'([一二三四五六七八九十百千万零两]+)话'}
|
||||
]
|
||||
|
||||
# 合并中文模式到episode_patterns
|
||||
if episode_patterns:
|
||||
episode_patterns.extend(chinese_patterns)
|
||||
else:
|
||||
episode_patterns = chinese_patterns
|
||||
|
||||
# 调用全局的集编号提取函数
|
||||
def extract_episode_number_local(filename):
|
||||
return extract_episode_number(filename, episode_patterns=episode_patterns)
|
||||
|
||||
# 构建剧集命名的正则表达式 (主要用于检测已命名文件)
|
||||
if episode_pattern == "[]":
|
||||
# 对于单独的[],使用特殊匹配
|
||||
regex_pattern = "^(\\d+)$" # 匹配纯数字文件名
|
||||
elif "[]" in episode_pattern:
|
||||
# 特殊处理E[]、EP[]等常见格式,使用更宽松的匹配方式
|
||||
if episode_pattern == "E[]":
|
||||
# 对于E[]格式,只检查文件名中是否包含形如E01的部分
|
||||
regex_pattern = "^E(\\d+)$" # 只匹配纯E+数字的文件名格式
|
||||
elif episode_pattern == "EP[]":
|
||||
# 对于EP[]格式,只检查文件名中是否包含形如EP01的部分
|
||||
regex_pattern = "^EP(\\d+)$" # 只匹配纯EP+数字的文件名格式
|
||||
else:
|
||||
# 对于其他带[]的格式,使用常规转义和替换
|
||||
regex_pattern = re.escape(episode_pattern).replace('\\[\\]', '(\\d+)')
|
||||
else:
|
||||
# 如果输入模式不包含[],则使用简单匹配模式,避免正则表达式错误
|
||||
regex_pattern = "^" + re.escape(episode_pattern) + "(\\d+)$"
|
||||
|
||||
# 实现高级排序算法
|
||||
def extract_sorting_value(file):
|
||||
if file["dir"]: # 跳过文件夹
|
||||
return (float('inf'), 0, 0, 0) # 返回元组以确保类型一致性
|
||||
|
||||
filename = file["file_name"]
|
||||
|
||||
# 尝试获取剧集序号
|
||||
episode_num = extract_episode_number_local(filename)
|
||||
if episode_num is not None:
|
||||
# 返回元组以确保类型一致性
|
||||
return (0, episode_num, 0, 0)
|
||||
|
||||
# 如果无法提取剧集号,则使用通用的排序函数
|
||||
return sort_file_by_name(file)
|
||||
|
||||
# 过滤出非目录文件,并且排除已经符合命名规则的文件
|
||||
files_to_process = []
|
||||
for f in share_detail["list"]:
|
||||
if f["dir"]:
|
||||
continue # 跳过目录
|
||||
|
||||
# 检查文件是否已符合命名规则
|
||||
if episode_pattern == "[]":
|
||||
# 对于单独的[],检查文件名是否为纯数字
|
||||
file_name_without_ext = os.path.splitext(f["file_name"])[0]
|
||||
if file_name_without_ext.isdigit():
|
||||
# 增加判断:如果是日期格式的纯数字,不视为已命名
|
||||
if not is_date_format(file_name_without_ext):
|
||||
continue # 跳过已符合命名规则的文件
|
||||
elif re.match(regex_pattern, f["file_name"]):
|
||||
continue # 跳过已符合命名规则的文件
|
||||
|
||||
# 添加到待处理文件列表
|
||||
files_to_process.append(f)
|
||||
|
||||
# 根据提取的排序值进行排序
|
||||
sorted_files = sorted(files_to_process, key=extract_sorting_value)
|
||||
episode_patterns.extend(chinese_patterns)
|
||||
|
||||
# 应用过滤词过滤
|
||||
filterwords = regex.get("filterwords", "")
|
||||
@ -675,32 +624,25 @@ def get_share_detail():
|
||||
# 同时支持中英文逗号分隔
|
||||
filterwords = filterwords.replace(",", ",")
|
||||
filterwords_list = [word.strip() for word in filterwords.split(',')]
|
||||
for item in sorted_files:
|
||||
# 被过滤的文件不会有file_name_re,与不匹配正则的文件显示一致
|
||||
for item in share_detail["list"]:
|
||||
# 被过滤的文件显示一个 ×
|
||||
if any(word in item['file_name'] for word in filterwords_list):
|
||||
item["filtered"] = True
|
||||
item["file_name_re"] = "×"
|
||||
|
||||
# 为每个文件生成新文件名并存储剧集编号用于排序
|
||||
for file in sorted_files:
|
||||
if not file.get("filtered"):
|
||||
# 获取文件扩展名
|
||||
file_ext = os.path.splitext(file["file_name"])[1]
|
||||
# 尝试提取剧集号
|
||||
episode_num = extract_episode_number_local(file["file_name"])
|
||||
if episode_num is not None:
|
||||
# 生成预览文件名
|
||||
if episode_pattern == "[]":
|
||||
# 对于单独的[],直接使用数字序号作为文件名
|
||||
file["file_name_re"] = f"{episode_num:02d}{file_ext}"
|
||||
else:
|
||||
file["file_name_re"] = episode_pattern.replace("[]", f"{episode_num:02d}") + file_ext
|
||||
# 存储原始的剧集编号,用于数值排序
|
||||
file["episode_number"] = episode_num
|
||||
else:
|
||||
# 无法提取剧集号,标记为无法处理
|
||||
file["file_name_re"] = "❌ 无法识别剧集号"
|
||||
file["episode_number"] = 9999999 # 给一个很大的值,确保排在最后
|
||||
# 处理未被过滤的文件
|
||||
for file in share_detail["list"]:
|
||||
if not file["dir"] and not file.get("filtered"): # 只处理未被过滤的非目录文件
|
||||
extension = os.path.splitext(file["file_name"])[1]
|
||||
# 从文件名中提取集号
|
||||
episode_num = extract_episode_number(file["file_name"], episode_patterns=episode_patterns)
|
||||
|
||||
if episode_num is not None:
|
||||
file["file_name_re"] = episode_pattern.replace("[]", f"{episode_num:02d}") + extension
|
||||
else:
|
||||
# 没有提取到集号,显示无法识别的提示
|
||||
file["file_name_re"] = "× 无法识别剧集编号"
|
||||
|
||||
return share_detail
|
||||
else:
|
||||
# 普通正则命名预览
|
||||
@ -990,7 +932,7 @@ def get_history_records():
|
||||
# 如果请求所有任务名称,单独查询并返回
|
||||
if get_all_task_names:
|
||||
cursor = db.conn.cursor()
|
||||
cursor.execute("SELECT DISTINCT task_name FROM transfer_records ORDER BY task_name")
|
||||
cursor.execute("SELECT DISTINCT task_name FROM transfer_records WHERE task_name NOT IN ('rename', 'undo_rename') ORDER BY task_name")
|
||||
all_task_names = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# 如果同时请求分页数据,继续常规查询
|
||||
@ -1001,7 +943,8 @@ def get_history_records():
|
||||
sort_by=sort_by,
|
||||
order=order,
|
||||
task_name_filter=task_name_filter,
|
||||
keyword_filter=keyword_filter
|
||||
keyword_filter=keyword_filter,
|
||||
exclude_task_names=["rename", "undo_rename"]
|
||||
)
|
||||
# 添加所有任务名称到结果中
|
||||
result["all_task_names"] = all_task_names
|
||||
@ -1021,7 +964,8 @@ def get_history_records():
|
||||
sort_by=sort_by,
|
||||
order=order,
|
||||
task_name_filter=task_name_filter,
|
||||
keyword_filter=keyword_filter
|
||||
keyword_filter=keyword_filter,
|
||||
exclude_task_names=["rename", "undo_rename"]
|
||||
)
|
||||
|
||||
# 处理记录格式化
|
||||
@ -1233,7 +1177,371 @@ def reset_folder():
|
||||
return jsonify({"success": False, "message": f"重置文件夹时出错: {str(e)}"})
|
||||
|
||||
|
||||
# 获取文件列表
|
||||
@app.route("/file_list")
|
||||
def get_file_list():
|
||||
if not is_login():
|
||||
return jsonify({"success": False, "message": "未登录"})
|
||||
|
||||
# 获取请求参数
|
||||
folder_id = request.args.get("folder_id", "root")
|
||||
sort_by = request.args.get("sort_by", "file_name")
|
||||
order = request.args.get("order", "asc")
|
||||
page = int(request.args.get("page", 1))
|
||||
page_size = int(request.args.get("page_size", 15))
|
||||
|
||||
try:
|
||||
# 初始化夸克网盘客户端
|
||||
account = Quark(config_data["cookie"][0], 0)
|
||||
|
||||
# 获取文件列表
|
||||
if folder_id == "root":
|
||||
folder_id = "0" # 根目录的ID为0
|
||||
paths = [] # 根目录没有路径
|
||||
else:
|
||||
# 获取当前文件夹的路径
|
||||
paths = account.get_paths(folder_id)
|
||||
|
||||
# 获取文件列表
|
||||
files = account.ls_dir(folder_id)
|
||||
if isinstance(files, dict) and files.get("error"):
|
||||
return jsonify({"success": False, "message": f"获取文件列表失败: {files.get('error', '未知错误')}"})
|
||||
|
||||
# 计算总数
|
||||
total = len(files)
|
||||
|
||||
# 排序
|
||||
if sort_by == "file_name":
|
||||
files.sort(key=lambda x: x["file_name"].lower())
|
||||
elif sort_by == "file_size":
|
||||
files.sort(key=lambda x: x["size"] if not x["dir"] else 0)
|
||||
else: # updated_at
|
||||
files.sort(key=lambda x: x["updated_at"])
|
||||
|
||||
if order == "desc":
|
||||
files.reverse()
|
||||
|
||||
# 根据排序字段决定是否将目录放在前面
|
||||
if sort_by == "updated_at":
|
||||
# 修改日期排序时严格按照日期排序,不区分文件夹和文件
|
||||
sorted_files = files
|
||||
else:
|
||||
# 其他排序时目录始终在前面
|
||||
directories = [f for f in files if f["dir"]]
|
||||
normal_files = [f for f in files if not f["dir"]]
|
||||
sorted_files = directories + normal_files
|
||||
|
||||
# 分页
|
||||
start_idx = (page - 1) * page_size
|
||||
end_idx = min(start_idx + page_size, total)
|
||||
paginated_files = sorted_files[start_idx:end_idx]
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": {
|
||||
"list": paginated_files,
|
||||
"total": total,
|
||||
"paths": paths
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f">>> 获取文件列表时出错: {str(e)}")
|
||||
return jsonify({"success": False, "message": f"获取文件列表时出错: {str(e)}"})
|
||||
|
||||
|
||||
# 预览重命名
|
||||
@app.route("/preview_rename")
|
||||
def preview_rename():
|
||||
if not is_login():
|
||||
return jsonify({"success": False, "message": "未登录"})
|
||||
|
||||
# 获取请求参数
|
||||
folder_id = request.args.get("folder_id", "root")
|
||||
pattern = request.args.get("pattern", "")
|
||||
replace = request.args.get("replace", "")
|
||||
naming_mode = request.args.get("naming_mode", "regex") # regex, sequence, episode
|
||||
include_folders = request.args.get("include_folders", "false") == "true"
|
||||
filterwords = request.args.get("filterwords", "")
|
||||
|
||||
if not pattern:
|
||||
pattern = ".*"
|
||||
|
||||
try:
|
||||
# 初始化夸克网盘客户端
|
||||
account = Quark(config_data["cookie"][0], 0)
|
||||
|
||||
# 获取文件列表
|
||||
if folder_id == "root":
|
||||
folder_id = "0" # 根目录的ID为0
|
||||
|
||||
files = account.ls_dir(folder_id)
|
||||
if isinstance(files, dict) and files.get("error"):
|
||||
return jsonify({"success": False, "message": f"获取文件列表失败: {files.get('error', '未知错误')}"})
|
||||
|
||||
# 过滤要排除的文件
|
||||
# 替换中文逗号为英文逗号
|
||||
filterwords = filterwords.replace(",", ",")
|
||||
filter_list = [keyword.strip() for keyword in filterwords.split(",") if keyword.strip()]
|
||||
filtered_files = []
|
||||
for file in files:
|
||||
# 如果不包含文件夹且当前项是文件夹,跳过
|
||||
if not include_folders and file["dir"]:
|
||||
continue
|
||||
|
||||
# 检查是否包含过滤关键词
|
||||
should_filter = False
|
||||
for keyword in filter_list:
|
||||
if keyword and keyword in file["file_name"]:
|
||||
should_filter = True
|
||||
break
|
||||
|
||||
if not should_filter:
|
||||
filtered_files.append(file)
|
||||
|
||||
# 按不同命名模式处理
|
||||
preview_results = []
|
||||
|
||||
if naming_mode == "sequence":
|
||||
# 顺序命名模式
|
||||
# 排序文件(按文件名或修改时间)
|
||||
filtered_files.sort(key=lambda x: sort_file_by_name(x["file_name"]))
|
||||
|
||||
sequence = 1
|
||||
for file in filtered_files:
|
||||
extension = os.path.splitext(file["file_name"])[1] if not file["dir"] else ""
|
||||
new_name = pattern.replace("{}", f"{sequence:02d}") + extension
|
||||
preview_results.append({
|
||||
"original_name": file["file_name"],
|
||||
"new_name": new_name,
|
||||
"file_id": file["fid"]
|
||||
})
|
||||
sequence += 1
|
||||
|
||||
elif naming_mode == "episode":
|
||||
# 剧集命名模式
|
||||
# 获取默认的剧集模式
|
||||
default_episode_pattern = {"regex": '第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?'}
|
||||
|
||||
# 获取配置的剧集模式,确保每个模式都是字典格式
|
||||
episode_patterns = []
|
||||
raw_patterns = config_data.get("episode_patterns", [default_episode_pattern])
|
||||
for p in raw_patterns:
|
||||
if isinstance(p, dict) and p.get("regex"):
|
||||
episode_patterns.append(p)
|
||||
elif isinstance(p, str):
|
||||
episode_patterns.append({"regex": p})
|
||||
|
||||
# 如果没有有效的模式,使用默认模式
|
||||
if not episode_patterns:
|
||||
episode_patterns = [default_episode_pattern]
|
||||
|
||||
# 添加中文数字匹配模式
|
||||
chinese_patterns = [
|
||||
{"regex": r'第([一二三四五六七八九十百千万零两]+)集'},
|
||||
{"regex": r'第([一二三四五六七八九十百千万零两]+)期'},
|
||||
{"regex": r'第([一二三四五六七八九十百千万零两]+)话'},
|
||||
{"regex": r'([一二三四五六七八九十百千万零两]+)集'},
|
||||
{"regex": r'([一二三四五六七八九十百千万零两]+)期'},
|
||||
{"regex": r'([一二三四五六七八九十百千万零两]+)话'}
|
||||
]
|
||||
episode_patterns.extend(chinese_patterns)
|
||||
|
||||
# 处理每个文件
|
||||
for file in filtered_files:
|
||||
extension = os.path.splitext(file["file_name"])[1] if not file["dir"] else ""
|
||||
# 从文件名中提取集号
|
||||
episode_num = extract_episode_number(file["file_name"], episode_patterns=episode_patterns)
|
||||
|
||||
if episode_num is not None:
|
||||
new_name = pattern.replace("[]", f"{episode_num:02d}") + extension
|
||||
preview_results.append({
|
||||
"original_name": file["file_name"],
|
||||
"new_name": new_name,
|
||||
"file_id": file["fid"]
|
||||
})
|
||||
else:
|
||||
# 没有提取到集号,显示无法识别的提示
|
||||
preview_results.append({
|
||||
"original_name": file["file_name"],
|
||||
"new_name": "× 无法识别剧集编号",
|
||||
"file_id": file["fid"]
|
||||
})
|
||||
|
||||
else:
|
||||
# 正则命名模式
|
||||
for file in filtered_files:
|
||||
try:
|
||||
# 应用正则表达式
|
||||
if replace:
|
||||
new_name = re.sub(pattern, replace, file["file_name"])
|
||||
else:
|
||||
# 如果没有提供替换表达式,则检查是否匹配
|
||||
if re.search(pattern, file["file_name"]):
|
||||
new_name = file["file_name"] # 匹配但不替换
|
||||
else:
|
||||
new_name = "" # 表示不匹配
|
||||
|
||||
preview_results.append({
|
||||
"original_name": file["file_name"],
|
||||
"new_name": new_name if new_name != file["file_name"] else "", # 如果没有改变,返回空表示不重命名
|
||||
"file_id": file["fid"]
|
||||
})
|
||||
except Exception as e:
|
||||
# 正则表达式错误
|
||||
preview_results.append({
|
||||
"original_name": file["file_name"],
|
||||
"new_name": "", # 表示无法重命名
|
||||
"file_id": file["fid"],
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": preview_results
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f">>> 预览重命名时出错: {str(e)}")
|
||||
return jsonify({"success": False, "message": f"预览重命名时出错: {str(e)}"})
|
||||
|
||||
|
||||
# 批量重命名
|
||||
@app.route("/batch_rename", methods=["POST"])
|
||||
def batch_rename():
|
||||
if not is_login():
|
||||
return jsonify({"success": False, "message": "未登录"})
|
||||
|
||||
# 获取请求参数
|
||||
data = request.json
|
||||
files = data.get("files", [])
|
||||
|
||||
if not files:
|
||||
return jsonify({"success": False, "message": "没有文件需要重命名"})
|
||||
|
||||
try:
|
||||
# 初始化夸克网盘客户端
|
||||
account = Quark(config_data["cookie"][0], 0)
|
||||
|
||||
# 批量重命名
|
||||
success_count = 0
|
||||
failed_files = []
|
||||
save_path = data.get("save_path", "")
|
||||
batch_time = int(time.time() * 1000) # 批次时间戳,确保同一批transfer_time一致
|
||||
for file_item in files:
|
||||
file_id = file_item.get("file_id")
|
||||
new_name = file_item.get("new_name")
|
||||
original_name = file_item.get("old_name") or ""
|
||||
if file_id and new_name:
|
||||
try:
|
||||
result = account.rename(file_id, new_name)
|
||||
if result.get("code") == 0:
|
||||
success_count += 1
|
||||
# 记录重命名
|
||||
record_db.add_record(
|
||||
task_name="rename",
|
||||
original_name=original_name,
|
||||
renamed_to=new_name,
|
||||
file_size=0,
|
||||
modify_date=int(time.time()),
|
||||
file_id=file_id,
|
||||
file_type="file",
|
||||
save_path=save_path,
|
||||
transfer_time=batch_time
|
||||
)
|
||||
else:
|
||||
failed_files.append({
|
||||
"file_id": file_id,
|
||||
"new_name": new_name,
|
||||
"error": result.get("message", "未知错误")
|
||||
})
|
||||
except Exception as e:
|
||||
failed_files.append({
|
||||
"file_id": file_id,
|
||||
"new_name": new_name,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"成功重命名 {success_count} 个文件,失败 {len(failed_files)} 个",
|
||||
"success_count": success_count,
|
||||
"failed_files": failed_files
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f">>> 批量重命名时出错: {str(e)}")
|
||||
return jsonify({"success": False, "message": f"批量重命名时出错: {str(e)}"})
|
||||
|
||||
|
||||
# 撤销重命名接口
|
||||
@app.route("/undo_rename", methods=["POST"])
|
||||
def undo_rename():
|
||||
if not is_login():
|
||||
return jsonify({"success": False, "message": "未登录"})
|
||||
data = request.json
|
||||
save_path = data.get("save_path", "")
|
||||
if not save_path:
|
||||
return jsonify({"success": False, "message": "缺少目录参数"})
|
||||
try:
|
||||
account = Quark(config_data["cookie"][0], 0)
|
||||
# 查询该目录下最近一次重命名(按transfer_time分组,task_name=rename)
|
||||
records = record_db.get_records_by_save_path(save_path)
|
||||
rename_records = [r for r in records if r["task_name"] == "rename"]
|
||||
if not rename_records:
|
||||
return jsonify({"success": False, "message": "没有可撤销的重命名记录"})
|
||||
# 找到最近一批(transfer_time最大值)
|
||||
latest_time = max(r["transfer_time"] for r in rename_records)
|
||||
latest_batch = [r for r in rename_records if r["transfer_time"] == latest_time]
|
||||
success_count = 0
|
||||
failed_files = []
|
||||
deleted_ids = []
|
||||
for rec in latest_batch:
|
||||
file_id = rec["file_id"]
|
||||
old_name = rec["original_name"]
|
||||
try:
|
||||
result = account.rename(file_id, old_name)
|
||||
if result.get("code") == 0:
|
||||
success_count += 1
|
||||
deleted_ids.append(rec["id"])
|
||||
else:
|
||||
failed_files.append({
|
||||
"file_id": file_id,
|
||||
"old_name": old_name,
|
||||
"error": result.get("message", "未知错误")
|
||||
})
|
||||
except Exception as e:
|
||||
failed_files.append({
|
||||
"file_id": file_id,
|
||||
"old_name": old_name,
|
||||
"error": str(e)
|
||||
})
|
||||
# 撤销成功的,直接删除对应rename记录
|
||||
for rid in deleted_ids:
|
||||
record_db.delete_record(rid)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"成功撤销 {success_count} 个文件重命名,失败 {len(failed_files)} 个",
|
||||
"success_count": success_count,
|
||||
"failed_files": failed_files
|
||||
})
|
||||
except Exception as e:
|
||||
logging.error(f">>> 撤销重命名时出错: {str(e)}")
|
||||
return jsonify({"success": False, "message": f"撤销重命名时出错: {str(e)}"})
|
||||
|
||||
|
||||
@app.route("/api/has_rename_record")
|
||||
def has_rename_record():
|
||||
save_path = request.args.get("save_path", "")
|
||||
db = RecordDB()
|
||||
records = db.get_records_by_save_path(save_path)
|
||||
has_rename = any(r["task_name"] == "rename" for r in records)
|
||||
return jsonify({"has_rename": has_rename})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init()
|
||||
reload_tasks()
|
||||
# 初始化全局db对象,确保所有接口可用
|
||||
record_db = RecordDB()
|
||||
app.run(debug=DEBUG, host="0.0.0.0", port=PORT)
|
||||
|
||||
@ -15,7 +15,7 @@ class RecordDB:
|
||||
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
|
||||
|
||||
# 创建数据库连接
|
||||
self.conn = sqlite3.connect(self.db_path)
|
||||
self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 创建表,如果不存在
|
||||
@ -49,13 +49,14 @@ class RecordDB:
|
||||
self.conn.close()
|
||||
|
||||
def add_record(self, task_name, original_name, renamed_to, file_size, modify_date,
|
||||
duration="", resolution="", file_id="", file_type="", save_path=""):
|
||||
duration="", resolution="", file_id="", file_type="", save_path="", transfer_time=None):
|
||||
"""添加一条转存记录"""
|
||||
cursor = self.conn.cursor()
|
||||
now_ms = int(time.time() * 1000) if transfer_time is None else transfer_time
|
||||
cursor.execute(
|
||||
"INSERT INTO transfer_records (transfer_time, task_name, original_name, renamed_to, file_size, "
|
||||
"duration, resolution, modify_date, file_id, file_type, save_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(int(time.time()), task_name, original_name, renamed_to, file_size,
|
||||
(now_ms, task_name, original_name, renamed_to, file_size,
|
||||
duration, resolution, modify_date, file_id, file_type, save_path)
|
||||
)
|
||||
self.conn.commit()
|
||||
@ -123,7 +124,7 @@ class RecordDB:
|
||||
return 0
|
||||
|
||||
def get_records(self, page=1, page_size=20, sort_by="transfer_time", order="desc",
|
||||
task_name_filter="", keyword_filter=""):
|
||||
task_name_filter="", keyword_filter="", exclude_task_names=None):
|
||||
"""获取转存记录列表,支持分页、排序和筛选
|
||||
|
||||
Args:
|
||||
@ -133,6 +134,7 @@ class RecordDB:
|
||||
order: 排序方向(asc/desc)
|
||||
task_name_filter: 任务名称筛选条件(精确匹配)
|
||||
keyword_filter: 关键字筛选条件(模糊匹配任务名)
|
||||
exclude_task_names: 需要排除的任务名称列表
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
offset = (page - 1) * page_size
|
||||
@ -159,6 +161,10 @@ class RecordDB:
|
||||
params.append(f"%{keyword_filter}%")
|
||||
params.append(f"%{keyword_filter}%")
|
||||
|
||||
if exclude_task_names:
|
||||
where_clauses.append("task_name NOT IN ({})".format(",".join(["?" for _ in exclude_task_names])))
|
||||
params.extend(exclude_task_names)
|
||||
|
||||
where_clause = " AND ".join(where_clauses)
|
||||
where_sql = f"WHERE {where_clause}" if where_clause else ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
155
app/static/js/sort_file_by_name.js
Normal file
155
app/static/js/sort_file_by_name.js
Normal file
@ -0,0 +1,155 @@
|
||||
// 与后端 quark_auto_save.py 的 sort_file_by_name 完全一致的排序逻辑
|
||||
// 用于前端文件列表排序
|
||||
|
||||
function chineseToArabic(chinese) {
|
||||
// 简单实现,支持一到一万
|
||||
const cnNums = {
|
||||
'零': 0, '一': 1, '二': 2, '两': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9,
|
||||
'十': 10, '百': 100, '千': 1000, '万': 10000
|
||||
};
|
||||
let result = 0, unit = 1, num = 0;
|
||||
for (let i = chinese.length - 1; i >= 0; i--) {
|
||||
const char = chinese[i];
|
||||
if (cnNums[char] >= 10) {
|
||||
unit = cnNums[char];
|
||||
if (unit === 10 && (i === 0 || cnNums[chinese[i - 1]] === undefined)) {
|
||||
num = 1;
|
||||
}
|
||||
} else if (cnNums[char] !== undefined) {
|
||||
num = cnNums[char];
|
||||
result += num * unit;
|
||||
}
|
||||
}
|
||||
return result || null;
|
||||
}
|
||||
|
||||
function sortFileByName(file) {
|
||||
// 兼容 dict 或字符串
|
||||
let filename = typeof file === 'object' ? (file.file_name || '') : file;
|
||||
let update_time = typeof file === 'object' ? (file.updated_at || 0) : 0;
|
||||
let file_name_without_ext = filename.replace(/\.[^/.]+$/, '');
|
||||
let date_value = Infinity, episode_value = Infinity, segment_value = 0;
|
||||
|
||||
// 1. 日期提取
|
||||
let match;
|
||||
// YYYY-MM-DD
|
||||
match = filename.match(/((?:19|20)\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})/);
|
||||
if (match) {
|
||||
date_value = parseInt(match[1]) * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]);
|
||||
}
|
||||
// YY-MM-DD
|
||||
if (date_value === Infinity) {
|
||||
match = filename.match(/((?:19|20)?\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})/);
|
||||
if (match && match[1].length === 2) {
|
||||
let year = parseInt('20' + match[1]);
|
||||
date_value = year * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]);
|
||||
}
|
||||
}
|
||||
// YYYYMMDD
|
||||
if (date_value === Infinity) {
|
||||
match = filename.match(/((?:19|20)\d{2})(\d{2})(\d{2})/);
|
||||
if (match) {
|
||||
date_value = parseInt(match[1]) * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]);
|
||||
}
|
||||
}
|
||||
// YYMMDD
|
||||
if (date_value === Infinity) {
|
||||
match = filename.match(/(?<!\d)(\d{2})(\d{2})(\d{2})(?!\d)/);
|
||||
if (match) {
|
||||
let month = parseInt(match[2]), day = parseInt(match[3]);
|
||||
if (1 <= month && month <= 12 && 1 <= day && day <= 31) {
|
||||
let year = parseInt('20' + match[1]);
|
||||
date_value = year * 10000 + month * 100 + day;
|
||||
}
|
||||
}
|
||||
}
|
||||
// MM/DD/YYYY
|
||||
if (date_value === Infinity) {
|
||||
match = filename.match(/(\d{1,2})[-./\s](\d{1,2})[-./\s]((?:19|20)\d{2})/);
|
||||
if (match) {
|
||||
let month = parseInt(match[1]), day = parseInt(match[2]), year = parseInt(match[3]);
|
||||
if (month > 12) [month, day] = [day, month];
|
||||
date_value = year * 10000 + month * 100 + day;
|
||||
}
|
||||
}
|
||||
// MM-DD
|
||||
if (date_value === Infinity) {
|
||||
match = filename.match(/(?<!\d)(\d{1,2})[-./\s](\d{1,2})(?!\d)/);
|
||||
if (match) {
|
||||
let month = parseInt(match[1]), day = parseInt(match[2]);
|
||||
if (month > 12) [month, day] = [day, month];
|
||||
date_value = 20000000 + month * 100 + day;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 期数/集数
|
||||
// 第X期/集/话
|
||||
match = filename.match(/第(\d+)[期集话]/);
|
||||
if (match) episode_value = parseInt(match[1]);
|
||||
// 第[中文数字]期/集/话
|
||||
if (episode_value === Infinity) {
|
||||
match = filename.match(/第([一二三四五六七八九十百千万零两]+)[期集话]/);
|
||||
if (match) {
|
||||
let arabic = chineseToArabic(match[1]);
|
||||
if (arabic !== null) episode_value = arabic;
|
||||
}
|
||||
}
|
||||
// X集/期/话
|
||||
if (episode_value === Infinity) {
|
||||
match = filename.match(/(\d+)[期集话]/);
|
||||
if (match) episode_value = parseInt(match[1]);
|
||||
}
|
||||
// [中文数字]集/期/话
|
||||
if (episode_value === Infinity) {
|
||||
match = filename.match(/([一二三四五六七八九十百千万零两]+)[期集话]/);
|
||||
if (match) {
|
||||
let arabic = chineseToArabic(match[1]);
|
||||
if (arabic !== null) episode_value = arabic;
|
||||
}
|
||||
}
|
||||
// S01E01
|
||||
if (episode_value === Infinity) {
|
||||
match = filename.match(/[Ss](\d+)[Ee](\d+)/);
|
||||
if (match) episode_value = parseInt(match[2]);
|
||||
}
|
||||
// E01/EP01
|
||||
if (episode_value === Infinity) {
|
||||
match = filename.match(/[Ee][Pp]?(\d+)/);
|
||||
if (match) episode_value = parseInt(match[1]);
|
||||
}
|
||||
// 1x01
|
||||
if (episode_value === Infinity) {
|
||||
match = filename.match(/(\d+)[Xx](\d+)/);
|
||||
if (match) episode_value = parseInt(match[2]);
|
||||
}
|
||||
// [数字]或【数字】
|
||||
if (episode_value === Infinity) {
|
||||
match = filename.match(/\[(\d+)\]|【(\d+)】/);
|
||||
if (match) episode_value = parseInt(match[1] || match[2]);
|
||||
}
|
||||
// 纯数字文件名
|
||||
if (episode_value === Infinity) {
|
||||
if (/^\d+$/.test(file_name_without_ext)) {
|
||||
episode_value = parseInt(file_name_without_ext);
|
||||
} else {
|
||||
match = filename.match(/(\d+)/);
|
||||
if (match) episode_value = parseInt(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 上中下
|
||||
if (/[上][集期话部篇]?|[集期话部篇]上/.test(filename)) segment_value = 1;
|
||||
else if (/[中][集期话部篇]?|[集期话部篇]中/.test(filename)) segment_value = 2;
|
||||
else if (/[下][集期话部篇]?|[集期话部篇]下/.test(filename)) segment_value = 3;
|
||||
|
||||
return [date_value, episode_value, segment_value, update_time];
|
||||
}
|
||||
|
||||
// 用法:
|
||||
// arr.sort((a, b) => {
|
||||
// const ka = sortFileByName(a), kb = sortFileByName(b);
|
||||
// for (let i = 0; i < ka.length; ++i) {
|
||||
// if (ka[i] !== kb[i]) return ka[i] > kb[i] ? 1 : -1;
|
||||
// }
|
||||
// return 0;
|
||||
// });
|
||||
File diff suppressed because it is too large
Load Diff
@ -229,6 +229,9 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
|
||||
Returns:
|
||||
int: 提取到的剧集号,如果无法提取则返回None
|
||||
"""
|
||||
# 首先去除文件扩展名
|
||||
file_name_without_ext = os.path.splitext(filename)[0]
|
||||
|
||||
# 预处理:排除文件名中可能是日期的部分,避免误识别
|
||||
date_patterns = [
|
||||
# YYYY-MM-DD 或 YYYY.MM.DD 或 YYYY/MM/DD 或 YYYY MM DD格式(四位年份)
|
||||
@ -245,10 +248,10 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
|
||||
r'(?<!\d)(\d{1,2})[-./\s](\d{1,2})(?!\d)',
|
||||
]
|
||||
|
||||
# 从文件名中移除日期部分,创建一个不含日期的文件名副本用于提取剧集号
|
||||
filename_without_dates = filename
|
||||
# 从不含扩展名的文件名中移除日期部分
|
||||
filename_without_dates = file_name_without_ext
|
||||
for pattern in date_patterns:
|
||||
matches = re.finditer(pattern, filename)
|
||||
matches = re.finditer(pattern, filename_without_dates)
|
||||
for match in matches:
|
||||
# 检查匹配的内容是否确实是日期
|
||||
date_str = match.group(0)
|
||||
@ -369,13 +372,9 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
|
||||
except:
|
||||
continue
|
||||
|
||||
# 如果从不含日期的文件名中没有找到剧集号,尝试从原始文件名中提取
|
||||
# 这是为了兼容某些特殊情况,但要检查提取的数字不是日期
|
||||
file_name_without_ext = os.path.splitext(filename)[0]
|
||||
|
||||
# 如果文件名是纯数字,且不是日期格式,则可能是剧集号
|
||||
if file_name_without_ext.isdigit() and not is_date_format(file_name_without_ext):
|
||||
return int(file_name_without_ext)
|
||||
if filename_without_dates.isdigit() and not is_date_format(filename_without_dates):
|
||||
return int(filename_without_dates)
|
||||
|
||||
# 最后尝试提取任何数字,但要排除日期可能性
|
||||
num_match = re.search(r'(\d+)', filename_without_dates)
|
||||
@ -959,6 +958,48 @@ class Quark:
|
||||
break
|
||||
return file_list
|
||||
|
||||
def get_paths(self, folder_id):
|
||||
"""
|
||||
获取指定文件夹ID的完整路径信息
|
||||
|
||||
Args:
|
||||
folder_id: 文件夹ID
|
||||
|
||||
Returns:
|
||||
list: 路径信息列表,每个元素包含fid和name
|
||||
"""
|
||||
if folder_id == "0" or folder_id == 0:
|
||||
return []
|
||||
|
||||
url = f"{self.BASE_URL}/1/clouddrive/file/sort"
|
||||
querystring = {
|
||||
"pr": "ucpro",
|
||||
"fr": "pc",
|
||||
"uc_param_str": "",
|
||||
"pdir_fid": folder_id,
|
||||
"_page": 1,
|
||||
"_size": "50",
|
||||
"_fetch_total": "1",
|
||||
"_fetch_sub_dirs": "0",
|
||||
"_sort": "file_type:asc,updated_at:desc",
|
||||
"_fetch_full_path": 1,
|
||||
}
|
||||
|
||||
try:
|
||||
response = self._send_request("GET", url, params=querystring).json()
|
||||
if response["code"] == 0 and "full_path" in response["data"]:
|
||||
paths = []
|
||||
for item in response["data"]["full_path"]:
|
||||
paths.append({
|
||||
"fid": item["fid"],
|
||||
"name": item["file_name"]
|
||||
})
|
||||
return paths
|
||||
except Exception as e:
|
||||
print(f"获取文件夹路径出错: {str(e)}")
|
||||
|
||||
return []
|
||||
|
||||
def save_file(self, fid_list, fid_token_list, to_pdir_fid, pwd_id, stoken):
|
||||
url = f"{self.BASE_URL}/1/clouddrive/share/sharepage/save"
|
||||
querystring = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user