diff --git a/app/run.py b/app/run.py
index 72c5607..d46dd9a 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,6 +586,27 @@ logging.basicConfig(
format="[%(asctime)s][%(levelname)s] %(message)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日志输出
if not DEBUG:
logging.getLogger("werkzeug").setLevel(logging.ERROR)
@@ -2092,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", "")
@@ -2537,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"] = {
@@ -3162,6 +3169,12 @@ def reset_folder():
logging.error(f">>> 删除记录时出错: {str(e)}")
# 即使删除记录失败,也返回文件删除成功
+ # 成功后通过 SSE 通知前端进行热更新
+ try:
+ notify_calendar_changed('reset_folder')
+ except Exception:
+ pass
+
return jsonify({
"success": True,
"message": f"重置成功,删除了 {deleted_files} 个文件和 {deleted_records} 条记录",
@@ -3379,55 +3392,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:
@@ -5661,6 +5665,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/sdk/tmdb_service.py b/app/sdk/tmdb_service.py
index af6e8d5..8d443ff 100644
--- a/app/sdk/tmdb_service.py
+++ b/app/sdk/tmdb_service.py
@@ -41,7 +41,7 @@ class TMDBService:
def reset_to_primary_url(self):
"""重置到主API地址"""
self.current_url = self.primary_url
- logger.info("TMDB API地址已重置为主地址")
+ logger.debug("TMDB API地址已重置为主地址")
def get_current_api_url(self) -> str:
"""获取当前使用的API地址"""
@@ -87,17 +87,17 @@ class TMDBService:
pass
return data
except Exception as e:
- logger.warning(f"TMDB主地址请求失败: {e}")
+ logger.debug(f"TMDB主地址请求失败: {e}")
# 如果当前使用的是主地址,尝试切换到备用地址
if self.current_url == self.primary_url:
- logger.info("尝试切换到TMDB备用地址...")
+ logger.debug("尝试切换到TMDB备用地址...")
self.current_url = self.backup_url
try:
url = f"{self.current_url}{endpoint}"
response = self.session.get(url, params=params, timeout=10)
response.raise_for_status()
- logger.info("TMDB备用地址连接成功")
+ logger.debug("TMDB备用地址连接成功")
data = response.json()
try:
self._cache[cache_key] = (_now(), data)
@@ -111,7 +111,7 @@ class TMDBService:
return None
else:
# 如果备用地址也失败,重置回主地址
- logger.error(f"TMDB备用地址请求失败: {e}")
+ logger.debug(f"TMDB备用地址请求失败: {e}")
self.current_url = self.primary_url
return None
@@ -186,7 +186,7 @@ class TMDBService:
return original_title
except Exception as e:
- logger.warning(f"获取中文标题失败: {e}, 使用原始标题: {original_title}")
+ logger.debug(f"获取中文标题失败: {e}, 使用原始标题: {original_title}")
return original_title
def get_tv_show_episodes(self, tv_id: int, season_number: int) -> Optional[Dict]:
@@ -458,7 +458,7 @@ class TMDBService:
if poster_path:
return poster_path
except Exception as e:
- logger.warning(f"获取原始语言海报失败: {e}")
+ logger.debug(f"获取原始语言海报失败: {e}")
# 如果设置为中文或原始语言获取失败,尝试中文海报
if self.poster_language == "zh-CN" or self.poster_language == "original":
@@ -477,7 +477,7 @@ class TMDBService:
if poster_path:
return poster_path
except Exception as e:
- logger.warning(f"获取中文海报失败: {e}")
+ logger.debug(f"获取中文海报失败: {e}")
# 如果都失败了,返回默认海报路径
return details.get('poster_path', '')
diff --git a/app/static/js/sort_file_by_name.js b/app/static/js/sort_file_by_name.js
index 1aaf4bf..f91ad94 100644
--- a/app/static/js/sort_file_by_name.js
+++ b/app/static/js/sort_file_by_name.js
@@ -28,6 +28,59 @@ function sortFileByName(file) {
let filename = typeof file === 'object' ? (file.file_name || '') : file;
let update_time = typeof file === 'object' ? (file.updated_at || 0) : 0;
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,
+ /(? 12) [month, day] = [day, month];
@@ -87,7 +140,7 @@ function sortFileByName(file) {
}
// MM-DD
if (date_value === Infinity) {
- match = filename.match(/(?
-
+
@@ -1886,7 +1886,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 }));
}
},
// 管理视图:按任务名(拼音)排序并应用顶部筛选
@@ -3748,11 +3752,9 @@
});
}
- // 如果没有剧集识别模式,添加默认模式
- if (!this.formData.episode_patterns || this.formData.episode_patterns.length === 0) {
- this.formData.episode_patterns = [
- { regex: '第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?' }
- ];
+ // 确保剧集识别模式字段存在(但不自动添加默认规则)
+ if (!this.formData.episode_patterns) {
+ this.formData.episode_patterns = [];
}
// 如果当前标签是历史记录,则加载历史记录
@@ -4777,11 +4779,23 @@
});
// 如果启用了合并集功能,则进行合并处理
- if (this.calendar.mergeEpisodes) {
- return this.mergeEpisodesByShow(filteredEpisodes);
- }
-
- return filteredEpisodes;
+ let result = this.calendar.mergeEpisodes
+ ? this.mergeEpisodesByShow(filteredEpisodes)
+ : 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--;
const previousResource = this.smart_param.taskSuggestions.data[this.smart_param.currentResourceIndex];
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.getShareDetail(0, 1);
@@ -8124,6 +8155,23 @@
this.smart_param.currentResourceIndex++;
const nextResource = this.smart_param.taskSuggestions.data[this.smart_param.currentResourceIndex];
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.getShareDetail(0, 1);
@@ -8370,6 +8418,8 @@
}
},
getShareDetail(retryCount = 0, maxRetries = 1) {
+ // 切换或重试前清理残留错误提示,避免覆盖新资源展示
+ this.fileSelect.error = undefined;
this.modalLoading = true;
// 检查index是否有效,如果无效则使用默认值
@@ -9604,14 +9654,12 @@
// 其他模态框:文件夹排在文件前面
if (a.dir && !b.dir) return -1;
if (!a.dir && b.dir) return 1;
- // 其他模态框:使用拼音排序
- let aValue = pinyinPro.pinyin(a.file_name, { toneType: 'none', type: 'string' }).toLowerCase();
- let bValue = pinyinPro.pinyin(b.file_name, { toneType: 'none', type: 'string' }).toLowerCase();
- if (this.fileSelect.sortOrder === 'asc') {
- return aValue > bValue ? 1 : -1;
- } else {
- return aValue < bValue ? 1 : -1;
+ // 其他模态框:使用与任务列表一致的自然排序算法
+ const ka = sortFileByName(a), kb = sortFileByName(b);
+ for (let i = 0; i < ka.length; ++i) {
+ if (ka[i] !== kb[i]) return this.fileSelect.sortOrder === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
}
+ return 0;
}
}
if (field === 'file_name_re') {
@@ -9641,14 +9689,48 @@
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 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 (typeof aValue === 'string' && typeof bValue === 'string') {
+ return aValue.localeCompare(bValue, undefined, { numeric: true, sensitivity: 'base' });
+ }
return aValue > bValue ? 1 : -1;
} else {
+ if (typeof aValue === 'string' && typeof bValue === 'string') {
+ return bValue.localeCompare(aValue, undefined, { numeric: true, sensitivity: 'base' });
+ }
return aValue < bValue ? 1 : -1;
}
}
@@ -9772,14 +9854,49 @@
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 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 (typeof aValue === 'string' && typeof bValue === 'string') {
+ return aValue.localeCompare(bValue, undefined, { numeric: true, sensitivity: 'base' });
+ }
return aValue > bValue ? 1 : -1;
} else {
+ // 如果都是字符串,使用自然排序
+ if (typeof aValue === 'string' && typeof bValue === 'string') {
+ return bValue.localeCompare(aValue, undefined, { numeric: true, sensitivity: 'base' });
+ }
return aValue < bValue ? 1 : -1;
}
}
@@ -10371,6 +10488,19 @@
.then(response => {
if (response.data.success) {
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') {
this.loadHistoryRecords();
@@ -10841,7 +10971,7 @@
this.fileSelect.paths = [];
this.fileSelect.error = undefined;
this.fileSelect.selectedFiles = [];
- // 设置排序方式为按重命名结果降序排序
+ // 预览默认按重命名列降序(与任务列表一致)
this.fileSelect.sortBy = "file_name_re";
this.fileSelect.sortOrder = "desc";
// 展示当前文件夹的文件
@@ -13246,11 +13376,9 @@
});
}
- // 如果没有剧集识别模式,添加默认模式
- if (!this.formData.episode_patterns || this.formData.episode_patterns.length === 0) {
- this.formData.episode_patterns = [
- { regex: '第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?' }
- ];
+ // 确保剧集识别模式字段存在(但不自动添加默认规则)
+ if (!this.formData.episode_patterns) {
+ this.formData.episode_patterns = [];
}
// 如果当前标签是历史记录,则加载历史记录
diff --git a/quark_auto_save.py b/quark_auto_save.py
index 9b06370..9096b0c 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,29 +497,110 @@ 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))
+
+ # 预处理顺序调整:先移除技术规格,再移除日期,降低误判
+ # 预处理:先移除技术规格信息,避免误提取技术参数中的数字为集编号
+ 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'(? 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 +859,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 +883,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
@@ -890,7 +1057,7 @@ def get_file_icon(file_name, is_dir=False):
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 "🎞️"
# 图片文件
@@ -1038,22 +1205,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 +3568,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 +3609,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": []
}