diff --git a/app/run.py b/app/run.py index 7a6b361..d285e89 100644 --- a/app/run.py +++ b/app/run.py @@ -144,7 +144,7 @@ def enrich_tasks_with_calendar_meta(tasks_info: list) -> list: except Exception: transferred_by_task = {} - # 统计“已播出集数”:读取本地 episodes 表中有 air_date 且 <= 今天的集数 + # 统计"已播出集数":读取本地 episodes 表中有 air_date 且 <= 今天的集数 from datetime import datetime as _dt today = _dt.now().strftime('%Y-%m-%d') aired_by_show_season = {} @@ -586,7 +586,7 @@ logging.basicConfig( format="[%(asctime)s][%(levelname)s] %(message)s", datefmt="%m-%d %H:%M:%S", ) -# 降低第三方网络库的重试噪音:将 urllib3/requests 的日志调为 ERROR,并把“Retrying ...”消息降级为 DEBUG +# 降低第三方网络库的重试噪音:将 urllib3/requests 的日志调为 ERROR,并把"Retrying ..."消息降级为 DEBUG try: logging.getLogger("urllib3").setLevel(logging.ERROR) logging.getLogger("requests").setLevel(logging.ERROR) @@ -2113,32 +2113,15 @@ 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]) + raw_patterns = config_data.get("episode_patterns", []) 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) # 应用高级过滤词过滤 filterwords = regex.get("filterwords", "") @@ -2558,6 +2541,9 @@ def init(): # 读取配置 config_data = Config.read_json(CONFIG_PATH) Config.breaking_change_update(config_data) + + # 自动清理剧集识别规则配置 + cleanup_episode_patterns_config(config_data) # 默认管理账号 config_data["webui"] = { @@ -3400,55 +3386,46 @@ def preview_rename(): 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]) + raw_patterns = config_data.get("episode_patterns", []) 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) - # 处理每个文件 + # 应用高级过滤词过滤(filterwords 已在函数开头获取) + if filterwords: + # 使用高级过滤函数 + filtered_files = advanced_filter_files(filtered_files, filterwords) + # 标记被过滤的文件 + for item in filtered_files: + if item not in filtered_files: + item["filtered"] = True + + # 处理未被过滤的文件 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"], - "episode_number": episode_num # 添加集数字段用于前端排序 - }) - else: - # 没有提取到集号,显示无法识别的提示 - preview_results.append({ - "original_name": file["file_name"], - "new_name": "× 无法识别剧集编号", - "file_id": file["fid"] - }) - + 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: + 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: @@ -5682,6 +5659,84 @@ def get_content_types(): 'message': f'获取节目内容类型失败: {str(e)}' }) +def cleanup_episode_patterns_config(config_data): + """清理剧集识别规则配置""" + try: + # 需要清理的默认剧集识别规则(按部分规则匹配) + default_pattern_parts = [ + "第(\\d+)集", + "第(\\d+)期", + "第(\\d+)话", + "(\\d+)集", + "(\\d+)期", + "(\\d+)话", + "[Ee][Pp]?(\\d+)", + "(\\d+)[-_\\s]*4[Kk]", + "(\\d+)[-_\\\\s]*4[Kk]", + "\\[(\\d+)\\]", + "【(\\d+)】", + "_?(\\d+)_?" + ] + + cleaned_tasks = 0 + cleaned_global = False + + # 1. 清理任务级别的 config_data.episode_patterns + if 'tasklist' in config_data: + for task in config_data['tasklist']: + if 'config_data' in task and 'episode_patterns' in task['config_data']: + del task['config_data']['episode_patterns'] + cleaned_tasks += 1 + # 如果 config_data 为空,删除整个 config_data + if not task['config_data']: + del task['config_data'] + + # 2. 清理全局配置中的默认规则 + if 'episode_patterns' in config_data: + current_patterns = config_data['episode_patterns'] + if isinstance(current_patterns, list): + # 过滤掉包含默认规则的配置 + filtered_patterns = [] + for pattern in current_patterns: + if isinstance(pattern, dict) and 'regex' in pattern: + pattern_regex = pattern['regex'] + # 用竖线分割规则 + pattern_parts = pattern_regex.split('|') + # 过滤掉默认规则部分,保留自定义规则 + custom_parts = [part.strip() for part in pattern_parts if part.strip() not in default_pattern_parts] + + if custom_parts: + # 如果有自定义规则,保留并重新组合 + filtered_patterns.append({ + 'regex': '|'.join(custom_parts) + }) + elif isinstance(pattern, str): + pattern_regex = pattern + # 用竖线分割规则 + pattern_parts = pattern_regex.split('|') + # 过滤掉默认规则部分,保留自定义规则 + custom_parts = [part.strip() for part in pattern_parts if part.strip() not in default_pattern_parts] + + if custom_parts: + # 如果有自定义规则,保留并重新组合 + filtered_patterns.append('|'.join(custom_parts)) + + # 更新配置 + if filtered_patterns: + config_data['episode_patterns'] = filtered_patterns + else: + # 如果没有剩余规则,清空配置 + config_data['episode_patterns'] = [] + cleaned_global = True + + # 静默执行清理操作,不输出日志 + + return True + + except Exception as e: + logging.error(f"清理剧集识别规则配置失败: {str(e)}") + return False + if __name__ == "__main__": init() reload_tasks() diff --git a/app/templates/index.html b/app/templates/index.html index d72889b..c9a9b60 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -692,7 +692,7 @@ -
+

剧集识别

@@ -705,7 +705,7 @@
集编号识别规则
- +
@@ -3256,11 +3256,15 @@ return this.formData.episode_patterns.map(p => p.regex || '').join('|'); }, set(value) { - // 允许直接输入正则表达式,当用户按下Enter键或失焦时再处理 - // 这里我们创建一个单一的正则表达式对象,而不是拆分 - this.formData.episode_patterns = [{ - regex: value.trim() - }]; + // 支持竖线分割的多个正则表达式 + if (!value || value.trim() === '') { + this.formData.episode_patterns = []; + return; + } + + // 按竖线分割并创建多个正则表达式对象 + const patterns = value.split('|').map(p => p.trim()).filter(p => p !== ''); + this.formData.episode_patterns = patterns.map(regex => ({ regex })); } }, // 管理视图:按任务名(拼音)排序并应用顶部筛选 @@ -9689,9 +9693,23 @@ bValue = String(b.episode_number); } } else { - // 否则使用重命名后的文件名进行拼音排序 - aValue = pinyinPro.pinyin(a.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase(); - bValue = pinyinPro.pinyin(b.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase(); + // 否则尝试从重命名结果中提取数字进行数值排序 + const aRename = a.file_name_re || ''; + const bRename = b.file_name_re || ''; + + // 尝试提取数字(包括小数) + const aMatch = aRename.match(/(\d+(?:\.\d+)?)/); + const bMatch = bRename.match(/(\d+(?:\.\d+)?)/); + + if (aMatch && bMatch) { + // 如果都能提取到数字,进行数值比较 + aValue = parseFloat(aMatch[1]); + bValue = parseFloat(bMatch[1]); + } else { + // 否则使用重命名后的文件名进行拼音排序 + aValue = pinyinPro.pinyin(aRename, { toneType: 'none', type: 'string' }).toLowerCase(); + bValue = pinyinPro.pinyin(bRename, { toneType: 'none', type: 'string' }).toLowerCase(); + } } if (this.fileSelect.sortOrder === 'asc') { @@ -9820,9 +9838,23 @@ bValue = String(b.episode_number); } } else { - // 否则使用重命名后的文件名进行拼音排序 - aValue = pinyinPro.pinyin(a.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase(); - bValue = pinyinPro.pinyin(b.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase(); + // 否则尝试从重命名结果中提取数字进行数值排序 + const aRename = a.file_name_re || ''; + const bRename = b.file_name_re || ''; + + // 尝试提取数字(包括小数) + const aMatch = aRename.match(/(\d+(?:\.\d+)?)/); + const bMatch = bRename.match(/(\d+(?:\.\d+)?)/); + + if (aMatch && bMatch) { + // 如果都能提取到数字,进行数值比较 + aValue = parseFloat(aMatch[1]); + bValue = parseFloat(bMatch[1]); + } else { + // 否则使用重命名后的文件名进行拼音排序 + aValue = pinyinPro.pinyin(aRename, { toneType: 'none', type: 'string' }).toLowerCase(); + bValue = pinyinPro.pinyin(bRename, { toneType: 'none', type: 'string' }).toLowerCase(); + } } if (order === 'asc') { diff --git a/quark_auto_save.py b/quark_auto_save.py index 9b06370..9f758fe 100644 --- a/quark_auto_save.py +++ b/quark_auto_save.py @@ -289,7 +289,9 @@ def sort_file_by_name(file): if episode_value == float('inf'): match_e = re.search(r'[Ee][Pp]?(\d+)', filename) if match_e: - episode_value = int(match_e.group(1)) + # 若数字位于含字母的中括号内部,跳过该匹配 + if not _in_alpha_brackets(filename, match_e.start(1), match_e.end(1)): + episode_value = int(match_e.group(1)) # 2.5 1x01格式 if episode_value == float('inf'): @@ -315,16 +317,33 @@ def sort_file_by_name(file): resolution_patterns = [ r'\b\d+[pP]\b', # 匹配 720p, 1080P, 2160p 等 r'\b\d+x\d+\b', # 匹配 1920x1080 等 - # 注意:不移除4K/8K,避免误删文件名中的4K标识 + r'(? 9999: + continue + candidates.append((m.start(), value)) + if candidates: + candidates.sort(key=lambda x: x[0]) + episode_value = candidates[0][1] # 3. 提取上中下标记或其他细分 - 第三级排序键 segment_base = 0 # 基础值:上=1, 中=2, 下=3 @@ -415,6 +434,54 @@ def sort_file_by_name(file): # 全局的剧集编号提取函数 +def _in_alpha_brackets(text, start, end): + """ + 判断 [start,end) 范围内的数字是否位于"含字母的中括号对"内部。 + 支持英文方括号 [] 和中文方括号 【】。 + 要求:数字左侧最近的未闭合括号与右侧最近的对应闭合括号形成对,且括号内容包含字母。 + 但是允许 E/e 和 EP/ep/Ep 这样的集数格式。 + """ + if start < 0 or end > len(text): + return False + + # 检查英文方括号 [] + last_open_en = text.rfind('[', 0, start) + if last_open_en != -1: + close_before_en = text.rfind(']', 0, start) + if close_before_en == -1 or close_before_en < last_open_en: + close_after_en = text.find(']', end) + if close_after_en != -1: + content = text[last_open_en + 1:close_after_en] + if _check_bracket_content(content): + return True + + # 检查中文方括号 【】 + last_open_cn = text.rfind('【', 0, start) + if last_open_cn != -1: + close_before_cn = text.rfind('】', 0, start) + if close_before_cn == -1 or close_before_cn < last_open_cn: + close_after_cn = text.find('】', end) + if close_after_cn != -1: + content = text[last_open_cn + 1:close_after_cn] + if _check_bracket_content(content): + return True + + return False + +def _check_bracket_content(content): + """ + 检查括号内容是否应该被排除 + """ + # 检查是否包含字母 + has_letters = bool(re.search(r'[A-Za-z]', content)) + if not has_letters: + return False + + # 如果是 E/e 或 EP/ep/Ep 格式,则允许通过 + if re.match(r'^[Ee][Pp]?\d+$', content): + return False + + return True def extract_episode_number(filename, episode_patterns=None, config_data=None): """ 从文件名中提取剧集编号 @@ -430,6 +497,11 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None): # 首先去除文件扩展名 file_name_without_ext = os.path.splitext(filename)[0] + # 特判:SxxEyy.zz 模式(例如 S01E11.11),在日期清洗前优先识别 + m_spec = re.search(r'[Ss](\d+)[Ee](\d{1,2})[._\-/]\d{1,2}', file_name_without_ext) + if m_spec: + return int(m_spec.group(2)) + # 预处理:排除文件名中可能是日期的部分,避免误识别 date_patterns = [ # YYYY-MM-DD 或 YYYY.MM.DD 或 YYYY/MM/DD 或 YYYY MM DD格式(四位年份) @@ -453,6 +525,11 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None): for match in matches: # 检查匹配的内容是否确实是日期 date_str = match.group(0) + # 针对短日期 x.x 或 xx.xx:前一字符为 E/e 时不清洗(保护 E11.11 场景) + if re.match(r'^\d{1,2}[./-]\d{1,2}$', date_str): + prev_char = filename_without_dates[match.start()-1] if match.start() > 0 else '' + if prev_char in 'Ee': + continue month = None day = None @@ -496,7 +573,7 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None): resolution_patterns = [ r'\b\d+[pP]\b', # 匹配 720p, 1080P, 2160p 等 r'\b\d+x\d+\b', # 匹配 1920x1080 等 - # 注意:不移除4K/8K,避免误删文件名中的4K标识 + r'(? 9999: - return None # 跳过过大的数字 - return episode_num - + candidates = [] + for m in re.finditer(r'\\d+', filename_without_dates): + num_str = m.group(0) + # 过滤日期模式 + if is_date_format(num_str): + continue + # 过滤中括号内且含字母的片段 + span_l, span_r = m.start(), m.end() + if _in_alpha_brackets(filename_without_dates, span_l, span_r): + continue + try: + value = int(num_str) + except ValueError: + continue + if value > 9999: + continue + candidates.append((m.start(), value)) + if candidates: + candidates.sort(key=lambda x: x[0]) + return candidates[0][1] + return None # 全局变量 @@ -684,7 +790,7 @@ NOTIFYS = [] def is_date_format(number_str): """ 判断一个纯数字字符串是否可能是日期格式 - 支持的格式:YYYYMMDD, MMDD, YYMMDD + 支持的格式:YYYYMMDD, YYMMDD """ # 判断YYYYMMDD格式 (8位数字) if len(number_str) == 8 and number_str.startswith('20'): @@ -708,16 +814,8 @@ def is_date_format(number_str): # 可能是日期格式 return True - # 判断MMDD格式 (4位数字) - elif len(number_str) == 4: - month = int(number_str[:2]) - day = int(number_str[2:4]) - - # 简单检查月份和日期是否有效 - if 1 <= month <= 12 and 1 <= day <= 31: - # 可能是日期格式 - return True - + # 不再将 4 位纯数字按 MMDD 视为日期,避免误伤集号(如 1124) + # 其他格式不视为日期格式 return False @@ -1038,22 +1136,6 @@ class Config: if task.get("media_id"): del task["media_id"] - # 添加剧集识别模式配置 - if not config_data.get("episode_patterns"): - print("🔼 添加剧集识别模式配置") - config_data["episode_patterns"] = [ - {"description": "第[]集", "regex": "第(\\d+)集"}, - {"description": "第[]期", "regex": "第(\\d+)期"}, - {"description": "第[]话", "regex": "第(\\d+)话"}, - {"description": "[]集", "regex": "(\\d+)集"}, - {"description": "[]期", "regex": "(\\d+)期"}, - {"description": "[]话", "regex": "(\\d+)话"}, - {"description": "E/EP[]", "regex": "[Ee][Pp]?(\\d+)"}, - {"description": "[]-4K", "regex": "(\\d+)[-_\\s]*4[Kk]"}, - {"description": "[[]", "regex": "\\[(\\d+)\\]"}, - {"description": "【[]】", "regex": "【(\\d+)】"}, - {"description": "_[]_", "regex": "_?(\\d+)_?"} - ] class Quark: @@ -3417,6 +3499,9 @@ class Quark: episode_pattern = task["episode_naming"] regex_pattern = task.get("regex_pattern") + # 初始化变量 + already_renamed_files = set() # 用于防止重复重命名 + # 获取目录文件列表 - 添加这行代码初始化dir_file_list savepath = re.sub(r"/{2,}", "/", f"/{task['savepath']}{subdir_path}") if not self.savepath_fid.get(savepath): @@ -3455,8 +3540,10 @@ class Quark: # 实现序号提取函数 def extract_episode_number_local(filename): - # 使用全局的统一提取函数 - return extract_episode_number(filename, config_data=task.get("config_data")) + # 使用全局的统一提取函数,直接使用全局CONFIG_DATA + if 'CONFIG_DATA' not in globals() or not CONFIG_DATA: + return extract_episode_number(filename) + return extract_episode_number(filename, config_data=CONFIG_DATA) # 找出已命名的文件列表,避免重复转存 existing_episode_numbers = set() diff --git a/quark_config.json b/quark_config.json index 77f590d..e248dc9 100644 --- a/quark_config.json +++ b/quark_config.json @@ -65,9 +65,5 @@ "enddate": "2099-01-30" } ], - "episode_patterns": [ - { - "regex": "第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?" - } - ] + "episode_patterns": [] }