改进选择起始文件模态框的排序方式,修复起始文件失效的问题

Merge pull request #34 from x1ao4/dev
This commit is contained in:
x1ao4 2025-07-06 03:08:00 +08:00 committed by GitHub
commit 9cbee5b324
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 266 additions and 43 deletions

View File

@ -87,11 +87,15 @@ function sortFileByName(file) {
}
// MM-DD
if (date_value === Infinity) {
match = filename.match(/(?<!\d)(\d{1,2})[-./\s](\d{1,2})(?!\d)/);
match = filename.match(/(?<!\d)(\d{1,2})[-./](\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;
// 验证是否为有效的月日组合
if ((month >= 1 && month <= 12 && day >= 1 && day <= 31) ||
(day >= 1 && day <= 12 && month >= 1 && month <= 31)) {
if (month > 12) [month, day] = [day, month];
date_value = 20000000 + month * 100 + day;
}
}
}
@ -145,7 +149,19 @@ function sortFileByName(file) {
if (/^\d+$/.test(file_name_without_ext)) {
episode_value = parseInt(file_name_without_ext);
} else {
match = filename.match(/(\d+)/);
// 预处理:移除分辨率标识(如 720p, 1080P, 2160p 等)
let filename_without_resolution = filename;
const resolution_patterns = [
/\b\d+[pP]\b/g, // 匹配 720p, 1080P, 2160p 等
/\b\d+x\d+\b/g, // 匹配 1920x1080 等
// 注意不移除4K/8K因为剧集匹配规则中有 (\d+)[-_\s]*4[Kk] 模式
];
for (const pattern of resolution_patterns) {
filename_without_resolution = filename_without_resolution.replace(pattern, ' ');
}
match = filename_without_resolution.match(/(\d+)/);
if (match) episode_value = parseInt(match[1]);
}
}

View File

@ -4447,20 +4447,32 @@
// 按选定字段和顺序对文件列表进行排序
this.fileSelect.fileList.sort((a, b) => {
if (field === 'file_name') {
// 文件夹始终在前
if (a.dir && !b.dir) return -1;
if (!a.dir && b.dir) return 1;
// 检查当前模态框类型,选择起始文件模态框使用全局文件排序函数
const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
if (modalType === 'start-file') {
// 选择起始文件模态框:使用全局文件排序函数
// 选择起始文件模态框:按过滤状态和类型排序
// 1. 检查过滤状态 - 包括没有file_name_re字段的情况
const aFiltered = !a.file_name_re || a.file_name_re === '×' || a.file_name_re.startsWith('×');
const bFiltered = !b.file_name_re || b.file_name_re === '×' || b.file_name_re.startsWith('×');
// 无论升序还是降序,未被过滤的项目都排在前面,被过滤的项目排在后面
if (!aFiltered && bFiltered) return -1;
if (aFiltered && !bFiltered) return 1;
// 2. 在同一过滤状态内,文件夹排在文件前面
if (a.dir && !b.dir) return -1;
if (!a.dir && b.dir) return 1;
// 使用全局文件排序函数
const ka = sortFileByName(a), kb = sortFileByName(b);
for (let i = 0; i < ka.length; ++i) {
if (ka[i] !== kb[i]) return this.fileSelect.sortOrder === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
}
return 0;
} else {
// 其他模态框:文件夹排在文件前面
if (a.dir && !b.dir) return -1;
if (!a.dir && b.dir) return 1;
// 其他模态框:使用拼音排序
let aValue = pinyinPro.pinyin(a.file_name, { toneType: 'none', type: 'string' }).toLowerCase();
let bValue = pinyinPro.pinyin(b.file_name, { toneType: 'none', type: 'string' }).toLowerCase();
@ -4568,14 +4580,39 @@
manualSortFileList(field, order) {
this.fileSelect.fileList.sort((a, b) => {
if (field === 'file_name') {
if (a.dir && !b.dir) return -1;
if (!a.dir && b.dir) return 1;
// 使用智能排序函数
const ka = sortFileByName(a), kb = sortFileByName(b);
for (let i = 0; i < ka.length; ++i) {
if (ka[i] !== kb[i]) return order === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
// 检查当前模态框类型,选择起始文件模态框使用特殊排序逻辑
const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
if (modalType === 'start-file') {
// 选择起始文件模态框:按过滤状态和类型排序
// 1. 检查过滤状态 - 包括没有file_name_re字段的情况
const aFiltered = !a.file_name_re || a.file_name_re === '×' || a.file_name_re.startsWith('×');
const bFiltered = !b.file_name_re || b.file_name_re === '×' || b.file_name_re.startsWith('×');
// 无论升序还是降序,未被过滤的项目都排在前面,被过滤的项目排在后面
if (!aFiltered && bFiltered) return -1;
if (aFiltered && !bFiltered) return 1;
// 2. 在同一过滤状态内,文件夹排在文件前面
if (a.dir && !b.dir) return -1;
if (!a.dir && b.dir) return 1;
// 使用全局文件排序函数
const ka = sortFileByName(a), kb = sortFileByName(b);
for (let i = 0; i < ka.length; ++i) {
if (ka[i] !== kb[i]) return order === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
}
return 0;
} else {
// 其他模态框:文件夹排在文件前面
if (a.dir && !b.dir) return -1;
if (!a.dir && b.dir) return 1;
// 使用智能排序函数
const ka = sortFileByName(a), kb = sortFileByName(b);
for (let i = 0; i < ka.length; ++i) {
if (ka[i] !== kb[i]) return order === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
}
return 0;
}
return 0;
}
if (field === 'file_name_re') {
const aHasValidRename = a.file_name_re && a.file_name_re !== '×' && !a.file_name_re.startsWith('×');

View File

@ -128,19 +128,22 @@ def sort_file_by_name(file):
month, day = day, month
date_value = year * 10000 + month * 100 + day
# 1.6 MM-DD 或 MM.DD 或 MM/DD 或 MM DD格式(无年份)
# 1.6 MM-DD 或 MM.DD 或 MM/DD格式(无年份,不包括空格分隔
if date_value == float('inf'):
match_date_short = re.search(r'(?<!\d)(\d{1,2})[-./\s](\d{1,2})(?!\d)', filename)
match_date_short = re.search(r'(?<!\d)(\d{1,2})[-./](\d{1,2})(?!\d)', 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
# 由于没有年份,使用一个较低的基数,确保任何有年份的日期都排在前面
# 使用20000000作为基准所以无年份日期都会排在有年份日期之后
date_value = 20000000 + month * 100 + day
# 验证是否为有效的月日组合
if ((month >= 1 and month <= 12 and day >= 1 and day <= 31) or
(day >= 1 and day <= 12 and month >= 1 and month <= 31)):
# 检查月份值如果大于12可能是欧式日期格式DD/MM
if month > 12:
month, day = day, month
# 由于没有年份,使用一个较低的基数,确保任何有年份的日期都排在前面
# 使用20000000作为基准所以无年份日期都会排在有年份日期之后
date_value = 20000000 + month * 100 + day
# 2. 提取期数/集数 - 第二级排序键
@ -207,8 +210,19 @@ def sort_file_by_name(file):
if file_name_without_ext.isdigit():
episode_value = int(file_name_without_ext)
else:
# 预处理:移除分辨率标识(如 720p, 1080P, 2160p 等)
filename_without_resolution = filename
resolution_patterns = [
r'\b\d+[pP]\b', # 匹配 720p, 1080P, 2160p 等
r'\b\d+x\d+\b', # 匹配 1920x1080 等
# 注意不移除4K/8K因为剧集匹配规则中有 (\d+)[-_\s]*4[Kk] 模式
]
for pattern in resolution_patterns:
filename_without_resolution = re.sub(pattern, ' ', filename_without_resolution)
# 否则尝试提取任何数字
any_num_match = re.search(r'(\d+)', filename)
any_num_match = re.search(r'(\d+)', filename_without_resolution)
if any_num_match:
episode_value = int(any_num_match.group(1))
@ -252,8 +266,8 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
r'(?<!\d)(\d{2})(\d{2})(\d{2})(?!\d)',
# MM/DD/YYYY 或 DD/MM/YYYY 格式
r'(\d{1,2})[-./\s](\d{1,2})[-./\s]((?:19|20)\d{2})',
# MM-DD 或 MM.DD 或 MM/DD 或 MM DD格式(无年份)
r'(?<!\d)(\d{1,2})[-./\s](\d{1,2})(?!\d)',
# MM-DD 或 MM.DD 或 MM/DD格式(无年份,不包括空格分隔
r'(?<!\d)(\d{1,2})[-./](\d{1,2})(?!\d)',
]
# 从不含扩展名的文件名中移除日期部分
@ -1961,27 +1975,33 @@ class Quark:
start_fid = task.get("startfid", "")
start_file_found = False
for share_file in share_file_list:
if share_file["dir"]:
# 顺序命名模式下未设置update_subdir时不处理文件夹
continue
# 改进的起始文件过滤逻辑 - 优先执行,在数据库查重之前
if start_fid:
if share_file["fid"] == start_fid:
start_file_found = True
# 找到起始文件,但不包含起始文件本身,只处理比它更新的文件
continue
elif start_file_found:
# 已经找到起始文件,跳过后续(更旧的)文件
continue
# 如果还没找到起始文件,说明当前文件比起始文件更新,需要处理
else:
# 没有设置起始文件,处理所有文件
pass
# 检查文件ID是否存在于转存记录中
file_id = share_file.get("fid", "")
if file_id and self.check_file_exists_in_records(file_id, task):
# 文件ID已存在于记录中跳过处理
continue
# 改进的起始文件过滤逻辑
if start_fid:
if share_file["fid"] == start_fid:
start_file_found = True
break # 找到起始文件,停止遍历
# 如果还没找到起始文件,继续添加到转存列表
else:
# 没有设置起始文件,处理所有文件
pass
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) or share_file.get("updated_at", 0)
@ -2013,9 +2033,13 @@ class Quark:
sort_tuple = sort_file_by_name(file)
# 返回排序元组,实现多级排序
return sort_tuple
# 对过滤后的文件进行排序(正序,确保顺序命名按正确顺序进行)
# 注意:这里使用正序排序,因为顺序命名需要按照正确的顺序分配序号
filtered_share_files.sort(key=extract_sorting_value)
# 判断是否使用单独的{}模式
# 需保存的文件清单
need_save_list = []
@ -2157,8 +2181,150 @@ class Quark:
share_file["has_updates"] = True
need_save_list.append(share_file)
elif task.get("use_episode_naming") and task.get("episode_naming"):
# 剧集命名模式
need_save_list = []
# 构建目标目录中所有文件的查重索引(按大小和修改时间)
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 = []
start_fid = task.get("startfid", "")
start_file_found = False
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
# 改进的起始文件过滤逻辑 - 优先执行,在数据库查重之前
if start_fid:
if share_file["fid"] == start_fid:
start_file_found = True
# 找到起始文件,但不包含起始文件本身,只处理比它更新的文件
continue
elif start_file_found:
# 已经找到起始文件,跳过后续(更旧的)文件
continue
# 如果还没找到起始文件,说明当前文件比起始文件更新,需要处理
else:
# 没有设置起始文件,处理所有文件
pass
# 检查文件ID是否存在于转存记录中
file_id = share_file.get("fid", "")
if file_id and self.check_file_exists_in_records(file_id, task):
# 文件ID已存在于记录中跳过处理
continue
# 从共享文件中提取剧集号
episode_num = extract_episode_number(share_file["file_name"])
is_duplicate = False
# 通过文件名判断是否已存在(新的查重逻辑)
if not is_duplicate:
# 如果没有重命名,判断原文件名是否已存在
original_name = share_file["file_name"]
# 如果有剧集号,判断重命名后的文件名是否已存在
file_ext = os.path.splitext(original_name)[1]
if episode_num is not None:
# 根据剧集命名模式生成目标文件名
episode_pattern = task["episode_naming"]
if episode_pattern == "[]":
target_name = f"{episode_num:02d}{file_ext}"
else:
target_name = episode_pattern.replace("[]", f"{episode_num:02d}") + file_ext
# 检查目标文件名是否已存在
target_exists = any(dir_file["file_name"] == target_name for dir_file in dir_file_list)
if target_exists:
is_duplicate = True
# 如果没有重复,检查文件大小和扩展名是否重复
if not is_duplicate:
file_size = share_file.get("size", 0)
file_ext_lower = file_ext.lower()
share_update_time = share_file.get("last_update_at", 0) or share_file.get("updated_at", 0)
# 检查是否已存在相同大小和扩展名的文件
key = f"{file_size}_{file_ext_lower}"
if key in dir_files_map:
for existing_file in dir_files_map[key]:
existing_update_time = existing_file.get("updated_at", 0)
# 防止除零错误
if existing_update_time == 0:
continue
# 如果修改时间相近30天内或者差距不大10%以内),认为是同一个文件
time_diff = abs(share_update_time - existing_update_time)
time_ratio = abs(1 - (share_update_time / existing_update_time)) if existing_update_time else 1
if time_diff < 2592000 or time_ratio < 0.1:
# 文件已存在,跳过处理
is_duplicate = True
break
if not is_duplicate:
share_file["save_name"] = share_file["file_name"] # 剧集命名模式下保持原文件名,重命名在后续步骤进行
share_file["original_name"] = share_file["file_name"]
filtered_share_files.append(share_file)
# 实现高级排序算法
def sort_by_episode(file):
if file["dir"]:
return (float('inf'), 0)
filename = file["file_name"]
# 优先匹配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, 0)
# 使用统一的剧集提取函数
episode_num = extract_episode_number(filename)
if episode_num is not None:
return (episode_num, 0)
# 无法识别,回退到修改时间排序
return (float('inf'), file.get("last_update_at", 0))
# 过滤出文件并排序
files_to_process = [f for f in filtered_share_files if not f["dir"]]
sorted_files = sorted(files_to_process, key=sort_by_episode)
# 要保存的文件列表
need_save_list = []
# 添加排序后的文件到保存列表
for share_file in sorted_files:
need_save_list.append(share_file)
# 处理文件夹
for share_file in filtered_share_files:
if share_file["dir"]:
need_save_list.append(share_file)
else:
# 正则命名模式
# 正则命名模式(普通正则命名模式)
need_save_list = []
# 构建目标目录中所有文件的查重索引(按大小和修改时间)- 加入文件查重机制
@ -2189,7 +2355,7 @@ class Quark:
break
if start_index >= 0:
# 只处理起始文件之前的文件(不包括起始文件本身)
# 只处理起始文件之前的文件(比起始文件更新的文件,不包括起始文件本身)
share_file_list = share_file_list[:start_index]
# 添加符合的
@ -2967,8 +3133,12 @@ class Quark:
if start_fid:
if share_file["fid"] == start_fid:
start_file_found = True
break # 找到起始文件,停止遍历
# 如果还没找到起始文件,继续添加到转存列表
# 找到起始文件,但不包含起始文件本身,只处理比它更新的文件
continue
elif start_file_found:
# 已经找到起始文件,跳过后续(更旧的)文件
continue
# 如果还没找到起始文件,说明当前文件比起始文件更新,需要处理
else:
# 没有设置起始文件,处理所有文件
pass