diff --git a/icons/ipAddress.png b/icons/ipAddress.png new file mode 100644 index 0000000..89e8708 Binary files /dev/null and b/icons/ipAddress.png differ diff --git a/package.json b/package.json index 5fd4b30..124e592 100644 --- a/package.json +++ b/package.json @@ -414,5 +414,13 @@ "icon": "Transmission_A.png", "author": "Hoey", "level": 1 + }, + "IpDetect": { + "name": "本地IP检测", + "description": "如果QB、TR等服务在本地部署,当本地IP改变时自动修改其server IP", + "version": "1.0", + "icon": "ipAddress.png", + "author": "DzAvril", + "level": 1 } } diff --git a/plugins/ipdetect/__init__.py b/plugins/ipdetect/__init__.py new file mode 100644 index 0000000..401a935 --- /dev/null +++ b/plugins/ipdetect/__init__.py @@ -0,0 +1,520 @@ +import socket +from typing import List, Tuple, Dict, Any +import re +from app.log import logger +from app.plugins import _PluginBase +from app.schemas import Notification, NotificationType +from app.core.config import settings +from dotenv import set_key +from app.core.module import ModuleManager +from app.scheduler import Scheduler +from apscheduler.triggers.cron import CronTrigger + + +class IpDetect(_PluginBase): + # 插件名称 + plugin_name = "本地IP检测" + # 插件描述 + plugin_desc = "如果QB、TR等服务在本地部署,当本地IP改变时自动修改其server IP" + # 插件图标 + plugin_icon = "ipAddress.png" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "DzAvril" + # 作者主页 + author_url = "https://github.com/DzAvril" + # 插件配置项ID前缀 + plugin_config_prefix = "ipdetect_" + # 加载顺序 + plugin_order = 0 + # 可使用的用户级别 + auth_level = 1 + + # preivate property + _enabled = False + _notify = False + _enable_qb = False + _enable_tr = False + _enable_emby = False + _enable_emby_play = False + _enable_jellyfin = False + _enable_jellyfin_play = False + _enable_plex = False + _enable_plex_play = False + _onlyonce = False + _cron = "" + _setting_keys = [] + + def init_plugin(self, config: dict = None): + logger.info(f"Hello IpDetect, config {config}") + if config: + self._enabled = config.get("enabled") + self._onlyonce = config.get("onlyonce") + self._cron = config.get("cron") + self._notify = config.get("notify") + self._enable_qb = config.get("enable_qb") + self._enable_tr = config.get("enable_tr") + self._enable_emby = config.get("enable_emby") + self._enable_emby_play = config.get("enable_emby_play") + self._enable_jellyfin = config.get("enable_jellyfin") + self._enable_jellyfin_play = config.get("enable_jellyfin_play") + self._enable_plex = config.get("enable_plex") + self._enable_plex_play = config.get("enable_plex_play") + + if not self._enabled: + return + self._setting_keys = [] + if self._enable_qb: + if settings.QB_HOST is not None: + self._setting_keys.append("QB_HOST") + else: + logger.warn("QB服务地址未设置,请检查配置!") + if self._enable_tr: + if settings.TR_HOST is not None: + self._setting_keys.append("TR_HOST") + else: + self._enable_tr = False + logger.warn("TR服务地址未设置,请检查配置!") + if self._enable_emby: + if settings.EMBY_HOST is not None: + self._setting_keys.append("EMBY_HOST") + else: + self._enable_emby = False + logger.warn("Emby服务地址未设置,请检查配置!") + if self._enable_emby_play: + if settings.EMBY_PLAY_HOST is not None: + self._setting_keys.append("EMBY_PLAY_HOST") + else: + self._enable_emby_play = False + logger.warn("Emby外网播放地址未设置,请检查配置!") + if self._enable_jellyfin: + if settings.JELLYFIN_HOST is not None: + self._setting_keys.append("JELLYFIN_HOST") + else: + self._enable_jellyfin = False + logger.warn("Jellyfin服务地址未设置,请检查配置!") + if self._enable_jellyfin_play: + if settings.JELLYFIN_PLAY_HOST is not None: + self._setting_keys.append("JELLYFIN_PLAY_HOST") + else: + self._enable_jellyfin_play = False + logger.warn("Jellyfin外网播放地址未设置,请检查配置!") + if self._enable_plex: + if settings.PLEX_HOST is not None: + self._setting_keys.append("PLEX_HOST") + else: + self._enable_plex = False + logger.warn("Plex服务地址未设置,请检查配置!") + if self._enable_plex_play: + if settings.PLEX_PLAY_HOST is not None: + self._setting_keys.append("PLEX_PLAY_HOST") + else: + self._enable_plex_play = False + logger.warn("Plex外网播放地址未设置,请检查配置!") + # 更新配置 + self.__update_config() + logger.info(f"_setting_keys: {self._setting_keys}") + if self._onlyonce: + self._onlyonce = False + self.__update_config() + self.detect_ip() + + def __update_config(self): + self.update_config({ + "enabled": self._enabled, + "onlyonce": self._onlyonce, + "cron": self._cron, + "notify": self._notify, + "enable_qb": self._enable_qb, + "enable_tr": self._enable_tr, + "enable_emby": self._enable_emby, + "enable_emby_play": self._enable_emby_play, + "enable_jellyfin": self._enable_jellyfin, + "enable_jellyfin_play": self._enable_jellyfin_play, + "enable_plex": self._enable_plex, + "enable_plex_play": self._enable_plex_play, + }) + + def get_state(self) -> bool: + return self._enabled + + def detect_ip(self): + if len(self._setting_keys) == 0: + return + local_ip = self.get_local_ip() + current_ip = self.parse_ip(self.get_value(self._setting_keys[0])) + logger.info(f"current_ip: {current_ip}") + if local_ip == current_ip: + logger.info(f"当前IP地址为{local_ip},没有变化!") + if self._notify: + self.post_message( + mtype=NotificationType.SiteMessage, + title=f"【本地IP检测】", + text=f"未检测到IP地址变化!", + ) + + return + for key in self._setting_keys: + prefix = ( + True + if key == "EMBY_PLAY_HOST" + or key == "PLEX_PLAY_HOST" + or key == "JELLYFIN_PLAY_HOST" + else False + ) + self.update_key_value(key, local_ip, prefix) + # 重新加载模块 + logger.info("重新加载模块") + ModuleManager().reload() + Scheduler().init() + if self._notify: + self.post_message( + mtype=NotificationType.SiteMessage, + title=f"【本地IP检测】", + text=f"检测到本地IP变为{local_ip},已更新服务地址!", + ) + + def update_key_value(self, k, v, prefix): + old_value = self.get_value(k) + if prefix: # http(s)://ip:port + ip_pattern = r"https?://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)" + v = re.sub( + ip_pattern, + lambda m: "{}://{}:{}".format(m.group(0).split(":")[0], v, m.group(2)), + old_value, + ) + else: # ip:port + ip_pattern = r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)' + v = re.sub(ip_pattern, r'{}:\2'.format(v), old_value) + if hasattr(settings, k): + if v == "None": + v = None + setattr(settings, k, v) + if v is None: + v = "" + else: + v = str(v) + set_key(settings.CONFIG_PATH / "app.env", k, v) + logger.info(f"重新设置服务地址{k}成功!") + + def get_value(self, key): + if key == "QB_HOST": + return settings.QB_HOST + elif key == "TR_HOST": + return settings.TR_HOST + elif key == "EMBY_HOST": + return settings.EMBY_HOST + elif key == "EMBY_PLAY_HOST": + return settings.EMBY_PLAY_HOST + elif key == "JELLYFIN_HOST": + return settings.JELLYFIN_HOST + elif key == "JELLYFIN_PLAY_HOST": + return settings.JELLYFIN_PLAY_HOST + elif key == "PLEX_HOST": + return settings.PLEX_HOST + elif key == "PLEX_PLAY_HOST": + return settings.PLEX_PLAY_HOST + else: + return None + + def parse_ip(self, ip): + ip_pattern = r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' + match = re.search(ip_pattern, ip) + if match: + return match.group(1) + else: + return None + + def get_service(self) -> List[Dict[str, Any]]: + """ + 注册插件公共服务 + [{ + "id": "服务ID", + "name": "服务名称", + "trigger": "触发器:cron/interval/date/CronTrigger.from_crontab()", + "func": self.xxx, + "kwargs": {} # 定时器参数 + }] + """ + if self._enabled and self._cron: + return [ + { + "id": "IpDetect", + "name": "检测本地IP变化", + "trigger": CronTrigger.from_crontab(self._cron), + "func": self.detect_ip, + "kwargs": {}, + } + ] + + def get_local_ip(self): + try: + # 创建一个 UDP 套接字 + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # 连接到一个虚拟的目标IP和端口 + s.connect(("10.255.255.255", 1)) + # 获取本地 IP 地址 + local_ip = s.getsockname()[0] + logger.info(f"当前本地IP为:{local_ip}") + return local_ip + except socket.error: + return "127.0.0.1" # 如果无法获取到本地IP,则返回本地回环地址 + + @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]]: + return [ + { + "component": "VForm", + "content": [ + { + "component": "VRow", + "content": [ + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "enabled", + "label": "启用插件", + }, + } + ], + }, + { + "component": "VCol", + "props": {"cols": 12, "md": 4}, + "content": [ + { + "component": "VTextField", + "props": { + "model": "cron", + "label": "检测周期", + }, + } + ], + }, + ], + }, + { + "component": "VRow", + "content": [ + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "notify", + "label": "发送通知", + }, + } + ], + }, + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "onlyonce", + "label": "立即运行一次", + }, + } + ], + }, + ], + }, + { + "component": "VRow", + "content": [ + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "enable_qb", + "label": "QB下载器", + }, + } + ], + }, + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "enable_tr", + "label": "TR下载器", + }, + } + ], + }, + ], + }, + { + "component": "VRow", + "content": [ + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "enable_emby", + "label": "Emby服务", + }, + } + ], + }, + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "enable_emby_play", + "label": "Emby外网播放", + }, + } + ], + }, + ], + }, + { + "component": "VRow", + "content": [ + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "enable_jellyfin", + "label": "Jellyfin服务", + }, + } + ], + }, + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "enable_jellyfin_play", + "label": "Jellyfin外网播放", + }, + } + ], + }, + ], + }, + { + "component": "VRow", + "content": [ + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "enable_plex", + "label": "Plex服务", + }, + } + ], + }, + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "enable_plex_play", + "label": "Plex外网播放", + }, + } + ], + }, + ], + }, + { + "component": "VRow", + "content": [ + { + "component": "VCol", + "props": { + "cols": 12, + }, + "content": [ + { + "component": "VAlert", + "props": { + "type": "info", + "variant": "tonal", + "text": "本插件针对部署在本地的服务,如QB下载器、Emby服务等,检测到本地IP变化时同步修改服务地址,请勾选部署在本地的服务。", + }, + } + ] + }, + { + "component": "VCol", + "props": { + "cols": 12, + }, + "content": [ + { + "component": "VAlert", + "props": { + "type": "info", + "variant": "tonal", + "text": "本插件不适用于桥接模式的Docker,因为获取不到Host的IP地址", + }, + } + ], + }, + + ], + }, + ], + } + ], { + "enabled": False, + "notify": False, + "onlyonce": False, + "enable_qb": False, + "enable_tr": False, + "enable_emby": False, + "enable_emby_play": False, + "enable_jellyfin": False, + "enable_jellyfin_play": False, + "enable_plex": False, + "enable_plex_play": False, + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + pass