import json import re import shutil from pathlib import Path from typing import Union, Optional from app.chain import ChainBase from app.runtime.config import settings from app.runtime.extensions.plugin_manager import PluginManager from app.runtime.state import SystemHelper from app.runtime.log import logger from app.schemas import Message, NotificationChannel from app.adapters.network.http import RequestUtils from app.adapters.system.host import SystemUtils from version import FRONTEND_VERSION, APP_VERSION class SystemChain(ChainBase): """ 系统级处理链 """ _restart_file = "__system_restart__" def remote_clear_cache(self, channel: NotificationChannel, userid: Union[int, str], source: Optional[str] = None): """ 清理系统缓存 """ self.clear_cache() self.post_message(Message( channel=channel, source=source, title=f"缓存清理完成!", userid=userid, save_history=False)) def restart(self, channel: NotificationChannel, userid: Union[int, str], source: Optional[str] = None): """ 重启系统 """ if channel and userid: self.post_message(Message( channel=channel, source=source, title="系统正在重启,请耐心等候!", userid=userid, save_history=False)) # 保存重启信息 self.save_cache({ "channel": channel.value, "userid": userid }, self._restart_file) # 主动备份一次插件 self.backup_plugins() # 重启 SystemHelper.restart() @staticmethod def backup_plugins(): """ 备份插件到用户配置目录(仅docker环境) """ # 非docker环境不处理 if not SystemUtils.is_docker(): return try: # 使用绝对路径确保准确性 plugins_dir = settings.ROOT_PATH / "app" / "plugins" backup_dir = settings.CONFIG_PATH / "plugins_backup" if not plugins_dir.exists(): logger.info("插件目录不存在,跳过备份") return # 确保备份目录存在 backup_dir.mkdir(parents=True, exist_ok=True) # 需要排除的文件和目录 exclude_items = {"__init__.py", "__pycache__", ".DS_Store"} # 遍历插件目录,备份除排除项外的所有内容 for item in plugins_dir.iterdir(): if item.name in exclude_items: continue target_path = backup_dir / item.name # 如果是目录 if item.is_dir(): if target_path.exists(): continue shutil.copytree(item, target_path) logger.debug(f"已备份插件目录: {item.name}") # 如果是文件 elif item.is_file(): if target_path.exists(): continue shutil.copy2(item, target_path) logger.info(f"已备份插件文件: {item.name}") logger.info(f"插件备份完成,备份位置: {backup_dir}") except Exception as e: logger.error(f"插件备份失败: {str(e)}") @staticmethod def restore_plugins(): """ 从备份恢复插件到app/plugins目录,恢复完成后删除备份(仅docker环境) """ # 非docker环境不处理 if not SystemUtils.is_docker(): return # 使用绝对路径确保准确性 plugins_dir = settings.ROOT_PATH / "app" / "plugins" backup_dir = settings.CONFIG_PATH / "plugins_backup" if not backup_dir.exists(): logger.info("插件备份目录不存在,跳过恢复") return # 系统被重置才恢复插件 if SystemHelper().is_system_reset(): # 确保插件目录存在 plugins_dir.mkdir(parents=True, exist_ok=True) # 遍历备份目录,恢复所有内容 restored_count = 0 for item in backup_dir.iterdir(): target_path = plugins_dir / item.name try: # 如果是目录,且目录内有内容 if item.is_dir() and any(item.iterdir()): if target_path.exists(): shutil.rmtree(target_path) shutil.copytree(item, target_path) logger.debug(f"已恢复插件目录: {item.name}") restored_count += 1 # 如果是文件 elif item.is_file(): shutil.copy2(item, target_path) logger.debug(f"已恢复插件文件: {item.name}") restored_count += 1 except Exception as e: logger.error(f"恢复插件 {item.name} 时发生错误: {str(e)}") continue logger.info(f"插件恢复完成,共恢复 {restored_count} 个项目") # 安装缺少的依赖 PluginManager.install_plugin_missing_dependencies() # 删除备份目录 try: shutil.rmtree(backup_dir) logger.info(f"已删除插件备份目录: {backup_dir}") except Exception as e: logger.warning(f"删除备份目录失败: {str(e)}") def __get_version_message(self) -> str: """ 获取版本信息文本 """ server_release_version = self.__get_server_release_version() front_release_version = self.__get_front_release_version() server_local_version = self.get_server_local_version() front_local_version = self.get_frontend_version() if server_release_version == server_local_version: title = f"当前后端版本:{server_local_version},已是最新版本\n" else: title = f"当前后端版本:{server_local_version},远程版本:{server_release_version}\n" if front_release_version == front_local_version: title += f"当前前端版本:{front_local_version},已是最新版本" else: title += f"当前前端版本:{front_local_version},远程版本:{front_release_version}" return title def version(self, channel: NotificationChannel, userid: Union[int, str], source: Optional[str] = None): """ 查看当前版本、远程版本 """ self.post_message(Message( channel=channel, source=source, title=self.__get_version_message(), userid=userid, save_history=False)) def restart_finish(self): """ 如通过交互命令重启, 重启完发送msg """ # 重启消息 restart_channel = self.load_cache(self._restart_file) if restart_channel: # 发送重启完成msg if not isinstance(restart_channel, dict): restart_channel = json.loads(restart_channel) channel = next( (channel for channel in NotificationChannel.__members__.values() if channel.value == restart_channel.get('channel')), None) userid = restart_channel.get('userid') # 版本号 title = self.__get_version_message() self.post_message(Message( channel=channel, title=f"系统已重启完成!\n{title}", userid=userid, save_history=False)) self.remove_cache(self._restart_file) @staticmethod def __get_server_release_version(): """ 获取后端V2最新版本 """ try: # 获取所有发布的版本列表 response = RequestUtils( proxies=settings.PROXY, headers=settings.GITHUB_HEADERS ).get_res("https://api.github.com/repos/jxxghp/MoviePilot/releases") if response: releases = [release['tag_name'] for release in response.json()] v2_releases = [tag for tag in releases if re.match(r"^v2\.", tag)] if not v2_releases: logger.warn("获取v2后端最新版本版本出错!") else: # 找到最新的v2版本 latest_v2 = sorted(v2_releases, key=lambda s: list(map(int, re.findall(r'\d+', s))))[-1] logger.info(f"获取到后端最新版本:{latest_v2}") return latest_v2 else: logger.error("无法获取后端版本信息,请检查网络连接或GitHub API请求。") except Exception as err: logger.error(f"获取后端最新版本失败:{str(err)}") return None @staticmethod def __get_front_release_version(): """ 获取前端V2最新版本 """ try: # 获取所有发布的版本列表 response = RequestUtils( proxies=settings.PROXY, headers=settings.GITHUB_HEADERS ).get_res("https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases") if response: releases = [release['tag_name'] for release in response.json()] v2_releases = [tag for tag in releases if re.match(r"^v2\.", tag)] if not v2_releases: logger.warn("获取v2前端最新版本版本出错!") else: # 找到最新的v2版本 latest_v2 = sorted(v2_releases, key=lambda s: list(map(int, re.findall(r'\d+', s))))[-1] logger.info(f"获取到前端最新版本:{latest_v2}") return latest_v2 else: logger.error("无法获取前端版本信息,请检查网络连接或GitHub API请求。") except Exception as err: logger.error(f"获取前端最新版本失败:{str(err)}") return None @staticmethod def get_server_local_version(): """ 查看当前版本 """ return APP_VERSION @staticmethod def get_frontend_version(): """ 获取前端版本 """ if SystemUtils.is_frozen() and SystemUtils.is_windows(): version_file = settings.CONFIG_PATH.parent / "nginx" / "html" / "version.txt" else: version_file = Path(settings.FRONTEND_PATH) / "version.txt" if version_file.exists(): try: with open(version_file, 'r', encoding='utf-8', errors='replace') as f: version = str(f.read()).strip() return version except Exception as err: logger.debug(f"加载版本文件 {version_file} 出错:{str(err)}") return FRONTEND_VERSION