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(/(? -
+

剧集识别

@@ -705,7 +705,7 @@
集编号识别规则
- +
@@ -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": [] }