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 @@
-
+
@@ -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": []
}