Merge pull request #47 from x1ao4/dev

新增电影命名规则配置功能及其他修复和优化
This commit is contained in:
x1ao4 2025-08-09 20:55:57 +08:00 committed by GitHub
commit 31b0060023
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 551 additions and 142 deletions

View File

@ -863,6 +863,20 @@ def get_share_detail():
share_detail["paths"] = paths share_detail["paths"] = paths
share_detail["stoken"] = stoken share_detail["stoken"] = stoken
# 处理文件夹的include_items字段确保空文件夹显示为0项而不是undefined
if "list" in share_detail and isinstance(share_detail["list"], list):
for file_item in share_detail["list"]:
if file_item.get("dir", False):
# 如果是文件夹确保include_items字段存在且为数字
if "include_items" not in file_item or file_item["include_items"] is None:
file_item["include_items"] = 0
elif not isinstance(file_item["include_items"], (int, float)):
# 如果include_items不是数字类型尝试转换为整数失败则设为0
try:
file_item["include_items"] = int(file_item["include_items"])
except (ValueError, TypeError):
file_item["include_items"] = 0
# 如果是GET请求或者不需要预览正则直接返回分享详情 # 如果是GET请求或者不需要预览正则直接返回分享详情
if request.method == "GET" or not request.json.get("regex"): if request.method == "GET" or not request.json.get("regex"):
return jsonify({"success": True, "data": share_detail}) return jsonify({"success": True, "data": share_detail})
@ -1028,6 +1042,20 @@ def get_share_detail():
share_detail = preview_regex(share_detail) share_detail = preview_regex(share_detail)
# 再次处理文件夹的include_items字段确保预览后的数据也正确
if "list" in share_detail and isinstance(share_detail["list"], list):
for file_item in share_detail["list"]:
if file_item.get("dir", False):
# 如果是文件夹确保include_items字段存在且为数字
if "include_items" not in file_item or file_item["include_items"] is None:
file_item["include_items"] = 0
elif not isinstance(file_item["include_items"], (int, float)):
# 如果include_items不是数字类型尝试转换为整数失败则设为0
try:
file_item["include_items"] = int(file_item["include_items"])
except (ValueError, TypeError):
file_item["include_items"] = 0
return jsonify({"success": True, "data": share_detail}) return jsonify({"success": True, "data": share_detail})
@ -1067,8 +1095,26 @@ def get_savepath_detail():
return jsonify({"success": False, "data": {"error": "获取fid失败"}}) return jsonify({"success": False, "data": {"error": "获取fid失败"}})
else: else:
fid = request.args.get("fid", "0") fid = request.args.get("fid", "0")
# 获取文件列表
files = account.ls_dir(fid)
# 处理文件夹的include_items字段确保空文件夹显示为0项而不是undefined
if isinstance(files, list):
for file_item in files:
if file_item.get("dir", False):
# 如果是文件夹确保include_items字段存在且为数字
if "include_items" not in file_item or file_item["include_items"] is None:
file_item["include_items"] = 0
elif not isinstance(file_item["include_items"], (int, float)):
# 如果include_items不是数字类型尝试转换为整数失败则设为0
try:
file_item["include_items"] = int(file_item["include_items"])
except (ValueError, TypeError):
file_item["include_items"] = 0
file_list = { file_list = {
"list": account.ls_dir(fid), "list": files,
"paths": paths, "paths": paths,
} }
return jsonify({"success": True, "data": file_list}) return jsonify({"success": True, "data": file_list})
@ -1921,6 +1967,19 @@ def get_file_list():
else: else:
return jsonify({"success": False, "message": f"获取文件列表失败: {error_msg}"}) return jsonify({"success": False, "message": f"获取文件列表失败: {error_msg}"})
# 处理文件夹的include_items字段确保空文件夹显示为0项而不是undefined
for file_item in files:
if file_item.get("dir", False):
# 如果是文件夹确保include_items字段存在且为数字
if "include_items" not in file_item or file_item["include_items"] is None:
file_item["include_items"] = 0
elif not isinstance(file_item["include_items"], (int, float)):
# 如果include_items不是数字类型尝试转换为整数失败则设为0
try:
file_item["include_items"] = int(file_item["include_items"])
except (ValueError, TypeError):
file_item["include_items"] = 0
# 计算总数 # 计算总数
total = len(files) total = len(files)

View File

@ -3967,17 +3967,44 @@ table.selectable-records .expand-button:hover {
top: 2px !important; /* 将"×"标记上移1px */ top: 2px !important; /* 将"×"标记上移1px */
} }
#fileSelectModal[data-modal-type="preview"] .table td.col-rename > * { #fileSelectModal[data-modal-type="preview"] .table td.col-rename > :not(.expand-button) {
position: relative; position: relative;
top: 3px !important; top: 3px !important;
} }
/* 文件整理页面命名预览模式下的重命名列通用样式 */ /* 文件整理页面命名预览模式下的重命名列通用样式 */
#fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename > * { #fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename > :not(.expand-button) {
position: relative; position: relative;
top: 3px !important; /* 与任务配置页面保持一致 */ top: 3px !important; /* 与任务配置页面保持一致 */
} }
/* 确保文件整理页面命名预览模式下的重命名列文本截断样式正确应用 */
#fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename div[style*="white-space: nowrap"] {
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
padding-right: 25px !important;
padding-top: 0.5px !important; /* 文本和超长省略号下移0.5px */
max-width: 100% !important;
display: block !important;
line-height: 19px !important;
box-sizing: border-box !important;
}
/* 强制文件整理页面重命名列的所有子元素使用inline显示确保文本截断正常工作 */
#fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename div[style*="white-space: nowrap"] * {
display: inline !important;
white-space: nowrap !important;
}
/* 命名预览模态框 - 重命名列展开按钮位置微调,仅作用于命名预览 */
#fileSelectModal[data-modal-type="preview"] .table td.col-rename .expand-button,
#fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename .expand-button {
top: 6.5px !important; /* 单独上移 1px基础为 7.5px */
right: 8px !important; /* 略向左移,参考模态框其他列的间距 */
}
/* 模态框通用文件夹图标样式 */ /* 模态框通用文件夹图标样式 */
#fileSelectModal .bi-folder-fill { #fileSelectModal .bi-folder-fill {
color: #098eff; color: #098eff;
@ -5510,7 +5537,7 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
/* 文件整理页面无法识别剧集编号样式 */ /* 文件整理页面无法识别剧集编号样式 */
#fileSelectModal[data-modal-type="preview-filemanager"] .episode-number-text { #fileSelectModal[data-modal-type="preview-filemanager"] .episode-number-text {
position: relative; position: relative;
top: 1.5px; /* 或你想要的像素 */ top: 1px; /* 上移0.5px从1.5px改为1px */
display: inline-block; display: inline-block;
} }
@ -5525,13 +5552,18 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
/* 文件整理页面无法识别剧集编号前面的 × 样式 */ /* 文件整理页面无法识别剧集编号前面的 × 样式 */
#fileSelectModal[data-modal-type="preview-filemanager"] .episode-x { #fileSelectModal[data-modal-type="preview-filemanager"] .episode-x {
position: relative; position: relative;
top: 0.5px; display: inline;
display: inline-block;
margin-right: 2px; margin-right: 2px;
} }
/* 确保文件整理页面重命名列中的episode-x在未展开状态下不影响文本截断 */
#fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename div[style*="white-space: nowrap"] .episode-x {
display: inline !important;
white-space: nowrap !important;
}
/* 文件整理页面命名预览模式下的绿色重命名文本上移0.5px */ /* 文件整理页面命名预览模式下的绿色重命名文本上移0.5px */
#fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename.text-success > * { #fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename.text-success > :not(.expand-button) {
position: relative; position: relative;
top: 3px !important; /* 原来是3px上移0.5px */ top: 3px !important; /* 原来是3px上移0.5px */
} }
@ -6281,3 +6313,15 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
} }
} }
/* 文件整理页面命名预览模式下的展开状态文本位置调整 - 最高优先级 */
#fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename > div[style*="white-space: normal"][style*="word-break: break-word"] {
position: relative !important;
top: 3.5px !important; /* 展开状态文本下移0.5px,覆盖所有其他设置 */
margin-bottom: 8px !important; /* 文件整理页面展开状态下边距增加1px */
}
/* 任务列表和影视发现页面命名预览模式下的展开状态文本下边距调整 */
#fileSelectModal[data-modal-type="preview"] .table td.col-rename > div[style*="white-space: normal"][style*="word-break: break-word"] {
margin-bottom: 7.5px !important; /* 任务列表页面展开状态下边距增加0.5px */
}

View File

@ -170,46 +170,84 @@ function sortFileByName(file) {
let segment_base = 0; // 基础值:上=1, 中=2, 下=3 let segment_base = 0; // 基础值:上=1, 中=2, 下=3
let sequence_number = 0; // 序号值:用于处理上中下后的数字或中文数字序号 let sequence_number = 0; // 序号值:用于处理上中下后的数字或中文数字序号
if (/[上][集期话部篇]?|[集期话部篇]上/.test(filename)) { // 严格匹配上中下标记:只有当上中下与集期话部篇相邻时才认为是段落标记
// 避免误匹配文件内容中偶然出现的上中下字符
if (/上[集期话部篇]|[集期话部篇]上/.test(filename)) {
segment_base = 1; segment_base = 1;
} else if (/[中][集期话部篇]?|[集期话部篇]中/.test(filename)) { } else if (/中[集期话部篇]|[集期话部篇]中/.test(filename)) {
segment_base = 2; segment_base = 2;
} else if (/[][集期话部篇]?|[集期话部篇]下/.test(filename)) { } else if (/下[集期话部篇]|[集期话部篇]下/.test(filename)) {
segment_base = 3; segment_base = 3;
} }
// 当有上中下标记时,进一步提取后续的序号 // 统一的序号提取逻辑,支持多种分隔符和格式
if (segment_base > 0) { // 无论是否有上中下标记,都使用相同的序号提取逻辑
// 提取上中下后的中文数字序号,如:上(一)、上(二)
let chinese_seq_match = filename.match(/[上中下][集期话部篇]?[(]([一二三四五六七八九十百千万零两]+)[)]/); // 定义序号提取的模式,使用正向匹配组合的方式
if (chinese_seq_match) { // 这样可以精准匹配,避免误判"星期六"等内容
let arabic_num = chineseToArabic(chinese_seq_match[1]); const sequence_patterns = [
// 第+中文数字+期集话部篇+序号:第一期(一)、第五十六期-二、第 一 期 三
{ pattern: /第\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]\s*[(]\s*([一二三四五六七八九十百千万零两]+)\s*[)]/u, type: 'chinese' },
{ pattern: /第\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]\s*[(]\s*(\d+)\s*[)]/u, type: 'arabic' },
{ pattern: /第\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]\s*[-_·丨]\s*([一二三四五六七八九十百千万零两]+)/u, type: 'chinese' },
{ pattern: /第\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]\s*[-_·丨]\s*(\d+)/u, type: 'arabic' },
{ pattern: /第\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]\s+([一二三四五六七八九十百千万零两]+)(?![一二三四五六七八九十])/u, type: 'chinese' },
{ pattern: /第\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]\s+(\d+)(?!\d)/u, type: 'arabic' },
{ pattern: /第\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]([一二三四五六七八九十])(?![一二三四五六七八九十])/u, type: 'chinese' },
{ pattern: /第\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇](\d+)(?!\d)/u, type: 'arabic' },
// 第+阿拉伯数字+期集话部篇+序号第1期、第100期-二、第 1 期 三
{ pattern: /第\s*\d+\s*[期集话部篇]\s*[(]\s*([一二三四五六七八九十百千万零两]+)\s*[)]/u, type: 'chinese' },
{ pattern: /第\s*\d+\s*[期集话部篇]\s*[(]\s*(\d+)\s*[)]/u, type: 'arabic' },
{ pattern: /第\s*\d+\s*[期集话部篇]\s*[-_·丨]\s*([一二三四五六七八九十百千万零两]+)/u, type: 'chinese' },
{ pattern: /第\s*\d+\s*[期集话部篇]\s*[-_·丨]\s*(\d+)/u, type: 'arabic' },
{ pattern: /第\s*\d+\s*[期集话部篇]\s+([一二三四五六七八九十百千万零两]+)(?![一二三四五六七八九十])/u, type: 'chinese' },
{ pattern: /第\s*\d+\s*[期集话部篇]\s+(\d+)(?!\d)/u, type: 'arabic' },
{ pattern: /第\s*\d+\s*[期集话部篇]([一二三四五六七八九十])(?![一二三四五六七八九十])/u, type: 'chinese' },
{ pattern: /第\s*\d+\s*[期集话部篇](\d+)(?!\d)/u, type: 'arabic' },
// 上中下+集期话部篇+序号:上集(一)、中部-二、下篇 三
{ pattern: /[上中下][集期话部篇]\s*[(]\s*([一二三四五六七八九十百千万零两]+)\s*[)]/u, type: 'chinese' },
{ pattern: /[上中下][集期话部篇]\s*[(]\s*(\d+)\s*[)]/u, type: 'arabic' },
{ pattern: /[上中下][集期话部篇]\s*[-_·丨]\s*([一二三四五六七八九十百千万零两]+)/u, type: 'chinese' },
{ pattern: /[上中下][集期话部篇]\s*[-_·丨]\s*(\d+)/u, type: 'arabic' },
{ pattern: /[上中下][集期话部篇]\s+([一二三四五六七八九十百千万零两]+)(?![一二三四五六七八九十])/u, type: 'chinese' },
{ pattern: /[上中下][集期话部篇]\s+(\d+)(?!\d)/u, type: 'arabic' },
{ pattern: /[上中下][集期话部篇]([一二三四五六七八九十])(?![一二三四五六七八九十])/u, type: 'chinese' },
{ pattern: /[上中下][集期话部篇](\d+)(?!\d)/u, type: 'arabic' },
// 集期话部篇+上中下+序号:集上(一)、部中-二、篇下 三
{ pattern: /[集期话部篇][上中下]\s*[(]\s*([一二三四五六七八九十百千万零两]+)\s*[)]/u, type: 'chinese' },
{ pattern: /[集期话部篇][上中下]\s*[(]\s*(\d+)\s*[)]/u, type: 'arabic' },
{ pattern: /[集期话部篇][上中下]\s*[-_·丨]\s*([一二三四五六七八九十百千万零两]+)/u, type: 'chinese' },
{ pattern: /[集期话部篇][上中下]\s*[-_·丨]\s*(\d+)/u, type: 'arabic' },
{ pattern: /[集期话部篇][上中下]\s+([一二三四五六七八九十百千万零两]+)(?![一二三四五六七八九十])/u, type: 'chinese' },
{ pattern: /[集期话部篇][上中下]\s+(\d+)(?!\d)/u, type: 'arabic' },
{ pattern: /[集期话部篇][上中下]([一二三四五六七八九十])(?![一二三四五六七八九十])/u, type: 'chinese' },
{ pattern: /[集期话部篇][上中下](\d+)(?!\d)/u, type: 'arabic' },
];
// 尝试匹配序号
for (const { pattern, type } of sequence_patterns) {
const match = filename.match(pattern);
if (match) {
if (type === 'chinese') {
const arabic_num = chineseToArabic(match[1]);
if (arabic_num !== null) { if (arabic_num !== null) {
sequence_number = arabic_num; sequence_number = arabic_num;
// 如果之前没有检测到上中下标记,给一个基础值
if (segment_base === 0) {
segment_base = 1;
} }
} else { break;
// 提取上中下后的阿拉伯数字序号上1、上2
let arabic_seq_match = filename.match(/[上中下][集期话部篇]?(\d+)/);
if (arabic_seq_match) {
sequence_number = parseInt(arabic_seq_match[1]);
} }
} else { // arabic
sequence_number = parseInt(match[1]);
// 如果之前没有检测到上中下标记,给一个基础值
if (segment_base === 0) {
segment_base = 1;
} }
} else { break;
// 如果没有上中下标记,检查是否有括号内的中文数字序号
// 匹配格式如第2期、第2期
let parentheses_chinese_match = filename.match(/[期集话部篇][(]([一二三四五六七八九十百千万零两]+)[)]/);
if (parentheses_chinese_match) {
let arabic_num = chineseToArabic(parentheses_chinese_match[1]);
if (arabic_num !== null) {
sequence_number = arabic_num;
segment_base = 1; // 给一个基础值,确保有括号序号的文件能正确排序
}
} else {
// 匹配格式如第2期(1)、第2期(2)等
let parentheses_arabic_match = filename.match(/[期集话部篇][(](\d+)[)]/);
if (parentheses_arabic_match) {
sequence_number = parseInt(parentheses_arabic_match[1]);
segment_base = 1; // 给一个基础值,确保有括号序号的文件能正确排序
} }
} }
} }

View File

@ -656,6 +656,16 @@
</div> </div>
</div> </div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">电影命名规则</span>
</div>
<input type="text" class="form-control" v-model="formData.task_settings.movie_naming_pattern" placeholder="^(.*)\.([^.]+)" title="设置电影类型内容的默认文件命名规则的匹配表达式,在影视发现页面创建电影任务时自动填充">
<input type="text" class="form-control" v-model="formData.task_settings.movie_naming_replace" placeholder="片名 (年份).\2" title="设置电影类型内容的默认文件命名规则的替换表达式,在影视发现页面创建电影任务时自动填充。支持变量:片名、年份等">
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
@ -1484,7 +1494,7 @@
<!-- 分页区域 --> <!-- 分页区域 -->
<div class="pagination-container d-flex justify-content-between align-items-center mt-3"> <div class="pagination-container d-flex justify-content-between align-items-center mt-3">
<div class="page-info text-secondary"> <div class="page-info text-secondary">
显示 {{ fileManager.currentPage > 0 ? ((fileManager.currentPage - 1) * fileManager.pageSize + 1) + '-' + Math.min(fileManager.currentPage * fileManager.pageSize, fileManager.total) : '0' }} 项,共 {{ fileManager.total }} 个项目{{ fileManager.selectedFiles.length > 0 ? ',已选中 ' + fileManager.selectedFiles.length + ' 项' : '' }} 显示 {{ fileManager.total > 0 ? ((fileManager.currentPage - 1) * fileManager.pageSize + 1) + '-' + Math.min(fileManager.currentPage * fileManager.pageSize, fileManager.total) : '0-0' }} 项,共 {{ fileManager.total }} 个项目{{ fileManager.selectedFiles.length > 0 ? ',已选中 ' + fileManager.selectedFiles.length + ' 项' : '' }}
</div> </div>
<div class="pagination-controls d-flex align-items-center"> <div class="pagination-controls d-flex align-items-center">
<button type="button" class="btn btn-outline-secondary btn-sm mx-1" :class="{ disabled: fileManager.currentPage <= 1 }" @click="changeFolderPage(fileManager.currentPage - 1)" :disabled="fileManager.currentPage <= 1"> <button type="button" class="btn btn-outline-secondary btn-sm mx-1" :class="{ disabled: fileManager.currentPage <= 1 }" @click="changeFolderPage(fileManager.currentPage - 1)" :disabled="fileManager.currentPage <= 1">
@ -2032,6 +2042,9 @@
<button type="button" class="btn btn-primary" :disabled="createTask.loading" @click="confirmCreateAndRunTask"> <button type="button" class="btn btn-primary" :disabled="createTask.loading" @click="confirmCreateAndRunTask">
创建并运行任务 创建并运行任务
</button> </button>
<button type="button" class="btn btn-primary" :disabled="createTask.loading" @click="confirmCreateRunAndDeleteTask">
创建、运行并删除任务
</button>
</div> </div>
</div> </div>
</div> </div>
@ -2068,6 +2081,8 @@
anime_save_path: "动画目录前缀/剧名 (年份)/剧名 - S季数", anime_save_path: "动画目录前缀/剧名 (年份)/剧名 - S季数",
variety_save_path: "综艺目录前缀/剧名 (年份)/剧名 - S季数", variety_save_path: "综艺目录前缀/剧名 (年份)/剧名 - S季数",
documentary_save_path: "纪录片目录前缀/剧名 (年份)/剧名 - S季数", documentary_save_path: "纪录片目录前缀/剧名 (年份)/剧名 - S季数",
movie_naming_pattern: "^(.*)\.([^.]+)",
movie_naming_replace: "片名 (年份).\\2",
tv_naming_rule: "剧名 - S季数E[]", tv_naming_rule: "剧名 - S季数E[]",
tv_ignore_extension: true tv_ignore_extension: true
}, },
@ -3163,11 +3178,20 @@
anime_save_path: "动画目录前缀/剧名 (年份)/剧名 - S季数", anime_save_path: "动画目录前缀/剧名 (年份)/剧名 - S季数",
variety_save_path: "综艺目录前缀/剧名 (年份)/剧名 - S季数", variety_save_path: "综艺目录前缀/剧名 (年份)/剧名 - S季数",
documentary_save_path: "纪录片目录前缀/剧名 (年份)/剧名 - S季数", documentary_save_path: "纪录片目录前缀/剧名 (年份)/剧名 - S季数",
movie_naming_pattern: "^(.*)\.([^.]+)",
movie_naming_replace: "片名 (年份).\\2",
tv_naming_rule: "剧名 - S季数E[]", tv_naming_rule: "剧名 - S季数E[]",
tv_ignore_extension: true, tv_ignore_extension: true,
auto_search_resources: "enabled" auto_search_resources: "enabled"
}; };
} }
// 确保电影命名规则字段存在
if (!config_data.task_settings.movie_naming_pattern) {
config_data.task_settings.movie_naming_pattern = "^(.*)\.([^.]+)";
}
if (!config_data.task_settings.movie_naming_replace) {
config_data.task_settings.movie_naming_replace = "片名 (年份).\\2";
}
// 确保电视忽略后缀设置存在 // 确保电视忽略后缀设置存在
if (config_data.task_settings.tv_ignore_extension === undefined) { if (config_data.task_settings.tv_ignore_extension === undefined) {
config_data.task_settings.tv_ignore_extension = true; config_data.task_settings.tv_ignore_extension = true;
@ -7499,8 +7523,25 @@
if (contentType === 'movie') { if (contentType === 'movie') {
this.createTask.taskData.taskname = title; this.createTask.taskData.taskname = title;
this.createTask.taskData.savepath = this.generateMovieSavePath(savePathTemplate, title, year); this.createTask.taskData.savepath = this.generateMovieSavePath(savePathTemplate, title, year);
// 应用电影命名规则
const movieNamingPattern = taskSettings.movie_naming_pattern && taskSettings.movie_naming_pattern.trim() !== ""
? taskSettings.movie_naming_pattern
: "";
const movieNamingReplace = taskSettings.movie_naming_replace && taskSettings.movie_naming_replace.trim() !== ""
? taskSettings.movie_naming_replace
: "";
if (movieNamingPattern && movieNamingReplace) {
// 生成智能填充的替换表达式
const generatedReplace = this.generateMovieNamingRule(movieNamingReplace, title, year);
this.createTask.taskData.pattern = movieNamingPattern;
this.createTask.taskData.replace = generatedReplace;
} else {
this.createTask.taskData.pattern = ""; this.createTask.taskData.pattern = "";
this.createTask.taskData.replace = ""; this.createTask.taskData.replace = "";
}
this.createTask.taskData.use_sequence_naming = false; this.createTask.taskData.use_sequence_naming = false;
this.createTask.taskData.use_episode_naming = false; this.createTask.taskData.use_episode_naming = false;
this.createTask.taskData.sequence_naming = ""; this.createTask.taskData.sequence_naming = "";
@ -7698,6 +7739,26 @@
return namingRule; return namingRule;
}, },
generateMovieNamingRule(replaceTemplate, movieTitle, year) {
// 生成电影命名规则
let namingRule = replaceTemplate;
// 替换片名
namingRule = namingRule.replace(/片名/g, movieTitle);
// 替换年份
if (year) {
namingRule = namingRule.replace(/年份/g, year);
} else {
// 如果没有年份,移除包含年份的部分
namingRule = namingRule.replace(/\s*\(年份\)/g, '');
namingRule = namingRule.replace(/\s*(年份)/g, '');
}
// 注意:正则表达式的反向引用(如\2保持不变将在实际重命名时由正则引擎处理
return namingRule;
},
generateCustomFolderPath(taskData) { generateCustomFolderPath(taskData) {
// 根据任务设置生成自定义文件夹路径 // 根据任务设置生成自定义文件夹路径
const taskSettings = this.formData.task_settings || {}; const taskSettings = this.formData.task_settings || {};
@ -7952,6 +8013,164 @@
this.createTask.loading = false; this.createTask.loading = false;
}); });
}, },
confirmCreateRunAndDeleteTask() {
// 确认创建、运行并删除任务
if (this.createTask.loading) return;
// 验证必填字段
if (!this.createTask.taskData.taskname.trim()) {
this.createTask.error = '任务名称不能为空';
return;
}
if (!this.createTask.taskData.shareurl.trim()) {
this.createTask.error = '分享链接不能为空';
return;
}
if (!this.createTask.taskData.savepath.trim()) {
this.createTask.error = '保存路径不能为空';
return;
}
this.createTask.loading = true;
this.createTask.error = null;
// 创建新任务
const newTask = { ...this.createTask.taskData };
// 处理命名模式
if (newTask.use_sequence_naming) {
newTask.pattern = newTask.sequence_naming;
} else if (newTask.use_episode_naming) {
newTask.pattern = newTask.episode_naming;
}
// 添加到任务列表
if (!this.formData.tasklist) {
this.formData.tasklist = [];
}
this.formData.tasklist.push(newTask);
// 保存配置(不显示配置更新消息)
axios.post('/update', this.formData)
.then(response => {
if (response.data.success) {
this.configModified = false;
// 保存成功后更新用户信息
this.fetchUserInfo();
// 显示任务创建成功消息
this.showToast('任务创建成功,开始运行并将在完成后自动删除', 'success');
this.createTask.loading = false;
// 先关闭创建任务模态框,然后运行新创建的任务
this.cancelCreateTask();
// 等待模态框完全关闭后再打开运行日志模态框并运行任务
setTimeout(async () => {
const taskIndex = this.formData.tasklist.length - 1;
// 运行任务并等待完成
await this.runScriptNowWithCallback(taskIndex, () => {
// 任务完成后删除该任务
setTimeout(() => {
this.removeTaskSilently(taskIndex);
this.showToast('一次性任务已完成并自动删除', 'info');
}, 1000); // 等待1秒确保任务状态更新
});
}, 300);
} else {
// 错误信息使用alert确保用户看到
alert(response.data.message);
this.createTask.loading = false;
}
})
.catch(error => {
// 错误处理
alert("保存失败: " + (error.response?.data?.message || error.message || "未知错误"));
this.createTask.loading = false;
});
},
async runScriptNowWithCallback(task_index, callback) {
// 运行任务的包装函数,支持完成回调
body = {};
if (task_index != null) {
task = { ...this.formData.tasklist[task_index] };
delete task.runweek;
delete task.enddate;
body = {
"tasklist": [task],
"original_index": task_index + 1 // 添加原始索引从1开始计数
};
} else if (this.configModified) {
if (!confirm('配置已修改但未保存,是否继续运行?')) {
return;
}
}
$('#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) {
this.modalLoading = false;
// 运行后刷新数据
this.fetchData();
// 调用完成回调
if (callback) callback();
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();
// 调用完成回调
if (callback) callback();
return;
}
this.run_log += eventData + '\n';
// 在更新 run_log 后将滚动条滚动到底部
this.$nextTick(() => {
const modalBody = document.querySelector('.modal-body');
modalBody.scrollTop = modalBody.scrollHeight;
});
}
}
partialData = '';
}
} catch (error) {
this.modalLoading = false;
// 即使出错也调用回调
if (callback) callback();
}
},
removeTaskSilently(index) {
// 静默删除任务,不显示确认对话框
if (index >= 0 && index < this.formData.tasklist.length) {
this.formData.tasklist.splice(index, 1);
// 保存配置
this.saveConfig();
}
},
openCreateTaskDatePicker() { openCreateTaskDatePicker() {
// 打开创建任务的日期选择器 // 打开创建任务的日期选择器
if (this.$refs.createTaskEnddate) { if (this.$refs.createTaskEnddate) {

View File

@ -147,14 +147,14 @@ def sort_file_by_name(file):
# 2. 提取期数/集数 - 第二级排序键 # 2. 提取期数/集数 - 第二级排序键
# 2.1 "第X期/集/话" 格式 # 2.1 "第X期/集/话/部/篇" 格式(支持空格)
match_chinese = re.search(r'(\d+)[期集话]', filename) match_chinese = re.search(r'\s*(\d+)\s*[期集话部篇]', filename)
if match_chinese: if match_chinese:
episode_value = int(match_chinese.group(1)) episode_value = int(match_chinese.group(1))
# 2.1.1 "第[中文数字]期/集/话" 格式 # 2.1.1 "第[中文数字]期/集/话/部/篇" 格式(支持空格)
if episode_value == float('inf'): if episode_value == float('inf'):
match_chinese_num = re.search(r'([一二三四五六七八九十百千万零两]+)[期集话]', filename) match_chinese_num = re.search(r'\s*([一二三四五六七八九十百千万零两]+)\s*[期集话部篇]', filename)
if match_chinese_num: if match_chinese_num:
chinese_num = match_chinese_num.group(1) chinese_num = match_chinese_num.group(1)
arabic_num = chinese_to_arabic(chinese_num) arabic_num = chinese_to_arabic(chinese_num)
@ -230,43 +230,80 @@ def sort_file_by_name(file):
segment_base = 0 # 基础值:上=1, 中=2, 下=3 segment_base = 0 # 基础值:上=1, 中=2, 下=3
sequence_number = 0 # 序号值:用于处理上中下后的数字或中文数字序号 sequence_number = 0 # 序号值:用于处理上中下后的数字或中文数字序号
if re.search(r'上[集期话部篇]?|[集期话部篇]上', filename): # 严格匹配上中下标记:只有当上中下与集期话部篇相邻时才认为是段落标记
# 避免误匹配文件内容中偶然出现的上中下字符
if re.search(r'上[集期话部篇]|[集期话部篇]上', filename):
segment_base = 1 segment_base = 1
elif re.search(r'中[集期话部篇]?|[集期话部篇]中', filename): elif re.search(r'中[集期话部篇]|[集期话部篇]中', filename):
segment_base = 2 segment_base = 2
elif re.search(r'下[集期话部篇]?|[集期话部篇]下', filename): elif re.search(r'下[集期话部篇]|[集期话部篇]下', filename):
segment_base = 3 segment_base = 3
# 当有上中下标记时,进一步提取后续的序号 # 统一的序号提取逻辑,支持多种分隔符和格式
if segment_base > 0: # 无论是否有上中下标记,都使用相同的序号提取逻辑
# 提取上中下后的中文数字序号,如:上(一)、上(二)
chinese_seq_match = re.search(r'[上中下][集期话部篇]?[(]([一二三四五六七八九十百千万零两]+)[)]', filename) # 定义序号提取的模式,使用正向匹配组合的方式
if chinese_seq_match: # 这样可以精准匹配,避免误判"星期六"等内容
chinese_num = chinese_seq_match.group(1) sequence_patterns = [
arabic_num = chinese_to_arabic(chinese_num) # 第+中文数字+期集话部篇+序号:第一期(一)、第五十六期-二、第 一 期 三
(r'\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]\s*[(]\s*([一二三四五六七八九十百千万零两]+)\s*[)]', 'chinese'),
(r'\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]\s*[(]\s*(\d+)\s*[)]', 'arabic'),
(r'\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]\s*[-_·丨]\s*([一二三四五六七八九十百千万零两]+)', 'chinese'),
(r'\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]\s*[-_·丨]\s*(\d+)', 'arabic'),
(r'\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]\s+([一二三四五六七八九十百千万零两]+)(?![一二三四五六七八九十])', 'chinese'),
(r'\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]\s+(\d+)(?!\d)', 'arabic'),
(r'\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇]([一二三四五六七八九十])(?![一二三四五六七八九十])', 'chinese'),
(r'\s*[一二三四五六七八九十百千万零两]+\s*[期集话部篇](\d+)(?!\d)', 'arabic'),
# 第+阿拉伯数字+期集话部篇+序号第1期、第100期-二、第 1 期 三
(r'\s*\d+\s*[期集话部篇]\s*[(]\s*([一二三四五六七八九十百千万零两]+)\s*[)]', 'chinese'),
(r'\s*\d+\s*[期集话部篇]\s*[(]\s*(\d+)\s*[)]', 'arabic'),
(r'\s*\d+\s*[期集话部篇]\s*[-_·丨]\s*([一二三四五六七八九十百千万零两]+)', 'chinese'),
(r'\s*\d+\s*[期集话部篇]\s*[-_·丨]\s*(\d+)', 'arabic'),
(r'\s*\d+\s*[期集话部篇]\s+([一二三四五六七八九十百千万零两]+)(?![一二三四五六七八九十])', 'chinese'),
(r'\s*\d+\s*[期集话部篇]\s+(\d+)(?!\d)', 'arabic'),
(r'\s*\d+\s*[期集话部篇]([一二三四五六七八九十])(?![一二三四五六七八九十])', 'chinese'),
(r'\s*\d+\s*[期集话部篇](\d+)(?!\d)', 'arabic'),
# 上中下+集期话部篇+序号:上集(一)、中部-二、下篇 三
(r'[上中下][集期话部篇]\s*[(]\s*([一二三四五六七八九十百千万零两]+)\s*[)]', 'chinese'),
(r'[上中下][集期话部篇]\s*[(]\s*(\d+)\s*[)]', 'arabic'),
(r'[上中下][集期话部篇]\s*[-_·丨]\s*([一二三四五六七八九十百千万零两]+)', 'chinese'),
(r'[上中下][集期话部篇]\s*[-_·丨]\s*(\d+)', 'arabic'),
(r'[上中下][集期话部篇]\s+([一二三四五六七八九十百千万零两]+)(?![一二三四五六七八九十])', 'chinese'),
(r'[上中下][集期话部篇]\s+(\d+)(?!\d)', 'arabic'),
(r'[上中下][集期话部篇]([一二三四五六七八九十])(?![一二三四五六七八九十])', 'chinese'),
(r'[上中下][集期话部篇](\d+)(?!\d)', 'arabic'),
# 集期话部篇+上中下+序号:集上(一)、部中-二、篇下 三
(r'[集期话部篇][上中下]\s*[(]\s*([一二三四五六七八九十百千万零两]+)\s*[)]', 'chinese'),
(r'[集期话部篇][上中下]\s*[(]\s*(\d+)\s*[)]', 'arabic'),
(r'[集期话部篇][上中下]\s*[-_·丨]\s*([一二三四五六七八九十百千万零两]+)', 'chinese'),
(r'[集期话部篇][上中下]\s*[-_·丨]\s*(\d+)', 'arabic'),
(r'[集期话部篇][上中下]\s+([一二三四五六七八九十百千万零两]+)(?![一二三四五六七八九十])', 'chinese'),
(r'[集期话部篇][上中下]\s+(\d+)(?!\d)', 'arabic'),
(r'[集期话部篇][上中下]([一二三四五六七八九十])(?![一二三四五六七八九十])', 'chinese'),
(r'[集期话部篇][上中下](\d+)(?!\d)', 'arabic'),
]
# 尝试匹配序号
for pattern, num_type in sequence_patterns:
match = re.search(pattern, filename)
if match:
if num_type == 'chinese':
arabic_num = chinese_to_arabic(match.group(1))
if arabic_num is not None: if arabic_num is not None:
sequence_number = arabic_num sequence_number = arabic_num
else: # 如果之前没有检测到上中下标记,给一个基础值
# 提取上中下后的阿拉伯数字序号上1、上2 if segment_base == 0:
arabic_seq_match = re.search(r'[上中下][集期话部篇]?(\d+)', filename) segment_base = 1
if arabic_seq_match: break
sequence_number = int(arabic_seq_match.group(1)) else: # arabic
else: sequence_number = int(match.group(1))
# 如果没有上中下标记,检查是否有括号内的中文数字序号 # 如果之前没有检测到上中下标记,给一个基础值
# 匹配格式如第2期、第2期 if segment_base == 0:
parentheses_chinese_match = re.search(r'[期集话部篇][(]([一二三四五六七八九十百千万零两]+)[)]', filename) segment_base = 1
if parentheses_chinese_match: break
chinese_num = parentheses_chinese_match.group(1)
arabic_num = chinese_to_arabic(chinese_num)
if arabic_num is not None:
sequence_number = arabic_num
segment_base = 1 # 给一个基础值,确保有括号序号的文件能正确排序
else:
# 匹配格式如第2期(1)、第2期(2)等
parentheses_arabic_match = re.search(r'[期集话部篇][(](\d+)[)]', filename)
if parentheses_arabic_match:
sequence_number = int(parentheses_arabic_match.group(1))
segment_base = 1 # 给一个基础值,确保有括号序号的文件能正确排序
# 组合segment_value基础值*1000 + 序号值,确保排序正确 # 组合segment_value基础值*1000 + 序号值,确保排序正确
segment_value = segment_base * 1000 + sequence_number segment_value = segment_base * 1000 + sequence_number
@ -363,6 +400,18 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
for pattern in resolution_patterns: for pattern in resolution_patterns:
filename_without_dates = re.sub(pattern, ' ', filename_without_dates) filename_without_dates = re.sub(pattern, ' ', filename_without_dates)
# 预处理:移除季编号标识,避免误提取季编号为集编号
season_patterns = [
r'[Ss]\d+(?![Ee])', # S1, S01 (但不包括S01E01中的S01)
r'[Ss]\s+\d+', # S 1, S 01
r'Season\s*\d+', # Season1, Season 1
r'\s*\d+\s*季', # 第1季, 第 1 季
r'\s*[一二三四五六七八九十百千万零两]+\s*季', # 第一季, 第 二 季
]
for pattern in season_patterns:
filename_without_dates = re.sub(pattern, ' ', filename_without_dates, flags=re.IGNORECASE)
# 优先匹配SxxExx格式 # 优先匹配SxxExx格式
match_s_e = re.search(r'[Ss](\d+)[Ee](\d+)', filename_without_dates) match_s_e = re.search(r'[Ss](\d+)[Ee](\d+)', filename_without_dates)
if match_s_e: if match_s_e: