mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 00:16:57 +08:00
refactor: reorganize backend module boundaries
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""插件和可选运行模块的发现及运行时生命周期管理。"""
|
||||
@@ -0,0 +1,201 @@
|
||||
import traceback
|
||||
from typing import Generator, Optional, Tuple, Any, Union, List
|
||||
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.events import EventHandlerBinding, eventmanager
|
||||
from app.foundation.reflection import ModuleHelper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import EventType, ModuleType, DownloaderType, MediaServerType, MessageChannel, StorageSchema, \
|
||||
OtherModulesType, MediaRecognizeType
|
||||
from app.foundation.reflection import ObjectUtils
|
||||
from app.foundation.singleton import Singleton
|
||||
|
||||
|
||||
class ModuleManager(metaclass=Singleton):
|
||||
"""
|
||||
模块管理器
|
||||
"""
|
||||
|
||||
# 子模块类型集合
|
||||
SubType = Union[
|
||||
DownloaderType,
|
||||
MediaServerType,
|
||||
MessageChannel,
|
||||
StorageSchema,
|
||||
OtherModulesType,
|
||||
MediaRecognizeType,
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
"""初始化模块注册表并装载当前启用的运行模块。"""
|
||||
# 模块列表
|
||||
self._modules: dict = {}
|
||||
# 运行态模块列表
|
||||
self._running_modules: dict = {}
|
||||
# 事件总线通过该解析器绑定已启用的模块实例。
|
||||
eventmanager.register_handler_instance_resolver(
|
||||
"modules",
|
||||
self.resolve_event_handler_instance,
|
||||
)
|
||||
self.load_modules()
|
||||
|
||||
def resolve_event_handler_instance(
|
||||
self,
|
||||
owner_class: type,
|
||||
) -> Optional[EventHandlerBinding]:
|
||||
"""为模块声明的事件方法解析当前运行实例。"""
|
||||
module_id = owner_class.__name__
|
||||
if module_id not in self._modules:
|
||||
return None
|
||||
module = self._running_modules.get(module_id)
|
||||
owner_name = module_id
|
||||
if module and callable(getattr(module, "get_name", None)):
|
||||
owner_name = module.get_name()
|
||||
return EventHandlerBinding(
|
||||
instance=module,
|
||||
owner_name=owner_name,
|
||||
)
|
||||
|
||||
def load_modules(self):
|
||||
"""
|
||||
加载所有模块
|
||||
"""
|
||||
# 扫描模块目录
|
||||
modules = ModuleHelper.load(
|
||||
"app.modules",
|
||||
filter_func=lambda _, obj: hasattr(obj, 'init_module') and hasattr(obj, 'init_setting')
|
||||
)
|
||||
self._running_modules = {}
|
||||
self._modules = {}
|
||||
for module in modules:
|
||||
module_id = module.__name__
|
||||
self._modules[module_id] = module
|
||||
try:
|
||||
# 生成实例
|
||||
_module = module()
|
||||
# 初始化模块
|
||||
if self.check_setting(_module.init_setting()):
|
||||
# 通过模板开关控制加载
|
||||
_module.init_module()
|
||||
self._running_modules[module_id] = _module
|
||||
logger.debug(f"Moudle Loaded:{module_id}")
|
||||
except Exception as err:
|
||||
logger.error(f"Load Moudle Error:{module_id},{str(err)} - {traceback.format_exc()}", exc_info=True)
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
停止所有模块
|
||||
"""
|
||||
logger.info("正在停止所有模块...")
|
||||
for module_id, module in self._running_modules.items():
|
||||
try:
|
||||
module.stop()
|
||||
logger.debug(f"Moudle Stoped:{module_id}")
|
||||
except Exception as err:
|
||||
logger.error(f"Stop Moudle Error:{module_id},{str(err)} - {traceback.format_exc()}", exc_info=True)
|
||||
logger.info("所有模块停止完成")
|
||||
|
||||
def reload(self):
|
||||
"""
|
||||
重新加载所有模块
|
||||
"""
|
||||
self.stop()
|
||||
self.load_modules()
|
||||
eventmanager.send_event(etype=EventType.ModuleReload, data={})
|
||||
|
||||
def test(self, modleid: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
测试模块
|
||||
"""
|
||||
if modleid not in self._running_modules:
|
||||
return False, ""
|
||||
module = self._running_modules[modleid]
|
||||
if hasattr(module, "test") \
|
||||
and ObjectUtils.check_method(getattr(module, "test")):
|
||||
result = module.test()
|
||||
if not result:
|
||||
return False, ""
|
||||
return result
|
||||
return True, "模块不支持测试"
|
||||
|
||||
@staticmethod
|
||||
def check_setting(setting: Optional[tuple]) -> bool:
|
||||
"""
|
||||
检查开关是否己打开,开关使用,分隔多个值,符合其中即代表开启
|
||||
"""
|
||||
if not setting:
|
||||
return True
|
||||
switch, value = setting
|
||||
option = getattr(settings, switch)
|
||||
if not option:
|
||||
return False
|
||||
if option and value is True:
|
||||
return True
|
||||
if value in option:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_running_module(self, module_id: str) -> Any:
|
||||
"""
|
||||
根据模块id获取模块运行实例
|
||||
"""
|
||||
if not module_id:
|
||||
return None
|
||||
if not self._running_modules:
|
||||
return None
|
||||
return self._running_modules.get(module_id)
|
||||
|
||||
def get_running_modules(self, method: str) -> Generator:
|
||||
"""
|
||||
获取实现了同一方法的模块列表
|
||||
"""
|
||||
if not self._running_modules:
|
||||
return
|
||||
for _, module in self._running_modules.items():
|
||||
if hasattr(module, method) \
|
||||
and ObjectUtils.check_method(getattr(module, method)):
|
||||
yield module
|
||||
|
||||
def get_running_type_modules(self, module_type: ModuleType) -> Generator:
|
||||
"""
|
||||
获取指定类型的模块列表
|
||||
"""
|
||||
if not self._running_modules:
|
||||
return
|
||||
for _, module in self._running_modules.items():
|
||||
if hasattr(module, 'get_type') \
|
||||
and module.get_type() == module_type:
|
||||
yield module
|
||||
|
||||
def get_running_subtype_module(self, module_subtype: SubType) -> Generator:
|
||||
"""
|
||||
获取指定子类型的模块
|
||||
"""
|
||||
if not self._running_modules:
|
||||
return
|
||||
for _, module in self._running_modules.items():
|
||||
if hasattr(module, 'get_subtype') \
|
||||
and module.get_subtype() == module_subtype:
|
||||
yield module
|
||||
|
||||
def get_module(self, module_id: str) -> Any:
|
||||
"""
|
||||
根据模块id获取模块
|
||||
"""
|
||||
if not module_id:
|
||||
return None
|
||||
if not self._modules:
|
||||
return None
|
||||
return self._modules.get(module_id)
|
||||
|
||||
def get_modules(self) -> dict:
|
||||
"""
|
||||
获取模块列表
|
||||
"""
|
||||
return self._modules
|
||||
|
||||
def get_module_ids(self) -> List[str]:
|
||||
"""
|
||||
获取模块id列表
|
||||
"""
|
||||
return list(self._modules.keys())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
||||
from typing import Dict, List, Optional, Type, TypeVar, Generic, Iterator
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.runtime.extensions.module_manager import ModuleManager
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas import DownloaderConf, MediaServerConf, NotificationConf, NotificationSwitchConf, ServiceInfo
|
||||
from app.schemas.types import NotificationType, SystemConfigKey, ModuleType
|
||||
|
||||
TConf = TypeVar("TConf")
|
||||
|
||||
|
||||
class ServiceConfigHelper:
|
||||
"""
|
||||
配置帮助类,获取不同类型的服务配置
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_configs(config_key: SystemConfigKey, conf_type: Type) -> List:
|
||||
"""
|
||||
通用获取配置的方法,根据 config_key 获取相应的配置并返回指定类型的配置列表
|
||||
|
||||
:param config_key: 系统配置的 key
|
||||
:param conf_type: 用于实例化配置对象的类类型
|
||||
:return: 配置对象列表
|
||||
"""
|
||||
config_data = SystemConfigOper().get(config_key)
|
||||
if not config_data:
|
||||
return []
|
||||
configs = []
|
||||
for conf in config_data:
|
||||
if not isinstance(conf, dict):
|
||||
logger.warn(f"{config_key.value} 配置格式不正确,已跳过:{conf}")
|
||||
continue
|
||||
try:
|
||||
# 直接使用 conf_type 来实例化配置对象
|
||||
configs.append(conf_type(**conf))
|
||||
except ValidationError as e:
|
||||
# 单条配置存在非法值时跳过,避免影响其它服务的初始化
|
||||
logger.error(f"{config_key.value} 配置 {conf.get('name')} 校验失败,已跳过:{e}")
|
||||
return configs
|
||||
|
||||
@staticmethod
|
||||
def get_downloader_configs() -> List[DownloaderConf]:
|
||||
"""
|
||||
获取下载器的配置
|
||||
"""
|
||||
return ServiceConfigHelper.get_configs(SystemConfigKey.Downloaders, DownloaderConf)
|
||||
|
||||
@staticmethod
|
||||
def get_mediaserver_configs() -> List[MediaServerConf]:
|
||||
"""
|
||||
获取媒体服务器的配置
|
||||
"""
|
||||
return ServiceConfigHelper.get_configs(SystemConfigKey.MediaServers, MediaServerConf)
|
||||
|
||||
@staticmethod
|
||||
def get_notification_configs() -> List[NotificationConf]:
|
||||
"""
|
||||
获取消息通知渠道的配置
|
||||
"""
|
||||
return ServiceConfigHelper.get_configs(SystemConfigKey.Notifications, NotificationConf)
|
||||
|
||||
@staticmethod
|
||||
def get_notification_switches() -> List[NotificationSwitchConf]:
|
||||
"""
|
||||
获取消息通知场景的开关
|
||||
"""
|
||||
return ServiceConfigHelper.get_configs(SystemConfigKey.NotificationSwitchs, NotificationSwitchConf)
|
||||
|
||||
@staticmethod
|
||||
def get_notification_switch(mtype: NotificationType) -> Optional[str]:
|
||||
"""
|
||||
获取指定类型的消息通知场景的开关
|
||||
"""
|
||||
switchs = ServiceConfigHelper.get_notification_switches()
|
||||
for switch in switchs:
|
||||
if switch.type == mtype.value:
|
||||
return switch.action
|
||||
return None
|
||||
|
||||
|
||||
class ServiceBaseHelper(Generic[TConf]):
|
||||
"""
|
||||
通用服务帮助类,抽象获取配置和服务实例的通用逻辑
|
||||
"""
|
||||
|
||||
def __init__(self, config_key: SystemConfigKey, conf_type: Type[TConf], module_type: ModuleType):
|
||||
"""绑定服务配置类型与对应的运行模块类型。"""
|
||||
self.modulemanager = ModuleManager()
|
||||
self.config_key = config_key
|
||||
self.conf_type = conf_type
|
||||
self.module_type = module_type
|
||||
|
||||
def get_configs(self, include_disabled: bool = False) -> Dict[str, TConf]:
|
||||
"""
|
||||
获取配置列表
|
||||
|
||||
:param include_disabled: 是否包含禁用的配置,默认 False(仅返回启用的配置)
|
||||
:return: 配置字典
|
||||
"""
|
||||
configs: List[TConf] = ServiceConfigHelper.get_configs(self.config_key, self.conf_type)
|
||||
return {
|
||||
config.name: config
|
||||
for config in configs
|
||||
if (config.name and config.type and config.enabled) or include_disabled
|
||||
} if configs else {}
|
||||
|
||||
def get_config(self, name: str) -> Optional[TConf]:
|
||||
"""
|
||||
获取指定名称配置
|
||||
"""
|
||||
if not name:
|
||||
return None
|
||||
configs = self.get_configs()
|
||||
return configs.get(name)
|
||||
|
||||
def iterate_module_instances(self) -> Iterator[ServiceInfo]:
|
||||
"""
|
||||
迭代所有模块的实例及其对应的配置,返回 ServiceInfo 实例
|
||||
"""
|
||||
configs = self.get_configs()
|
||||
for module in self.modulemanager.get_running_type_modules(self.module_type):
|
||||
if not module:
|
||||
continue
|
||||
module_instances = module.get_instances()
|
||||
if not isinstance(module_instances, dict):
|
||||
continue
|
||||
for name, instance in module_instances.items():
|
||||
if not instance:
|
||||
continue
|
||||
config = configs.get(name)
|
||||
service_info = ServiceInfo(
|
||||
name=name,
|
||||
instance=instance,
|
||||
module=module,
|
||||
type=config.type if config else None,
|
||||
config=config
|
||||
)
|
||||
yield service_info
|
||||
|
||||
def get_services(self, type_filter: Optional[str] = None, name_filters: Optional[List[str]] = None) \
|
||||
-> Dict[str, ServiceInfo]:
|
||||
"""
|
||||
获取服务信息列表,并根据类型和名称列表进行过滤
|
||||
|
||||
:param type_filter: 需要过滤的服务类型
|
||||
:param name_filters: 需要过滤的服务名称列表
|
||||
:return: 过滤后的服务信息字典
|
||||
"""
|
||||
name_filters_set = set(name_filters) if name_filters else None
|
||||
|
||||
return {
|
||||
service_info.name: service_info
|
||||
for service_info in self.iterate_module_instances()
|
||||
if service_info.config and (
|
||||
type_filter is None or service_info.type == type_filter
|
||||
) and (
|
||||
name_filters_set is None or service_info.name in name_filters_set)
|
||||
}
|
||||
|
||||
def get_service(self, name: str, type_filter: Optional[str] = None) -> Optional[ServiceInfo]:
|
||||
"""
|
||||
获取指定名称的服务信息,并根据类型过滤
|
||||
|
||||
:param name: 服务名称
|
||||
:param type_filter: 需要过滤的服务类型
|
||||
:return: 对应的服务信息,若不存在或类型不匹配则返回 None
|
||||
"""
|
||||
if not name:
|
||||
return None
|
||||
for service_info in self.iterate_module_instances():
|
||||
if service_info.name == name:
|
||||
if service_info.config and (type_filter is None or service_info.type == type_filter):
|
||||
return service_info
|
||||
return None
|
||||
Reference in New Issue
Block a user