diff --git a/app/chain/__init__.py b/app/chain/__init__.py index c63cc00c..ded8ff99 100644 --- a/app/chain/__init__.py +++ b/app/chain/__init__.py @@ -65,6 +65,15 @@ class ChainBase(metaclass=ABCMeta): del cache gc.collect() + @staticmethod + def remove_cache(filename: str) -> None: + """ + 删除本地缓存 + """ + cache_path = settings.TEMP_PATH / filename + if cache_path.exists(): + Path(cache_path).unlink() + def run_module(self, method: str, *args, **kwargs) -> Any: """ 运行包含该方法的所有模块,然后返回结果 diff --git a/app/chain/system.py b/app/chain/system.py index 75c0ab98..d18dd402 100644 --- a/app/chain/system.py +++ b/app/chain/system.py @@ -1,7 +1,13 @@ +import json +import os +import re from typing import Union from app.chain import ChainBase +from app.core.config import settings +from app.log import logger from app.schemas import Notification, MessageChannel +from app.utils.http import RequestUtils from app.utils.system import SystemUtils @@ -10,6 +16,9 @@ class SystemChain(ChainBase): 系统级处理链 """ + _restart_file = "__system_restart__" + _update_file = "__system_update__" + def remote_clear_cache(self, channel: MessageChannel, userid: Union[int, str]): """ 清理系统缓存 @@ -22,6 +31,131 @@ class SystemChain(ChainBase): """ 重启系统 """ - self.post_message(Notification(channel=channel, - title=f"系统正在重启,请耐心等候!", userid=userid)) + if channel and userid: + self.post_message(Notification(channel=channel, + title="系统正在重启,请耐心等候!", userid=userid)) + # 保存重启信息 + self.save_cache({ + "channel": channel.value, + "userid": userid + }, self._restart_file) SystemUtils.restart() + + def update(self, channel: MessageChannel = None, userid: Union[int, str] = None): + """ + 重启系统 + """ + if SystemUtils.is_windows(): + logger.error("windows暂不支持") + return + if channel and userid: + self.post_message(Notification(channel=channel, + title="系统正在更新,请耐心等候!", userid=userid)) + # 保存重启信息 + self.save_cache({ + "channel": channel.value, + "userid": userid + }, self._update_file) + + # 重启系统 + os.system("bash /usr/local/bin/mp_update") + if channel and userid: + self.post_message(Notification(channel=channel, + title="暂无新版本!", userid=userid)) + self.remove_cache(self._update_file) + + def version(self, channel: MessageChannel, userid: Union[int, str]): + """ + 查看当前版本、远程版本 + """ + release_version = self.__get_release_version() + local_version = self.get_local_version() + if release_version == local_version: + title = f"当前版本:{local_version},已是最新版本" + else: + title = f"当前版本:{local_version},远程版本:{release_version}" + + self.post_message(Notification(channel=channel, + title=title, userid=userid)) + + def restart_finish(self): + """ + 如通过交互命令重启, + 重启完发送msg + """ + cache_file, action, channel, userid = None, None, None, None + # 重启消息 + restart_channel = self.load_cache(self._restart_file) + if restart_channel: + cache_file = self._restart_file + action = "重启" + # 发送重启完成msg + if not isinstance(restart_channel, dict): + restart_channel = json.loads(restart_channel) + channel = next( + (channel for channel in MessageChannel.__members__.values() if + channel.value == restart_channel.get('channel')), None) + userid = restart_channel.get('userid') + + # 更新消息 + update_channel = self.load_cache(self._update_file) + if update_channel: + cache_file = self._update_file + action = "更新" + # 发送重启完成msg + if not isinstance(update_channel, dict): + update_channel = json.loads(update_channel) + channel = next( + (channel for channel in MessageChannel.__members__.values() if + channel.value == update_channel.get('channel')), None) + userid = update_channel.get('userid') + + # 发送消息 + if channel and userid: + # 版本号 + release_version = self.__get_release_version() + local_version = self.get_local_version() + if release_version == local_version: + title = f"当前版本:{local_version}" + else: + title = f"当前版本:{local_version},远程版本:{release_version}" + self.post_message(Notification(channel=channel, + title=f"系统已{action}完成!{title}", + userid=userid)) + self.remove_cache(cache_file) + + @staticmethod + def __get_release_version(): + """ + 获取最新版本 + """ + version_res = RequestUtils(proxies=settings.PROXY).get_res( + "https://api.github.com/repos/jxxghp/MoviePilot/releases/latest") + if version_res: + ver_json = version_res.json() + version = f"{ver_json['tag_name']}" + return version + else: + return None + + @staticmethod + def get_local_version(): + """ + 查看当前版本 + """ + version_file = settings.ROOT_PATH / "version.py" + if version_file.exists(): + try: + with open(version_file, 'rb') as f: + version = f.read() + pattern = r"v(\d+\.\d+\.\d+)" + match = re.search(pattern, str(version)) + + if match: + version = match.group(1) + return f"v{version}" + else: + logger.warn("未找到版本号") + return None + except Exception as err: + logger.error(f"加载版本文件 {version_file} 出错:{err}") diff --git a/app/command.py b/app/command.py index e4b5a1f8..34740489 100644 --- a/app/command.py +++ b/app/command.py @@ -142,6 +142,18 @@ class Command(metaclass=Singleton): "description": "重启系统", "category": "管理", "data": {} + }, + "/version": { + "func": SystemChain(self._db).version, + "description": "当前版本", + "category": "管理", + "data": {} + }, + "/update": { + "func": SystemChain(self._db).update, + "description": "更新系统", + "category": "管理", + "data": {} } } # 汇总插件命令 @@ -163,6 +175,8 @@ class Command(metaclass=Singleton): self._thread = Thread(target=self.__run) # 启动事件处理线程 self._thread.start() + # 重启msg + SystemChain(self._db).restart_finish() def __run(self): """ diff --git a/app/modules/qbittorrent/__init__.py b/app/modules/qbittorrent/__init__.py index 121a9b74..32ac3403 100644 --- a/app/modules/qbittorrent/__init__.py +++ b/app/modules/qbittorrent/__init__.py @@ -211,7 +211,7 @@ class QbittorrentModule(_ModuleBase): :param hashs: 种子Hash :return: bool """ - return self.qbittorrent.start_torrents(ids=hashs) + return self.qbittorrent.stop_torrents(ids=hashs) def torrent_files(self, tid: str) -> Optional[TorrentFilesList]: """ diff --git a/app/plugins/dirmonitor/__init__.py b/app/plugins/dirmonitor/__init__.py index e9c3dfcd..1cd7b5d6 100644 --- a/app/plugins/dirmonitor/__init__.py +++ b/app/plugins/dirmonitor/__init__.py @@ -404,6 +404,7 @@ class DirMonitor(_PluginBase): ) if self._notify: self.chain.post_message(Notification( + mtype=NotificationType.Manual, title=f"{mediainfo.title_year}{file_meta.season_episode} 入库失败!", text=f"原因:{transferinfo.message or '未知'}", image=mediainfo.get_message_image() diff --git a/app/plugins/invitessignin/__init__.py b/app/plugins/invitessignin/__init__.py index f8bd39fd..ac6f9511 100644 --- a/app/plugins/invitessignin/__init__.py +++ b/app/plugins/invitessignin/__init__.py @@ -30,7 +30,7 @@ class InvitesSignin(_PluginBase): # 作者主页 author_url = "https://github.com/thsrite" # 插件配置项ID前缀 - plugin_config_prefix = "invitessignin" + plugin_config_prefix = "invitessignin_" # 加载顺序 plugin_order = 24 # 可使用的用户级别 diff --git a/app/plugins/mediasyncdel/__init__.py b/app/plugins/mediasyncdel/__init__.py index 0ca45f4d..5127d704 100644 --- a/app/plugins/mediasyncdel/__init__.py +++ b/app/plugins/mediasyncdel/__init__.py @@ -969,6 +969,13 @@ class MediaSyncDel(_PluginBase): self.chain.stop_torrents(torrent_hash) handle_cnt += 1 + logger.info(f"暂停转种后下载任务:{download} - {download_id}") + # 删除转种后下载任务 + if download == "transmission": + self.tr.stop_torrents(ids=download_id) + else: + self.qb.stop_torrents(ids=download_id) + handle_cnt += 1 else: # 未转种de情况 if delete_flag: diff --git a/app/plugins/moviepilotupdatenotify/__init__.py b/app/plugins/moviepilotupdatenotify/__init__.py new file mode 100644 index 00000000..68f43956 --- /dev/null +++ b/app/plugins/moviepilotupdatenotify/__init__.py @@ -0,0 +1,234 @@ +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger + +from app.chain.system import SystemChain +from app.core.config import settings +from app.plugins import _PluginBase +from typing import Any, List, Dict, Tuple, Optional +from app.log import logger +from app.schemas import NotificationType +from app.utils.http import RequestUtils + + +class MoviePilotUpdateNotify(_PluginBase): + # 插件名称 + plugin_name = "MoviePilot更新推送" + # 插件描述 + plugin_desc = "MoviePilot推送release更新通知、自动更新。" + # 插件图标 + plugin_icon = "update.png" + # 主题色 + plugin_color = "#4179F4" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "thsrite" + # 作者主页 + author_url = "https://github.com/thsrite" + # 插件配置项ID前缀 + plugin_config_prefix = "moviepilotupdatenotify_" + # 加载顺序 + plugin_order = 25 + # 可使用的用户级别 + auth_level = 1 + + # 私有属性 + _enabled = False + # 任务执行间隔 + _cron = None + _update = False + _notify = False + + # 定时器 + _scheduler: Optional[BackgroundScheduler] = None + + def init_plugin(self, config: dict = None): + # 停止现有任务 + self.stop_service() + + if config: + self._enabled = config.get("enabled") + self._cron = config.get("cron") + self._update = config.get("update") + self._notify = config.get("notify") + + # 加载模块 + if self._enabled: + # 定时服务 + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + + if self._cron: + try: + self._scheduler.add_job(func=self.__check_update, + trigger=CronTrigger.from_crontab(self._cron), + name="检查MoviePilot更新") + except Exception as err: + logger.error(f"定时任务配置错误:{err}") + + # 启动任务 + if self._scheduler.get_jobs(): + self._scheduler.print_jobs() + self._scheduler.start() + + def __check_update(self): + """ + 检查MoviePilot更新 + """ + release_version, description, update_time = self.__get_release_version() + if not release_version: + logger.error("最新版本获取失败,停止运行") + return + + # 本地版本 + local_version = SystemChain(self.db).get_local_version() + if release_version == local_version: + logger.info(f"当前版本:{local_version} 远程版本:{release_version} 停止运行") + return + + # 推送更新消息 + if self._notify: + self.post_message( + mtype=NotificationType.SiteMessage, + title="【MoviePilot更新通知】", + text=f"{release_version} \n" + f"\n" + f"{description} \n" + f"{update_time}") + + # 自动更新 + if self._update: + logger.info("开始执行自动更新…") + SystemChain(self.db).update() + + @staticmethod + def __get_release_version(): + """ + 获取最新版本 + """ + version_res = RequestUtils(proxies=settings.PROXY).get_res( + "https://api.github.com/repos/jxxghp/MoviePilot/releases/latest") + if version_res: + ver_json = version_res.json() + version = f"{ver_json['tag_name']}" + description = f"{ver_json['body']}" + update_time = f"{ver_json['published_at']}" + return version, description, update_time + else: + return None, None, None + + def get_state(self) -> bool: + return self._enabled + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + pass + + def get_api(self) -> List[Dict[str, Any]]: + pass + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + """ + 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 + """ + return [ + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'enabled', + 'label': '启用插件', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'update', + 'label': '自动更新', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'notify', + 'label': '发送通知', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'cron', + 'label': '检查周期', + 'placeholder': '5位cron表达式' + } + } + ] + }, + ] + } + ] + } + ], { + "enabled": False, + "update": False, + "notify": False, + "cron": "0 9 * * *" + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + """ + 退出插件 + """ + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + except Exception as e: + logger.error("退出插件失败:%s" % str(e))