diff --git a/app/monitor.py b/app/monitor.py index 489331cd..7323fa60 100644 --- a/app/monitor.py +++ b/app/monitor.py @@ -120,6 +120,71 @@ class Monitor(metaclass=Singleton): except Exception as e: logger.error(f"保存快照失败: {e}") + def reset_snapshot(self, storage: str) -> bool: + """ + 重置快照,强制下次扫描时重新建立基准 + :param storage: 存储名称 + :return: 是否成功 + """ + try: + cache_file = self._snapshot_cache_dir / f"{storage}_snapshot.json" + if cache_file.exists(): + cache_file.unlink() + logger.info(f"快照已重置: {storage}") + return True + logger.debug(f"快照文件不存在,无需重置: {storage}") + return True + except Exception as e: + logger.error(f"重置快照失败: {storage} - {e}") + return False + + def force_full_scan(self, storage: str, mon_path: Path) -> bool: + """ + 强制全量扫描并处理所有文件(包括已存在的文件) + :param storage: 存储名称 + :param mon_path: 监控路径 + :return: 是否成功 + """ + try: + logger.info(f"开始强制全量扫描: {storage}:{mon_path}") + + # 生成快照 + new_snapshot = StorageChain().snapshot_storage( + storage=storage, + path=mon_path, + last_snapshot_time=0 # 全量扫描,不使用增量 + ) + + if new_snapshot is None: + logger.warn(f"获取 {storage}:{mon_path} 快照失败") + return False + + file_count = len(new_snapshot) + logger.info(f"{storage}:{mon_path} 全量扫描完成,发现 {file_count} 个文件") + + # 处理所有文件 + processed_count = 0 + for file_path, file_info in new_snapshot.items(): + try: + logger.info(f"处理文件:{file_path}") + file_size = file_info.get('size', 0) if isinstance(file_info, dict) else file_info + self.__handle_file(storage=storage, event_path=Path(file_path), file_size=file_size) + processed_count += 1 + except Exception as e: + logger.error(f"处理文件 {file_path} 失败: {e}") + continue + + logger.info(f"{storage}:{mon_path} 全量扫描完成,共处理 {processed_count}/{file_count} 个文件") + + # 保存快照 + self.save_snapshot(storage, new_snapshot, file_count) + + return True + + except Exception as e: + logger.error(f"强制全量扫描失败: {storage}:{mon_path} - {e}") + return False + def load_snapshot(self, storage: str) -> Optional[Dict]: """ 从文件加载快照 @@ -131,7 +196,9 @@ class Monitor(metaclass=Singleton): if cache_file.exists(): with open(cache_file, 'r', encoding='utf-8') as f: data = json.load(f) + logger.debug(f"成功加载快照: {cache_file}, 包含 {len(data.get('snapshot', {}))} 个文件") return data + logger.debug(f"快照文件不存在: {cache_file}") return None except Exception as e: logger.error(f"加载快照失败: {e}") @@ -553,6 +620,9 @@ class Monitor(metaclass=Singleton): old_snapshot = old_snapshot_data.get('snapshot', {}) if old_snapshot_data else {} last_snapshot_time = old_snapshot_data.get('timestamp', 0) if old_snapshot_data else 0 + # 判断是否为首次快照:检查快照文件是否存在且有效 + is_first_snapshot = old_snapshot_data is None + # 生成新快照(增量模式) new_snapshot = StorageChain().snapshot_storage( storage=storage, @@ -567,7 +637,7 @@ class Monitor(metaclass=Singleton): file_count = len(new_snapshot) logger.info(f"{storage}:{mon_path} 快照完成,发现 {file_count} 个文件") - if old_snapshot: + if not is_first_snapshot: # 比较快照找出变化 changes = self.compare_snapshots(old_snapshot, new_snapshot) diff --git a/moviepilot_snapshot_fix.md b/moviepilot_snapshot_fix.md new file mode 100644 index 00000000..7504c270 --- /dev/null +++ b/moviepilot_snapshot_fix.md @@ -0,0 +1,101 @@ +# MoviePilot 文件整理快照处理问题修复 + +## 问题描述 + +MoviePilot 在版本 2.6.1 中存在一个严重的文件整理问题: + +1. **症状**: 待整理的文件一直没有被处理,即使是新加入的文件或对已存在文件重命名后仍无法触发整理 +2. **日志表现**: 频繁出现 "首次快照仅建立基准,不会处理现有文件" 的日志消息 +3. **根本原因**: 快照处理逻辑存在缺陷,导致系统始终认为每次扫描都是首次快照 + +## 问题分析 + +### 问题出现在 `app/monitor.py` 的 `polling_observer` 方法中: + +```python +# 原始有问题的代码 +if old_snapshot: # 这里的判断逻辑有问题 + # 比较快照找出变化 + changes = self.compare_snapshots(old_snapshot, new_snapshot) + # ... 处理变化的文件 +else: + # 首次快照,不处理文件 + logger.info("*** 首次快照仅建立基准,不会处理现有文件 ***") +``` + +### 问题的根本原因: + +1. `old_snapshot` 是从JSON文件中的 `snapshot` 字段提取的字典 +2. 当快照文件不存在或快照为空时,`old_snapshot` 为空字典 `{}` +3. 在Python中,空字典 `{}` 在布尔上下文中评估为 `False` +4. 因此条件 `if old_snapshot:` 始终为 `False`,导致系统认为每次都是首次快照 + +## 修复方案 + +### 修复1: 修正首次快照判断逻辑 + +```python +# 修复后的代码 +# 判断是否为首次快照:检查快照文件是否存在且有效 +is_first_snapshot = old_snapshot_data is None + +# 使用新的判断条件 +if not is_first_snapshot: + # 比较快照找出变化 + changes = self.compare_snapshots(old_snapshot, new_snapshot) + # ... 处理变化的文件 +else: + # 首次快照,不处理文件 + logger.info("*** 首次快照仅建立基准,不会处理现有文件 ***") +``` + +### 修复2: 增强调试日志 + +```python +def load_snapshot(self, storage: str) -> Optional[Dict]: + # 添加调试日志 + logger.debug(f"成功加载快照: {cache_file}, 包含 {len(data.get('snapshot', {}))} 个文件") + logger.debug(f"快照文件不存在: {cache_file}") +``` + +### 修复3: 新增实用方法 + +1. **重置快照方法**: 允许用户手动重置快照 +```python +def reset_snapshot(self, storage: str) -> bool: + """重置快照,强制下次扫描时重新建立基准""" +``` + +2. **强制全量扫描方法**: 允许用户处理所有现有文件 +```python +def force_full_scan(self, storage: str, mon_path: Path) -> bool: + """强制全量扫描并处理所有文件(包括已存在的文件)""" +``` + +## 修复效果 + +修复后的系统行为: + +1. **首次运行**: 创建快照基准,不处理现有文件(符合设计) +2. **后续运行**: 正确比较快照差异,处理新增和修改的文件 +3. **调试友好**: 增加了详细的调试日志,便于排查问题 +4. **管理灵活**: 提供了重置快照和强制全量扫描的功能 + +## 验证方法 + +1. 检查日志中不再频繁出现 "首次快照仅建立基准" 消息 +2. 新增文件能够被正确检测和处理 +3. 修改文件能够被正确检测和处理 +4. 快照文件能够正确保存和加载 + +## 其他建议 + +1. 如果用户想要处理现有文件,可以使用新增的 `force_full_scan` 方法 +2. 如果快照数据出现问题,可以使用 `reset_snapshot` 方法重置 +3. 建议在生产环境中先测试修复效果,确认无误后再部署 + +## 相关文件 + +- 主要修复文件: `app/monitor.py` +- 涉及的方法: `polling_observer`, `save_snapshot`, `load_snapshot` +- 新增方法: `reset_snapshot`, `force_full_scan` \ No newline at end of file