diff --git a/package.json b/package.json index 50a93d4..c49c9dc 100644 --- a/package.json +++ b/package.json @@ -559,7 +559,6 @@ "icon": "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/sqlite.png", "author": "thsrite", "level": 1, - "v2": true, "history": { "v1.3": "修复执行delete锁表失败的bug", "v1.2": "调整交互命令返回信息", @@ -575,7 +574,6 @@ "icon": "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/command.png", "author": "thsrite", "level": 1, - "v2": true, "history": { "v1.2": "调整交互命令返回信息", "v1.1": "支持交互命令/cmd [sql]执行,需主程序1.9.4+", diff --git a/package.v2.json b/package.v2.json index c3c32ef..3ee0628 100644 --- a/package.v2.json +++ b/package.v2.json @@ -325,5 +325,36 @@ "v1.3": "增加质量、分辨率、特效信息填充", "v1.2": "修复订阅已存在包含关键词和订阅站点" } + }, + "SqlExecute": { + "name": "Sql执行器", + "description": "自定义MoviePilot数据库Sql执行。", + "labels": "工具", + "version": "1.4", + "icon": "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/sqlite.png", + "author": "thsrite", + "level": 1, + "history": { + "v1.4": "兼容v2", + "v1.3": "修复执行delete锁表失败的bug", + "v1.2": "调整交互命令返回信息", + "v1.1": "支持交互命令/sql [command]执行,需主程序1.9.4+", + "v1.0": "自定义MoviePilot数据库Sql执行" + } + }, + "CommandExecute": { + "name": "命令执行器", + "description": "自定义容器命令执行。", + "labels": "工具", + "version": "1.3", + "icon": "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/command.png", + "author": "thsrite", + "level": 1, + "history": { + "v1.3": "兼容v2", + "v1.2": "调整交互命令返回信息", + "v1.1": "支持交互命令/cmd [sql]执行,需主程序1.9.4+", + "v1.0": "自定义容器命令执行" + } } } diff --git a/plugins.v2/commandexecute/__init__.py b/plugins.v2/commandexecute/__init__.py new file mode 100644 index 0000000..38c969b --- /dev/null +++ b/plugins.v2/commandexecute/__init__.py @@ -0,0 +1,242 @@ +import subprocess + +from app.core.event import eventmanager, Event +from app.plugins import _PluginBase +from typing import Any, List, Dict, Tuple +from app.log import logger +from app.schemas.types import EventType, MessageChannel + + +class CommandExecute(_PluginBase): + # 插件名称 + plugin_name = "命令执行器" + # 插件描述 + plugin_desc = "自定义容器命令执行。" + # 插件图标 + plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/command.png" + # 插件版本 + plugin_version = "1.3" + # 插件作者 + plugin_author = "thsrite" + # 作者主页 + author_url = "https://github.com/thsrite" + # 插件配置项ID前缀 + plugin_config_prefix = "commandexecute_" + # 加载顺序 + plugin_order = 99 + # 可使用的用户级别 + auth_level = 1 + + # 私有属性 + _onlyonce = None + _command = None + + def init_plugin(self, config: dict = None): + if config: + self._onlyonce = config.get("onlyonce") + self._command = config.get("command") + + if self._onlyonce and self._command: + # 执行SQL语句 + try: + for command in self._command.split("\n"): + logger.info(f"开始执行命令 {command}") + ouptut = self.execute_command(command) + # logger.info('\n'.join(ouptut)) + except Exception as e: + logger.error(f"命令执行失败 {str(e)}") + return + finally: + self._onlyonce = False + self.update_config({ + "onlyonce": self._onlyonce, + "command": self._command + }) + + @staticmethod + def execute_command(command: str): + """ + 执行命令 + :param command: 命令 + """ + result = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + ouptut = [] + while True: + error = result.stderr.readline().decode("utf-8") + if error == '' and result.poll() is not None: + break + if error: + logger.info(error.strip()) + ouptut.append(error.strip()) + while True: + output = result.stdout.readline().decode("utf-8") + if output == '' and result.poll() is not None: + break + if output: + logger.info(output.strip()) + ouptut.append(output.strip()) + + return ouptut + + @eventmanager.register(EventType.PluginAction) + def execute(self, event: Event = None): + if event: + event_data = event.event_data + if not event_data or event_data.get("action") != "command_execute": + return + logger.info(f"收到命令执行事件 ...{event_data}") + args = event_data.get("arg_str") + if not args: + return + + logger.info(f"收到命令,开始执行命令 ...{args}") + ouptut = self.execute_command(args) + result = '\n'.join(ouptut) + + if event.event_data.get("channel") == MessageChannel.Telegram: + result = f"```plaintext\n{result}\n```" + self.post_message(channel=event.event_data.get("channel"), + title="命令执行结果", + text=result, + userid=event.event_data.get("user")) + + def get_state(self) -> bool: + return True + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + """ + 定义远程控制命令 + :return: 命令关键字、事件、描述、附带数据 + """ + return [{ + "cmd": "/cmd", + "event": EventType.PluginAction, + "desc": "自定义命令执行", + "category": "", + "data": { + "action": "command_execute" + } + }] + + 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': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '执行命令' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'command', + 'rows': '2', + 'label': 'command命令', + 'placeholder': '一行一条' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal' + }, + 'content': [ + { + 'component': 'span', + 'text': '执行日志将会输出到控制台,请谨慎操作。' + } + ] + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal' + }, + 'content': [ + { + 'component': 'span', + 'text': '可使用交互命令/cmd ls' + } + ] + } + ] + } + ] + } + ] + } + ], { + "onlyonce": False, + "command": "", + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + """ + 退出插件 + """ + pass diff --git a/plugins.v2/sqlexecute/__init__.py b/plugins.v2/sqlexecute/__init__.py new file mode 100644 index 0000000..29ba46d --- /dev/null +++ b/plugins.v2/sqlexecute/__init__.py @@ -0,0 +1,289 @@ +import sqlite3 + +from app.core.event import eventmanager, Event +from app.plugins import _PluginBase +from typing import Any, List, Dict, Tuple +from app.log import logger +from app.schemas.types import EventType, MessageChannel + + +class SqlExecute(_PluginBase): + # 插件名称 + plugin_name = "Sql执行器" + # 插件描述 + plugin_desc = "自定义MoviePilot数据库Sql执行。" + # 插件图标 + plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/sqlite.png" + # 插件版本 + plugin_version = "1.4" + # 插件作者 + plugin_author = "thsrite" + # 作者主页 + author_url = "https://github.com/thsrite" + # 插件配置项ID前缀 + plugin_config_prefix = "sqlexecute_" + # 加载顺序 + plugin_order = 99 + # 可使用的用户级别 + auth_level = 1 + + # 私有属性 + _onlyonce = None + _sql = None + + def init_plugin(self, config: dict = None): + if config: + self._onlyonce = config.get("onlyonce") + self._sql = config.get("sql") + + if self._onlyonce and self._sql: + # 读取sqlite数据 + try: + gradedb = sqlite3.connect("/config/user.db") + except Exception as e: + logger.error(f"数据库链接失败 {str(e)}") + return + + # 创建游标cursor来执行executeSQL语句 + cursor = gradedb.cursor() + + # 执行SQL语句 + try: + for sql in self._sql.split("\n"): + logger.info(f"开始执行SQL语句 {sql}") + # 执行SQL语句 + cursor.execute(sql) + + if 'select' in sql.lower(): + rows = cursor.fetchall() + # 获取列名 + columns = [desc[0] for desc in cursor.description] + # 将查询结果转换为key-value对的列表 + results = [] + for row in rows: + result = dict(zip(columns, row)) + results.append(result) + result = "\n".join([str(i) for i in results]) + result = str(result).replace("'", "\"") + logger.info(result) + else: + gradedb.commit() + result = f"执行成功,影响行数:{cursor.rowcount}" + logger.info(result) + except Exception as e: + logger.error(f"SQL语句执行失败 {str(e)}") + return + finally: + # 关闭游标 + cursor.close() + + self._onlyonce = False + self.update_config({ + "onlyonce": self._onlyonce, + "sql": self._sql + }) + + @eventmanager.register(EventType.PluginAction) + def execute(self, event: Event = None): + if event: + event_data = event.event_data + if not event_data or event_data.get("action") != "sql_execute": + return + args = event_data.get("arg_str") + if not args: + return + + logger.info(f"收到命令,开始执行SQL ...{args}") + + # 读取sqlite数据 + try: + gradedb = sqlite3.connect("/config/user.db") + except Exception as e: + logger.error(f"数据库链接失败 {str(e)}") + return + + # 创建游标cursor来执行executeSQL语句 + cursor = gradedb.cursor() + + # 执行SQL语句 + try: + # 执行SQL语句 + cursor.execute(args) + if 'select' in args.lower(): + rows = cursor.fetchall() + # 获取列名 + columns = [desc[0] for desc in cursor.description] + # 将查询结果转换为key-value对的列表 + results = [] + for row in rows: + result = dict(zip(columns, row)) + results.append(result) + result = "\n".join([str(i) for i in results]) + result = str(result).replace("'", "\"") + logger.info(result) + + if event.event_data.get("channel") == MessageChannel.Telegram: + result = f"```plaintext\n{result}\n```" + self.post_message(channel=event.event_data.get("channel"), + title="SQL执行结果", + text=result, + userid=event.event_data.get("user")) + else: + gradedb.commit() + result = f"执行成功,影响行数:{cursor.rowcount}" + logger.info(result) + + if event.event_data.get("channel") == MessageChannel.Telegram: + result = f"```plaintext\n{result}\n```" + self.post_message(channel=event.event_data.get("channel"), + title="SQL执行结果", + text=result, + userid=event.event_data.get("user")) + + except Exception as e: + logger.error(f"SQL语句执行失败 {str(e)}") + return + finally: + # 关闭游标 + cursor.close() + + def get_state(self) -> bool: + return True + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + """ + 定义远程控制命令 + :return: 命令关键字、事件、描述、附带数据 + """ + return [{ + "cmd": "/sql", + "event": EventType.PluginAction, + "desc": "自定义sql执行", + "category": "", + "data": { + "action": "sql_execute" + } + }] + + 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': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '执行sql' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'sql', + 'rows': '2', + 'label': 'sql语句', + 'placeholder': '一行一条' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal' + }, + 'content': [ + { + 'component': 'span', + 'text': '执行日志将会输出到控制台,请谨慎操作。' + } + ] + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal' + }, + 'content': [ + { + 'component': 'span', + 'text': '可使用交互命令/sql select *****' + } + ] + } + ] + } + ] + } + ] + } + ], { + "onlyonce": False, + "sql": "", + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + """ + 退出插件 + """ + pass