mirror of
https://github.com/Cp0204/quark-auto-save.git
synced 2026-01-16 09:20:43 +08:00
Merge pull request #65 from x1ao4/dev
优化剧集编号提取、追剧日历排序,修复资源切换异常,修改集编号识别规则配置逻辑
This commit is contained in:
commit
1d3894ba85
202
app/run.py
202
app/run.py
@ -144,7 +144,7 @@ def enrich_tasks_with_calendar_meta(tasks_info: list) -> list:
|
|||||||
except Exception:
|
except Exception:
|
||||||
transferred_by_task = {}
|
transferred_by_task = {}
|
||||||
|
|
||||||
# 统计“已播出集数”:读取本地 episodes 表中有 air_date 且 <= 今天的集数
|
# 统计"已播出集数":读取本地 episodes 表中有 air_date 且 <= 今天的集数
|
||||||
from datetime import datetime as _dt
|
from datetime import datetime as _dt
|
||||||
today = _dt.now().strftime('%Y-%m-%d')
|
today = _dt.now().strftime('%Y-%m-%d')
|
||||||
aired_by_show_season = {}
|
aired_by_show_season = {}
|
||||||
@ -586,6 +586,27 @@ logging.basicConfig(
|
|||||||
format="[%(asctime)s][%(levelname)s] %(message)s",
|
format="[%(asctime)s][%(levelname)s] %(message)s",
|
||||||
datefmt="%m-%d %H:%M:%S",
|
datefmt="%m-%d %H:%M:%S",
|
||||||
)
|
)
|
||||||
|
# 降低第三方网络库的重试噪音:将 urllib3/requests 的日志调为 ERROR,并把"Retrying ..."消息降级为 DEBUG
|
||||||
|
try:
|
||||||
|
logging.getLogger("urllib3").setLevel(logging.ERROR)
|
||||||
|
logging.getLogger("requests").setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
class _RetryWarningToDebug(logging.Filter):
|
||||||
|
def filter(self, record: logging.LogRecord) -> bool:
|
||||||
|
try:
|
||||||
|
msg = str(record.getMessage())
|
||||||
|
if "Retrying (Retry(" in msg or "Retrying (" in msg:
|
||||||
|
# 将此条记录的等级降到 DEBUG
|
||||||
|
record.levelno = logging.DEBUG
|
||||||
|
record.levelname = "DEBUG"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
|
_urllib3_logger = logging.getLogger("urllib3.connectionpool")
|
||||||
|
_urllib3_logger.addFilter(_RetryWarningToDebug())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
# 过滤werkzeug日志输出
|
# 过滤werkzeug日志输出
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
||||||
@ -2092,32 +2113,15 @@ def get_share_detail():
|
|||||||
episode_pattern = regex.get("episode_naming")
|
episode_pattern = regex.get("episode_naming")
|
||||||
episode_patterns = regex.get("episode_patterns", [])
|
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 = []
|
episode_patterns = []
|
||||||
raw_patterns = config_data.get("episode_patterns", [default_episode_pattern])
|
raw_patterns = config_data.get("episode_patterns", [])
|
||||||
for p in raw_patterns:
|
for p in raw_patterns:
|
||||||
if isinstance(p, dict) and p.get("regex"):
|
if isinstance(p, dict) and p.get("regex"):
|
||||||
episode_patterns.append(p)
|
episode_patterns.append(p)
|
||||||
elif isinstance(p, str):
|
elif isinstance(p, str):
|
||||||
episode_patterns.append({"regex": p})
|
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", "")
|
filterwords = regex.get("filterwords", "")
|
||||||
@ -2537,6 +2541,9 @@ def init():
|
|||||||
# 读取配置
|
# 读取配置
|
||||||
config_data = Config.read_json(CONFIG_PATH)
|
config_data = Config.read_json(CONFIG_PATH)
|
||||||
Config.breaking_change_update(config_data)
|
Config.breaking_change_update(config_data)
|
||||||
|
|
||||||
|
# 自动清理剧集识别规则配置
|
||||||
|
cleanup_episode_patterns_config(config_data)
|
||||||
|
|
||||||
# 默认管理账号
|
# 默认管理账号
|
||||||
config_data["webui"] = {
|
config_data["webui"] = {
|
||||||
@ -3162,6 +3169,12 @@ def reset_folder():
|
|||||||
logging.error(f">>> 删除记录时出错: {str(e)}")
|
logging.error(f">>> 删除记录时出错: {str(e)}")
|
||||||
# 即使删除记录失败,也返回文件删除成功
|
# 即使删除记录失败,也返回文件删除成功
|
||||||
|
|
||||||
|
# 成功后通过 SSE 通知前端进行热更新
|
||||||
|
try:
|
||||||
|
notify_calendar_changed('reset_folder')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"重置成功,删除了 {deleted_files} 个文件和 {deleted_records} 条记录",
|
"message": f"重置成功,删除了 {deleted_files} 个文件和 {deleted_records} 条记录",
|
||||||
@ -3379,55 +3392,46 @@ def preview_rename():
|
|||||||
|
|
||||||
elif naming_mode == "episode":
|
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 = []
|
episode_patterns = []
|
||||||
raw_patterns = config_data.get("episode_patterns", [default_episode_pattern])
|
raw_patterns = config_data.get("episode_patterns", [])
|
||||||
for p in raw_patterns:
|
for p in raw_patterns:
|
||||||
if isinstance(p, dict) and p.get("regex"):
|
if isinstance(p, dict) and p.get("regex"):
|
||||||
episode_patterns.append(p)
|
episode_patterns.append(p)
|
||||||
elif isinstance(p, str):
|
elif isinstance(p, str):
|
||||||
episode_patterns.append({"regex": p})
|
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:
|
for file in filtered_files:
|
||||||
extension = os.path.splitext(file["file_name"])[1] if not file["dir"] else ""
|
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)
|
# 从文件名中提取集号
|
||||||
|
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
|
if episode_num is not None:
|
||||||
preview_results.append({
|
new_name = pattern.replace("[]", f"{episode_num:02d}") + extension
|
||||||
"original_name": file["file_name"],
|
preview_results.append({
|
||||||
"new_name": new_name,
|
"original_name": file["file_name"],
|
||||||
"file_id": file["fid"],
|
"new_name": new_name,
|
||||||
"episode_number": episode_num # 添加集数字段用于前端排序
|
"file_id": file["fid"]
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
# 没有提取到集号,显示无法识别的提示
|
# 没有提取到集号,显示无法识别的提示
|
||||||
preview_results.append({
|
preview_results.append({
|
||||||
"original_name": file["file_name"],
|
"original_name": file["file_name"],
|
||||||
"new_name": "× 无法识别剧集编号",
|
"new_name": "× 无法识别剧集编号",
|
||||||
"file_id": file["fid"]
|
"file_id": file["fid"]
|
||||||
})
|
})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# 正则命名模式
|
# 正则命名模式
|
||||||
for file in filtered_files:
|
for file in filtered_files:
|
||||||
@ -5661,6 +5665,84 @@ def get_content_types():
|
|||||||
'message': f'获取节目内容类型失败: {str(e)}'
|
'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__":
|
if __name__ == "__main__":
|
||||||
init()
|
init()
|
||||||
reload_tasks()
|
reload_tasks()
|
||||||
|
|||||||
@ -41,7 +41,7 @@ class TMDBService:
|
|||||||
def reset_to_primary_url(self):
|
def reset_to_primary_url(self):
|
||||||
"""重置到主API地址"""
|
"""重置到主API地址"""
|
||||||
self.current_url = self.primary_url
|
self.current_url = self.primary_url
|
||||||
logger.info("TMDB API地址已重置为主地址")
|
logger.debug("TMDB API地址已重置为主地址")
|
||||||
|
|
||||||
def get_current_api_url(self) -> str:
|
def get_current_api_url(self) -> str:
|
||||||
"""获取当前使用的API地址"""
|
"""获取当前使用的API地址"""
|
||||||
@ -87,17 +87,17 @@ class TMDBService:
|
|||||||
pass
|
pass
|
||||||
return data
|
return data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"TMDB主地址请求失败: {e}")
|
logger.debug(f"TMDB主地址请求失败: {e}")
|
||||||
|
|
||||||
# 如果当前使用的是主地址,尝试切换到备用地址
|
# 如果当前使用的是主地址,尝试切换到备用地址
|
||||||
if self.current_url == self.primary_url:
|
if self.current_url == self.primary_url:
|
||||||
logger.info("尝试切换到TMDB备用地址...")
|
logger.debug("尝试切换到TMDB备用地址...")
|
||||||
self.current_url = self.backup_url
|
self.current_url = self.backup_url
|
||||||
try:
|
try:
|
||||||
url = f"{self.current_url}{endpoint}"
|
url = f"{self.current_url}{endpoint}"
|
||||||
response = self.session.get(url, params=params, timeout=10)
|
response = self.session.get(url, params=params, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
logger.info("TMDB备用地址连接成功")
|
logger.debug("TMDB备用地址连接成功")
|
||||||
data = response.json()
|
data = response.json()
|
||||||
try:
|
try:
|
||||||
self._cache[cache_key] = (_now(), data)
|
self._cache[cache_key] = (_now(), data)
|
||||||
@ -111,7 +111,7 @@ class TMDBService:
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
# 如果备用地址也失败,重置回主地址
|
# 如果备用地址也失败,重置回主地址
|
||||||
logger.error(f"TMDB备用地址请求失败: {e}")
|
logger.debug(f"TMDB备用地址请求失败: {e}")
|
||||||
self.current_url = self.primary_url
|
self.current_url = self.primary_url
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -186,7 +186,7 @@ class TMDBService:
|
|||||||
return original_title
|
return original_title
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"获取中文标题失败: {e}, 使用原始标题: {original_title}")
|
logger.debug(f"获取中文标题失败: {e}, 使用原始标题: {original_title}")
|
||||||
return original_title
|
return original_title
|
||||||
|
|
||||||
def get_tv_show_episodes(self, tv_id: int, season_number: int) -> Optional[Dict]:
|
def get_tv_show_episodes(self, tv_id: int, season_number: int) -> Optional[Dict]:
|
||||||
@ -458,7 +458,7 @@ class TMDBService:
|
|||||||
if poster_path:
|
if poster_path:
|
||||||
return poster_path
|
return poster_path
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"获取原始语言海报失败: {e}")
|
logger.debug(f"获取原始语言海报失败: {e}")
|
||||||
|
|
||||||
# 如果设置为中文或原始语言获取失败,尝试中文海报
|
# 如果设置为中文或原始语言获取失败,尝试中文海报
|
||||||
if self.poster_language == "zh-CN" or self.poster_language == "original":
|
if self.poster_language == "zh-CN" or self.poster_language == "original":
|
||||||
@ -477,7 +477,7 @@ class TMDBService:
|
|||||||
if poster_path:
|
if poster_path:
|
||||||
return poster_path
|
return poster_path
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"获取中文海报失败: {e}")
|
logger.debug(f"获取中文海报失败: {e}")
|
||||||
|
|
||||||
# 如果都失败了,返回默认海报路径
|
# 如果都失败了,返回默认海报路径
|
||||||
return details.get('poster_path', '')
|
return details.get('poster_path', '')
|
||||||
|
|||||||
@ -28,6 +28,59 @@ function sortFileByName(file) {
|
|||||||
let filename = typeof file === 'object' ? (file.file_name || '') : file;
|
let filename = typeof file === 'object' ? (file.file_name || '') : file;
|
||||||
let update_time = typeof file === 'object' ? (file.updated_at || 0) : 0;
|
let update_time = typeof file === 'object' ? (file.updated_at || 0) : 0;
|
||||||
let file_name_without_ext = filename.replace(/\.[^/.]+$/, '');
|
let file_name_without_ext = filename.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
|
// 0. 预处理(前移):移除技术规格与季号,供后续“日期与集数”提取共同使用
|
||||||
|
// 这样可以避免 30FPS/1080p/Season 等噪音影响识别
|
||||||
|
let cleanedName = file_name_without_ext;
|
||||||
|
try {
|
||||||
|
const techSpecs = [
|
||||||
|
// 分辨率相关(限定常见p档)
|
||||||
|
/\b(?:240|360|480|540|720|900|960|1080|1440|2160|4320)[pP]\b/g,
|
||||||
|
// 常见分辨率 WxH(白名单)
|
||||||
|
/\b(?:640x360|640x480|720x480|720x576|854x480|960x540|1024x576|1280x720|1280x800|1280x960|1366x768|1440x900|1600x900|1920x1080|2560x1080|2560x1440|3440x1440|3840x1600|3840x2160|4096x2160|7680x4320)\b/g,
|
||||||
|
/(?<!\d)[248]\s*[Kk](?!\d)/g, // 2K/4K/8K
|
||||||
|
|
||||||
|
// 视频编码相关(包含数字的编码)
|
||||||
|
/\b[Hh]\.?264\b/g, // h264, h.264, H264, H.264
|
||||||
|
/\b[Hh]\.?265\b/g, // h265, h.265, H265, H.265
|
||||||
|
/\b[Xx]264\b/g, // x264, X264
|
||||||
|
/\b[Xx]265\b/g, // x265, X265
|
||||||
|
|
||||||
|
// 音频采样率(限定常见采样率)
|
||||||
|
/\b(?:44\.1|48|96)\s*[Kk][Hh][Zz]\b/g,
|
||||||
|
/\b(?:44100|48000|96000)\s*[Hh][Zz]\b/g,
|
||||||
|
|
||||||
|
// 常见码率(白名单)
|
||||||
|
/\b(?:64|96|128|160|192|256|320)\s*[Kk][Bb][Pp][Ss]\b/g,
|
||||||
|
/\b(?:1|1\.5|2|2\.5|3|4|5|6|8|10|12|15|20|25|30|35|40|50|80|100)\s*[Mm][Bb][Pp][Ss]\b/g,
|
||||||
|
|
||||||
|
// 位深(白名单)
|
||||||
|
/\b(?:8|10|12)\s*[Bb][Ii][Tt]s?\b/g,
|
||||||
|
// 严格限定常见帧率,避免将 "07.30FPS" 视为帧率从而连带清除集数
|
||||||
|
/\b(?:23\.976|29\.97|59\.94|24|25|30|50|60|90|120)\s*[Ff][Pp][Ss]\b/g,
|
||||||
|
|
||||||
|
// 频率相关(白名单,含空格/无空格)
|
||||||
|
/\b(?:100|144|200|240|400|800)\s*[Mm][Hh][Zz]\b/g,
|
||||||
|
/\b(?:1|1\.4|2|2\.4|5|5\.8)\s*[Gg][Hh][Zz]\b/g,
|
||||||
|
/\b(?:100|144|200|240|400|800)[Mm][Hh][Zz]\b/g,
|
||||||
|
/\b(?:1|1\.4|2|2\.4|5|5\.8)[Gg][Hh][Zz]\b/g,
|
||||||
|
|
||||||
|
// 声道相关(限定常见声道)
|
||||||
|
/\b(?:1\.0|2\.0|5\.1|7\.1)\s*[Cc][Hh]\b/g,
|
||||||
|
/\b(?:1\.0|2\.0|5\.1|7\.1)[Cc][Hh]\b/g,
|
||||||
|
/\b(?:1\.0|2\.0|5\.1|7\.1)\s*[Cc][Hh][Aa][Nn][Nn][Ee][Ll]\b/g,
|
||||||
|
|
||||||
|
// 其他技术参数(白名单)
|
||||||
|
/\b(?:8|12|16|24|32|48|50|64|108)\s*[Mm][Pp]\b/g,
|
||||||
|
/\b(?:720|1080|1440|1600|1920|2160|4320)\s*[Pp][Ii][Xx][Ee][Ll]\b/g,
|
||||||
|
/\b(?:5400|7200)\s*[Rr][Pp][Mm]\b/g,
|
||||||
|
/\b(?:720|1080|1440|1600|1920|2160|4320)[Pp][Ii][Xx][Ee][Ll]\b/g,
|
||||||
|
/\b(?:5400|7200)[Rr][Pp][Mm]\b/g,
|
||||||
|
];
|
||||||
|
const seasons = [/[Ss]\d+(?![Ee])/gi, /[Ss]\s+\d+/gi, /Season\s*\d+/gi, /第\s*\d+\s*季/gi, /第\s*[一二三四五六七八九十百千万零两]+\s*季/gi];
|
||||||
|
for (const p of techSpecs) cleanedName = cleanedName.replace(p, ' ');
|
||||||
|
for (const p of seasons) cleanedName = cleanedName.replace(p, ' ');
|
||||||
|
} catch (e) {}
|
||||||
let date_value = Infinity, episode_value = Infinity, segment_value = 0;
|
let date_value = Infinity, episode_value = Infinity, segment_value = 0;
|
||||||
|
|
||||||
// 生成拼音排序键(第五级排序)
|
// 生成拼音排序键(第五级排序)
|
||||||
@ -43,16 +96,16 @@ function sortFileByName(file) {
|
|||||||
pinyin_sort_key = filename.toLowerCase();
|
pinyin_sort_key = filename.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 日期提取
|
// 1. 日期提取(改为基于 cleanedName,以避免技术规格噪音干扰)
|
||||||
let match;
|
let match;
|
||||||
// YYYY-MM-DD
|
// YYYY-MM-DD
|
||||||
match = filename.match(/((?:19|20)\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})/);
|
match = cleanedName.match(/((?:19|20)\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})/);
|
||||||
if (match) {
|
if (match) {
|
||||||
date_value = parseInt(match[1]) * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]);
|
date_value = parseInt(match[1]) * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]);
|
||||||
}
|
}
|
||||||
// YY-MM-DD
|
// YY-MM-DD
|
||||||
if (date_value === Infinity) {
|
if (date_value === Infinity) {
|
||||||
match = filename.match(/((?:19|20)?\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})/);
|
match = cleanedName.match(/(?<![Ee][Pp]?)((?:19|20)?\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})/);
|
||||||
if (match && match[1].length === 2) {
|
if (match && match[1].length === 2) {
|
||||||
let year = parseInt('20' + match[1]);
|
let year = parseInt('20' + match[1]);
|
||||||
date_value = year * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]);
|
date_value = year * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]);
|
||||||
@ -60,14 +113,14 @@ function sortFileByName(file) {
|
|||||||
}
|
}
|
||||||
// YYYYMMDD
|
// YYYYMMDD
|
||||||
if (date_value === Infinity) {
|
if (date_value === Infinity) {
|
||||||
match = filename.match(/((?:19|20)\d{2})(\d{2})(\d{2})/);
|
match = cleanedName.match(/((?:19|20)\d{2})(\d{2})(\d{2})/);
|
||||||
if (match) {
|
if (match) {
|
||||||
date_value = parseInt(match[1]) * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]);
|
date_value = parseInt(match[1]) * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// YYMMDD
|
// YYMMDD
|
||||||
if (date_value === Infinity) {
|
if (date_value === Infinity) {
|
||||||
match = filename.match(/(?<!\d)(\d{2})(\d{2})(\d{2})(?!\d)/);
|
match = cleanedName.match(/(?<!\d)(\d{2})(\d{2})(\d{2})(?!\d)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
let month = parseInt(match[2]), day = parseInt(match[3]);
|
let month = parseInt(match[2]), day = parseInt(match[3]);
|
||||||
if (1 <= month && month <= 12 && 1 <= day && day <= 31) {
|
if (1 <= month && month <= 12 && 1 <= day && day <= 31) {
|
||||||
@ -78,7 +131,7 @@ function sortFileByName(file) {
|
|||||||
}
|
}
|
||||||
// MM/DD/YYYY
|
// MM/DD/YYYY
|
||||||
if (date_value === Infinity) {
|
if (date_value === Infinity) {
|
||||||
match = filename.match(/(\d{1,2})[-./\s](\d{1,2})[-./\s]((?:19|20)\d{2})/);
|
match = cleanedName.match(/(\d{1,2})[-./\s](\d{1,2})[-./\s]((?:19|20)\d{2})/);
|
||||||
if (match) {
|
if (match) {
|
||||||
let month = parseInt(match[1]), day = parseInt(match[2]), year = parseInt(match[3]);
|
let month = parseInt(match[1]), day = parseInt(match[2]), year = parseInt(match[3]);
|
||||||
if (month > 12) [month, day] = [day, month];
|
if (month > 12) [month, day] = [day, month];
|
||||||
@ -87,7 +140,7 @@ function sortFileByName(file) {
|
|||||||
}
|
}
|
||||||
// MM-DD
|
// MM-DD
|
||||||
if (date_value === Infinity) {
|
if (date_value === Infinity) {
|
||||||
match = filename.match(/(?<!\d)(\d{1,2})[-./](\d{1,2})(?!\d)/);
|
match = cleanedName.match(/(?<![Ee][Pp]?)(?<!\d)(\d{1,2})[-./](\d{1,2})(?!\d)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
let month = parseInt(match[1]), day = parseInt(match[2]);
|
let month = parseInt(match[1]), day = parseInt(match[2]);
|
||||||
// 验证是否为有效的月日组合
|
// 验证是否为有效的月日组合
|
||||||
@ -99,13 +152,13 @@ function sortFileByName(file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 期数/集数
|
// 2. 期数/集数(同样基于 cleanedName)
|
||||||
// 第X期/集/话
|
// 第X期/集/话
|
||||||
match = filename.match(/第(\d+)[期集话]/);
|
match = cleanedName.match(/第(\d+)[期集话]/);
|
||||||
if (match) episode_value = parseInt(match[1]);
|
if (match) episode_value = parseInt(match[1]);
|
||||||
// 第[中文数字]期/集/话
|
// 第[中文数字]期/集/话
|
||||||
if (episode_value === Infinity) {
|
if (episode_value === Infinity) {
|
||||||
match = filename.match(/第([一二三四五六七八九十百千万零两]+)[期集话]/);
|
match = cleanedName.match(/第([一二三四五六七八九十百千万零两]+)[期集话]/);
|
||||||
if (match) {
|
if (match) {
|
||||||
let arabic = chineseToArabic(match[1]);
|
let arabic = chineseToArabic(match[1]);
|
||||||
if (arabic !== null) episode_value = arabic;
|
if (arabic !== null) episode_value = arabic;
|
||||||
@ -113,12 +166,12 @@ function sortFileByName(file) {
|
|||||||
}
|
}
|
||||||
// X集/期/话
|
// X集/期/话
|
||||||
if (episode_value === Infinity) {
|
if (episode_value === Infinity) {
|
||||||
match = filename.match(/(\d+)[期集话]/);
|
match = cleanedName.match(/(\d+)[期集话]/);
|
||||||
if (match) episode_value = parseInt(match[1]);
|
if (match) episode_value = parseInt(match[1]);
|
||||||
}
|
}
|
||||||
// [中文数字]集/期/话
|
// [中文数字]集/期/话
|
||||||
if (episode_value === Infinity) {
|
if (episode_value === Infinity) {
|
||||||
match = filename.match(/([一二三四五六七八九十百千万零两]+)[期集话]/);
|
match = cleanedName.match(/([一二三四五六七八九十百千万零两]+)[期集话]/);
|
||||||
if (match) {
|
if (match) {
|
||||||
let arabic = chineseToArabic(match[1]);
|
let arabic = chineseToArabic(match[1]);
|
||||||
if (arabic !== null) episode_value = arabic;
|
if (arabic !== null) episode_value = arabic;
|
||||||
@ -126,42 +179,31 @@ function sortFileByName(file) {
|
|||||||
}
|
}
|
||||||
// S01E01
|
// S01E01
|
||||||
if (episode_value === Infinity) {
|
if (episode_value === Infinity) {
|
||||||
match = filename.match(/[Ss](\d+)[Ee](\d+)/);
|
match = cleanedName.match(/[Ss](\d+)[Ee](\d+)/);
|
||||||
if (match) episode_value = parseInt(match[2]);
|
if (match) episode_value = parseInt(match[2]);
|
||||||
}
|
}
|
||||||
// E01/EP01
|
// E01/EP01
|
||||||
if (episode_value === Infinity) {
|
if (episode_value === Infinity) {
|
||||||
match = filename.match(/[Ee][Pp]?(\d+)/);
|
match = cleanedName.match(/[Ee][Pp]?(\d+)/);
|
||||||
if (match) episode_value = parseInt(match[1]);
|
if (match) episode_value = parseInt(match[1]);
|
||||||
}
|
}
|
||||||
// 1x01
|
// 1x01
|
||||||
if (episode_value === Infinity) {
|
if (episode_value === Infinity) {
|
||||||
match = filename.match(/(\d+)[Xx](\d+)/);
|
match = cleanedName.match(/(\d+)[Xx](\d+)/);
|
||||||
if (match) episode_value = parseInt(match[2]);
|
if (match) episode_value = parseInt(match[2]);
|
||||||
}
|
}
|
||||||
// [数字]或【数字】
|
// [数字]或【数字】
|
||||||
if (episode_value === Infinity) {
|
if (episode_value === Infinity) {
|
||||||
match = filename.match(/\[(\d+)\]|【(\d+)】/);
|
match = cleanedName.match(/\[(\d+)\]|【(\d+)】/);
|
||||||
if (match) episode_value = parseInt(match[1] || match[2]);
|
if (match) episode_value = parseInt(match[1] || match[2]);
|
||||||
}
|
}
|
||||||
// 纯数字文件名
|
// 纯数字文件名
|
||||||
if (episode_value === Infinity) {
|
if (episode_value === Infinity) {
|
||||||
if (/^\d+$/.test(file_name_without_ext)) {
|
if (/^\d+$/.test(cleanedName)) {
|
||||||
episode_value = parseInt(file_name_without_ext);
|
episode_value = parseInt(cleanedName);
|
||||||
} else {
|
} else {
|
||||||
// 预处理:移除分辨率标识(如 720p, 1080P, 2160p 等)
|
// 兜底:直接从已清洗的 cleanedName 中提取第一个数字
|
||||||
let filename_without_resolution = filename;
|
match = cleanedName.match(/(\d+)/);
|
||||||
const resolution_patterns = [
|
|
||||||
/\b\d+[pP]\b/g, // 匹配 720p, 1080P, 2160p 等
|
|
||||||
/\b\d+x\d+\b/g, // 匹配 1920x1080 等
|
|
||||||
// 注意:不移除4K/8K,避免误删文件名中的4K标识
|
|
||||||
];
|
|
||||||
|
|
||||||
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]);
|
if (match) episode_value = parseInt(match[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -692,7 +692,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 剧集识别模块 -->
|
<!-- 剧集识别模块 -->
|
||||||
<div class="row title" title="识别文件名中的剧集编号,用于自动重命名,查阅Wiki了解详情">
|
<div class="row title" title="识别文件名中的剧集编号,用于自动重命名,留空时使用内置默认规则,支持输入自定义规则作为补充,查阅Wiki了解详情">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2 style="display: inline-block; font-size: 1.5rem;">剧集识别</h2>
|
<h2 style="display: inline-block; font-size: 1.5rem;">剧集识别</h2>
|
||||||
<span class="badge badge-pill badge-light">
|
<span class="badge badge-pill badge-light">
|
||||||
@ -705,7 +705,7 @@
|
|||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<span class="input-group-text">集编号识别规则</span>
|
<span class="input-group-text">集编号识别规则</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" v-model="episodePatternsText" placeholder="输入用于识别集编号的正则表达式,多个表达式用竖线分隔,特殊符号需要转义">
|
<input type="text" class="form-control" v-model="episodePatternsText" placeholder="留空使用内置默认规则,或输入自定义正则表达式作为补充规则,多个表达式用竖线分隔,特殊符号需要转义">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1886,7 +1886,7 @@
|
|||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<span class="input-group-text">名称筛选</span>
|
<span class="input-group-text">名称筛选</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" v-model="calendar.nameFilter" placeholder="任务名称或剧集名称关键词">
|
<input type="text" class="form-control" v-model="calendar.nameFilter" placeholder="任务或节目名称关键词">
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearCalendarFilter('nameFilter')"><i class="bi bi-x-lg"></i></button>
|
<button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearCalendarFilter('nameFilter')"><i class="bi bi-x-lg"></i></button>
|
||||||
</div>
|
</div>
|
||||||
@ -3256,11 +3256,15 @@
|
|||||||
return this.formData.episode_patterns.map(p => p.regex || '').join('|');
|
return this.formData.episode_patterns.map(p => p.regex || '').join('|');
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
// 允许直接输入正则表达式,当用户按下Enter键或失焦时再处理
|
// 支持竖线分割的多个正则表达式
|
||||||
// 这里我们创建一个单一的正则表达式对象,而不是拆分
|
if (!value || value.trim() === '') {
|
||||||
this.formData.episode_patterns = [{
|
this.formData.episode_patterns = [];
|
||||||
regex: value.trim()
|
return;
|
||||||
}];
|
}
|
||||||
|
|
||||||
|
// 按竖线分割并创建多个正则表达式对象
|
||||||
|
const patterns = value.split('|').map(p => p.trim()).filter(p => p !== '');
|
||||||
|
this.formData.episode_patterns = patterns.map(regex => ({ regex }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 管理视图:按任务名(拼音)排序并应用顶部筛选
|
// 管理视图:按任务名(拼音)排序并应用顶部筛选
|
||||||
@ -3748,11 +3752,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有剧集识别模式,添加默认模式
|
// 确保剧集识别模式字段存在(但不自动添加默认规则)
|
||||||
if (!this.formData.episode_patterns || this.formData.episode_patterns.length === 0) {
|
if (!this.formData.episode_patterns) {
|
||||||
this.formData.episode_patterns = [
|
this.formData.episode_patterns = [];
|
||||||
{ regex: '第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?' }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果当前标签是历史记录,则加载历史记录
|
// 如果当前标签是历史记录,则加载历史记录
|
||||||
@ -4777,11 +4779,23 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 如果启用了合并集功能,则进行合并处理
|
// 如果启用了合并集功能,则进行合并处理
|
||||||
if (this.calendar.mergeEpisodes) {
|
let result = this.calendar.mergeEpisodes
|
||||||
return this.mergeEpisodesByShow(filteredEpisodes);
|
? this.mergeEpisodesByShow(filteredEpisodes)
|
||||||
}
|
: filteredEpisodes;
|
||||||
|
|
||||||
return filteredEpisodes;
|
// 统一对同一天节目按节目名称拼音排序(与内容管理一致)
|
||||||
|
try {
|
||||||
|
result = result.slice().sort((a, b) => {
|
||||||
|
const an = (a && a.show_name) ? String(a.show_name) : '';
|
||||||
|
const bn = (b && b.show_name) ? String(b.show_name) : '';
|
||||||
|
const ak = pinyinPro.pinyin(an, { toneType: 'none', type: 'string' }).toLowerCase();
|
||||||
|
const bk = pinyinPro.pinyin(bn, { toneType: 'none', type: 'string' }).toLowerCase();
|
||||||
|
if (ak === bk) return 0;
|
||||||
|
return ak > bk ? 1 : -1;
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 根据剧集名称查找对应的任务
|
// 根据剧集名称查找对应的任务
|
||||||
@ -8112,6 +8126,23 @@
|
|||||||
this.smart_param.currentResourceIndex--;
|
this.smart_param.currentResourceIndex--;
|
||||||
const previousResource = this.smart_param.taskSuggestions.data[this.smart_param.currentResourceIndex];
|
const previousResource = this.smart_param.taskSuggestions.data[this.smart_param.currentResourceIndex];
|
||||||
if (previousResource) {
|
if (previousResource) {
|
||||||
|
// 在切换资源时,重置展示相关状态,避免错误提示残留
|
||||||
|
this.fileSelect.error = undefined;
|
||||||
|
this.fileSelect.fileList = [];
|
||||||
|
this.fileSelect.paths = [];
|
||||||
|
// 确保处于“选择需转存的文件夹”界面
|
||||||
|
this.fileSelect.previewRegex = false;
|
||||||
|
this.fileSelect.selectDir = true;
|
||||||
|
// 如果基础链接变化,清空stoken强制刷新
|
||||||
|
try {
|
||||||
|
const oldBase = this.getShareurl(this.fileSelect.shareurl);
|
||||||
|
const newBase = this.getShareurl(previousResource.shareurl);
|
||||||
|
if (oldBase !== newBase) {
|
||||||
|
this.fileSelect.stoken = "";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.fileSelect.stoken = "";
|
||||||
|
}
|
||||||
// 更新当前分享链接并重新加载内容
|
// 更新当前分享链接并重新加载内容
|
||||||
this.fileSelect.shareurl = previousResource.shareurl;
|
this.fileSelect.shareurl = previousResource.shareurl;
|
||||||
this.getShareDetail(0, 1);
|
this.getShareDetail(0, 1);
|
||||||
@ -8124,6 +8155,23 @@
|
|||||||
this.smart_param.currentResourceIndex++;
|
this.smart_param.currentResourceIndex++;
|
||||||
const nextResource = this.smart_param.taskSuggestions.data[this.smart_param.currentResourceIndex];
|
const nextResource = this.smart_param.taskSuggestions.data[this.smart_param.currentResourceIndex];
|
||||||
if (nextResource) {
|
if (nextResource) {
|
||||||
|
// 在切换资源时,重置展示相关状态,避免错误提示残留
|
||||||
|
this.fileSelect.error = undefined;
|
||||||
|
this.fileSelect.fileList = [];
|
||||||
|
this.fileSelect.paths = [];
|
||||||
|
// 确保处于“选择需转存的文件夹”界面
|
||||||
|
this.fileSelect.previewRegex = false;
|
||||||
|
this.fileSelect.selectDir = true;
|
||||||
|
// 如果基础链接变化,清空stoken强制刷新
|
||||||
|
try {
|
||||||
|
const oldBase = this.getShareurl(this.fileSelect.shareurl);
|
||||||
|
const newBase = this.getShareurl(nextResource.shareurl);
|
||||||
|
if (oldBase !== newBase) {
|
||||||
|
this.fileSelect.stoken = "";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.fileSelect.stoken = "";
|
||||||
|
}
|
||||||
// 更新当前分享链接并重新加载内容
|
// 更新当前分享链接并重新加载内容
|
||||||
this.fileSelect.shareurl = nextResource.shareurl;
|
this.fileSelect.shareurl = nextResource.shareurl;
|
||||||
this.getShareDetail(0, 1);
|
this.getShareDetail(0, 1);
|
||||||
@ -8370,6 +8418,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getShareDetail(retryCount = 0, maxRetries = 1) {
|
getShareDetail(retryCount = 0, maxRetries = 1) {
|
||||||
|
// 切换或重试前清理残留错误提示,避免覆盖新资源展示
|
||||||
|
this.fileSelect.error = undefined;
|
||||||
this.modalLoading = true;
|
this.modalLoading = true;
|
||||||
|
|
||||||
// 检查index是否有效,如果无效则使用默认值
|
// 检查index是否有效,如果无效则使用默认值
|
||||||
@ -9604,14 +9654,12 @@
|
|||||||
// 其他模态框:文件夹排在文件前面
|
// 其他模态框:文件夹排在文件前面
|
||||||
if (a.dir && !b.dir) return -1;
|
if (a.dir && !b.dir) return -1;
|
||||||
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();
|
const ka = sortFileByName(a), kb = sortFileByName(b);
|
||||||
let bValue = pinyinPro.pinyin(b.file_name, { toneType: 'none', type: 'string' }).toLowerCase();
|
for (let i = 0; i < ka.length; ++i) {
|
||||||
if (this.fileSelect.sortOrder === 'asc') {
|
if (ka[i] !== kb[i]) return this.fileSelect.sortOrder === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
|
||||||
return aValue > bValue ? 1 : -1;
|
|
||||||
} else {
|
|
||||||
return aValue < bValue ? 1 : -1;
|
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (field === 'file_name_re') {
|
if (field === 'file_name_re') {
|
||||||
@ -9641,14 +9689,48 @@
|
|||||||
bValue = String(b.episode_number);
|
bValue = String(b.episode_number);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 否则使用重命名后的文件名进行拼音排序
|
// 否则使用智能排序:优先数值排序,其次日期排序,最后字符串排序
|
||||||
aValue = pinyinPro.pinyin(a.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase();
|
const aRename = a.file_name_re || '';
|
||||||
bValue = pinyinPro.pinyin(b.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase();
|
const bRename = b.file_name_re || '';
|
||||||
|
|
||||||
|
// 尝试提取所有数字进行数值排序
|
||||||
|
const aNumbers = aRename.match(/\d+/g);
|
||||||
|
const bNumbers = bRename.match(/\d+/g);
|
||||||
|
|
||||||
|
if (aNumbers && bNumbers && aNumbers.length > 0 && bNumbers.length > 0) {
|
||||||
|
// 如果都能提取到数字,进行数值比较
|
||||||
|
// 优先比较第一个数字,如果相同则比较后续数字
|
||||||
|
for (let i = 0; i < Math.max(aNumbers.length, bNumbers.length); i++) {
|
||||||
|
const aNum = parseInt(aNumbers[i] || '0', 10);
|
||||||
|
const bNum = parseInt(bNumbers[i] || '0', 10);
|
||||||
|
if (aNum !== bNum) {
|
||||||
|
aValue = aNum;
|
||||||
|
bValue = bNum;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果所有数字都相同,使用自然排序
|
||||||
|
if (aValue === undefined) {
|
||||||
|
aValue = aRename;
|
||||||
|
bValue = bRename;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 否则使用自然排序(支持数值和日期的智能排序)
|
||||||
|
aValue = aRename;
|
||||||
|
bValue = bRename;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.fileSelect.sortOrder === 'asc') {
|
if (this.fileSelect.sortOrder === 'asc') {
|
||||||
|
// 字符串使用自然排序,数值直接比较
|
||||||
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||||
|
return aValue.localeCompare(bValue, undefined, { numeric: true, sensitivity: 'base' });
|
||||||
|
}
|
||||||
return aValue > bValue ? 1 : -1;
|
return aValue > bValue ? 1 : -1;
|
||||||
} else {
|
} else {
|
||||||
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||||
|
return bValue.localeCompare(aValue, undefined, { numeric: true, sensitivity: 'base' });
|
||||||
|
}
|
||||||
return aValue < bValue ? 1 : -1;
|
return aValue < bValue ? 1 : -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9772,14 +9854,49 @@
|
|||||||
bValue = String(b.episode_number);
|
bValue = String(b.episode_number);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 否则使用重命名后的文件名进行拼音排序
|
// 否则使用智能排序:优先数值排序,其次日期排序,最后字符串排序
|
||||||
aValue = pinyinPro.pinyin(a.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase();
|
const aRename = a.file_name_re || '';
|
||||||
bValue = pinyinPro.pinyin(b.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase();
|
const bRename = b.file_name_re || '';
|
||||||
|
|
||||||
|
// 尝试提取所有数字进行数值排序
|
||||||
|
const aNumbers = aRename.match(/\d+/g);
|
||||||
|
const bNumbers = bRename.match(/\d+/g);
|
||||||
|
|
||||||
|
if (aNumbers && bNumbers && aNumbers.length > 0 && bNumbers.length > 0) {
|
||||||
|
// 如果都能提取到数字,进行数值比较
|
||||||
|
// 优先比较第一个数字,如果相同则比较后续数字
|
||||||
|
for (let i = 0; i < Math.max(aNumbers.length, bNumbers.length); i++) {
|
||||||
|
const aNum = parseInt(aNumbers[i] || '0', 10);
|
||||||
|
const bNum = parseInt(bNumbers[i] || '0', 10);
|
||||||
|
if (aNum !== bNum) {
|
||||||
|
aValue = aNum;
|
||||||
|
bValue = bNum;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果所有数字都相同,使用自然排序
|
||||||
|
if (aValue === undefined) {
|
||||||
|
aValue = aRename;
|
||||||
|
bValue = bRename;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 否则使用自然排序(支持数值和日期的智能排序)
|
||||||
|
aValue = aRename;
|
||||||
|
bValue = bRename;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order === 'asc') {
|
if (order === 'asc') {
|
||||||
|
// 如果都是字符串,使用自然排序
|
||||||
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||||
|
return aValue.localeCompare(bValue, undefined, { numeric: true, sensitivity: 'base' });
|
||||||
|
}
|
||||||
return aValue > bValue ? 1 : -1;
|
return aValue > bValue ? 1 : -1;
|
||||||
} else {
|
} else {
|
||||||
|
// 如果都是字符串,使用自然排序
|
||||||
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||||
|
return bValue.localeCompare(aValue, undefined, { numeric: true, sensitivity: 'base' });
|
||||||
|
}
|
||||||
return aValue < bValue ? 1 : -1;
|
return aValue < bValue ? 1 : -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -10371,6 +10488,19 @@
|
|||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
this.showToast(`重置成功:删除了 ${response.data.deleted_files || 0} 个文件,${response.data.deleted_records || 0} 条记录`);
|
this.showToast(`重置成功:删除了 ${response.data.deleted_files || 0} 个文件,${response.data.deleted_records || 0} 条记录`);
|
||||||
|
// 重置成功后,主动热更新最近转存文件与任务元数据,避免等待 SSE 或轮询
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const latestRes = await axios.get('/task_latest_info');
|
||||||
|
if (latestRes.data && latestRes.data.success) {
|
||||||
|
const latestFiles = latestRes.data.data.latest_files || {};
|
||||||
|
this.taskLatestFiles = latestFiles;
|
||||||
|
this.taskLatestRecords = latestRes.data.data.latest_records || {};
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
// 重新加载任务元数据(海报与详细信息)
|
||||||
|
try { await this.loadTasklistMetadata(); } catch (e) {}
|
||||||
|
})();
|
||||||
// 如果当前是历史记录页面,刷新记录
|
// 如果当前是历史记录页面,刷新记录
|
||||||
if (this.activeTab === 'history') {
|
if (this.activeTab === 'history') {
|
||||||
this.loadHistoryRecords();
|
this.loadHistoryRecords();
|
||||||
@ -10841,7 +10971,7 @@
|
|||||||
this.fileSelect.paths = [];
|
this.fileSelect.paths = [];
|
||||||
this.fileSelect.error = undefined;
|
this.fileSelect.error = undefined;
|
||||||
this.fileSelect.selectedFiles = [];
|
this.fileSelect.selectedFiles = [];
|
||||||
// 设置排序方式为按重命名结果降序排序
|
// 预览默认按重命名列降序(与任务列表一致)
|
||||||
this.fileSelect.sortBy = "file_name_re";
|
this.fileSelect.sortBy = "file_name_re";
|
||||||
this.fileSelect.sortOrder = "desc";
|
this.fileSelect.sortOrder = "desc";
|
||||||
// 展示当前文件夹的文件
|
// 展示当前文件夹的文件
|
||||||
@ -13246,11 +13376,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有剧集识别模式,添加默认模式
|
// 确保剧集识别模式字段存在(但不自动添加默认规则)
|
||||||
if (!this.formData.episode_patterns || this.formData.episode_patterns.length === 0) {
|
if (!this.formData.episode_patterns) {
|
||||||
this.formData.episode_patterns = [
|
this.formData.episode_patterns = [];
|
||||||
{ regex: '第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?' }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果当前标签是历史记录,则加载历史记录
|
// 如果当前标签是历史记录,则加载历史记录
|
||||||
|
|||||||
@ -289,7 +289,9 @@ def sort_file_by_name(file):
|
|||||||
if episode_value == float('inf'):
|
if episode_value == float('inf'):
|
||||||
match_e = re.search(r'[Ee][Pp]?(\d+)', filename)
|
match_e = re.search(r'[Ee][Pp]?(\d+)', filename)
|
||||||
if match_e:
|
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格式
|
# 2.5 1x01格式
|
||||||
if episode_value == float('inf'):
|
if episode_value == float('inf'):
|
||||||
@ -315,16 +317,33 @@ def sort_file_by_name(file):
|
|||||||
resolution_patterns = [
|
resolution_patterns = [
|
||||||
r'\b\d+[pP]\b', # 匹配 720p, 1080P, 2160p 等
|
r'\b\d+[pP]\b', # 匹配 720p, 1080P, 2160p 等
|
||||||
r'\b\d+x\d+\b', # 匹配 1920x1080 等
|
r'\b\d+x\d+\b', # 匹配 1920x1080 等
|
||||||
# 注意:不移除4K/8K,避免误删文件名中的4K标识
|
r'(?<!\d)[248]\s*[Kk](?!\d)', # 匹配 2K/4K/8K
|
||||||
]
|
]
|
||||||
|
|
||||||
for pattern in resolution_patterns:
|
for pattern in resolution_patterns:
|
||||||
filename_without_resolution = re.sub(pattern, ' ', filename_without_resolution)
|
filename_without_resolution = re.sub(pattern, ' ', filename_without_resolution)
|
||||||
|
|
||||||
# 否则尝试提取任何数字
|
# 否则尝试提取任何数字
|
||||||
any_num_match = re.search(r'(\d+)', filename_without_resolution)
|
candidates = []
|
||||||
if any_num_match:
|
for m in re.finditer(r'\d+', filename_without_resolution):
|
||||||
episode_value = int(any_num_match.group(1))
|
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_resolution, 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])
|
||||||
|
episode_value = candidates[0][1]
|
||||||
|
|
||||||
# 3. 提取上中下标记或其他细分 - 第三级排序键
|
# 3. 提取上中下标记或其他细分 - 第三级排序键
|
||||||
segment_base = 0 # 基础值:上=1, 中=2, 下=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):
|
def extract_episode_number(filename, episode_patterns=None, config_data=None):
|
||||||
"""
|
"""
|
||||||
从文件名中提取剧集编号
|
从文件名中提取剧集编号
|
||||||
@ -430,29 +497,110 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
|
|||||||
# 首先去除文件扩展名
|
# 首先去除文件扩展名
|
||||||
file_name_without_ext = os.path.splitext(filename)[0]
|
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))
|
||||||
|
|
||||||
|
# 预处理顺序调整:先移除技术规格,再移除日期,降低误判
|
||||||
|
# 预处理:先移除技术规格信息,避免误提取技术参数中的数字为集编号
|
||||||
|
filename_without_dates = file_name_without_ext
|
||||||
|
tech_spec_patterns = [
|
||||||
|
# 分辨率相关(限定常见p档)
|
||||||
|
r'\b(?:240|360|480|540|720|900|960|1080|1440|2160|4320)[pP]\b',
|
||||||
|
# 常见分辨率 WxH(白名单)
|
||||||
|
r'\b(?:640x360|640x480|720x480|720x576|854x480|960x540|1024x576|1280x720|1280x800|1280x960|1366x768|1440x900|1600x900|1920x1080|2560x1080|2560x1440|3440x1440|3840x1600|3840x2160|4096x2160|7680x4320)\b',
|
||||||
|
r'(?<!\d)[248]\s*[Kk](?!\d)', # 匹配 2K/4K/8K
|
||||||
|
|
||||||
|
# 视频编码相关(包含数字的编码)
|
||||||
|
r'\b[Hh]\.?(?:264|265)\b', # 匹配 h264/h265, h.264/h.265 等
|
||||||
|
r'\b[Xx](?:264|265)\b', # 匹配 x264/x265, X264/X265
|
||||||
|
|
||||||
|
# 音频采样率(限定常见采样率)
|
||||||
|
r'\b(?:44\.1|48|96)\s*[Kk][Hh][Zz]\b',
|
||||||
|
r'\b(?:44100|48000|96000)\s*[Hh][Zz]\b',
|
||||||
|
|
||||||
|
# 比特率
|
||||||
|
# 常见码率(白名单)
|
||||||
|
r'\b(?:64|96|128|160|192|256|320)\s*[Kk][Bb][Pp][Ss]\b',
|
||||||
|
r'\b(?:1|1\.5|2|2\.5|3|4|5|6|8|10|12|15|20|25|30|35|40|50|80|100)\s*[Mm][Bb][Pp][Ss]\b',
|
||||||
|
|
||||||
|
# 视频相关
|
||||||
|
# 位深(白名单)
|
||||||
|
r'\b(?:8|10|12)\s*[Bb][Ii][Tt]s?\b',
|
||||||
|
# 严格限定常见帧率,避免将 "07.30FPS" 视为帧率从而连带清除集数
|
||||||
|
r'\b(?:23\.976|29\.97|59\.94|24|25|30|50|60|90|120)\s*[Ff][Pp][Ss]\b',
|
||||||
|
|
||||||
|
# 频率相关
|
||||||
|
# 频率相关(白名单,含空格/无空格)
|
||||||
|
r'\b(?:100|144|200|240|400|800)\s*[Mm][Hh][Zz]\b',
|
||||||
|
r'\b(?:1|1\.4|2|2\.4|5|5\.8)\s*[Gg][Hh][Zz]\b',
|
||||||
|
r'\b(?:100|144|200|240|400|800)[Mm][Hh][Zz]\b',
|
||||||
|
r'\b(?:1|1\.4|2|2\.4|5|5\.8)[Gg][Hh][Zz]\b',
|
||||||
|
|
||||||
|
# 声道相关(限定常见声道)
|
||||||
|
r'\b(?:1\.0|2\.0|5\.1|7\.1)\s*[Cc][Hh]\b',
|
||||||
|
r'\b(?:1\.0|2\.0|5\.1|7\.1)[Cc][Hh]\b',
|
||||||
|
r'\b(?:1\.0|2\.0|5\.1|7\.1)\s*[Cc][Hh][Aa][Nn][Nn][Ee][Ll]\b',
|
||||||
|
|
||||||
|
# 位深相关
|
||||||
|
r'\b\d+\.?\d*\s*[Bb][Ii][Tt][Ss]\b', # 匹配 128bits, 256bits 等
|
||||||
|
|
||||||
|
# 其他技术参数
|
||||||
|
# 其他技术参数(白名单)
|
||||||
|
r'\b(?:8|12|16|24|32|48|50|64|108)\s*[Mm][Pp]\b',
|
||||||
|
r'\b(?:720|1080|1440|1600|1920|2160|4320)\s*[Pp][Ii][Xx][Ee][Ll]\b',
|
||||||
|
r'\b(?:5400|7200)\s*[Rr][Pp][Mm]\b',
|
||||||
|
r'\b(?:720|1080|1440|1600|1920|2160|4320)[Pp][Ii][Xx][Ee][Ll]\b',
|
||||||
|
r'\b(?:5400|7200)[Rr][Pp][Mm]\b',
|
||||||
|
]
|
||||||
|
for pattern in tech_spec_patterns:
|
||||||
|
filename_without_dates = re.sub(pattern, ' ', filename_without_dates, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
# 预处理:移除季编号标识,避免误提取季编号为集编号(放在日期清洗之前)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 预处理:再排除文件名中可能是日期的部分,避免误识别
|
||||||
date_patterns = [
|
date_patterns = [
|
||||||
# YYYY-MM-DD 或 YYYY.MM.DD 或 YYYY/MM/DD 或 YYYY MM DD格式(四位年份)
|
# YYYY-MM-DD 或 YYYY.MM.DD 或 YYYY/MM/DD 或 YYYY MM DD格式(四位年份)
|
||||||
r'((?:19|20)\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})',
|
r'((?:19|20)\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})',
|
||||||
# YY-MM-DD 或 YY.MM.DD 或 YY/MM/DD 或 YY MM DD格式(两位年份)
|
# YY-MM-DD 或 YY.MM.DD 或 YY/MM/DD 或 YY MM DD格式(两位年份),但排除E/EP后面的数字
|
||||||
r'((?:19|20)?\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})',
|
r'(?<![Ee])(?<![Ee][Pp])((?:19|20)?\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})',
|
||||||
# 完整的YYYYMMDD格式(无分隔符)
|
# 完整的YYYYMMDD格式(无分隔符)
|
||||||
r'((?:19|20)\d{2})(\d{2})(\d{2})',
|
r'((?:19|20)\d{2})(\d{2})(\d{2})',
|
||||||
# YYMMDD格式(两位年份,无分隔符)
|
# YYMMDD格式(两位年份,无分隔符)
|
||||||
r'(?<!\d)(\d{2})(\d{2})(\d{2})(?!\d)',
|
r'(?<!\d)(\d{2})(\d{2})(\d{2})(?!\d)',
|
||||||
# MM/DD/YYYY 或 DD/MM/YYYY 格式
|
# MM/DD/YYYY 或 DD/MM/YYYY 格式
|
||||||
r'(\d{1,2})[-./\s](\d{1,2})[-./\s]((?:19|20)\d{2})',
|
r'(\d{1,2})[-./\s](\d{1,2})[-./\s]((?:19|20)\d{2})',
|
||||||
# MM-DD 或 MM.DD 或 MM/DD格式(无年份,不包括空格分隔)
|
# MM-DD 或 MM.DD 或 MM/DD格式(无年份,不包括空格分隔),但排除E/EP后面的数字和分辨率标识
|
||||||
r'(?<!\d)(\d{1,2})[-./](\d{1,2})(?!\d)',
|
r'(?<![Ee])(?<![Ee][Pp])(?<!\d)(\d{1,2})[-./](\d{1,2})(?!\d)(?![KkPp])',
|
||||||
]
|
]
|
||||||
|
|
||||||
# 从不含扩展名的文件名中移除日期部分
|
# 从已清除技术规格的信息中移除日期部分
|
||||||
filename_without_dates = file_name_without_ext
|
|
||||||
for pattern in date_patterns:
|
for pattern in date_patterns:
|
||||||
matches = re.finditer(pattern, filename_without_dates)
|
matches = re.finditer(pattern, filename_without_dates)
|
||||||
for match in matches:
|
for match in matches:
|
||||||
# 检查匹配的内容是否确实是日期
|
# 检查匹配的内容是否确实是日期
|
||||||
date_str = match.group(0)
|
date_str = match.group(0)
|
||||||
|
# 针对短日期 x.x 或 xx.xx:前一字符为 E/e/EP/Ep 时不清洗(保护 E11.11, EP08.7 场景)
|
||||||
|
if re.match(r'^\d{1,2}[./-]\d{1,2}$', date_str):
|
||||||
|
prev_chars = filename_without_dates[max(0, match.start()-2):match.start()]
|
||||||
|
if prev_chars.endswith(('E', 'e', 'EP', 'Ep', 'ep')):
|
||||||
|
continue
|
||||||
|
# 保护 E/EP 格式的集编号,如 E07, E14, EP08 等
|
||||||
|
if re.match(r'^\d+$', date_str) and len(date_str) <= 3:
|
||||||
|
prev_chars = filename_without_dates[max(0, match.start()-2):match.start()]
|
||||||
|
if prev_chars.endswith(('E', 'e', 'EP', 'Ep', 'ep')):
|
||||||
|
continue
|
||||||
month = None
|
month = None
|
||||||
day = None
|
day = None
|
||||||
|
|
||||||
@ -487,33 +635,29 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
# 转换失败,保持month和day为None
|
# 转换失败,保持month和day为None
|
||||||
pass
|
pass
|
||||||
|
elif len(match.groups()) == 2:
|
||||||
|
# 处理两个分组的模式(如MM-DD格式)
|
||||||
|
try:
|
||||||
|
month = int(match.group(1))
|
||||||
|
day = int(match.group(2))
|
||||||
|
|
||||||
|
# 检查月和日的有效性
|
||||||
|
if not (1 <= month <= 12 and 1 <= day <= 31):
|
||||||
|
# 尝试另一种解释:日-月
|
||||||
|
month = int(match.group(2))
|
||||||
|
day = int(match.group(1))
|
||||||
|
if not (1 <= month <= 12 and 1 <= day <= 31):
|
||||||
|
# 仍然无效,重置month和day
|
||||||
|
month = None
|
||||||
|
day = None
|
||||||
|
except ValueError:
|
||||||
|
# 转换失败,保持month和day为None
|
||||||
|
pass
|
||||||
|
|
||||||
# 如果能确定月日且是有效的日期,则从文件名中删除该日期
|
# 如果能确定月日且是有效的日期,则从文件名中删除该日期
|
||||||
if month and day and 1 <= month <= 12 and 1 <= day <= 31:
|
if month and day and 1 <= month <= 12 and 1 <= day <= 31:
|
||||||
filename_without_dates = filename_without_dates.replace(date_str, " ")
|
filename_without_dates = filename_without_dates.replace(date_str, " ")
|
||||||
|
|
||||||
# 预处理:移除分辨率标识(如 720p, 1080P, 2160p 等)
|
|
||||||
resolution_patterns = [
|
|
||||||
r'\b\d+[pP]\b', # 匹配 720p, 1080P, 2160p 等
|
|
||||||
r'\b\d+x\d+\b', # 匹配 1920x1080 等
|
|
||||||
# 注意:不移除4K/8K,避免误删文件名中的4K标识
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in resolution_patterns:
|
|
||||||
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:
|
||||||
@ -523,7 +667,9 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
|
|||||||
# 其次匹配E01格式
|
# 其次匹配E01格式
|
||||||
match_e = re.search(r'[Ee][Pp]?(\d+)', filename_without_dates)
|
match_e = re.search(r'[Ee][Pp]?(\d+)', filename_without_dates)
|
||||||
if match_e:
|
if match_e:
|
||||||
return int(match_e.group(1))
|
# 若数字位于含字母的中括号内部,跳过该匹配
|
||||||
|
if not _in_alpha_brackets(filename_without_dates, match_e.start(1), match_e.end(1)):
|
||||||
|
return int(match_e.group(1))
|
||||||
|
|
||||||
# 添加中文数字匹配模式(优先匹配)
|
# 添加中文数字匹配模式(优先匹配)
|
||||||
chinese_patterns = [
|
chinese_patterns = [
|
||||||
@ -547,19 +693,6 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 智能4K匹配:检查是否匹配到4K模式,但要验证这个匹配是否合理
|
|
||||||
match_4k = re.search(r'(\d+)[-_\s]*4[Kk]', filename_without_dates)
|
|
||||||
if match_4k:
|
|
||||||
episode_num = int(match_4k.group(1))
|
|
||||||
# 检查文件名中是否已经有明确的剧集标识(中文数字或阿拉伯数字)
|
|
||||||
has_episode_indicator = re.search(r'第[一二三四五六七八九十百千万零两]+[期集话]|第\d+[期集话]', filename_without_dates)
|
|
||||||
if has_episode_indicator:
|
|
||||||
# 如果已经有明确的剧集标识,跳过4K匹配,避免冲突
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# 没有明确的剧集标识,4K匹配有效
|
|
||||||
return episode_num
|
|
||||||
|
|
||||||
# 尝试匹配更多格式(注意:避免匹配季数)
|
# 尝试匹配更多格式(注意:避免匹配季数)
|
||||||
default_patterns = [
|
default_patterns = [
|
||||||
r'第(\d+)集',
|
r'第(\d+)集',
|
||||||
@ -569,25 +702,48 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
|
|||||||
r'(?<!第\d+季\s*)(\d+)期', # 避免匹配"第X季 Y期"中的季数
|
r'(?<!第\d+季\s*)(\d+)期', # 避免匹配"第X季 Y期"中的季数
|
||||||
r'(?<!第\d+季\s*)(\d+)话', # 避免匹配"第X季 Y话"中的季数
|
r'(?<!第\d+季\s*)(\d+)话', # 避免匹配"第X季 Y话"中的季数
|
||||||
r'[Ee][Pp]?(\d+)',
|
r'[Ee][Pp]?(\d+)',
|
||||||
r'(\d+)[-_\s]*4[Kk]',
|
|
||||||
r'\[(\d+)\]',
|
r'\[(\d+)\]',
|
||||||
r'【(\d+)】',
|
r'【(\d+)】',
|
||||||
r'_?(\d+)_?'
|
# 中文数字匹配模式
|
||||||
|
r'第([一二三四五六七八九十百千万零两]+)集',
|
||||||
|
r'第([一二三四五六七八九十百千万零两]+)期',
|
||||||
|
r'第([一二三四五六七八九十百千万零两]+)话',
|
||||||
|
r'([一二三四五六七八九十百千万零两]+)集',
|
||||||
|
r'([一二三四五六七八九十百千万零两]+)期',
|
||||||
|
r'([一二三四五六七八九十百千万零两]+)话',
|
||||||
|
# 先匹配"前方有分隔符"的数字,避免后一个规则优先命中单字符
|
||||||
|
r'[- _\s\.]([0-9]+)(?:[^0-9]|$)',
|
||||||
|
r'(?:^|[^0-9])(\d+)(?=[- _\s\.][^0-9])',
|
||||||
|
# 新增:文件名起始纯数字后接非数字(如 1094(1).mp4)
|
||||||
|
r'^(\d+)(?=\D)'
|
||||||
]
|
]
|
||||||
|
|
||||||
patterns = None
|
# 构建最终的patterns:默认模式 + 用户补充模式
|
||||||
|
patterns = []
|
||||||
|
|
||||||
|
# 1. 首先添加默认模式(除了最后的纯数字模式)
|
||||||
|
default_non_numeric = [p for p in default_patterns if not re.match(r'^[- _\\s\\.]\([0-9]+\)', p) and not re.match(r'^\([^)]*\)\([0-9]+\)', p)]
|
||||||
|
patterns.extend(default_non_numeric)
|
||||||
|
|
||||||
|
# 2. 添加用户补充的模式
|
||||||
|
user_patterns = []
|
||||||
|
|
||||||
# 检查传入的episode_patterns参数
|
# 检查传入的episode_patterns参数
|
||||||
if episode_patterns:
|
if episode_patterns:
|
||||||
patterns = [p.get("regex", "(\\d+)") for p in episode_patterns]
|
user_patterns = [p.get("regex", "(\\d+)") for p in episode_patterns if p.get("regex", "").strip()]
|
||||||
# 如果配置了task的自定义规则,优先使用
|
# 如果配置了task的自定义规则
|
||||||
elif config_data and isinstance(config_data.get("episode_patterns"), list) and config_data["episode_patterns"]:
|
elif config_data and isinstance(config_data.get("episode_patterns"), list) and config_data["episode_patterns"]:
|
||||||
patterns = [p.get("regex", "(\\d+)") for p in config_data["episode_patterns"]]
|
user_patterns = [p.get("regex", "(\\d+)") for p in config_data["episode_patterns"] if p.get("regex", "").strip()]
|
||||||
# 尝试从全局配置获取
|
# 尝试从全局配置获取
|
||||||
elif 'CONFIG_DATA' in globals() and isinstance(globals()['CONFIG_DATA'].get("episode_patterns"), list) and globals()['CONFIG_DATA']["episode_patterns"]:
|
elif 'CONFIG_DATA' in globals() and isinstance(globals()['CONFIG_DATA'].get("episode_patterns"), list) and globals()['CONFIG_DATA']["episode_patterns"]:
|
||||||
patterns = [p.get("regex", "(\\d+)") for p in globals()['CONFIG_DATA']["episode_patterns"]]
|
user_patterns = [p.get("regex", "(\\d+)") for p in globals()['CONFIG_DATA']["episode_patterns"] if p.get("regex", "").strip()]
|
||||||
else:
|
|
||||||
patterns = default_patterns
|
# 添加用户补充的模式
|
||||||
|
patterns.extend(user_patterns)
|
||||||
|
|
||||||
|
# 3. 最后添加默认的纯数字模式
|
||||||
|
default_numeric = [p for p in default_patterns if re.match(r'^[- _\\s\\.]\([0-9]+\)', p) or re.match(r'^\([^)]*\)\([0-9]+\)', p)]
|
||||||
|
patterns.extend(default_numeric)
|
||||||
|
|
||||||
# 尝试使用每个正则表达式匹配文件名(使用不含日期的文件名)
|
# 尝试使用每个正则表达式匹配文件名(使用不含日期的文件名)
|
||||||
for pattern_regex in patterns:
|
for pattern_regex in patterns:
|
||||||
@ -623,6 +779,10 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
|
|||||||
# 遍历所有捕获组,找到第一个非空的
|
# 遍历所有捕获组,找到第一个非空的
|
||||||
for group_num in range(1, len(match.groups()) + 1):
|
for group_num in range(1, len(match.groups()) + 1):
|
||||||
if match.group(group_num):
|
if match.group(group_num):
|
||||||
|
# 若数字位于含字母的中括号内部,跳过
|
||||||
|
span_l, span_r = match.start(group_num), match.end(group_num)
|
||||||
|
if _in_alpha_brackets(filename_without_dates, span_l, span_r):
|
||||||
|
continue
|
||||||
episode_num = int(match.group(group_num))
|
episode_num = int(match.group(group_num))
|
||||||
|
|
||||||
# 检查提取的数字是否可能是日期的一部分
|
# 检查提取的数字是否可能是日期的一部分
|
||||||
@ -646,6 +806,10 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
|
|||||||
# 单一模式的正则表达式
|
# 单一模式的正则表达式
|
||||||
match = re.search(pattern_regex, filename_without_dates)
|
match = re.search(pattern_regex, filename_without_dates)
|
||||||
if match:
|
if match:
|
||||||
|
# 若数字位于含字母的中括号内部,跳过
|
||||||
|
span_l, span_r = match.start(1), match.end(1)
|
||||||
|
if _in_alpha_brackets(filename_without_dates, span_l, span_r):
|
||||||
|
continue
|
||||||
episode_num = int(match.group(1))
|
episode_num = int(match.group(1))
|
||||||
|
|
||||||
# 检查提取的数字是否可能是日期的一部分
|
# 检查提取的数字是否可能是日期的一部分
|
||||||
@ -661,16 +825,27 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
|
|||||||
return int(filename_without_dates)
|
return int(filename_without_dates)
|
||||||
|
|
||||||
# 最后尝试提取任何数字,但要排除日期可能性
|
# 最后尝试提取任何数字,但要排除日期可能性
|
||||||
num_match = re.search(r'(\d+)', filename_without_dates)
|
candidates = []
|
||||||
if num_match:
|
for m in re.finditer(r'\d+', filename_without_dates):
|
||||||
episode_num = int(num_match.group(1))
|
num_str = m.group(0)
|
||||||
# 检查提取的数字是否可能是日期
|
# 过滤日期模式
|
||||||
if not is_date_format(str(episode_num)):
|
if is_date_format(num_str):
|
||||||
# 检查是否是过大的数字(可能是时间戳、文件大小等)
|
continue
|
||||||
if episode_num > 9999:
|
# 过滤中括号内且含字母的片段
|
||||||
return None # 跳过过大的数字
|
span_l, span_r = m.start(), m.end()
|
||||||
return episode_num
|
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
|
return None
|
||||||
|
|
||||||
# 全局变量
|
# 全局变量
|
||||||
@ -684,7 +859,7 @@ NOTIFYS = []
|
|||||||
def is_date_format(number_str):
|
def is_date_format(number_str):
|
||||||
"""
|
"""
|
||||||
判断一个纯数字字符串是否可能是日期格式
|
判断一个纯数字字符串是否可能是日期格式
|
||||||
支持的格式:YYYYMMDD, MMDD, YYMMDD
|
支持的格式:YYYYMMDD, YYMMDD
|
||||||
"""
|
"""
|
||||||
# 判断YYYYMMDD格式 (8位数字)
|
# 判断YYYYMMDD格式 (8位数字)
|
||||||
if len(number_str) == 8 and number_str.startswith('20'):
|
if len(number_str) == 8 and number_str.startswith('20'):
|
||||||
@ -708,16 +883,8 @@ def is_date_format(number_str):
|
|||||||
# 可能是日期格式
|
# 可能是日期格式
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 判断MMDD格式 (4位数字)
|
# 不再将4位纯数字按MMDD视为日期,避免误伤集号(如1124)
|
||||||
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
|
|
||||||
|
|
||||||
# 其他格式不视为日期格式
|
# 其他格式不视为日期格式
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -890,7 +1057,7 @@ def get_file_icon(file_name, is_dir=False):
|
|||||||
lower_name = file_name.lower()
|
lower_name = file_name.lower()
|
||||||
|
|
||||||
# 视频文件
|
# 视频文件
|
||||||
if any(lower_name.endswith(ext) for ext in ['.mp4', '.mkv', '.avi', '.mov', '.rmvb', '.flv', '.wmv', '.m4v', '.ts']):
|
if any(lower_name.endswith(ext) for ext in ['.mp4', '.mkv', '.avi', '.mov', '.rmvb', '.flv', '.wmv', '.m4v', '.ts', '.webm', '.3gp', '.f4v']):
|
||||||
return "🎞️"
|
return "🎞️"
|
||||||
|
|
||||||
# 图片文件
|
# 图片文件
|
||||||
@ -1038,22 +1205,6 @@ class Config:
|
|||||||
if task.get("media_id"):
|
if task.get("media_id"):
|
||||||
del task["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:
|
class Quark:
|
||||||
@ -3417,6 +3568,9 @@ class Quark:
|
|||||||
episode_pattern = task["episode_naming"]
|
episode_pattern = task["episode_naming"]
|
||||||
regex_pattern = task.get("regex_pattern")
|
regex_pattern = task.get("regex_pattern")
|
||||||
|
|
||||||
|
# 初始化变量
|
||||||
|
already_renamed_files = set() # 用于防止重复重命名
|
||||||
|
|
||||||
# 获取目录文件列表 - 添加这行代码初始化dir_file_list
|
# 获取目录文件列表 - 添加这行代码初始化dir_file_list
|
||||||
savepath = re.sub(r"/{2,}", "/", f"/{task['savepath']}{subdir_path}")
|
savepath = re.sub(r"/{2,}", "/", f"/{task['savepath']}{subdir_path}")
|
||||||
if not self.savepath_fid.get(savepath):
|
if not self.savepath_fid.get(savepath):
|
||||||
@ -3455,8 +3609,10 @@ class Quark:
|
|||||||
|
|
||||||
# 实现序号提取函数
|
# 实现序号提取函数
|
||||||
def extract_episode_number_local(filename):
|
def extract_episode_number_local(filename):
|
||||||
# 使用全局的统一提取函数
|
# 使用全局的统一提取函数,直接使用全局CONFIG_DATA
|
||||||
return extract_episode_number(filename, config_data=task.get("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()
|
existing_episode_numbers = set()
|
||||||
|
|||||||
@ -65,9 +65,5 @@
|
|||||||
"enddate": "2099-01-30"
|
"enddate": "2099-01-30"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"episode_patterns": [
|
"episode_patterns": []
|
||||||
{
|
|
||||||
"regex": "第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user